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 A | Input B | Output (A XOR B) |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
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
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 namedflag.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. Thei % 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 thexored
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.
- It sets
-
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 thesetup
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
andif __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
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 thexor
function from thepwnlib
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 withTHM{
. 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 prefixb'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 suffixb'}'
. 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)
: Usespwnlib
'sxor
function to decrypt. Thexor
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?
- Connect to the Server: You connect to the server using
nc $TARGET_IP 1337
. - Receive Ciphertext: The server sends you a hexadecimal string (the ciphertext).
- Run the Script: You run your
solve.py
script, providing the ciphertext. - 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. - Decrypt Ciphertext: The script decrypts the ciphertext using the recovered key, revealing the fake flag (this is "Flag 1").
- Submit Key: You take the recovered key (printed as "Flag 2" by the script) and send it back to the server.
- 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.