Xfactor 2/2

Quick recap

Great! We’ve finally HACKED the first part of the user logon in Xfactor1/2 and we’re now facing a 2FA form.

Me when I absolutely HACK my way into the Hypersecret company

Let’s get started!

The FCSC team gave us a pcap file, let’s see what’s inside.

img

Well alright, I already knew that this capture contained USB-related data, but I’m still lost here; I’m going to need something to make this clearer. Since we’re dealing with an hardware token, I’m guessing that the packets in the capture might be related to U2F stuff. I have found two potential dissectors that should (I hope they will) help me analyse these packets a bit faster :

After using both of them, I decided to continue the challenge using the first one as I just found it easier to work with.

Installing and using a new dissector

To install a dissector, just run these commands:

mkdir -p ~/.local/lib/wireshark/plugins
cd ~/.local/lib/wireshark/plugins
wget https://gist.github.com/woodrow/cb1496975e131e37d5dd716127a250a4

Then either restart Wireshark or just press Ctrl+Caps+L to reload your Lua plugins, then right-click one of the URB_INTERRUPT packets, click on Decode as, then, under the Current column, select CTAPHID.

There we go! Much better (right?!?!?)

img

You might notice a few differences if you actually use the aforementionned dissector and that’s normal; I patched it to show a bit more information, I’ll post it on my gitlab after the FCSC2022 ends! EDIT: Here it is

CTAP…HID? What does that mean?!?!?!?!

CTAP means Client To Authenticator Protocol and HID means Human Interface Device (quick note: your keyboard and your mouse are HIDs, more infos about that here). CTAP is a part of the FIDO2 project and there are 2 different version of CTAP right now :

  • CTAP1/U2F: This is the one that will be used in the challenge
  • CTAP2: Authenticators that use this one are called CTAP2 authenticators, FIDO2 authenticators, or WebAuthn Authenticators (and you surely have heard about WebAuthn before)

If you want more informations about CTAP and such, please click here.

Packet buildin'

Thanks to the dissector, I can now at least see the U2F conversations. img

That sure is a lot of CTAPHID Initialization and CTAPHID Continuation packets! Judging from what’s being said here, an CTAPHID Initialization packet comes first and one or more CTAPHID Continuation packets follow to complete the payload.

U2F Requests and Reponses

This section tells us that there are two ways of encoding messages in U2F, one for the requests and one for the responses.

Requests

Thanks to my best friend (the new dissector), Wireshark now shows the content of the U2F messages, so here’s an example of an U2F request: img

As you can see right there, a request contains these elements:

  • CLA: 1 byte, reserved for the transport protocol (if applicable), set to zero by the host.
  • INS: 1 byte, contains the U2F command code (we’ll talk more about some of them a bit later)
  • P1: 1 byte, first parameter
  • P2: 1 byte, second parameter
  • LC: 3 bytes, length of the request’s data
  • Request data: LC Bytes, the request’s data, omitted if LC is 0
  • LE: 3 bytes, expected length of the response, optionnal if no response if expected

A quick note about the various lengths: as the data is transmitted via CTAPHID, the messages are encoded using Extended length encoding, so LC and LE are 3 bytes long. Using Short encoding would make them fit on 1 byte, but that would reduce the maximal request/response data size to 255 bytes (compared to 65535 byte).

Responses

Here’s a response (that contains data, and that is not always the case): img

As you can see, a responses are a bit simpler:

  • Response data: LE bytes if specified, n bytes if not
  • SW1: 1 byte
  • SW2: 1 byte

SW1 (MSB) and SW2 (LSB) are status word bytes 1 and 2, together they form a 16-bit status word. We’ll just care about SW_NO_ERROR (0x9000) and SW_CONDITIONS_NOT_SATISFIED (0x6985), as they are these are the only status code that appear in this challenge.

That's all folks!

U2F command codes

Chose promise chose due, I will now talk about the various commands code.

U2F_VERSION (0x03)

This command (sent by the host), is used to retrieve the U2F version that is supported by the hardware token. In this case, the token sent back U2F_V2, which means that it supports CTAP1/U2F.

More information about that command here: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#getversion-request-and-response---u2f_version

U2F_AUTHENTICATE (0x02)

This command (also sent by the host), is used to iniate an U2F authentication. Its payload is contained in the following structure:

Shamelessly borrowed from the Fido Alliance's website

To be honest, I only care about the control byte here, and I’ve only seen two specific values used in this challenge:

  • CHECK-ONLY: 0x07, makes the U2F token check whether the provided key handle was originally created by this token, and whether it was created for the provided application parameter
  • ENFORCE-USER-PRESENCE-AND-SIGN: 0x03, asks the U2F token to perform a real signature and respond

As always, you can get more info here: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#u2f-message-framing

Ok but what do I do with this???

Ok sorry I may have gone a bit too far with this U2F mess, and you’re right: reading specs and RFCs is great but it won’t make the flag pop out of the capture. Let’s login on https://x-factor.france-cybersecurity-challenge.fr/login, right click and inspect the Check Token button: it’s just a link that launches the beginAuthen function with a parameter that never changes!

On to that beginAuthen function:

function beginAuthen(keyHandle) {
  $.getJSON(
    "/beginAuthen",
    { keyHandle: keyHandle },
    function(startAuthen) {
      // call U2F API to generate response for the challenge
      u2f.sign(startAuthen.appId, startAuthen.challenge,
        [ { version: startAuthen.version, keyHandle: startAuthen.keyHandle } ],
        function(data) {
          logU2FResponse(logger, data);
          logMsg(logger, "Calling server to finish the authentication...");
          finishAuthen(data);
        }, U2F_TIMEOUT_SEC);
    });
}

By opening the network inspector of my browser’s developer tools, I’ll inspect the requests/responses sent to https://x-factor.france-cybersecurity-challenge.fr/beginAuthen.

Here’s what I’ve got:

[
    {
        "version": "U2F_V2",
        "appId": "https://x-factor.france-cybersecurity-challenge.fr",
        "keyHandle": "MiXECXjEbxAAe7QOH2gsiNiK7bXeuGJnLUGO7kbJutdODZvuqV-T1TPpTVEVIrynmScyNOjQaRAUi0PSH8LUtQ",
        "challenge": "9rlDOo98PIKIiubib97v4IDCJ1FBB2uRUhNgwH89wqw"
    },
    {
        "version": "U2F_V2",
        "appId": "https://x-factor.france-cybersecurity-challenge.fr",
        "keyHandle": "MiXECXjEbxAAe7QOH2gsiNiK7bXeuGJnLUGO7kbJutdODZvuqV-T1TPpTVEVIrynmScyNOjQaRAUi0PSH8LUtQ",
        "challenge": "L8tsCkDErRPzV9SAOlOj2JzFMXAOjmUs7JnimkH9_gI"
    },
    {
        "version": "U2F_V2",
        "appId": "https://x-factor.france-cybersecurity-challenge.fr",
        "keyHandle": "MiXECXjEbxAAe7QOH2gsiNiK7bXeuGJnLUGO7kbJutdODZvuqV-T1TPpTVEVIrynmScyNOjQaRAUi0PSH8LUtQ",
        "challenge": "D5CxgaFPGIQu5fGYPEjo-YA9Dqd6y2PBoWP6p56TpFw"
    }
]
HOLD UP, SOMETHING AINT RIGHT

Only 3 different challenges? But I sent at least 12 requests just to make sure… Alright, let’s try replaying the responses from the capture then!

The fun part

I had almost drained of Google's resources, my determination was beginning to falter and I thought that the only way to get this flag was to code an U2F emulator from scratch... But I found the light; my saviour, César aka MattGorko, appeared on my last googling attempt with his Github repo.

I present to you, U2F-Emulated.

Rare picture of Cesar the GOAT making this first blood possible

Let’s patch this thing

Having an emulator is great an all, but it won’t help much without some modifications. We first need to go back to the pcap and retrieve the values contained in the SW_NO_ERROR responses.

Here is the array of responses (with its associated array of response lengths) that made me get the flag:

size_t respsize[] = {76, 76, 75, 76, 76, 76, 77, 75, 76};
char *responses[] = {
"\x01\x00\x00\x00\x00\x30\x45\x02\x21\x00\xb2\x8f\x52\xe2\xe8\xc1" \
"\x9a\x2c\xe1\xd8\xf6\x93\x7a\x14\x56\xfa\xd2\x78\x5d\x35\xa0\xa2" \
"\xa3\xa2\x0c\x73\x55\x63\x4c\x52\x22\x76\x02\x20\x24\xc1\xb7\x9a" \
"\xb0\x9a\x46\xa7\x75\x1b\x35\x4f\x31\x3f\x24\x71\xcb\x98\xa3\x52" \
"\xd0\xfe\x22\x99\x4d\xdb\x4f\x3d\x01\xff\xd9\x9e",
"\x01\x00\x00\x00\x00\x30\x45\x02\x20\x09\xba\x7e\xb1\xbd\xb2\xaf" \
"\x07\x03\x8e\x60\xc9\x1f\x84\xf4\x09\x62\xf3\xe0\x88\xdb\xba\x3c" \
"\xb3\x4b\x69\x87\xa6\xa4\xc8\xab\x1a\x02\x21\x00\xf5\x73\x8d\xaf" \
"\xa5\xc6\x1a\xd7\xef\xe6\x90\xbf\xe7\x74\x66\x94\x51\xfe\x52\xe1" \
"\x6e\xec\xf0\x6a\xf1\x2a\x6a\x12\xe7\x8e\x84\x51",
"\x01\x00\x00\x00\x00\x30\x44\x02\x20\x67\x40\xe2\x46\x34\xbb\xaf" \
"\x0b\x0a\x54\x5e\x9d\x8d\xa1\xac\x71\x75\xc9\x3c\xc5\xae\xaa\xf2" \
"\x89\xb9\x3b\x8b\xba\x00\x3d\x74\xd8\x02\x20\x7b\x00\x8c\xfc\xbc" \
"\xf5\x16\x78\xd9\x11\x95\x96\x8e\x98\x39\xed\x02\xad\xbc\xc4\xd3" \
"\x47\x03\xb6\xbe\x5a\xe8\x8e\x1e\x97\x0b\xac",
"\x01\x00\x00\x00\x00\x30\x45\x02\x20\x22\xbf\xdb\x52\xc6\x94\x41" \
"\x6b\x2c\x39\xbb\x4f\x33\x51\x26\x53\xe6\x87\x43\x52\xfb\xfb\x51" \
"\x33\x1f\xc0\x24\x2a\x24\x51\xe5\xbb\x02\x21\x00\xaa\x61\x0d\xa5" \
"\x3e\xd8\x09\xb2\x30\x32\x6c\x1b\xa3\x33\xc2\xd5\xd5\xb0\xe6\x05" \
"\x2c\xe1\xc6\x07\xfd\xb7\x5c\x5b\x3d\x2d\x25\x66",
"\x01\x00\x00\x00\x00\x30\x45\x02\x20\x36\xfa\x3a\xc1\x25\x93\xbe" \
"\x5f\x31\xe6\x49\x9f\x24\xdc\xaf\x57\xd3\x8e\x18\x5e\x16\x10\x99" \
"\x6c\x04\xbd\x71\xa5\xd1\x12\x58\x57\x02\x21\x00\xa7\xec\x50\xe8" \
"\xbb\x73\x98\xd3\xe0\x08\x8a\xbf\xab\x26\x53\x69\xa8\x13\x39\x53" \
"\x2b\xc9\x9c\x2e\xc5\x5a\x77\x62\x9d\x8c\xd7\xed",
"\x01\x00\x00\x00\x00\x30\x45\x02\x20\x7b\xea\xf7\x96\xa9\x34\xdf" \
"\xb7\xc7\x13\x06\x94\x7b\x59\x90\x63\xb2\x51\x65\x13\x00\x3f\x34" \
"\x97\xd3\xfc\xa4\x1a\x7f\x38\xa0\xa4\x02\x21\x00\xaf\xcc\x79\x20" \
"\x05\xac\x3c\x60\x1a\x0f\xf3\x9b\x43\xec\xae\x9d\x74\x1c\xde\x39" \
"\x8b\x0b\x70\x48\x0c\xc9\xc4\xa6\x13\x64\xd5\xb6",
"\x01\x00\x00\x00\x00\x30\x46\x02\x21\x00\xf6\x0a\xf8\x4c\xfc\x0f" \
"\x3d\x9f\x33\xae\x8a\xd0\x4b\x61\x7a\xb7\xcf\x78\x2f\x70\xcd\x08" \
"\x3a\x71\xaa\xd7\x38\xb4\xe7\x5d\x51\xc0\x02\x21\x00\xb4\x3a\x62" \
"\x93\x49\xce\xa1\x34\x15\x26\x3b\x86\xa1\xaf\xc6\x37\xcd\x4c\x92" \
"\xdc\x86\x73\xa3\x60\x57\x73\x11\x71\x05\x82\xf9\xda",
"\x01\x00\x00\x00\x00\x30\x44\x02\x20\x50\xac\xfd\x22\xab\x96\xe9" \
"\x7e\xf4\x9d\xe4\xff\x5c\x12\xf1\x4d\xd1\xdb\x99\x3e\x6c\x62\x4a" \
"\x42\xab\x27\xd9\x41\xd6\x80\x8a\x4d\x02\x20\x53\x7b\x98\x9a\x10" \
"\x2b\x22\x8b\x51\x17\x50\x10\xf3\x8e\xaa\x4d\x55\x0a\x9c\xaf\xbc" \
"\x14\x11\x0f\xfd\xa2\xe3\x90\x8c\x2f\xc5\xb0",
"\x01\x00\x00\x00\x00\x30\x45\x02\x20\x6f\x1b\xfc\x40\x2f\xe8\x91" \
"\x1c\xdc\x9a\x92\xec\xc7\x13\x1e\xeb\xa3\x17\x06\xce\xc4\x81\x0d" \
"\x8e\x60\xef\x75\x9d\x1e\x87\x42\x6c\x02\x21\x00\xca\x29\x24\x79" \
"\xb5\xc4\x42\x67\x79\x45\x30\x6e\x3f\xcd\x17\x5e\x87\x57\x7c\x9e" \
"\x45\xd8\xc6\xa3\xdf\x5d\x4b\xb0\x00\x97\x40\xc3"};

I have patched (or ruined) the raw_authenticate_enforce function in src/raw/authenticate.c, here it is (bear with me, my solution is horrendous):

int auth_enforce_count = 0;
// This int is important, don't forget to put it outside the function

static struct payload *raw_authenticate_enforce(u2f_emu_vdev *vdev,
        const uint8_t *apdu, size_t size)
{
    (void) size;
    /* Parmas */
    struct authentification_params params;
    memcpy(&params, apdu + 7, sizeof(params));

    size_t respsize[] = {76, 76, 75, 76, 76, 76, 77, 75, 76};
    char *responses[] = {
"\x01\x00\x00\x00\x00\x30\x45\x02\x21\x00\xb2\x8f\x52\xe2\xe8\xc1" \
"\x9a\x2c\xe1\xd8\xf6\x93\x7a\x14\x56\xfa\xd2\x78\x5d\x35\xa0\xa2" \
"\xa3\xa2\x0c\x73\x55\x63\x4c\x52\x22\x76\x02\x20\x24\xc1\xb7\x9a" \
"\xb0\x9a\x46\xa7\x75\x1b\x35\x4f\x31\x3f\x24\x71\xcb\x98\xa3\x52" \
"\xd0\xfe\x22\x99\x4d\xdb\x4f\x3d\x01\xff\xd9\x9e",
"\x01\x00\x00\x00\x00\x30\x45\x02\x20\x09\xba\x7e\xb1\xbd\xb2\xaf" \
"\x07\x03\x8e\x60\xc9\x1f\x84\xf4\x09\x62\xf3\xe0\x88\xdb\xba\x3c" \
"\xb3\x4b\x69\x87\xa6\xa4\xc8\xab\x1a\x02\x21\x00\xf5\x73\x8d\xaf" \
"\xa5\xc6\x1a\xd7\xef\xe6\x90\xbf\xe7\x74\x66\x94\x51\xfe\x52\xe1" \
"\x6e\xec\xf0\x6a\xf1\x2a\x6a\x12\xe7\x8e\x84\x51",
"\x01\x00\x00\x00\x00\x30\x44\x02\x20\x67\x40\xe2\x46\x34\xbb\xaf" \
"\x0b\x0a\x54\x5e\x9d\x8d\xa1\xac\x71\x75\xc9\x3c\xc5\xae\xaa\xf2" \
"\x89\xb9\x3b\x8b\xba\x00\x3d\x74\xd8\x02\x20\x7b\x00\x8c\xfc\xbc" \
"\xf5\x16\x78\xd9\x11\x95\x96\x8e\x98\x39\xed\x02\xad\xbc\xc4\xd3" \
"\x47\x03\xb6\xbe\x5a\xe8\x8e\x1e\x97\x0b\xac",
"\x01\x00\x00\x00\x00\x30\x45\x02\x20\x22\xbf\xdb\x52\xc6\x94\x41" \
"\x6b\x2c\x39\xbb\x4f\x33\x51\x26\x53\xe6\x87\x43\x52\xfb\xfb\x51" \
"\x33\x1f\xc0\x24\x2a\x24\x51\xe5\xbb\x02\x21\x00\xaa\x61\x0d\xa5" \
"\x3e\xd8\x09\xb2\x30\x32\x6c\x1b\xa3\x33\xc2\xd5\xd5\xb0\xe6\x05" \
"\x2c\xe1\xc6\x07\xfd\xb7\x5c\x5b\x3d\x2d\x25\x66",
"\x01\x00\x00\x00\x00\x30\x45\x02\x20\x36\xfa\x3a\xc1\x25\x93\xbe" \
"\x5f\x31\xe6\x49\x9f\x24\xdc\xaf\x57\xd3\x8e\x18\x5e\x16\x10\x99" \
"\x6c\x04\xbd\x71\xa5\xd1\x12\x58\x57\x02\x21\x00\xa7\xec\x50\xe8" \
"\xbb\x73\x98\xd3\xe0\x08\x8a\xbf\xab\x26\x53\x69\xa8\x13\x39\x53" \
"\x2b\xc9\x9c\x2e\xc5\x5a\x77\x62\x9d\x8c\xd7\xed",
"\x01\x00\x00\x00\x00\x30\x45\x02\x20\x7b\xea\xf7\x96\xa9\x34\xdf" \
"\xb7\xc7\x13\x06\x94\x7b\x59\x90\x63\xb2\x51\x65\x13\x00\x3f\x34" \
"\x97\xd3\xfc\xa4\x1a\x7f\x38\xa0\xa4\x02\x21\x00\xaf\xcc\x79\x20" \
"\x05\xac\x3c\x60\x1a\x0f\xf3\x9b\x43\xec\xae\x9d\x74\x1c\xde\x39" \
"\x8b\x0b\x70\x48\x0c\xc9\xc4\xa6\x13\x64\xd5\xb6",
"\x01\x00\x00\x00\x00\x30\x46\x02\x21\x00\xf6\x0a\xf8\x4c\xfc\x0f" \
"\x3d\x9f\x33\xae\x8a\xd0\x4b\x61\x7a\xb7\xcf\x78\x2f\x70\xcd\x08" \
"\x3a\x71\xaa\xd7\x38\xb4\xe7\x5d\x51\xc0\x02\x21\x00\xb4\x3a\x62" \
"\x93\x49\xce\xa1\x34\x15\x26\x3b\x86\xa1\xaf\xc6\x37\xcd\x4c\x92" \
"\xdc\x86\x73\xa3\x60\x57\x73\x11\x71\x05\x82\xf9\xda",
"\x01\x00\x00\x00\x00\x30\x44\x02\x20\x50\xac\xfd\x22\xab\x96\xe9" \
"\x7e\xf4\x9d\xe4\xff\x5c\x12\xf1\x4d\xd1\xdb\x99\x3e\x6c\x62\x4a" \
"\x42\xab\x27\xd9\x41\xd6\x80\x8a\x4d\x02\x20\x53\x7b\x98\x9a\x10" \
"\x2b\x22\x8b\x51\x17\x50\x10\xf3\x8e\xaa\x4d\x55\x0a\x9c\xaf\xbc" \
"\x14\x11\x0f\xfd\xa2\xe3\x90\x8c\x2f\xc5\xb0",
"\x01\x00\x00\x00\x00\x30\x45\x02\x20\x6f\x1b\xfc\x40\x2f\xe8\x91" \
"\x1c\xdc\x9a\x92\xec\xc7\x13\x1e\xeb\xa3\x17\x06\xce\xc4\x81\x0d" \
"\x8e\x60\xef\x75\x9d\x1e\x87\x42\x6c\x02\x21\x00\xca\x29\x24\x79" \
"\xb5\xc4\x42\x67\x79\x45\x30\x6e\x3f\xcd\x17\x5e\x87\x57\x7c\x9e" \
"\x45\xd8\xc6\xa3\xdf\x5d\x4b\xb0\x00\x97\x40\xc3"};

    payload_add_data(payload,
                     (uint8_t *) responses[auth_enforce_count],
                     respsize[auth_enforce_count]);
    printf("Sending hardcoded enforce number %d\n", auth_enforce_count);
    authenticate_response_sw(payload, SW_NO_ERROR);
    auth_enforce_count = (auth_enforce_count + 1) %
                         (sizeof(datasize) / sizeof(size_t));
    
    /* Increment counter */
    vdev->counter->counter_increment(vdev->counter);

    return payload;
}

I have also modified raw_authenticate_check to make it send the same SW_CONDITIONS_NOT_SATISFIED response everytime, just as in the capture:

static struct payload *raw_authenticate_check(u2f_emu_vdev *vdev,
        const uint8_t *apdu, size_t size)
{
    /* Parmas */
    struct authentification_params params;
    memcpy(&params, apdu + 7, sizeof(params));

    /* Start Response payload */
    struct payload *payload = payload_new();
    
    printf("Sending hardcoded check\n");
    authenticate_response_sw(payload, SW_CONDITIONS_NOT_SATISFIED);

    return payload;
}

I am not showing every single modifications, so you WILL need to remove the static keyword from some definitions to make the whole project compile normally (or change the CFLAGS).

BRING ME TO LIFE

Let’s compile my frankenmulator. From the project root (I use cmake btw 😎):

mkdir build && cd build
cmake ..
make && make examples

Here’s the magic formula to bring him to life:

cd examples/usb/
sudo ./u2f-emu-usb

And it now waits indefinitely for requests.

FLAGGITTY FLAG FLAG

Launch the frankenmulator, hop on the website, login, click on Check Token and img

Bruh moment

Woopsie, sorry but this solution needs you to repeat these instructions until you get the flag. I know I know, this isn’t very fancy, but it works. I think that it might be able to optimise this solution even further by inspecting the requests and responding with the correct answer (that actually was the next step if this didn’t work).

Anyway, after a few tries, the screen looks a bit different: img

YAY! Now onto the next challenge! (Oh and don’t forget to kill the frankenmulator if you don’t want it to eat all your U2F challenges 😉)