Post

TryHackMe: W1seGuy

W1seGuy was a simple room, where we use known plaintext attack to discover a XOR key and use it to get the flags.

Tryhackme Room Link

Examining the Source Code

At the start of the room, we are given the source code for the application running on port 1337.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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()

Looking at the source code, at the start it binds to port 1337 on all interfaces and sets the RequestHandler class to handle all incoming requests.

1
2
3
4
if __name__ == '__main__':
    socketserver.ThreadingTCPServer.allow_reuse_address = True
    server = socketserver.ThreadingTCPServer(('0.0.0.0', 1337), RequestHandler)
    server.serve_forever()

RequestHandler class will simply call the start function with the request upon receiving one.

1
2
3
class RequestHandler(socketserver.BaseRequestHandler):
    def handle(self):
        start(self.request)

The start function will first generate a random key of length 5 using the concatenation of string.ascii_letters (abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ) and string.digits (0123456789) as the character set.

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

After that, it will call the setup function with the generated key.

1
hex_encoded = setup(server, key)

The setup function will take the key and use it to XOR encrypt the first flag by iterating over all characters of the flag and XOR‘ing it with key. After that, it will hex encode the result and return it.

1
2
3
4
5
6
7
8
9
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

It also uses the modulo operator to index the character of the key to use, since due to the key being shorter, it will need to cycle through the key like this:

\[Flag = F, Key = K, Encrypted flag = E\]
  • \(F_{1} \oplus K_{1} = E_{1}\)
  • \(F_{2} \oplus K_{2} = E_{2}\)
  • \(F_{3} \oplus K_{3} = E_{3}\)
  • \(F_{4} \oplus K_{4} = E_{4}\)
  • \(F_{5} \oplus K_{5} = E_{5}\)
  • \(F_{6} \oplus K_{1} = E_{6}\)
  • \(F_{7} \oplus K_{2} = E_{7}\)
  • \(...\)

Now, back to the start function. First, it will print the XOR encrypted and hex encoded flag returned from the setup function.

1
send_message(server, "This XOR encoded text has flag 1: " + hex_encoded + "\n")

After that, it will ask for the encryption key and read our answer. If the key we answered matches the key randomly generated, it will print the second flag read from flag.txt.

1
2
3
4
5
6
7
8
9
10
11
12
13
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()

Recovering the Key

Connecting the machine on port 1337 using nc, we get the encrypted flag as expected.

1
2
3
$ nc 10.10.51.17 1337
This XOR encoded text has flag 1: 1d037d3c32782a5c29360c334406363d7f532c2108254274232507492f173b3f4977373b337f353f
What is the encryption key?

After examining the source code, we get to know a couple of details that will help us recover the key and decrypt the encrypted flag.

First, the XOR key has a length of 5.

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

Second, the flag has the format of THM{...}.

1
flag = 'THM{thisisafakeflag}' 

One thing to note about XOR is that:

If \(A \oplus B = C\), then:

  • \(A \oplus C = B\)
  • \(B \oplus C = A\)

Knowing this, we can apply it to our current case to recover the first 4 characters of the key, since we know the first 4 characters of the flag ('THM{') and the encrypted flag.

\[Flag = F, Key = K, Encrypted flag = E\]

Since \(F_{1} \oplus K_{1} = E_{1}\) then \(F_{1} \oplus E_{1} = K_{1}\) will also be true, and we know the first character of the flag (\(F_{1}\)) and the first character of the encrypted flag (\(E_{1}\)), we can recover the first character of the key (\(K_{1}\)) with:

  • \(F_{1}\) ('T') \(\oplus\) \(E_{1}\) (0x1d) = \(K_{1}\) ('I')

We can also apply this to the next 3 characters to recover the first 4 characters of the key.

  • Since \(F_{2} \oplus K_{2} = E_{2}\), then \(F_{2} \oplus E_{2} = K_{2}\) ('H' \(\oplus\) 0x03 = 'K')
  • Since \(F_{3} \oplus K_{3} = E_{3}\), then \(F_{3} \oplus E_{3} = K_{3}\) ('M' \(\oplus\) 0x7d = '0')
  • Since \(F_{4} \oplus K_{4} = E_{4}\), then \(F_{4} \oplus E_{4} = K_{4}\) ('{' \(\oplus\) 0x3c = 'G')

Using Python to do this, we get the first 4 characters of the key by XOR‘ing the first 4 characters of the flag with the first 4 characters of the encrypted flag.

1
2
3
4
>>> from pwn import xor
>>> encrypted_flag = bytes.fromhex('1d037d3c32782a5c29360c334406363d7f532c2108254274232507492f173b3f4977373b337f353f')
>>> xor(encrypted_flag[:4], b"THM{")
b'IK0G'

This leaves us with not knowing only the last and fifth character of the key.

At this point, we can simply notice the flag has a length of 40, which is a multiple of 5.

1
2
>>> len(encrypted_flag)
40

This means the last character of the flag will be XOR‘ed with the fifth character of the key.

  • \(F_{1} \oplus K_{1} = E_{1}\)
  • \(F_{2} \oplus K_{2} = E_{2}\)
  • \(F_{3} \oplus K_{3} = E_{3}\)
  • \(F_{4} \oplus K_{4} = E_{4}\)
  • \(F_{5} \oplus K_{5} = E_{5}\)
  • \(F_{6} \oplus K_{1} = E_{6}\)
  • \(...\)
  • \(F_{40} \oplus K_{5} = E_{40}\)

We also know the last character of the flag ('}') and the last character of the encrypted flag (0x3f), using this we can recover the last character of the key same way as before.

  • Since \(F_{40} \oplus K_{5} = E_{40}\), then \(F_{40} \oplus E_{40} = K_{5}\) ('}' \(\oplus\) 0x3f = 'B')
1
2
>>> xor(encrypted_flag[-1], b"}")
b'B'

Flag 1

Now that we know our key is IK0GB, we can use it to decrypt the flag.

1
2
>>> xor(encrypted_flag, b"IK0GB")
b'THM{...}'

Flag 2

Also, answering the question from the server with the key we recovered, we receive the second flag.

1
2
What is the encryption key? IK0GB
Congrats! That is the correct key! Here is flag 2: THM{...}

Extra

If the flag had a length that was not a multiple of the key length or we didn’t know the last character of the flag, we could also use a brute forcing attack to recover the last character of the key with a script like this:

1
2
3
4
5
6
7
8
from pwn import xor
import string

encrypted_flag = bytes.fromhex("1d037d3c32782a5c29360c334406363d7f532c2108254274232507492f173b3f4977373b337f353f")
key_start = xor(b"THM{", encrypted_flag[:4])
for i in string.ascii_letters + string.digits:
	key = key_start + i.encode()
	print(f"{key} : {xor(encrypted_flag, key)}")
1
2
3
4
5
6
7
8
$ python3 xor_brute.py
...
b'IK0Gz' : b'THM{H1alnLExtALt4ck[Anr3YlLyhmrty0MrxOrE'
b'IK0GA' : b'THM{s1alnwExtAwt4ck`Anr3blLyhVrty0vrxOr~'
b'IK0GB' : b'THM{...}'
b'IK0GC' : b'THM{q1alnuExtAut4ckbAnr3`lLyhTrty0trxOr|'
b'IK0GD' : b'THM{v1alnrExtArt4ckeAnr3glLyhSrty0srxOr{'
...
This post is licensed under CC BY 4.0 by the author.