https://app.hackthebox.com/challenges/641

Description

After studying about vulnerabilities on signing servers, a group of researchers gathered one night and comitted into creating a modern and more secure way of signing tokens for authentication. They are certain that their product is ready for distribution and want to do a last security audit before publicizing their work. They provided you with access to the server. Is their way of signing messages the solution to all previous attacks?

Exploitation

#!/usr/bin/python3
from base64 import b64decode, b64encode
from pwn import *
from sympy import mod_inverse, factorint
import hashlib,string,random,re

golden_ratio = 2654435761
admin = "System_Administrator"

def get_process():
    try:
        host, port = sys.argv[1].split(':')
        return remote(host, int(port))
    except IndexError:
        print(f'Usage: python {sys.argv[0]} <ip:port>')
        exit(1)

def extract_base64_token(response):
    match = re.search(r"Your session token is b'([A-Za-z0-9+/=]+)'", response)
    if match:
        return match.group(1)
    else:
        raise ValueError("Failed to extract Base64 token from response")

def parse_e_n_from_response(response):
    match = re.search(r"\(e,N\) = \((\d+),\s*(\d+)\)", response)
    if match:
        return int(match.group(1)), int(match.group(2))
    else:
        raise ValueError("Failed to parse e and n from response")

def calculate_h_n_from_equations(equations):
    equation_re = re.compile(r"equation\(unknown, (\d+), (\d+)\) = (\d+)")
    unknowns = []
    for match in equation_re.finditer(equations):
        rnd = int(match.group(1))
        equation_output = int(match.group(3))
        inverse_rnd = mod_inverse(rnd, golden_ratio)
        unknown = (equation_output * inverse_rnd) % golden_ratio
        unknowns.append(unknown)
    h_n = 1
    for unknown in unknowns:
        h_n *= unknown
    return h_n

def hash_username(username):
    numeric_username = int(username.encode().hex(), 16)
    h = hash_var(numeric_username)
    return int(h)

def hash_var(key):
    return (((key % golden_ratio) * golden_ratio) >> 32)

def reverse_hash_var(hashed):
    shifted_value = hashed << 32
    for candidate in range(golden_ratio):
        if (((candidate % golden_ratio) * golden_ratio) >> 32) == hashed:
            return candidate
    return None

def numeric_to_username(numeric_username):
    hex_string = format(numeric_username, "x")
    if len(hex_string) % 2 != 0:
        hex_string = "0" + hex_string
    return bytes.fromhex(hex_string).decode("utf-8")

def reverse_hash_username(hashed_value):
    numeric_username = reverse_hash_var(hashed_value)
    return numeric_to_username(numeric_username)

def get_auth_token(username, d, n):
    numeric_username = int(username.encode().hex(), 16)
    h = hash_var(numeric_username)
    auth = pow(int(h), d, n)
    return b64encode(str(auth).encode()).decode()

def get_signature_and_public_key(p, plaintext):
    p.sendline(b"0")
    p.recvuntil(b"Enter a username: ")
    p.sendline(plaintext.encode())
    response = p.recvuntil(b"[+] Option >> ").decode()
    token_base64 = extract_base64_token(response)
    p.sendline(b"2")
    equations = p.recvuntil(b"Enter the hash(N): ").decode()
    h_n = calculate_h_n_from_equations(equations)
    p.sendline(str(h_n).encode())
    response = p.recv().decode()
    e, n = parse_e_n_from_response(response)
    auth = int(b64decode(token_base64).decode())
    return auth, e, n

def get_flag(p, auth_token):
    p.sendline(b"1")
    p.recvuntil(b"Enter your username: ")
    p.sendline(admin.encode())
    p.recvuntil(b"Enter your authentication token: ")
    p.sendline(auth_token.encode())
    response = p.recvline().decode()
    return response

def prepare_auth_token(forged_signature):
    auth_token = b64encode(str(forged_signature).encode()).decode()
    return auth_token

def is_alphanumeric_latin(s):
    latin_alphanumeric = set(string.ascii_letters + string.digits)
    return all(char in latin_alphanumeric for char in s)

def main():
    p = get_process()
    factors = factorint(hash_username(admin))
    ciphers = []
    for factor in factors:
        i = 1
        numeric_username = reverse_hash_var(factor)
        while True:
            try:
                uname = numeric_to_username(numeric_username)
                if not is_alphanumeric_latin(uname):
                    raise
                break
            except:
                numeric_username = numeric_username + (golden_ratio * i)
                i += 1
        cipher, e, n = get_signature_and_public_key(p, uname)
        ciphers.append(cipher)
    forged_signature = 1
    for cipher in ciphers:
        forged_signature = (forged_signature * cipher) % n
    auth_token = prepare_auth_token(forged_signature)
    response = get_flag(p, auth_token)
    print(f"Server response: {response}")
    p.close()

if __name__ == "__main__":
    main()

Summary

Signing Factory: reconstruct the PRNG state from the leak, replay it, and recover the flag.