Flash CTF – Ashhay

Challenge Overview

Looking briefly at the functionality of the challenge, it’s clear that the goal is to log in with the username “admin”, which is explicitly blocked from registering. However, instead of storing the username in plaintext, the username is hashed in the login structure. This means that hypothetically, if we had a second username that hashed to the same value as hashing “admin”, we could register with this second username, and then login with the username admin.

Source

#!/usr/local/bin/python
import signal

# In-memory storage for user asheshay and asswordspay
user_data = {}

def ashhay(message):
    # Initial ashhay values
    A, B, C, D = 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476

    # Pad the message to 64 bytes
    message = message.ljust(64, b'\x00')

    # Process each 64-byte block
    for i in range(0, len(message), 64):
        block = message[i:i+64]
        M = [int.from_bytes(block[j:j+4], 'little') for j in range(0, 64, 4)]

        for m in M:
            A = (A + m + (B ^ C ^ D)) & 0xFFFFFFFF
            A = ((A << 3) | (A >> (32 - 3))) & 0xFFFFFFFF
            A, B, C, D = D, A, B, C

    # Concatenate the final ashhay values
    return f"{A:08x}{B:08x}{C:08x}{D:08x}"

def egisterray():
    print("\n--- Egistrationray ---")
    usernameyay = input("Enter usernameyay: ").strip()
    if usernameyay == 'admin':
        print("Registration failed: Usernameyay cannot be 'admin'.")
        return False
    for c in usernameyay:
        if ord(c) < 33 or ord(c) > 126:
            print("Registration failed: Unsupported character in usernameyay.")
            return False

    asswordpay = input("Enter asswordpay: ").strip()
    usernameyay_ashhay = ashhay(usernameyay.encode().ljust(64, b'\x00'))
    asswordpay_ashhay = ashhay(asswordpay.encode().ljust(64, b'\x00'))

    if usernameyay_ashhay in user_data:
        print("Registration failed: Usernameyay already exists.")
        return False

    user_data[usernameyay_ashhay] = asswordpay_ashhay
    print("Registration successful!")
    return True

def oginlay():
    print("\n--- oginlay ---")
    usernameyay = input("Enter usernameyay: ").strip()
    asswordpay = input("Enter asswordpay: ").strip()
    usernameyay_ashhay = ashhay(usernameyay.encode().ljust(64, b'\x00'))
    asswordpay_ashhay = ashhay(asswordpay.encode().ljust(64, b'\x00'))

    if usernameyay_ashhay in user_data and user_data[usernameyay_ashhay] == asswordpay_ashhay:
        if usernameyay == 'admin':
            print("oginlay successful! Admin session granted.")
            return usernameyay, True
        else:
            print("oginlay successful! User session granted.")
            return usernameyay, False
    else:
        print("oginlay failed: Invalid usernameyay or asswordpay.")
        return None, False

def iewvay_agflay():
    print("\n--- Iewvay Agflay ---")
    print(open('flag.txt','r').read())

def pig_latinify():
    print("\n--- Pig Latin-ify ---")
    text = input("Enter a string to convert to Pig Latin: ").strip()
    words = text.split()
    pig_latin_words = []

    for word in words:
        if word[0].lower() in 'aeiou':
            pig_latin_word = word + 'way'
        else:
            pig_latin_word = word[1:] + word[0] + 'ay'
        pig_latin_words.append(pig_latin_word)

    pig_latin_text = ' '.join(pig_latin_words)
    print(f"Pig Latin: {pig_latin_text}")

def handle_client():
    print("=====================================")
    print(" Welcome to the Pig Latin Translator!")
    print("=====================================")
    print("You can egisterray and oginlay with your usernameyay and asswordpay.")

    usernameyay = None
    is_admin = False

    while True:
        if not usernameyay:
            print("\nOptions:\n1. egisterray\n2. oginlay\n3. Exit")
        else:
            if is_admin:
                print("\nOptions:\n1. Iewvay Agflay\n2. Logout\n3. Exit")
            else:
                print("\nOptions:\n1. Pig Latin-ify\n2. Logout\n3. Exit")
        choice = input("Choose an option: ").strip().lower()

        if choice in ["1", "egisterray"] and not usernameyay:
            egisterray()
        elif choice in ["2", "oginlay"] and not usernameyay:
            usernameyay, is_admin = oginlay()
        elif choice in ["1", "iewvay_agflay", "iewvay agflay"] and is_admin:
            iewvay_agflay()
        elif choice in ["1", "pig_latinify", "pig latin-ify", "pig_latinify", "pig latin-ify"] and not is_admin:
            pig_latinify()
        elif choice in ["2", "logout"] and usernameyay:
            print("Logged out successfully.")
            usernameyay = None
            is_admin = False
        elif choice in ["3", "exit"]:
            print("Goodbye!")
            break
        else:
            print("Invalid option. Please try again.")

def main():
    # Set a timeout for the session
    signal.alarm(300)
    handle_client()

if __name__ == "__main__":
    main()

The Hash Function

Since the only path to reach the admin view flag function is a hash collision, the hashing algorithm is going to be the critical part of the challenge. Thankfully, instead of using MD5, or a SHA variant, the challenge is using a completely custom algorithm.

def ashhay(message):
    # Initial ashhay values
    A, B, C, D = 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476

    # Pad the message to 64 bytes
    message = message.ljust(64, b'\x00')

    # Process each 64-byte block
    for i in range(0, len(message), 64):
        block = message[i:i+64]
        M = [int.from_bytes(block[j:j+4], 'little') for j in range(0, 64, 4)]

        for m in M:
            A = (A + m + (B ^ C ^ D)) & 0xFFFFFFFF
            A = ((A << 3) | (A >> (32 - 3))) & 0xFFFFFFFF
            A, B, C, D = D, A, B, C

    # Concatenate the final ashhay values
    return f"{A:08x}{B:08x}{C:08x}{D:08x}"

We could do proper cryptanalyis on this hashing function to determine this is a very bad one, and to maybe even manually calculate collisions, but instead, we’re going to use an awesome tool named z3 that can solve this challenge by using satisfiability modulo theories.

z3 solver

from z3 import *

def ashhay(message):
    # Initial hash values
    A, B, C, D = BitVecVal(0x67452301, 32), BitVecVal(0xEFCDAB89, 32), BitVecVal(0x98BADCFE, 32), BitVecVal(0x10325476, 32)

    # Pad the message to 64 bytes
    message = message + [BitVecVal(0, 8)] * (64 - len(message))

    # Process each 64-byte block
    for i in range(0, len(message), 64):
        block = message[i:i+64]
        M = [Concat(block[j+3], block[j+2], block[j+1], block[j]) for j in range(0, 64, 4)]

        for m in M:
            A = (A + m + (B ^ C ^ D)) & 0xFFFFFFFF
            A = RotateLeft(A, 3)
            A, B, C, D = D, A, B, C

    # Concatenate the final hash values
    return A, B, C, D

Rewriting the hashing function in z3 is reasonably easy, only needing to replace a bit of traditional python syntax with z3 values, this is the core of our solutution.

def find_collision():
    # Create symbolic variables for two messages
    message1 = [BitVec(f'm1_{i}', 8) for i in range(64)]
    message2 = [BitVec(f'm2_{i}', 8) for i in range(64)]

    # Define the character set: printable ASCII characters excluding whitespace
    charset = [i for i in range(33, 127)]  # ASCII range for printable characters excluding space

    # Create a solver
    s = Solver()

    # Add constraints that each byte of message1 must be in the charset
    for i in range(64):
        s.add(Or([message1[i] == c for c in charset]))

    # Add constraint that message2 must be "admin" followed by zeros
    admin_bytes = [BitVecVal(ord(c), 8) for c in "admin"]
    for i, byte in enumerate(admin_bytes):
        s.add(message2[i] == byte)
    for i in range(len(admin_bytes), 64):
        s.add(message2[i] == 0)

    # Compute hashes for both messages
    hash1 = ashhay(message1)
    hash2 = ashhay(message2)

    # Add constraint that the hashes are equal
    s.add(hash1[0] == hash2[0], hash1[1] == hash2[1], hash1[2] == hash2[2], hash1[3] == hash2[3])

    # Add constraint that the messages are different
    s.add(Or([message1[i] != message2[i] for i in range(64)]))

    # Check for a solution
    if s.check() == sat:
        model = s.model()
        m1 = bytes([model[message1[i]].as_long() for i in range(64)])
        m2 = bytes([model[message2[i]].as_long() for i in range(64)])
        print(f"Collision found:\nMessage 1: {m1}\nMessage 2: {m2}")
        return m1, m2
    else:
        print("No collision found.")
        return None, None

# Run the collision finder
find_collision()

the actual solver part of the script is a tiny bit more complex, but is still realitively easy to understand. We esentially set up two solved messages, each of which is 64 bytes. The first message is constrained to only contain ascii characters (and not whitespace), and the second message is set to the static value “admin” padded with zeroes. Finally, the hashes of the two messages both have to match each other. Running this script give us a username that allows us to register a user with the admin hash, letting us log in to get the flag! (If you’re curious, the shortest username that you can use is 25 characters)


### Getting the flag

With the hash collision generated, we simply register our account then log in with the username admin!

```bash
nc kubenode.mctf.io 30020
=====================================
 Welcome to the Pig Latin Translator!
=====================================
You can egisterray and oginlay with your usernameyay and asswordpay.

Options:
1. egisterray
2. oginlay
3. Exit
Choose an option: 1

--- Egistrationray ---
Enter usernameyay: QZvMr#E|!zV}!D&l.e_Z/lt#|/ge[>RqNmB8:/Xsk+:ZNM`nAN/vwx&H$<1WWe;#
Enter asswordpay: password
Registration successful!

Options:
1. egisterray
2. oginlay
3. Exit
Choose an option: 2

--- oginlay ---
Enter usernameyay: admin
Enter asswordpay: password
oginlay successful! Admin session granted.

Options:
1. Iewvay Agflay
2. Logout
3. Exit
Choose an option: 1

--- Iewvay Agflay ---
MetaCTF{ym4y_1g5p4y_ar3y4y_a7in3dlay_4ndy4y_ym4y_a5h3sh4y_4rey4y_0l1id3dc4y}