Flagsrv was a 300 point web challenge in this year’s BSidesSF CTF. The description was a simple one:

We’ve built a service for the sole purpose of serving up flags!

The account you want is named ‘flag’.

Background

This challenge was actually the result of a conversation with a coworker less than 48 hours before the CTF. We were discussing the different types of multi-factor authentication and their merits, and the importance of strong sources of entropy for many of these mechanisms. I joked about a deterministic method of generating keys, then decided to build a level around it.

Exploring

So, what does the application do? Well, when you first visited it, you were given a fairly generic-looking login page, with an option to register that was also pretty generic.

Register Screen

Okay, let’s register an account and we get a page to store our flag. Well, great, but I want the flag from the user named flag, not whoever I just register. I already have my own flag, I want the one for the challenge!

You could try tampering with the session cookie to try to get a session for the flag user, but I’m pretty sure I implemented it correctly.

There was an option to enable two factor authentication using the widely supported Time-based One Time Passwords (TOTP). When this was done, a QR code compatible with many authentication applications was displayed along with the raw Base32-encoded seed.

2FA Screen

Vulnerability/Exploitation

If you examine the 2FA registration page, you’ll note that the secrets are generated entirely in client-side javascript. If you spend a bit of time doing static analysis, or even just set a breakpoint on the retrieveKey function, you’ll notice the seed is entirely based on a single input:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  var retrieveKey = async function(u) {
    var buf = new ArrayBuffer(u.length);
    var v = new Uint8Array(buf);
    for (var i=0; i<u.length; i++) {
      v[i] = u.charCodeAt(i);
    }
    var k = await crypto.subtle.importKey(
      'raw',
      buf,
      {name: 'HMAC', hash: 'SHA-256', length: u.length*8},
      false,
      ['sign', 'verify']);
    var k2 = await crypto.subtle.sign(
      'HMAC',
      k,
      buf);
    return base32encode(k2);
  };

This function has the effect of performing Base32(HMAC-SHA256(u, u)), and it turns out if you examine its callsite, the only parameter passed in the username. Given this, you can construct a secret for any username you’d like.

However, there’s still no password for the flag user. However, if we log in with the user we registered and enabled 2FA for, we’ll notice the following in the 2FA verification page:

1
2
3
<input type="hidden" name="username" value="foo" />
<input type="number" id="passcode" min="0" max="999999"
    class="form-control" name="passcode" placeholder="000000">

It turns out that if we change the username in the hidden field and use the correpsonding TOTP seed/value, it will log us in as that user, and not the user from the first stage of the login! Consequently, supplying flag as the value for the hidden field along with a TOTP derived from the username-based HMAC key will log in as the flag user and give access to the (read-only) game flag.