Introduction

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.
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.
res = ''.join(random.choices(string.ascii_letters + string.digits, k=5))
Además si introducimos bien la clave, nos devolverá la flag 2.
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í:
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.
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.

Nos conectamos al servidor por el puerto 1337 con nc:

Copiamos la flag 1 cifrada en nuestro script.

Ahora escribimos la clave en el servidor:

Y así obtenemos las dos flags.
