W1seGuy

A w1se guy 0nce said, the answer is usually as plain as day. - by hadrian3689 and DrGonz0


Analysis

The challenge is a cryptographic task that needs to be solved. We are provided with the source and the open port 1337 of the machine that sets the task.

In the source, we see that it is an XOR encryption. Here, the numerical ASCII value of each i-th letter of the flag is xored with the i-th letter of the key. We see that the key has a length of 5, and if the text is longer than the key, the key is used again.

Our task is now to correctly derive the key used and reconstruct the plaintext flag from a given ciphertext.

With the knowledge of the plaintext and the ciphertext, we can reconstruct the key by 'xoring' the values of the encrypted text with the plaintext.

In short:

where C = Ciphertext, P = Plaintext and K = Key

If we assume that the plaintext starts with THM{, we already have the possibility of determining the first 4 characters of the key.

Now we can either determine the 5th character of the key offline using brute force or make the assumption that the plaintext is the same length and that the key was inserted exactly x times, so that the last letter of the key was encrypted with the last letter of the key. We assume that the last encrypted plaintext is the letter }.

Below is the source that is also provided in the challenge:

source.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()

As already mentioned, we can see here the xoring of the key with the plaintext flag.

    flag = 'THM{thisisafakeflag}' 
    xored = ""

    for i in range(0,len(flag)):
        xored += chr(ord(flag[i]) ^ ord(key[i%len(key)]))

The key length is 5 and is constructed randomly from a set of digits and ASCII letters.

    res = ''.join(random.choices(string.ascii_letters + string.digits, k=5))
    key = str(res)

We use the following script to implement our previous findings. We reconstruct the key using the first four letters and the last letter of the flag by xoring the ciphertext. We then use the reconstructed key to decrypt the ciphertext.

Decryption Script

wise.py
import argparse

def derive_key_part(hex_encoded, known_plaintext, start_index):
    encrypted_bytes = bytes.fromhex(hex_encoded)
    derived_key = ""
    
    for i in range(len(known_plaintext)):
        derived_key += chr(encrypted_bytes[start_index + i] ^ ord(known_plaintext[i]))
    
    return derived_key

def xor_decrypt(hex_encoded, key):
    encrypted_bytes = bytes.fromhex(hex_encoded)
    decrypted_message = ""
    
    for i in range(len(encrypted_bytes)):
        decrypted_message += chr(encrypted_bytes[i] ^ ord(key[i % len(key)]))
    
    return decrypted_message

def main():
    parser = argparse.ArgumentParser(description='W1seGuy XOR Decryption')
    parser.add_argument('hex_encoded', type=str, help='Hex encoded string to decrypt')
    #parser.add_argument('key_length', type=int, help='Length of the encryption key')

    args = parser.parse_args()
    hex_encoded = args.hex_encoded
    key_length = 5

    # Example usage
    known_start_plaintext = 'THM{'
    known_end_plaintext = '}'

    # Derive the first part of the key using the known starting plaintext
    derived_key_start = derive_key_part(hex_encoded, known_start_plaintext, 0)
    print("Derived start of the key:", derived_key_start)

    # Derive the last character of the key using the known ending plaintext
    derived_key_end = derive_key_part(hex_encoded, known_end_plaintext, len(hex_encoded) // 2 - 1)
    print("Derived end of the key:", derived_key_end)

    # Since the key length is key_length, the derived key will repeat
    derived_key = (derived_key_start + derived_key_end)[0:key_length]
    print("Derived key:", derived_key)

    # Decrypt the full message using the derived key
    decrypted_message = xor_decrypt(hex_encoded, derived_key)
    print("Decrypted message:", decrypted_message)

if __name__ == '__main__':
    main()

First Flag

We get the key and the first flag, and we can now hand this key over to the server.

Second Flag

After passing the derived key, we receive the second flag.

Recommendation

Don't miss out on Jaxafeds more elegant approach using pwntools:

Last updated