Overview
W1seGuy is a cryptography challenge centered on a weak XOR encryption scheme.
The server encrypts a flag with a randomly generated 5-character alphanumeric key
and sends the hex-encoded ciphertext. You must recover the key to obtain a second flag.
The vulnerability: the flag format THM{...} is known — enabling
a classic known-plaintext XOR attack that recovers all 5 key bytes
with zero brute-force.
Connect to TCP 1337
↓
Server sends hex-encoded XOR ciphertext (flag 1 encrypted)
↓
Known-plaintext attack: "THM{" recovers key[0..3], "}" recovers key[4]
↓
Decrypt ciphertext → Flag 1
↓
Submit key to server → Flag 2
Server Interaction
$ nc 10.49.149.93 1337 This XOR encoded text has flag 1: 1007234d32752e02583601371a7736307b0d5d2105211c05232803175e17363b170637363721443f What is the encryption key?
The server workflow:
| Step | Action |
|---|---|
| 1 | Generates a random 5-char key from [a-zA-Z0-9] |
| 2 | XOR-encrypts the flag with that key (cycling every 5 bytes) |
| 3 | Sends the ciphertext as a hex string (this is flag 1 — encrypted) |
| 4 | Prompts for the key; if correct, reveals flag 2 |
Source Code & Vulnerability
def setup(server, key):
flag = open('flag.txt', 'r').read().strip()
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
Key observations:
flag ⊕ key = ct, so ct ⊕ flag = key and
ct ⊕ key = flag
THM{...} is public knowledge — 4 prefix bytes
+ 1 suffix byte = all 5 key positions recovered.
Known-Plaintext XOR Attack
XOR encryption satisfies: key[i % 5] = flag[i] ⊕ ciphertext[i].
Since all TryHackMe flags begin with THM{ and end with },
we have 5 known plaintext bytes covering all key positions:
| Position | Known Plaintext | Operation | Recovers |
|---|---|---|---|
| 0 | T (0x54) | 0x54 ⊕ ct[0] | key[0] |
| 1 | H (0x48) | 0x48 ⊕ ct[1] | key[1] |
| 2 | M (0x4d) | 0x4d ⊕ ct[2] | key[2] |
| 3 | { (0x7b) | 0x7b ⊕ ct[3] | key[3] |
| n-1 | } (0x7d) | 0x7d ⊕ ct[n-1] | key[4] |
For a 40-byte flag, (40-1) % 5 = 4, so ct[39] recovers
key[4]. All five key bytes recovered with zero brute-force.
hex_ct = "1007234d32752e02583601371a773630..." ct = bytes.fromhex(hex_ct) # Recover key[0..3] from "THM{" key = [None] * 5 for i, ch in enumerate(b"THM{"): key[i] = chr(ch ^ ct[i]) # Recover key[4] from closing "}" key[(len(ct) - 1) % 5] = chr(ord('}') ^ ct[-1]) key_str = ''.join(key) flag1 = ''.join(chr(ct[i] ^ ord(key_str[i % 5])) for i in range(len(ct))) print(key_str) # e.g. "DOn6B" print(flag1) # THM{p1alntExtAtt4ckcAnr3alLyhUrty0urxOr}
Both Flags Captured
Decrypting the ciphertext with the recovered key yields flag 1. Submitting the key back to the server triggers the flag 2 response:
$ python3 solve.py 10.49.149.93 1337 [*] Connecting to 10.49.149.93:1337 [+] Recovered key : ysMQN [+] Flag 1 : THM{p1alntExtAtt4ckcAnr3alLyhUrty0urxOr} [*] Server: Congrats! That is the correct key! [+] Flag 2 : THM{BrUt3_ForC1nG_XOR_cAn_B3_FuN_nO?}
Attack Chain
THM{ ⊕ ct[0..3] → key[0..3], } ⊕ ct[-1] → key[4]Vulnerabilities
| Finding | Impact | Severity |
|---|---|---|
| XOR with 5-byte repeating key | Full key recovery from known plaintext — zero brute-force | Critical |
Predictable flag format (THM{...}) |
Provides 5 known-plaintext bytes covering all key positions | High |
| No authenticated encryption | Ciphertext is malleable — no integrity protection | High |
Takeaways
Automated Solver
solve.py handles the full chain — connect, known-plaintext attack,
key submission, and flag extraction.
$ python3 solve.py 10.49.149.93 1337 [*] Connecting to 10.49.149.93:1337 [+] Recovered key : ysMQN [+] Flag 1 : THM{p1alntExtAtt4ckcAnr3alLyhUrty0urxOr} [*] Server: Congrats! That is the correct key! [+] Flag 2 : THM{BrUt3_ForC1nG_XOR_cAn_B3_FuN_nO?}
Tools Used
| Tool | Purpose |
|---|---|
netcat | Initial server interaction and reconnaissance |
python3 | Known-plaintext XOR attack and key recovery |
solve.py | Automated end-to-end solver (Python 3) |