Hacklu CTF 2015 Writeups

During the last two days, the Hacklu CTF 2015 was held. It’s a jeopardy-style CTF and Sebastian joined to have some fun ;) Here’s the writeup of the following challenges:

  • Module Loader (Web, 100)
  • PHP Golf (Coding, 75)
  • Guessthenumber (Coding, 150)
  • Bashful (Web, 200)

First of all I want to say that CTFs are fun. If you haven’t participated in one yet, go to ctftime.org to find a list of upcoming CTFs. During this CTF I teamed up with Denis and Mazen chimed in for bashful.

Module Loader

This was an easy warm-up challenge: A web application which took a $_GET['module'] parameter an then executed the given module. Having a quick look into the sourcecode of the website tells us where the modules are located.

Sceenshot of module loader html sourcecode

The /modules/ folder greets us with a nice directory listing and all available modules:

Sceenshot of module loader html sourcecode

You could even click on the modules and see their full sourcecode, but that didn’t seem to help a lot. So let’s see if this is a local file inclusion and if we can manipulate the path:

Sceenshot of module loader local file inclusion

Okay, that’s cool. Denis came up with the idea of including the .htaccess-file from the document root.

Sceenshot of module loader .htaccess file

The last step was to include the flag.php file in the hidden directory.

Sceenshot of module loader flag

Done :)

PHP Golf

This challenge was pretty cool, because you had to write a php program for the following task and conditions:

Sceenshot of php golf task

I first started out to just implement the functionality without looking at the length of the code. The code did what it should, but it was way too long, so it became obvious that you can’t solve this challenge without regular expressions.

The second version used regular expressions and preg_replace with the e modifier to convert the matches to upper/lower case:

1
<?=preg_replace('/(\w)([^\w]*)(\w)?/e',"strtoupper('$1').'$2'.strtolower('$3')", $argv[1]);?>

However, this version was still too long with about 90 characters. This was when I started to look for ways to replace the long strtoupper / strtolower calls. The solution are so called unicode character properties. I somehow didn’t manage to get them to work with the replacement parameter of preg_match, so I tested them with perl:

1
<?=exec("echo '$argv[1]'|perl -pe 's~(\w)([^\w]*)(\w)?~\U\\1\E\\2\L\\3\E~g'");?>

The trick is, that everything between \U and \E will be converted to it’s uppercase representation. \L will convert to lowercase. Unfortunately, the submission server didn’t offer perl and this version was again too long (~80), but we were allowed to use exec and other commands. My next thought was about using sed. However, this was at around 2 o’clock in the morning and I posted this to our mailing list. I decided to go to sleep and recharge for the next day.

The next morning came and before I was able to continue on the challenge, I received an email from Denis with a working solution:

1
<?=exec("echo $argv[1]|sed -r 's/(\w)(\W*\w?)/\U\\1\L\\2/g'"); 

Exactly 62 characters! Some notes about this:

  • [^\w] is the same as \W
  • <?= is the same as <? echo
  • You can omit the trailing ?> if the code ends with a semicolon.

This solution still had problems with underscores (_) in the input string, but we had luck to get one without these and successfully recovered the flag:

Sceenshot of php golf solution

Done

Guessthenumber

The task of this challenge was to guess 100 numbers in the correct order.

Sceenshot of guessthenumber task

The following hints were given:

I wanted to solve the challenge with python. I googled for a LCG implementation/library and found an example. The next step was to change the parameters to the glibc standards and to write the basic server/client communication code. So far, easy going.

The server always told us his current date and time. (See screenshot above) This had to do something with the initialization format YmdHMS. After extracting the values with a regular expression, I concatenated them into the given format:

1
2
    timedata=str(year)+str(month)+str(day)+str(hour)+str(minute)+str(second)
    seed(int(timedata))

The last important thing was to apply a modulo operation on the generated randon numbers. As both 0 and 99 are in the range of possible numbers, rnd() %100 was used.

Unfortunately, this didn’t solve the challenge as the first guess was always wrong. It turned out that you had to generate 100 numbers and send them in reversed order:

Sceenshot of guessthenumber flag

The full quick & dirty code: Pastebin

Done :)

Bashful

This was the challenge, I’ve spent the most time on, because I was trying way too hard. Mazen joined me on this one. But okay, let’s start slowly. Bashful was a web application, written in pure bash, that could be used to store notes (simple strings).

I’m going to post my final solution right away and write about other possibilites and chances afterwards. I’m still not sure if this solution was the intended one or not, but it worked very well :)

Screen of bashful flag extraction

As you can see, I was able to use the most standard Shellshock payload in the request headers:

1
X-Foo: () { :;}; /bin/bash -c "cat /var/www/flag"

Psst: There was even a XSS by sending a http header with a XSS payload :D

I have to say, that I was kind of dissapointed by this solution. There was so much more fun within the code. While going through it, I came across the following three functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
function explode {
        IFS="$1" read -ra "$2" <<< "$3"
}
function filter_nonalpha {
        echo $(echo $1 | sed 's/[^a-zA-Z0-9.!$;?_]//g')
}
function parse {
        explode '&' 'pairs' "$1"
        for pair in "${pairs[@]}"; do
        explode '=' 'keyval' "$pair"
        export $(filter_nonalpha "${keyval[0]}")="${keyval[1]}"
   done 
}

These functions were later used to parse user input:

1
2
3
if [ -v QUERY_STRING ]; then
    parse "$QUERY_STRING"
fi

The first interesting thing is the sed command in filter_nonalpha, because it replaces all characters except the ones in the square brackets. So our values can contain .!$;? which may be useful in the context of bash. The second interesting thing is the following line from parse:

1
export $(filter_nonalpha "${keyval[0]}")="${keyval[1]}"

Note that only the environment variable’s name is filtered, but not the value. Additionally we can use the parse function to set abitary environment variables. E.g. the query string DEBUG=1 will set the variable $DEBUG to 1.

A bit further down in the sourcecode, we find the following lines:

1
2
3
4
5
if [ -v DEBUG ]; then
    echo -ne '<pre>'
    printenv
    echo -ne '</pre>'
fi

As said above, putting DEBUG= into the url will print us all current environment variables:

Screen of bashful debug information

Knowing that I can set other variables and/or overwrite existing ones, made the following two code block really interesting:

1
2
3
4
5
6
7
8
9
10
11
sessid=$(filter_nonalpha $sessid)
if [ -z $sessid ] || [ "${#sessid}" -lt 60 ]; then 
   echo 'like... really?'
   exit
fi
sessfile=$SESSION_DIR/$sessid
if [ -f $sessfile ]; then
    explode '#' 'messages' "$(cat $sessfile)"
else
    messages=()
fi

Controlling $SESSION_DIR and $sessid to set an abitary file path as the $sessfile variable and reading the content from it, sounded like a nice way to get the flag. Long story short: Setting $SESSION_DIR wasn’t the problem, but rather the length check of the $sessid variable. I was able to bypass it with $IFS$IFS...$IFS, but that failed the file existance check ([ -f $sessfile ]).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if [ ! -v page ]; then
    page=home
else
    page=$(filter_nonalpha "$page")
fi
if [[ "$page" == "index" ]]; then
    page=home
fi
file="$DOCUMENT_ROOT/$page.sh"
if [ ! -f $file ]; then
    >&2 echo "Can't load $file"
    file="$DOCUMENT_ROOT/404.sh"
fi
source $file

This seemed even more interesting, because that may directly lead to a remote code execution. Again, we should be able to control/set the values of $DOCUMENT_ROOT and $page. The combination of both would then be sourced (= executed). We only need to put our commands into a file with the ending .sh in the document root or somewhere else and change it.

I could think of two ways to trigger the RCE:

The first one was to set $SESSION_DIR=/var/www/ and $sessid=aaaa...aaa.sh (60+ times a and .sh). This should set $sessfile=/var/www/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.sh. Saving a note with these parameters in the url should create the $sessfile. Unfortunately, this didn’t work due to missing write permissions. The only thing the server did was returning a 500 error. :(

The other idea was to set $SESSION_DIR=/var/sessions and $sessid as above, to create a session file with .sh at the end. The second step would consist of setting DOCUMENT_ROOT=/var/sessions and $page=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, but this failed with a 500 error again.

Maybe there’s some other bash magic that can be used to modify the parameters and trigger the RCE. I’m quite disappointed that I couldn’t get this to work. However, it’s funny to see that the solution was really easy and I just somehow tried too hard to find another way.

Other challenges

I had a look at other challenges and can only post some ideas that I had. Probably all in the wrong direction and senseless:

Grading-Board (Web):

  • Some kind of SQL Injection
  • Probably need to use the grant options to give other people access to your own table to bypass the request limit
  • No time to test this

Dr.Bob (Forensic):

  • Mounting .vdi image with qemu-nbd
  • LVM volume, but encrypted and password unknown
  • Use VirtualBox to start the saved state, but no password for users
  • Try/Use volatility to extract some (useful) information
  • Boot disk and use init=/bin/bash rw kernel parameters to drop into root-shell. Look for suspicious/useful files. No luck :(
  • Edit saved-state with hexeditor to change /etc/passwd contents. This didn’t come into my mind at 5 o’clock :(

Teacher’s Pinboard (Web):

  • Bottom of pickle.js says that splice/slice are mixed up and need to be fixed
  • Pickle is some kind of encoding. Information from the cookie accountinfo will be decoded and used
  • Idea: Save the single-page app and fix pickle.js. Hope that this helps to extract some information/flag from the ‘default’ notes.
  • No time to test this

Future CTFs

I think, I’ll participate more often in jeopardy-style CTFs if my spare time allows it. It’s really really fun and lets you discover new stuff/get fresh ideas. Let me know if you want to team up for a particular challenge/CTF

Hope that helps, Sebastian