← Back to Writeups
THMEasyCrypto

W1seGuy

XESXOR9/2/20264 min read
#crypto#thm#easy#flag_prefix_key_recovery#known_plaintext_attack#repeating_key_xor_recovery

W1seGuy

Platform: THM | Category: Crypto | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: flag_prefix_key_recovery, known_plaintext_attack, repeating_key_xor_recovery

Summary

Walkthrough of the THM Crypto W1seGuy. Task: TCP service XOR-encrypts a THM{...} flag with a random 5-char repeating key and hex-encodes it, revealing a second flag if you submit the correct key. Solution: known-plaintext attack using the

Description

A w1se guy 0nce said, the answer is usually as plain as day.

A Python source file and a live TCP service on port 1337 are provided. The service XOR-encrypts the real flag with a random 5-character key, hex-encodes it, and sends it as "flag 1". It then asks for the encryption key; submitting the correct key reveals a second flag. There are two flags to recover.

The title/description hint — "the answer is usually as plain as day" — points directly at a plaintext (known-plaintext) attack. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Recon / Analysis

The relevant server logic:

python
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)]))
    return xored.encode().hex()

def start(server):
    res = ''.join(random.choices(string.ascii_letters + string.digits, k=5))
    key = str(res)                         # random 5-char key from [A-Za-z0-9]
    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()
    if key_answer == key:
        send_message(server, "Congrats! ... Here is flag 2: " + flag + "\n")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key observations:

  • The flag is encrypted with repeating-key XOR, key[i % len(key)], where len(key) == 5.
  • The output is hex-encoded and sent in plaintext over the socket.
  • The flag format THM{...} is known, so the first 4 characters and the closing } are known plaintext.

Vulnerability

Repeating-key XOR with a short key plus a known flag format is a textbook known-plaintext attack. Given ciphertext C and known plaintext P at position i, the key byte is k[i % 5] = C[i] XOR P[i]. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • THM{ (indices 0–3) directly recovers key bytes k0..k3.
  • The trailing } (last index) recovers the remaining byte. For a 40-byte ciphertext, index 39 → 39 % 5 = 4, giving k4.

All 5 key bytes are recovered with zero brute force. (Brute-forcing the 62^5 ≈ 916M key space is possible but unnecessary.)

Exploitation steps

  1. Connect to the service and read "flag 1" (the hex string).
  2. Hex-decode it to get the raw XOR ciphertext (40 bytes here).
  3. Recover k0..k3 from the THM{ prefix and k4 from the trailing }.
  4. XOR the ciphertext with the recovered repeating key → flag 1.
  5. Submit the recovered key string back to the service → flag 2. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Live run against 10.114.139.134:1337:

This XOR encoded text has flag 1: 322f0829335706293c37231f311337125326392027093761220a2b3c3a1614133c6236141f0a203e

Ciphertext is 40 bytes; recovered key = fgERC.

  • Flag 1 (decrypting the hex): THM{p1alntExtAtt4ckcAnr3alLyhUrty0urxOr}
  • Submitting key fgERC → Flag 2: THM{REDACTED}

Solver script

python
#!/usr/bin/env python3
import socket, sys

HOST = sys.argv[1]
PORT = int(sys.argv[2])

def recv_until(s, marker, timeout=10):
    s.settimeout(timeout)
    buf = b""
    while marker not in buf:
        d = s.recv(4096)
        if not d:
            break
        buf += d
    return buf
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

s = socket.create_connection((HOST, PORT))
data = recv_until(s, b"key?").decode(errors="replace")

hexstr = None
for line in data.splitlines():
    if "flag 1:" in line:
        hexstr = line.split("flag 1:")[1].strip()
        break

ct = bytes.fromhex(hexstr)

# Known-plaintext recovery of the 5-byte repeating key
known_prefix = b"THM{"
key = [None] * 5
for i, ch in enumerate(known_prefix):
    key[i % 5] = ct[i] ^ ch
li = len(ct) - 1
key[li % 5] = ct[li] ^ ord('}')          # trailing '}' -> last key byte
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

keystr = ''.join(chr(k) for k in key)
flag1 = bytes(ct[i] ^ key[i % 5] for i in range(len(ct))).decode(errors="replace")
print("KEY:", keystr, "FLAG1:", flag1)

# Replay the key to unlock flag 2
s.sendall(keystr.encode() + b"\n")
resp = recv_until(s, b"flag 2", timeout=10).decode(errors="replace")
print(resp)
s.close()

Run: python3 solve.py 10.114.139.134 1337

Flags

THM{p1alntExtAtt4ckcAnr3alLyhUrty0urxOr}
THM{REDACTED}

Key Indicators / Takeaways

Use this technique when:

  • A service uses repeating-key XOR with a short key (key[i % len(key)]).
  • The flag format prefix is known (THM{, FLAG{, etc.), giving free known-plaintext bytes.
  • The ciphertext ends in a predictable character (}) whose modular index maps onto the remaining key byte — this recovers the last key position without brute force.
  • Ciphertext is hex-encoded and delivered over a plaintext TCP channel.
  • The challenge hint literally says "plain as day" → plaintext attack. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering flag_prefix_key_recovery, known_plaintext_attack, repeating_key_xor_recovery was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-14 • THM • W1seGuy • Difficulty: Easy