Starting point

We start the challenge with an address — http://lg.hackable.software:8080, and a hint:

“We found some shady looking glass service. Pretty sure there is a way to get that delicious /flag”

We can guess that our goal is to find whatever is in /flag on lg.hackable.software.

Let’s open http://lg.hackable.software:8080. After exploring the website for a bit, we can see that there are two features:

Sending ping requests Sending traceroute requests

Inspecting the server

The next thing is to look into the website’s code, which is implemented in GO (a great language to learn on the way).

The first thing that came to mind is to inspect the server’s implementation for sending the requests:

The server’s command execute method

The server simply runs shell commands! This seems like a classic opportunity for a shell injection! Sadly, after a bit of trial and error I realized the server validates its input and prevents “malicious” commands:

The server’s validation method

What’s now? I tried different approaches and searched for common GO vulnerabilities without any success. After a bit more looking around, I found another interesting part in the server’s code:

More validation code in the server

Looks like the server implements command caching using MD5! The flow we see here is as follows:

The server checks if the current request is in its cache (by its md5)

If the MD5 is found — the server performs the request

Only if it isn’t in the cache it validates the request

If the request is valid the server performs it and adds it to the cache

Playing around with MD5

What can we do with this? We can craft 2 requests with the same MD5, the first will be valid and the second will be the shell injection. After sending the first one, the server will cache the MD5 and we’ll be able to send the second. We may have a chance for a shell injection after all!

Great, but how can we make an MD5 collision? After a bit of googling, I understood that chosen-prefix md5 collision is very plausible:

“Stevens et. al’s showed that, with an approximately 2 ^ 39 calls to the MD5 compression function, it is possible, for any chosen m1 and m2, to construct s1 and s2 such that D5(m1||s1)=MD5(m2||s2).”

I highly recommend going into details here.

The next step is to find a tool that will generate the collision. The one I chose is Unicoll — A fast chosen-prefix md5 collision tool:

This seems great, but we actually can’t write arbitrary data in the request — because the server serializes our input on send and fails when receiving malformed request. Chosen prefix md5 collision starts to look less promising…

Or is it?

The server uses Protobuf for the serialization (a good read about this can be found here). let’s understand the structure of the data, and then recreate it using python. Each argument sent is constructed of these parts:

Identifier, which also indicated its type (e.g. 2A is the domain, string)

The data length

The data itself

This doesn’t look very good for us, since the strict structure of the requests leaves little room for “collision making”. Having no other leads, I started fuzzing the server with different requests, which mostly replied with “500 denied” . Suddenly I noticed something strange — if I send the same field more then once, only the last value will be used!

This is great because we can now send arbitrary data alongside the request and it will be ignored (classic for MD5 collisions!).

At this point we pretty much finished the “web part” of the challenge, let’s get to some crypto fun!

Creating colliding requests

First, an important note about MD5 (you’re welcome to read more about it in wikipedia) — Because MD5 works in blocks, if A and B has the same MD5 (MD5(a) == MD5(b)), then we can append any postfix and the two MD5s will stay equal (MD5 (a | c) == MD5 (b | c)). You’ll understand why this is important in a second.

Now, we can start planning our MD5 collision. Note that this is only my implementation of the solution, and other alternatives exists (e.g. different field sizes).

A valid request has this structure:

(prefix) (2A [length] [arbitrary data]) (2A [length] [valid domain])

Prefix — information about the request which we don’t control:

Counter IPv4 Ping/traceroute

Our “malicious” command — normal ID (2A) and length fields, following arbitrary data for our pleasure

A valid command — which will be actually executed

Using Unicoll, we can find a chosen prefix collision of the following structure:

1. [prefix] 2A 100 [60 random bytes]

2. [prefix] 2A 80 [60 random bytes]

Now, we can add to a suffix to the two “requests” we found (while keeping the MD5s valid). We’ll add the following data:

[random 20 bytes] 2A 20 ;cat /flag;# 2A 00 2A 00 2A 00 2A 00

Our final commands are:

Prefix 2A 100 [60 random bytes] [random 20 bytes] 2A 20 ;cat /flag;# 2A 00 2A 00 2A 00 2A 00

Prefix 2A 80 [60 random bytes] [(same) random 20 bytes] 2A 20 ;cat /flag;# 2A 00 2A 00 2A 00 2A 00

Let’s think about what we’ve accomplished

In the first (legitimate) command, we specified that the first value’s length is 100. So, the first value is:

[60 random bytes] [random 20 bytes] 2A 20 ;cat /flag;# 2A 00 2A 00 2A 00

What’s left is — 2A 00: an empty (yet legal) command, which the server will cache. As we said earlier, the first value is simply ignored when there’s another value.

In the second command, the first value is smaller (80 bytes long):

[60 random bytes] [random 20 bytes]

Now, the second (and not ignored) value will be:

2A 20 ;cat /flag;# 2A 00 2A 00 2A 00