XUT_ / MACHINES / THM

0051

W1seGuy

THMMACHINENONEEASY

Introduction

w1seguy

Una vez un hombre sabio ijo: "La respuesta es tan plana como el dia (the answer is usually as plain as day)".

Getting the Flags

Nos dan un archivo con un código fuente para revisar y nos piden encontrar las dos flags.

python
import random
import socketserver 
import socket, os
import string

flag = open('flag.txt','r').read().strip()

def send_message(server, message):
    enc = message.encode()
    server.send(enc)

def setup(server, key):
    flag = 'THM{thisisafakeflag}' 
    xored = ""

    for i in range(0,len(flag)):
        xored += chr(ord(flag[i]) ^ ord(key[i%len(key)]))

    hex_encoded = xored.encode().hex()
    return hex_encoded

def start(server):
    res = ''.join(random.choices(string.ascii_letters + string.digits, k=5))
    key = str(res)
    hex_encoded = setup(server, key)
    send_message(server, "This XOR encoded text has flag 1: " + hex_encoded + "\n")
    
    send_message(server,"What is the encryption key? ")
    key_answer = server.recv(4096).decode().strip()

    try:
        if key_answer == key:
            send_message(server, "Congrats! That is the correct key! Here is flag 2: " + flag + "\n")
            server.close()
        else:
            send_message(server, 'Close but no cigar' + "\n")
            server.close()
    except:
        send_message(server, "Something went wrong. Please try again. :)\n")
        server.close()

class RequestHandler(socketserver.BaseRequestHandler):
    def handle(self):
        start(self.request)

if __name__ == '__main__':
    socketserver.ThreadingTCPServer.allow_reuse_address = True
    server = socketserver.ThreadingTCPServer(('0.0.0.0', 1337), RequestHandler)
    server.serve_forever()

Por lo que dicen en la segunda parte de la actividad, el servidor está escuchando en el puerto 1337 de la máquina dada, y seguramente este es el código que ejecuta, por lo que tenemos que descifrar cómo obtener las flags.

Si nos paramos a analizar el código, podemos ver que se está generando una clave aleatoria con números y letras de 5 caracteres.

python
res = ''.join(random.choices(string.ascii_letters + string.digits, k=5))

Además si introducimos bien la clave, nos devolverá la flag 2.

python
if key_answer == key:
	send_message(server, "Congrats! That is the correct key! Here is flag 2: " + flag + "\n")
	server.close()

Con esa clave podemos descodificar la primera. Sin embargo, no podemos saber qué clave se va a generar aparentemente, pero conocemos 5 caracteres de la flag: THM{}. Conocemos las posiciones 0, 1, 2, 3, n. Con una clave de longitud 5, la cosa queda así:

text
posición : 0 1 2 3 4 5 6 7 ... N
plaintext: T H M { ? ? ? ? ... }
key idx  : 0 1 2 3 4 0 1 2 ... N%5

El único caso en el que se nos revela todos los bytes es si $Nmod5 = 0$ porque esto revelaría el mismo byte de clave que la posición 4. De otra forma, debemos hacer fuerza bruta para conseguir ese último bit. Por lo que vamos a escribir un script de fuerza bruta.

python
import string

def brute_force_xor(hex_cipher, key_length=5):
    cipher = bytes.fromhex(hex_cipher.strip())
    charset = string.ascii_letters + string.digits
    
    print(f"Ciphertext length: {len(cipher)} bytes")
    print(f"len % {key_length} = {len(cipher) % key_length}\n")
    
    # Bytes de clave recuperables por formato THM{...}
    known = {}
    if len(cipher) > 0: known[0] = cipher[0] ^ ord('T')
    if len(cipher) > 1: known[1] = cipher[1] ^ ord('H')
    if len(cipher) > 2: known[2] = cipher[2] ^ ord('M')
    if len(cipher) > 3: known[3] = cipher[3] ^ ord('{')
    if len(cipher) % key_length == 0:
        known[4] = cipher[-1] ^ ord('}')
        print("key[4] recuperado con '}' final\n")

    print("Clave parcial conocida:", {k: chr(v) for k, v in sorted(known.items())})
    
    # Si ya tenemos todos los bytes
    if len(known) == key_length:
        key = ''.join(chr(known[i]) for i in range(key_length))
        plaintext = ''.join(chr(cipher[i] ^ ord(key[i % key_length])) for i in range(len(cipher)))
        print(f"\n[+] Clave única: {key}")
        print(f"[+] Flag: {plaintext}")
        return

    # Fuerza bruta sobre los bytes desconocidos
    unknown_positions = [i for i in range(key_length) if i not in known]
    print(f"\nPosiciones desconocidas: {unknown_positions}")
    print(f"Espacio de búsqueda: {len(charset)**len(unknown_positions)} combinaciones\n")
    
    candidates = []

    def recurse(pos_idx, current_known):
        if pos_idx == len(unknown_positions):
            key = ''.join(chr(current_known[i]) for i in range(key_length))
            plaintext = ''.join(
                chr(cipher[i] ^ ord(key[i % key_length]))
                for i in range(len(cipher))
            )
            # Filtro: ASCII imprimible y empieza por THM{
            if (all(32 <= ord(c) <= 126 for c in plaintext) and
                plaintext.startswith('THM{') and
                plaintext.endswith('}')):
                candidates.append((key, plaintext))
            return

        pos = unknown_positions[pos_idx]
        for c in charset:
            current_known[pos] = ord(c)
            recurse(pos_idx + 1, current_known)

    recurse(0, dict(known))

    if candidates:
        print(f"[+] {len(candidates)} flag(s) encontrada(s):\n")
        for key, flag in candidates:
            print(f"  Clave : {key}")
            print(f"  Flag  : {flag}\n")
    else:
        print("[-] Sin resultados con filtro estricto. Mostrando candidatos ASCII:\n")
        # Segundo pase sin exigir THM{ }
        def recurse_loose(pos_idx, current_known):
            if pos_idx == len(unknown_positions):
                key = ''.join(chr(current_known[i]) for i in range(key_length))
                plaintext = ''.join(
                    chr(cipher[i] ^ ord(key[i % key_length]))
                    for i in range(len(cipher))
                )
                if all(32 <= ord(c) <= 126 for c in plaintext):
                    print(f"  Clave: {key} -> {plaintext}")
                return
            pos = unknown_positions[pos_idx]
            for c in charset:
                current_known[pos] = ord(c)
                recurse_loose(pos_idx + 1, current_known)

        recurse_loose(0, dict(known))

# --- USO ---
hex_input = input("Pega el ciphertext en hex: ").strip()
brute_force_xor(hex_input)

Lo ejecutamos.

w1seguy

Nos conectamos al servidor por el puerto 1337 con nc:

w1seguy

Copiamos la flag 1 cifrada en nuestro script.

w1seguy

Ahora escribimos la clave en el servidor:

w1seguy

Y así obtenemos las dos flags.