CTF Writeup

W1seGuy

TryHackMe · Cryptography · Easy · by 0xb0rn3

Platform TryHackMe Category Cryptography / XOR Difficulty Easy Target 10.49.149.93:1337 Technique Known-Plaintext XOR Attack Flags 2 flags captured
0
Context

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.

ATTACK CHAIN
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
1
Reconnaissance

Server Interaction

BASH
$ nc 10.49.149.93 1337

This XOR encoded text has flag 1:
1007234d32752e02583601371a7736307b0d5d2105211c05232803175e17363b170637363721443f
What is the encryption key?

The server workflow:

StepAction
1Generates a random 5-char key from [a-zA-Z0-9]
2XOR-encrypts the flag with that key (cycling every 5 bytes)
3Sends the ciphertext as a hex string (this is flag 1 — encrypted)
4Prompts for the key; if correct, reveals flag 2
2
Analysis

Source Code & Vulnerability

PYTHON — SERVER
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:

5-Byte Key
The key is only 5 characters long, cycling over the entire plaintext.
XOR Is Symmetric
flag ⊕ key = ct, so ct ⊕ flag = key and ct ⊕ key = flag
Known Plaintext
The flag format THM{...} is public knowledge — 4 prefix bytes + 1 suffix byte = all 5 key positions recovered.
3
Exploitation

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:

PositionKnown PlaintextOperationRecovers
0T (0x54)0x54 ⊕ ct[0]key[0]
1H (0x48)0x48 ⊕ ct[1]key[1]
2M (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.

PYTHON
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}
4
Flags

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:

SOLVE OUTPUT
$ 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?}
 Flag 1 — Decrypted Ciphertext
THM{p1alntExtAtt4ckcAnr3alLyhUrty0urxOr}
 Flag 2 — Server Reward
THM{BrUt3_ForC1nG_XOR_cAn_B3_FuN_nO?}
Visualization

Attack Chain

1
TCP Connection
Connect to port 1337 — server sends hex-encoded XOR ciphertext
2
Known-Plaintext Recovery
THM{ct[0..3] → key[0..3], }ct[-1] → key[4]
3
Full Decryption
XOR ciphertext with recovered key → Flag 1
Key Submission
Send key to server → Flag 2: THM{BrUt3_ForC1nG_XOR_cAn_B3_FuN_nO?}
Assessment

Vulnerabilities

FindingImpactSeverity
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
Defense

Takeaways

Never Use Short-Key XOR
XOR with a repeating key shorter than the plaintext is fundamentally broken against known-plaintext. No amount of key randomness helps.
Use Authenticated Encryption
AES-GCM or ChaCha20-Poly1305 with cryptographically random keys provide both confidentiality and integrity.
Avoid Predictable Formats
Known plaintext headers, magic bytes, or structured formats make XOR and other weak ciphers trivially breakable.
Automation

Automated Solver

solve.py handles the full chain — connect, known-plaintext attack, key submission, and flag extraction.

BASH
$ 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?}

View source on GitHub

Arsenal

Tools Used

ToolPurpose
netcatInitial server interaction and reconnaissance
python3Known-plaintext XOR attack and key recovery
solve.pyAutomated end-to-end solver (Python 3)