Introduction
In this writeup, I'll walk through solving the W1seGuy challenge from TryHackMe. This room demonstrates a fundamental weakness in XOR-based encryption: when attackers know part of the plaintext, they can recover the encryption key.
"A w1se guy 0nce said, the answer is usually as plain as day." โ a clever reference to the known plaintext attack we'll exploit.
Challenge Overview
Upon starting the machine, we're given access to:
- A Python source file revealing the encryption logic
- A TCP service running on port 1337
The goal is to capture two flags:
- Flag 1: Hidden in the XOR-encrypted message
- Flag 2: Revealed after providing the correct encryption key
Source Code Analysis
Let's examine the provided source code to understand exactly what we're dealing with:
import random
import socketserver
import socket, os
import string
flag = open('flag.txt','r').read().strip()
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")
Key Observations
| Component | Details |
|---|---|
| Key Generation | 5 random alphanumeric characters (a-z, A-Z, 0-9) |
| Encryption | XOR with repeating key |
| Output Format | Hex-encoded ciphertext |
| Flag Format | THM{...} (known prefix!) |
๐ก The Critical Vulnerability
The combination of a short, repeating key (only 5 characters) and a known plaintext prefix (all flags start with THM{) makes this encryption trivially breakable.
Understanding XOR Encryption
The Crucial Property
XOR has a self-inverse property that makes it both useful for encryption and vulnerable to attack:
plaintext โ key = ciphertext
ciphertext โ key = plaintext
ciphertext โ plaintext = key โ This is our attack vector!
If we know the plaintext, we can recover the key by XORing it with the ciphertext.
The Attack: Known Plaintext Attack
Since we know:
- The flag format starts with
THM{(4 known characters) - The key is exactly 5 characters long
We can recover 4 of the 5 key characters immediately:
ciphertext[0] โ 'T' = key[0]
ciphertext[1] โ 'H' = key[1]
ciphertext[2] โ 'M' = key[2]
ciphertext[3] โ '{' = key[3]
For the 5th character, we have only 62 possibilities (a-z, A-Z, 0-9). We can brute-force this trivially!
Attack Complexity
| Scenario | Possibilities |
|---|---|
| Full brute-force (5 chars) | 62โต = 916,132,832 |
| With known plaintext | 62ยน = 62 |
๐ฅ Result
We reduced the keyspace by a factor of 14.7 million!
Building the Exploit
#!/usr/bin/env python3
"""
W1seGuy CTF Solver - TryHackMe
Exploits XOR encryption weakness using known plaintext attack
"""
import socket
import string
import sys
def xor_decrypt(ciphertext_bytes, key):
"""Decrypt XOR'd bytes using the given key (repeating)"""
decrypted = ""
for i in range(len(ciphertext_bytes)):
decrypted += chr(ciphertext_bytes[i] ^ ord(key[i % len(key)]))
return decrypted
def recover_key(ciphertext_bytes, known_plaintext="THM{"):
"""
Recover the encryption key using known plaintext attack.
We know flags start with 'THM{' - that's 4 characters.
Key is 5 characters, so we brute-force the 5th.
"""
# Recover first 4 characters of the key
key_partial = ""
for i in range(len(known_plaintext)):
key_partial += chr(ciphertext_bytes[i] ^ ord(known_plaintext[i]))
print(f"[*] Recovered partial key (4 chars): {key_partial}")
# Brute-force the 5th character
charset = string.ascii_letters + string.digits
for c in charset:
test_key = key_partial + c
decrypted = xor_decrypt(ciphertext_bytes, test_key)
if (decrypted.startswith("THM{") and
decrypted.endswith("}") and
all(32 <= ord(ch) <= 126 for ch in decrypted)):
return test_key, decrypted
return None, None
def solve(host, port=1337):
"""Connect to server and solve the challenge"""
print(f"[*] Connecting to {host}:{port}...")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
data = s.recv(4096).decode()
hex_encoded = data.split("flag 1: ")[1].split("\n")[0].strip()
ciphertext_bytes = bytes.fromhex(hex_encoded)
key, flag1 = recover_key(ciphertext_bytes)
if key:
print(f"[+] Encryption Key: {key}")
print(f"[+] FLAG 1: {flag1}")
# Send key to get flag 2
s.recv(4096)
s.send((key + "\n").encode())
response = s.recv(4096).decode()
print(f"[+] {response}")
s.close()
if __name__ == "__main__":
solve(sys.argv[1])
Execution & Results
Running the exploit against the target:
$ python3 w1seguy_solver.py 10.65.181.150
[*] Connecting to 10.65.181.150:1337...
[+] Connected!
[*] Recovered partial key (4 chars): 6QWs
[+] SUCCESS!
[+] Encryption Key: 6QWsu
[+] FLAG 1: THM{p1alntExtAtt4ckcAnr3alLyhUrty0urxOr}
[*] Sending key to get Flag 2...
Congrats! That is the correct key! Here is flag 2: THM{BrUt3_ForC1nG_XOR_cAn_B3_FuN_nO?}
๐ฉ Flag 1
THM{p1alntExtAtt4ckcAnr3alLyhUrty0urxOr}
๐ฉ Flag 2
THM{BrUt3_ForC1nG_XOR_cAn_B3_FuN_nO?}
Lessons Learned
Why This Encryption Failed
- Short Key Length: A 5-character key provides minimal security
- Key Reuse: The same key encrypts the entire message
- Predictable Plaintext: Known flag format leaked key material
- No Key Derivation: The random key was used directly
Secure Alternatives
| Weak Approach | Secure Alternative |
|---|---|
| Short repeating key | AES-256 with proper key generation |
| Direct key usage | Key derivation functions (PBKDF2, Argon2) |
| XOR cipher | Authenticated encryption (AES-GCM) |
โ ๏ธ Key Takeaways
Never roll your own crypto. XOR alone is not encryption โ it's a building block, not a complete solution. Always use battle-tested cryptographic libraries.
Conclusion
The W1seGuy challenge elegantly demonstrates why simple XOR encryption fails in practice. By exploiting the known THM{ prefix, we recovered the encryption key with minimal effort โ reducing 916 million possibilities down to just 62.
The wise lesson here? Never underestimate the power of known plaintext attacks.