Skip to main content

W1seGuy

What is XOR (Very Briefly)?

Imagine a light switch. It has two states: ON or OFF. XOR is like a special light switch that follows these rules:

  • If both inputs are the same (both ON or both OFF), the output is OFF.
  • If the inputs are different (one ON, one OFF), the output is ON.

In computers, instead of ON/OFF, we use 1 (ON) and 0 (OFF). Here's the XOR "truth table":

Input AInput BOutput (A XOR B)
000
011
101
110

The crucial property of XOR is that it's reversible. If you XOR something with a key, and then XOR the result with the same key again, you get back the original thing. This is why it's used in cryptography.

The Server Code

server.py
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()
  • Imports: The code imports libraries for:

    • random: Generating random numbers/characters.
    • socketserver: Creating a simple network server.
    • socket: Low-level network communication.
    • os: Operating system interaction (not directly used in the core logic here).
    • string: Working with strings (like letters and digits).
  • flag = ...: Reads the real flag from a file named flag.txt. This is what we want to get in the end.

  • send_message(server, message): This function takes a message (a string), converts it to bytes (because networks send bytes, not text), and sends it to the client (you!).

  • setup(server, key): This is the crucial encryption part.

    • It sets flag = 'THM{thisisafakeflag}'. This is a fake flag, used to hide the real flag. This is the text that gets XORed.
    • xored = "": Creates an empty string to store the encrypted result.
    • The Loop:
      • for i in range(0, len(flag)):: This loop goes through each character of the fake flag.
      • xored += chr(ord(flag[i]) ^ ord(key[i%len(key)])): This is the XOR operation!
        • ord(flag[i]): Gets the numerical (ASCII) value of the current character in the fake flag.
        • ord(key[i%len(key)]): Gets the numerical value of the current character in the key. The i % len(key) part means that the key is repeated if it's shorter than the flag. For example, if the key is "ABC" and the flag is "DEFGH", the key is effectively used as "ABCAB".
        • ^: This is the XOR operator! It XORs the ASCII value of the flag character with the ASCII value of the key character.
        • chr(...): Converts the resulting numerical value back into a character.
        • xored += ...: Adds the XORed character to the xored string.
    • hex_encoded = xored.encode().hex(): Converts the XORed string into a hexadecimal representation (a string of numbers and letters A-F). This is a common way to represent binary data as text.
    • return hex_encoded: Returns the hex-encoded, XORed string.
  • start(server): This is the main logic that runs when you connect to the server.

    • res = ''.join(random.choices(string.ascii_letters + string.digits, k=5)): Generates a random 5-character key (e.g., "aB3xZ").
    • key = str(res): stores the string representation of the generated key.
    • hex_encoded = setup(server, key): Calls the setup function to XOR the fake flag with the random key and get the hexadecimal result.
    • send_message(...): Sends the hex-encoded string to the client (you). This is the "ciphertext" you see when you connect.
    • send_message(...): Asks you for the encryption key.
    • key_answer = server.recv(4096).decode().strip(): Receives your answer, converts it from bytes to a string, and removes any extra whitespace.
    • The try...except block:
      • if key_answer == key:: Checks if your answer is exactly the same as the randomly generated key.
        • If it's correct, it sends you the real flag ("Flag 2").
      • else:: If your answer is wrong, it sends you a "Close but no cigar" message.
      • except:: If there's any error (like you send invalid data), it sends an error message.
  • RequestHandler and if __name__ == '__main__':: This part sets up the server to listen for connections on port 1337. You don't need to understand the details of this for solving the challenge, but it's the code that makes the server run.

The Solution Script

solve.py
from pwn import xor

def get_key(ciphertext_hex):
ciphertext = bytes.fromhex(ciphertext_hex)
fake_flag_prefix = b'THM{'
fake_flag_suffix = b'}'

# Get the first four key characters using the known prefix 'THM{'
key_part = xor(ciphertext[:4], fake_flag_prefix)

# Get the fifth key character using the last byte of the ciphertext and the known suffix '}'
fifth_key_char = xor(ciphertext[-1:], fake_flag_suffix)

# Combine to form the full key (assuming the key is 5 characters)
key = key_part + fifth_key_char
return key

def decrypt(ciphertext_hex, key):
ciphertext = bytes.fromhex(ciphertext_hex)
decrypted_bytes = xor(ciphertext, key) # The pwn xor function handles repeating the key
return decrypted_bytes.decode('utf-8', 'ignore') # Decode to UTF-8, handling potential errors

# Example usage with the provided ciphertext
ciphertext_hex = "1505373232702c16273604350e083635791922210023087a232d0103211733390379373335353b3f"
key = get_key(ciphertext_hex)
print(f"Flag 2: {key.decode()} (pass it to the server)") # Decode for printing Flag 2
decrypted_message = decrypt(ciphertext_hex, key)
print("Flag 1:", decrypted_message)
  • from pwn import xor: Imports the xor function from the pwnlib library. This function is very convenient because it handles key repetition automatically.

  • get_key(ciphertext_hex): This function figures out the key.

    • ciphertext = bytes.fromhex(ciphertext_hex): Converts the hexadecimal ciphertext (the string you get from the server) into bytes.
    • fake_flag_prefix = b'THM{': We know the fake flag starts with THM{. This is our "known plaintext".
    • fake_flag_suffix = b'}': We know the fake flag ends with }.
    • key_part = xor(ciphertext[:4], fake_flag_prefix): This is the clever part! It XORs the first four bytes of the ciphertext with the known prefix b'THM{'. Because XOR is reversible, this gives us the first four bytes of the key!
    • fifth_key_char = xor(ciphertext[-1:], fake_flag_suffix): This XORs the last byte of the ciphertext with the known suffix b'}'. This gives us the last byte of the key.
    • key = key_part + fifth_key_char: Combines the two parts to get the full 5-byte key.
    • return key: Returns the key (as a byte string).
  • decrypt(ciphertext_hex, key): This function decrypts the ciphertext using the key.

    • ciphertext = bytes.fromhex(ciphertext_hex): Converts the hex ciphertext to bytes.
    • decrypted_bytes = xor(ciphertext, key): Uses pwnlib's xor function to decrypt. The xor function automatically repeats the key as needed.
    • return decrypted_bytes.decode('utf-8', 'ignore'): Converts the decrypted bytes back into a string (using UTF-8 encoding) and ignores any decoding errors.

The Attack

export TARGET_IP=10.10.205.107
nc $TARGET_IP 1337
This XOR encoded text has flag 1: 6c123e4821093b1f5d257d220772254c6e105832793401003054160a5b044a2e0a03244a223c412c
What is the encryption key?
  1. Connect to the Server: You connect to the server using nc $TARGET_IP 1337.
  2. Receive Ciphertext: The server sends you a hexadecimal string (the ciphertext).
  3. Run the Script: You run your solve.py script, providing the ciphertext.
  4. Key Recovery: The script uses the known prefix (THM{) and suffix (}) of the fake flag to XOR parts of the ciphertext and recover the 5-character key.
  5. Decrypt Ciphertext: The script decrypts the ciphertext using the recovered key, revealing the fake flag (this is "Flag 1").
  6. Submit Key: You take the recovered key (printed as "Flag 2" by the script) and send it back to the server.
  7. Get Real Flag: Because you provided the correct key, the server sends you the real flag.

This challenge is a classic example of a "known-plaintext attack" on a simple XOR cipher. Because we know part of the original message (the fake flag's prefix and suffix), we can reverse the XOR operation to find the key. The pwnlib library makes the XOR operation and key repetition very easy.