Tags: cbc aes rce python
Rating:
The goal of this challenge is to break AES CBC encryption to get RCE and pwn the server.
When you connect to the server, it prompts you for 4 options:
1. Run a script. After selecting this option, it prompts you again to upload your base64 encoded, AES CBC encrypted python script
2. Get the server source code. This is not the entire server code, you can't see the socket implementation. But you can see the implementation of the AES CBC encryption (including the IV which is 0) and the example script source code. The AES key has been redacted.
3. Get an example script. Returns a constant base64 encoded ciphertext which can be sent to the run script prompt.
4. Exit
The server readily gives you a valid ciphertext/plaintext pair, the encryption method used, and the IV for the CBC. It is probably infeasible to break the AES key with just this, but the contents of the script are a good stepping stone for breaking the key.
```
exit()
#AAAAAAAAA
#AAAAAAAAAAAAAAAA
a = r"0000000000000000000000000000"
if a!=r"0000000000000000000000000000":
testok = True
if testok:
result = b"See? " + KEY[:-4]
#reset 4 security
result = b"nope"
```
A few things I noticed here:
* The first line contains an `exit()` which prevents the rest of the script from running. This might be difficult to remove since it would require modifying the first block.
* Long strings of identical characters, probably a good scratch pad for bit manipulation.
* All but the last 4 digits of the secret key are saved in the `result` variable.
* The key is only saved in the result variable if the variable `a` does not equal `"000..."`. This could be easy to break if it weren't for the fact that half of all bytes are not valid ascii.
* The `result` variable is reset at the end, but this could probably be omitted by removing the last couble blocks of ciphertext.
At this point I decided to try the example script. The server spit out the decrypted script then closed the session (the `exit()` was executed).
Next step I took was to remove the `result = b"nope"` from the end of the script. To do this I split the ciphertext and plaintext into their 16 byte blocks. This is what the plaintext looks like in those 16 byte blocks:
```
\nexit()\n#AAAAAAA
AA\n#AAAAAAAAAAAA
AAAA\na = r"00000
0000000000000000
0000000"\nif a!=r
"000000000000000
0000000000000":\n
testok = Tru
e\nif testok:\n
result = b"See?
" + KEY[:-4]\n#r
eset 4 security\n
result = b"nope"
\n
```
Removing the last 2 blocks of ciphertext would be sufficient to remove the `result` variable reset, I think I removed the last 3 which trims off most of the last comment.
Base64 encoding this ciphertext and uploading it to the server gave the expected, the program still exited early, but the last bit of the script was trimmed off.
To continue from here, I thought I would need to find the first block of ciphertext that would give a full line of valid python code, which I thought would take ages to brute force. But it turn out that the server actually somewhat ignores invalid bytes, as shown in this line:
```
dec = decrypted.decode("utf-8", "replace")
```
This replaces invalid `utf-8` byte sequences with the character `\ufffd`, which probably does not make for valid Python code but might be ignored in a comment.
Now I have a way to break the first block, just find any block of ciphertext that produces a `#` followed by a sequence of bytes that doesn't contain `\n`. This will take a bit more than 256 tries on average. But if I use any block of ciphertext for the first block then the next block will be garbled because CBC xors the first block's ciphertext with the next block's AES decryption to produce the final plaintext. So I need to find the first block of ciphertext that does not significantly modify the next block's plaintext.
I opted to only modify the last 8 bytes of the first block of the ciphertext, and this is how I did it without destroying the next line. The last 8 bytes of the second line of plaintext is `AAAAAAAA`. So I decided to convert this into any random sequence of 8 capital letters. To do this, ex for the random sequence `GGGGGGGG`, I xored the last 8 bytes of the first block of ciphertext with `GGGGGGGG ^ AAAAAAAA`. Then when the second block's AES decrypted text is xored with the first block of ciphertext, the result is `AA\n#AAAAGGGGGGGG`. In hindsight I probably could have chosen almost any random sequence of 8 bytes as long as it didn't produce a newline in the next block of ciphertext, but this method worked.
The last step of exposing the `result` variable was to make the inequality check `if a!=r"000...` true. For this I used a similar method by attacking the fourth block, the long sequence of 0s. I modified the first few bytes so that the next block would not break.
Finally I had a ciphertext that when uploaded to the server revealed the first 12 characters of the 16 character key. The characters were all hexidecimal digits, so I was pretty confident that the last 4 characters would be as well. I borrowed the Python encryption code from the server and set out to find the full key by appending all combinations of 4 hexidecimal digits to the exposed key, then checking if the encrypted original script matched the original ciphertext. This took only 1 or 2 seconds to crack.
Once I had discovered the full AES key I could then craft any Python script I want and get full RCE. To find the flag I made a subprocess call to `find / -name '*flag'` and found the flag at `/flag`.