Breaking XOR Encryption: W1seGuy CTF Writeup

A practical demonstration of why repeating-key XOR encryption fails against known plaintext attacks

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:

The goal is to capture two flags:

  1. Flag 1: Hidden in the XOR-encrypted message
  2. 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:

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

  1. Short Key Length: A 5-character key provides minimal security
  2. Key Reuse: The same key encrypts the entire message
  3. Predictable Plaintext: Known flag format leaked key material
  4. 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.

Share this writeup

๐Ÿ“ Also published on Medium

#CTF #TryHackMe #Cryptography #XOR #KnownPlaintextAttack #Python
โ† Back to all writeups