troy@home:~$

Brute-forcing XOR encryption: tryhackme.com W1seGuy

Introduction

Today let’s take a look at the W1seGuy tryhackme.com room. This is a newly-released room that provides an easy example of brute-forcing XOR encryption when part of the cleartext is known. I really enjoyed this one so I decided to do a writeup. This writeup does not provide the answers to the room, but will demonstrate one way to get them using some simple python. Let’s get started.

Task 1

The room has two tasks. Task 1 presents us with a download for some source code. Download the code and confirm you’ve done so by hitting the button.

We’ll come back to the code in a second. First, let’s open Task 2 and have a look.

Task 2

After starting up the VM. open an Attack Box or connect to tryhackme via VPN on your own box. I’m going to do the latter. The task states that there is a server listening on port 1337 and that we can connect to it with netcat. We also see that we will need to obtain two flags.

Let’s open a terminal and connect to the machine with netcat. After doing so, we are presented with an XOR-encrypted flag and asked for the encryption key:

Here I enter a random guess and am presented with the following:

Now that we’ve observed the general behavior of the server, let’s take a look at the source code to see what’s going on under the hood. Open the python code in the text editor of your choice. Doing so, we see the following (truncated):

We only need to concern ourselves with certain parts of this code, as much of it is for running the server. We are only interested in that part of the code that performs the XOR encryption. On lines 23 and 24, we see the key being generated. Line 23 gives us two very important pieces of information about the key - the keyspace and the key length. The keyspace consists of the variables ascii_letters and digits from the string library. Let’s confirm their values using python:

We see that ascii_letters contains all of the uppercase and lowercase letters while digits contains the 10 digits. Returning to the code, we also see the k=5 argument provided to the random.choices() function. So the code is randomly selecting 5 characters from the defined keyspace. Although the key itself is randomly chosen each time the server starts, the key length remains constant at 5.

Now take a look at the setup() function starting on line 12, which shows us how the flag is encoded. Note that the code provided in the download is not the exact code running the server and that the flag shown here ‘THM{thisisafakeflag}’ is not the real flag. Take a look at lines 16 and 17. This is where the encryption occurs. For each character in the flag the ord() function is applied, which returns the unicode code of that character. Then the ord() function is applied to the respective key character. These are then XOR’d using the ^ operator and finally the result is converted back to a character. Once the entire flag in encrypted, the result is converted to hex before being sent to the user.

Although we don’t know what the flag is, we are in luck because we do know part of the flag. Tryhackme flags are almost always of the format THM{sometext} and this room is no exception. We can confirm this in the screenshot of Task 2, where the placeholder for the flags are of the format ***{********************************************}. So we know that the flag starts with THM{ and ends with }. This works out in our favor because there are five known characters and we’ve already determined that our key length is also five. Also note that the ciphertext (in hex) is 80 characters long, which means that the ciphertext when converted back to utf-8 is 40 characters long. Since 40 is a multiple of our key length, the trailing } is in the perfect position to brute-force the fifth character in our key.

Now that we have all of the information we need to brute-force the encryption key, let’s write a python script to do it. We’ll begin with some boilerplate code that imports the random and string libraries. First, we’ll get the ciphertext from the user and convert it from hex back to utf-8. Then we’ll define the keyspace and create a placeholder variable for the key.

Next we will write a function that will brute force a single key character given the keyspace, the position, and the known cleartext character. The brute_single() function loops through each value of the keyspace, XORs it with the respective ciphertext character and checks to see if it matches the provided cleartext character. For example, in the first position, where the cleartext character is known to be ‘T’, the function will XOR each character in the keyspace with the first ciphertext character until it gets a ‘T’. When it does, the key character is then known and the function will return it.

Next, we add five calls to the function, supplying the keyspace, position, and known cleartext character. Then we print out the key:

Let’s now test out our script so far. I saved the python script to my Desktop as brute_xor.py. Then I open up a terminal to run the script. When prompted, we enter our hex-encoded ciphertext that we got from the server and we get the resulting key of ‘f1m30’. Note that you will get a different key, as the key is randomized every time the server runs.

Now that we have the key, we can use it to decrypt the flag. Here we write a new function called decrypt_xor(). This function takes the ciphertext and the key. First it creates a placeholder variable for the cleartext. Then it loops through every character of the ciphertext and XORs that character with the respective key character, recycling the key each time we reach the end. It then returns the cleartext. Then we add some code to call the decrypt_xor() function, passing in our ciphertext and the key we found. Finally, we print the cleartext to the screen. This is now the final version of our script:

Next we run the script again, passing in the ciphertext from the server. Now in addition to providing the key, the script also prints the decrypted flag (redacted), which we can provide as our answer to the first question:

Last of all, let’s return to the server and provide the XOR key we discovered. Doing so should yield the second flag (redacted):

Conclusion

In this room we learned how to brute force an XOR-encrypted message where both the key length and a piece of the cleartext were known. I hope that you learned as much as I did while creating this post. Cheers.