Vending was a Python 3 service seemingly implementing a vending machine. It used a ThreadingHTTPServer to route requests to the corresponding Python code. So, we first have a look at the routing table used by the service.
We see that there are two CREATE commands available; one for adding a vending machine and one for adding a vending item. Since the gameservers needs to store the flag somewhere, we just assumed that the CREATE methods were a good place to start.
Flaw #1: Storing flags in created machines
in particular the
CreateMachineHandler. The handle method of this handler reads a name, an inventor, meta information, a key, and a master key from the request, and invokves
When we follow that function invocation, we find something interesting.
So, basically, this service uses a
memoryview in Python to store information about a vending machine. In particular, the name can be up to 8 bytes, then 8 bytes manufacturer, 112 bytes meta information, 32 bytes key, and finally 128 bytes master key. The weirdly looking construct in
add_new_machine merely packs the information into a 288 byte long representation (lines 11-14). We also see that
get_machine_meta takes three parameters: a vending machine ID, a start parameter, and a stop parameter. We see that this returns information from the whole 288 byte long string. In particular, it only returns information if both parameter
stop are less than the end of the
meta part of a vending machine. Given that we are in Python, though, this already screams for a vulnerability. In particular, lists in Python can be accessed relative to the end of the list, by using a negative index. So, if we want to just get the complete, packed information on the vending machine, we have to call the function with
While this would spit out the flag, it is also noisy as hell :-) So we decided to exploit this in a bit more subtle way. Instead of stealing the complete blob of data, we are only really interested in the flag. Assuming that someone might be analyzing their traffic for flag patterns, stealing this in a single go seems like a bad idea, though. So, we waited for the first traffic to come in and identified that the flag was stored in the
master_key, i.e., offsets 160 to 192. Since we are indexing this from the back, this means the offsets are -128 to -96. Instead of stealing the complete flag in a single attempt, we built the following exploit:
Essentially, we used redis to speed up our attack. Since we are looking to extract information on all the machines that were created between runs of exploits, we just retrieve the previous from redis. Next, we create a new vending machine, for which we get an ID in the response. Then, given our current id and the previous one, we iterate over all the machines in between, first requesting the first 16 bytes of the flag (lines 13-15), and subsequently the next 16 (17-19). Finally, we just print the resulting combination, i.e., the stolen flag.
Flaw #2: Storing flags in vending items
The second way of storing flags was in the vending items. For that, the gameserver would use the
CREATE /vending_item endpoint, which is shown below.
So, basically the data posted by the server is truncated to 32 bytes, passed to
add_new_vending_machine_items together with the second parameter
8. The return value is then is a tuple of something public and something private (as the names suggest). Let’s have a look at that function then.
This code highlights that the second parameter is a partitioning. First, in line 6 and random
vm_guid is computed. In line 8, the original 32 byte passed data is split into 4 parts of 8 bytes each. Then, the code starting from line 9 is a real braintwister (originally, that was even just one line). So let’s tackle this from the inside out, starting with line 17. The list comprehension iterates over all elements in
partitioned_data, putting the index in the list into
i, and the value into
x. The, the left side of line 17 maps that into
i as well as the result of the invocation with of
__generate_rand_key(partitioning-i) for each
i. This is then used the loop above (line 16) as
rnd_key. Those values are used in the invocation of
map in line 13, which applies the function
str to the tuple of
dt, rnd_key, vm_guid, i.e., they are forcefully cast to a string. Finally, these three strings are joined together (line 12) and put to
Well, that was fun to wrap your head around. This code seems to imply that we are generating truely random values that end up in the variable
result. Those values are then used in lines 22 and 23 to store the partitioned flag in the
__keys dictionary. Finally, the
vm_guid and the seemingly secret entries in the dictionary are returned to the gameserver.
Now, however, we need to have a look at
__generate_rand_key, which is called when
results is being generated. It uses the
secrets library in Python 3 to get a number of random bytes; specifically
2**2**ln for the given parameter
ln. If, however, we check again line 17, we find that it is called with
partitioning - i. Since
partitioning is passed and set to 8, and
i is a value between 0 and 3, for the first element, the function will be called with 8, then with 7, then with 6, and finally once with 5. If we, however, do the math here, the flaw jumps out at us: for the first entry, with
i=0, we are trying to get
2**2**8 (in Python actually is
2**256) random bytes. Just a side-note, this is 105,312,291,668,557,186,697,918,027,683,670,432,318,895,095,400,549,111,254,310,977,536 TB worth of data ;-)
This results in an
OverflowError: Python int too large to convert to C ssize_t for
i=0, i=1, i=2. For
2**(2**5) bytes, the result is “just” 4GB. However, since the VM only had 4GB, we get a
MemoryError in that case as well. So, long story short, we will have an exception in that function, which means we will always just end in line 32; and if a function does not return anything in python, its return value is set to
None. So, instead of actually using randomness to calculate the “secret” partitioning keys, we are just concatenating the
dt (which is 0 to 3), the string
None, and the
So, when we know the
vm_guid, we win. Luckily, when we look at how the gameserver stores flags, we find that this actually easy. One example from early in the CTF is as follows (GS means gameserver, VB is the vulnbox’ answer):
GS: CREATE /vending_item RCMX82QQU0CRT5R7OCZ0G0ED544D980=
VB: 81ce53509e4a645d: b2b786aae9734888d2cede2a9e85a35e773d39af5ed4128988a814c64ba5c143: b43d84a05b34e988d01728eb10e9049e8fe73689cc68121dcb448b8f8f44dc59: aa008524f40677cab385a51f80d88866c683d9cf12b22635cffe1cafa3e38763: 98136708198c9a9f55fdfcf1dc544c2838350e58cb163291ea37021196cadc22
GS: CREATE /vending_machine JUjvSuRM PJoGNake 81ce53509e4a645d FfyLyFlVaBZmneAbMIMC NjkPXNfdHPIyUBZpflGL
So, we can clearly see two things: first, the
81ce53509e4a645d) returned from our vulnbox is immediately used by the gameserver to create a vending machine, whose meta data is set to this
vm_guid. Second, if we calculate
sha256('0None81ce53509e4a645d'), the result is
b2b786aae9734888d2cede2a9e85a35e773d39af5ed4128988a814c64ba5c143 (the second element in the colon-separated list). So, once we know the
vm_guid, we can easily retrieve the flags. Since the gameserver regularly used the meta data retrieval feature (without negative indexes, of course), no team could disable this. So, our final exploit looked as follows.
This exploit was our absolute cash cow, stealing a total of 18284 valid flags :-) The first one, which was easy to spot and fix still netted us 6566 valid flags.
For the first flaw, the fix is trivial: just check that
stop are not negative. If so, just return nothing. For the second flaw, a proper fix would have been to just replace the call to
secrets.token_hex with a fixed number so that the Exception would not be thrown. In the CTF, however, we opted to just return the string
secret in the error case. Not that is really secure at all, but is was more than enough to prevent automated exploitation :-)
Really cool service, with one very obvious vulnerability (first attack traffic we saw trying to exploit it was around 10 minutes after around first successfully captured flag) and one very involved one :) I think we also were the first to score on the second flaw (at least nobody stole a flag from us). I must note, though, that we did lose one flag on the service. Stupidly enough, when building the initial exploit, I checked a real storing of a flag (you might have noticed the pattern in the exploits…) and used the complete data from there. When I first tested it against another team, storing must have worked and the flag was later stolen from that team. However, this is perfectly in line with me accidentall leaking one flag in each CTF ;-)