You have been asked by a client to recover top secret data from a competing company. You have tried several approaches to find vulnerabilities on the exposed servers, which unfortunately proved unsuccessful: the company’s servers look solid and well protected. Physical intrusion into the premises seems complex given all the necessary access badges and surveillance cameras.
One possibility lies in the remote access that the company’s employees have to their collaborative work portal: access to it is done via two authentication factors, a password as well as a physical token to plug into the USB with biometric fingerprint recognition. Even if it is stolen, it will be difficult to exploit it. Installing an evil maid malware on a company laptop is not an option: these are very well protected with secure boot via TPM, and disk encryption using the token.
But all hope is not lost! You take advantage of the train trip of one of the employees and of their fleeting absence at the bar to discreetly plug a miniaturized USB sniffer in their laptop. You also slip a hidden camera over their seat which could only capture a few seconds. You retrieve the camera and the sniffer stealthily after their work session: will you be able to exploit the data collected to complete your contract?
To get the
X-Factor 1/2flag, you have to log in with login and password. Then you have to log in with the second authentication factor to get the flag for
X-Factor 1/2 recap
X-Factor 1/2 is a “misc” challenge that was scored by 207 people for 140
points. It consists in getting credentials from
From this video, we get:
After login in, we get a flag.
replacing clear password characters with “
After login we are prompted for a second factor token through the browser Universal 2nd Factor (U2F) API. The name of the challenge now makes sense.
Web page exploration
If we cancel the browser U2F prompt or use a random U2F token (such as rust-u2f emulator or a hardware token),
then we get
Bad second factor authentication! error.
Let’s look at the U2F login page source code and list points of interest:
Check Tokenbutton calls
beginAuthen('UezmElyJs4+StNBS<snip>')on click, the parameter looks like base64 and does not seem to change.
At first glance
finishEnroll seems interesting as this may
allow us to register a new U2F token, but
return HTTP 404 with the following error:
Enrollment is not allowed on this portal. Please contact your administrator
Let’s analyse the login flow when
Check Token is clicked:
beginAuthenis called with a
- HTTP GET request is sent to
- On response, we get a
startAuthenobject from the server containing:
- the expected U2F version (
- a key handle (
- an app identifier (
- a challenge (
- the expected U2F version (
- The browser U2F API is called to sign the data in
- After signing or failure,
finishAuthenis called with U2F returned data,
- HTTP GET request is sent to
/finishAuthenwith the returned data,
- The user is redirected to
location.assign('/check') and print to console intermediate
startAuthen.keyHandleis a constant web-safe base641,
startAuthen.challengelooks like a random web-safe base64 value. On closer inspection, it takes randomly one of the following values:
L8tsCkDErRPzV9SAOlOj2JzFMXAOjmUs7JnimkH9_gI D5CxgaFPGIQu5fGYPEjo-YA9Dqd6y2PBoWP6p56TpFw 9rlDOo98PIKIiubib97v4IDCJ1FBB2uRUhNgwH89wqw
USB capture exploration
We are given a
capture_USB.pcapng trace. Opening it directly in Wireshark
shows USB traffic. It starts with
GET DESCRIPTOR exchanges containing
idProduct=0x1337 and a USB HID configuration.
As the vendor identifier is null, we cannot determine which is exactly this
device, but it is a USB HID device and there is a high probability this is
an U2F token.
Wireshark is unable to dissect the following packets further than the USB URB layer. Let’s search for a U2F dissector. We use u2f_fido2_dissector.lua by Yuxiang Zhang, but with the following extra line to detect the custom U2F token:
Now we may start Wireshark with this extra dissector:
We now see CTAPHID layer with ISO 7816 APDU.
We confirm that it is indeed an U2F token.
We download the Universal 2nd Factor (U2F) Overview from fidoalliance.org and start
coloring packets with filters.
In previous screenshot, red packets are 0x6985 errors, meaning that the token
did not find user presence. Green packets are successful signatures.
Blue packets are version responses containing
The dissector is not ideal and does not properly detect the signature message, but we can use the hexadecimal dump to get these values.
After a bit of documentation reading and dissection, we recover 11 successful requests/responses signatures. The request has the following structure:
- Challenge parameter (32 bytes), varying but we see repetitions,
- Application parameter (32 bytes), constant,
- Key length \(L\) (1 byte),
- Key handle (\(L\) bytes), constant.
The response has the following structure:
- User presence (1 byte),
- Counter (4 bytes),
- An ASN.1 ECDSA signature of the concatenation of the application parameter, user presence, counter and challenge parameter.
The key handle is checked by the U2F token. If it does not satisfy the U2F token, it returns an error rather than giving a signature.
Bad ECDSA nonces? I lost a lot of time in this challenge trying to attack the ECDSA nonce by making the assumption that because this may be a custom U2F token, it might not correctly generated nonces. This did not succeed.
As the returned counter is
0x00000000, it must also be
0x00000000 in the
signed message, else the server wouldn’t be able to check the signature.
As the fidoalliance.org overview states, this counter is a mitigation against
replay attacks, and it should be increasing at each signature.
We are going to attack the login flow by replaying what we captured in the USB trace, but first we need to understand how the challenge received on the device is derived from the given server challenge.
From client data to token challenge
We need a working U2F authentification flow to analyse the object passed to
Option A: using a patched hardware token. I modified my Ledger Nano S+ U2F app to skip a check on the key handle by:
- putting random
- renaming the app to
- doing some last minute hacks to make the app compile with the latest SDK, this was time inefficient.
Option B: using an online U2F demo. By doing the registration and signing process on https://mdp.github.io/u2fdemo/ we are able to quickly get the generated response.
From this working login flow we deduce that
finishAuthen is called with this object:
The web-safe base64 decoding of
According to fidoalliance.org overview, the challenge signed by the device is the
clientData. We are now able to build correspondence between server
and device challenge:
D5CxgaFPGIQu5fGYPEjo-YA9Dqd6y2PBoWP6p56TpFw -> 9e5e67419d90aa711dda3c361678a4cd8ddf835051bc7369ccf14bf9da7bcfb3 L8tsCkDErRPzV9SAOlOj2JzFMXAOjmUs7JnimkH9_gI -> 8736c5b6cb8b27617a7ccbec9f599ba460eefee042fe25b9b2a673bf43ddb1e8 9rlDOo98PIKIiubib97v4IDCJ1FBB2uRUhNgwH89wqw -> 153db9a93297ea0b55d3a3e0898213c251ec3cf3ae40c8d6518642fc8b64fd0e
Oh surprise! We recognize challenges from the USB capture.
This section could have been done with another unpatched U2F token by changing
u2f.sign application identifier and key handler to match values working on
Replaying captured signature
Let’s go back to the U2F login page and override
beginAuthen() function by
putting this in the developer console:
Now we call
beginAuthen() and get
From the last section, we know that this correspond to the signature
8736c5b6cb8b27617a7ccbec9f599ba460eefee042fe25b9b2a673bf43ddb1e8 in the USB
We manually forge the response:
Then send the response in the browser console:
This challenge was really well designed. During this challenge, I learned:
- A lot about U2F internals,
- More about APDU and SmartCards,
- ECDSA bad nonce attacks, even if this was not the solution,
- What is web-safe base64 and how to build it using base64 Python module.
base64 variant in which (
=) is replaced by (
-, ‘’). ↩︎