CTF — AceBear Security Contest 2019

md5Converter

Flipping bits in an AES-CBC IV to rewrite a decrypted filename, then brute-forcing a 2-byte HMAC gap to read an arbitrary file off the server.

WEBCRYPTOAES-CBCIV-MANIPULATIONHMAC

Info

What do we have?

The landing page

We arrive at a landing page that lets us submit a secret.

We submit some data

Submitting the word "test" gets us a page with a link to our secret.

We click the link

Clicking through shows what looks like an MD5 hash of our secret.

Let's solve it

View page source

Viewing the source of the landing page and scrolling to the bottom turns up something interesting - a potential link.

View page source of secret link

Visiting that secret URL and viewing its source is where things get useful - bingo. The full snapshot of the code is included below; it's clearly a Python backend.

Found the flag location

A quick read through the source turns up a helpful comment telling us exactly where the flag lives - in a file called flag that we need to somehow retrieve.

Function to get the flag

This is the function responsible for retrieving files, driven by an AES-encrypted string. There has to be a flaw somewhere in here that gets us to the flag.

Raw create secret

Here's the raw request used to create a secret. Doing so gets us back a URL containing a file and a sign parameter.

Reading the code for clues

Reading through this:

  • Point 1 - a random session ID gets generated using Python's random library. Not something we can realistically attack.
  • That session ID is run through MD5, producing a hash in the form xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
  • The hash gets prefixed with md5_, giving us a filename like md5_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
  • Point 2 - that filename gets concatenated with a | and then an HMAC of the filename itself.

So what we actually need is for the decrypted payload to read flag|<HMAC of flag>. Worth checking next how the AES encryption itself is implemented - standard library, or something custom?

AES encryption function

  • Line 17: the IV is generated randomly.
  • The payload gets encrypted using that IV and a key we don't have visibility into.
  • Line 19: both the IV and the encrypted payload get returned to us.

Bingo - if we control the IV, we can manipulate the decrypted plaintext directly. Let me explain how.

Classic AES CBC diagram

This is the standard diagram for how AES-CBC works - here's the plain-English version. AES is a block cipher: it splits your data into fixed-size blocks and encrypts (or decrypts) block by block. We care about decryption here specifically.

The first block gets decrypted using the key, producing what looks like garbage - that garbage is then XORed with the IV to produce the actual plaintext. Critically, regardless of the key, the plaintext is directly tied to the IV.

We already know the first 4 characters of the plaintext are going to be md5_. If we modify the first 4 bytes of the IV, we can predictably change what those 4 characters decrypt to - enough to turn md5_ into flag, deterministically. We also know the next character should be a pipe (|), followed by the HMAC.

Logic bug

And here's the actual logic flaw - see if you can spot it before I say it. The server splits the decrypted string on the | character: the first part becomes the filename, and the second part is the secret used to generate the HMAC.

So what happens with flag||? The filename becomes flag, and the HMAC secret becomes an empty string. We can compute the HMAC of flag with a blank secret ourselves, since the HMAC generation function is right there in the source we already have - trivial.

The catch: we only control the first 4 characters of the plaintext through IV manipulation. The next 32 characters (the MD5-derived filename portion) are effectively random hex - 0-9a-f, 16 possible values per character. We only need to brute-force 2 of those characters to line things up: 16 × 16 = 256 possibilities. Very reasonable.

Since only the first block is affected by IV manipulation, most of that 32-character hash segment stays exactly as originally generated - I already know its value from the file parameter returned when the secret was created. The only genuinely unknown piece is the small slice landing right at the point where I need the pipe to appear, which is where the 2-character brute force comes in.

I calculated the HMAC for flag with a blank secret ahead of time - it came out to f7101d3ad5cb2622672fb15e079d8db3, using the same Python HMAC logic pulled straight from the leaked source.

Prepare Burp

I wrote a quick Node.js script to generate all 256 possible payloads, then loaded Burp, set the payload position on the file parameter, and fed in our known HMAC. Hit go and hoped for the best.

Flag obtained

It worked through the 256 guesses, and attempt number 92 looked different from the rest. Bingo - that's the flag.

Something extra

Really enjoyed this one - genuinely satisfying to solve.

Need help with security testing?

CONTACT US →