π Enumeration and Exploitation of JSON Web Tokens (JWT)
JSON Web Tokens (JWT) are the most common mechanism nowadays for handling authentication and authorization in web applications. It’s an open standard (RFC 7519) that defines a compact format for transmitting signed information between client and server. The problem is that many implementations blindly trust the token’s content without properly validating its signature, and that’s where an attacker can get in. In this post we’ll see, with two hands-on labs, how to enumerate the structure of a JWT and exploit two classic flaws: acceptance of the none algorithm and the use of weak secrets.
π Scenario
We’ll be working on two labs from the SKF Labs project, both Node.js applications with a login that accepts a username and password. The goal in both cases is the same: authenticate as a regular user (user:user), capture the JWT the application generates for us, and manipulate it to escalate privileges up to the admin user, which is the lab’s “mortal” (target) account.
π‘ Step 1 β Spin up the JWT-null lab
We go to the lab’s directory:
/skf-labs/nodejs/JWT-null
We install dependencies and start the application:
npm install
npm start
We access localhost:5000, where we find a login with two users into which we can enter a username and password. We authenticate as user:user and, after logging in, the interface shows an admin button which, if we click it as a regular user, denies us access.
π‘ Step 2 β Locate the token
The JWT the application hands us upon authentication is stored in the browser’s localStorage. That’s where we need to go to copy it so we can analyze and manipulate it.
π‘ Step 3 β Analyze the JWT’s structure
With the token in hand, we paste it into jsonwebtoken.io to decode it. In the header we find something like this:
{
"alg": "HS256",
"typ": "JWT"
}
The alg field indicates the signing algorithm. This is the key to the attack: some JWT library implementations, if the alg field is set to none, accept the token without verifying any signature. If this happens, we no longer need to know the server’s signing secret: we simply build our own token by hand.
π‘ Step 4 β Forge a token with alg: none
A JWT is made up of three parts separated by dots: header.payload.signature, each Base64-encoded. If the algorithm can be none, the signature part can be omitted entirely.
We build the header:
echo -n '{"alg":"NONE","typ":"JWT"}' | base64
And the payload, specifying the user id we want to impersonate (in this case id:2, corresponding to a role with more privileges) and the issued-at and expiration dates:
echo -n '{"id":2,"iat":fechacreacion,"exp":fechaexpiracion}' | base64
With the header and payload encoded, the final token looks like this:
header.payload.
Watch out for this important detail: the trailing dot after the payload must be kept, even though there’s no signature. That dot marks the JWT’s third part (empty), and many vulnerable implementations accept it anyway because they don’t even check that a signature is present when alg is none.
We replace the token in localStorage with the one we’ve forged and click the admin button again. If the lab is vulnerable, the application recognizes us as the user with id:2 without ever needing a secret.
π Scenario 2 β Weak secrets (JWT-secret)
The second lab is located at:
/skf-labs/nodejs/JWT-secret
We repeat the installation:
npm install
When accessing the application, this time the attack is called weak secrets. Here the algorithm is still valid (for example HS256), but the server signs tokens with a secret key so short or predictable that it can be broken by brute force or dictionary attack with tools like hashcat or john. Once the secret has been recovered, we can sign a modified token ourselves (for example, changing the id to an admin user) and the application will accept it as legitimate because the signature matches. We repeat the same steps as in the previous lab here: capture the token from localStorage, decode it on jsonwebtoken.io, and once the secret is obtained, generate a valid token with the data we’re interested in.
π‘ Generating keys for RS256
If instead of a symmetric algorithm (HS256) we find a token whose header indicates:
"alg":"rs256"
we’re dealing with an asymmetric algorithm, and if we need to generate our own key pair for testing, we can do so with:
openssl genrsa -out priv.Key.key 2048
π Types of signing algorithms in JWT
It’s important to know what type of key each algorithm requires, because that determines whether the viable attack is brute-forcing a shared secret, or abusing configuration with public/private keys.
1. HS256 (HMAC with SHA-256) β shared (symmetric) key. Generated like this:
openssl rand -base64 32
This generates a 256-bit (32-byte) shared key in Base64 format. It’s the most common algorithm for weak-secret attacks, since server and client use the same key to sign and verify.
2. RS256 (RSA with SHA-256) β RSA key pair (private for signing, public for verifying). Private key:
openssl genpkey -algorithm RSA -out private_key.pem -aes256
This generates an RSA private key protected with 256-bit AES encryption, saved to private_key.pem. Public key derived from the private one:
openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pem
This generates the corresponding RSA public key, saved to public_key.pem. A typical configuration flaw here is the well-known algorithm confusion attack (RS256 β HS256), where the server uses the public key as if it were an HMAC secret.
3. ES256 (ECDSA with SHA-256) β elliptic curve key pair. Private key:
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem
This generates an elliptic curve private key using the prime256v1 curve, saved to private_key.pem. Public key derived from the private one:
openssl ec -in private_key.pem -pubout -out public_key.pem
This generates the corresponding public key.
4. PS256 (RSA-PSS with SHA-256) β RSA key pair (private for signing, public for verifying). Private key generation is similar to the RS256 one mentioned above.
π« What’s actually happening?
The enumeration phase of a JWT consists of gathering information about how the application builds and validates its tokens: what algorithm it uses, whether it accepts alg: none, whether the secret is guessable, what fields the payload carries (roles, IDs, permissions). An attacker can keep trying fake tokens βbuilt by hand or via brute-forcing the secretβ while watching at all times whether the application accepts or rejects them. If successful, they can extract sensitive information: usernames, roles, or session data.
Exploitation happens when that information is used to bypass authentication or authorization controls: if the application doesn’t properly validate the JWT’s signature (as in the alg: none case) or uses a weak secret, an attacker can forge a token and impersonate any user, including an administrator, without knowing their real credentials.
β
Best practices
To avoid this type of flaw: never accept alg: none in server-side validation, explicitly enforce the expected algorithm when verifying the token (rather than reading it from the JWT itself), use long, random secrets of at least 256 bits for HS256, rotate signing keys periodically, and always limit token expiration time to reduce the exploitation window if one is compromised.
