W1seGuy (CTF) Writeup | TryHackMe
Cracking Custom Crypto: A Deep Dive into Repeating-Key XOR Vulnerabilities.

"A w1se guy 0nce said, the answer is usually as plain as day."
In this room, we are going to break down a classic cryptographic vulnerability: the Repeating-Key XOR Cipher. We will explore how XOR works under the hood, identify the critical flaws in this specific implementation, and walk through the attacker's mindset required to build a Known Plaintext Attack (KPA) to bypass the encryption.
At the start of the room, we are given the source code to analyze, which is executed whenever someone connects to it via TCP port 1337.
Source Code
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()
🛠️ Let's decode the source code :
1. The Global Flag
flag = open('flag.txt','r').read().strip()
When the server starts, it reads a file called flag.txt from its own hard drive and stores it in memory. This is our ultimate target. We want the server to print this variable to us.
2. The start(server) Function (The Game Loop)
res = ''.join(random.choices(string.ascii_letters + string.digits, k=5))
key = str(res)
Every time a user connects, the server generates a completely random, 5-character alphanumeric string. This acts as the secret key. Because it is generated dynamically on every connection, you cannot guess it, and you cannot reuse a key from a previous session.
hex_encoded = setup(server, key)
send_message(server, "This XOR encoded text has flag 1: " + hex_encoded + "\n")
The server passes this random key to the setup() function, gets a hexadecimal string back, and sends it to the user.
if key_answer == key:
send_message(server, "Congrats! That is the correct key! Here is flag 2: " + flag + "\n")
Finally, it asks the user to input the key. If the user successfully guesses the random 5-character string, the server hands over the real flag from flag.txt.
3. The setup(server, key) Function (The Weakness)
flag = 'THM{thisisafakeflag}'
xored = ""
for i in range(0,len(flag)):
xored += chr(ord(flag[i]) ^ ord(key[i%len(key)]))
This is where the cryptography happens, and where the critical flaw exists.
It creates a fake plaintext flag:
THM{thisisafakeflag}.It iterates through each character of this fake flag.
It uses the bitwise XOR operator (
^) to scramble each character of the fake flag against the random 5-characterkey.i%len(key)ensures that if the fake flag is longer than 5 characters (which it is), the 5-character key just loops over and over until the end of the text.
How XOR Encryption Works
XOR (Exclusive OR) is a fundamental bitwise operation used extensively in computer science and cryptography. Unlike standard logical OR (where the result is true if either or both inputs are true), XOR outputs true strictly if the inputs are different.
If you XOR a value with the same key twice, you get the original value back.
$$\text{Encryption}: Plaintext \oplus Key = Ciphertext$$
$$\text{Decryption}:Ciphertext \oplus Plaintext = Key$$
1. Reconnaissance and Alignment
We know the key is 5 characters long:
K1K2K3K4K5.We know the plaintext starts with 4 specific characters:
THM{.
We map the alignment exactly as the application processes it:
2. Exploiting the Math (The KPA)
We can isolate the first four characters of the key with absolute mathematical certainty. We don't need to guess them; we calculate them directly:
3. Bridging the Gap (Brute-Forcing the Remainder)
At this stage, we have 80% of the key. We are only missing \(K_5\). Why? Because we do not know what the 5th character of the plaintext flag is.
However, the application limits the key space to alphanumeric characters (string.ascii_letters + string.digits). This means there are only 62 possible characters for that final missing byte. Instead of trying to calculate it, it is computationally trivial to just guess it.
We script a loop to append all 62 possible characters to our known 4-character key prefix. We use each 5-character combination to decrypt the full ciphertext.
4. The Bypass
The final step in the attacker's methodology is the verification phase. As our script brute-forces the 5th character, it checks the decrypted output. If the decrypted string starts with THM{ and ends with }, we know we have successfully reconstructed the full encryption key.
Once the script identifies the correct key, we pass it back to the server. The server verifies our input against its internal variable, authenticates our bypass, and grants us the final flag.
netcat to connectnetcat 1.xx.xx.xx 1337
Let's Code:
import string
# The hex string you received from the server
hex_encoded = "hash-text"
ciphertext = bytes.fromhex(hex_encoded)
# We know the flag format starts with "THM{"
known_plaintext = b"THM{"
# Step 1: Derive the first 4 characters of the key
key_chars = []
for i in range(4):
# Ciphertext ^ Plaintext = Key
key_chars.append(chr(ciphertext[i] ^ known_plaintext[i]))
partial_key = "".join(key_chars)
print(f"[*] Partial key derived: {partial_key}")
# Step 2: Brute-force the 5th character
print("[*] Brute-forcing the 5th character...\n")
charset = string.ascii_letters + string.digits
for char in charset:
test_key = partial_key + char
# Decrypt the entire ciphertext with the current test key
decrypted = ""
for i in range(len(ciphertext)):
decrypted += chr(ciphertext[i] ^ ord(test_key[i % 5]))
# Check if the decrypted text looks like a valid flag (ends with '}')
# and consists of mostly printable characters
if decrypted.startswith("THM{") and decrypted.endswith("}"):
print(f"[+] SUCCESS!")
print(f"[+] Correct Key: {test_key}")
print(f"[+] Decrypted Flag 1: {decrypted}")
break
Provide the correct key to get Flag 2 directly.
💡Advanced Code (If you want to automate):
from pwn import *
import string
# Set up the connection (replace with the actual IP address)
HOST = '10.10.x.x' # <--- Change this to the target IP
PORT = 1337
r = remote(HOST, PORT)
# Receive the line with the hex encoded text
prompt = r.recvline().decode()
print(f"Server sent: {prompt.strip()}")
# Extract the hex string from the line
hex_encoded = prompt.split("flag 1: ")[1].strip()
ciphertext = bytes.fromhex(hex_encoded)
# Crack the key
known_plaintext = b"THM{"
partial_key = "".join([chr(ciphertext[i] ^ known_plaintext[i]) for i in range(4)])
charset = string.ascii_letters + string.digits
correct_key = ""
for char in charset:
test_key = partial_key + char
decrypted = "".join([chr(ciphertext[i] ^ ord(test_key[i % 5])) for i in range(len(ciphertext))])
if decrypted.startswith("THM{"):
correct_key = test_key
print(f"\n[+] Cracked Key for this session: {correct_key}")
print(f"[+] Flag 1: {decrypted}")
break
# Send the cracked key back to the server
r.recvuntil(b"What is the encryption key? ")
r.sendline(correct_key.encode())
# Receive Flag 2
response = r.recvall().decode()
print(f"\n[+] Server Response:\n{response}")
Conclusion
The repeating-key XOR cipher is a fantastic teaching tool for cryptography. It beautifully illustrates why strong encryption relies on more than just complex mathematical operations—it relies heavily on robust key management, adequate key length, and avoiding predictable data structures.
The takeaway? Never hardcode predictable formats at the start of encrypted data if you are using simple stream ciphers, and always rely on heavily vetted, industry-standard cryptographic libraries (like AES) rather than attempting to build your own.