JWT token vulnerabilities in CTF: from alg none to RCE

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
464
Reaction score
752
Deposit
0$
Before you break the token, read it. JWT consists of three parts separated by points: header.payload.signature. Header and payload are base64url-coded JSON objects. Signature is a cryptographic signature calculated by the formula HMAC/RSA(base64url(header) + "." + base64url(payload), secret). Signature is the only thing that ensures the integrity of the token. Without it, payload turns into a postcard, the contents of which will be rewritten by anyone. More details in our article about web application pentest.

The first action when detecting JWT on CTF is to decode. Insert the token on jwt.io or execute in the terminal echo '<header>' | base64 -d. In the header, look at three parameters:

alg – declared signature algorithm (HS256, RS256, ES256, none)
kid – key identifier if present (potential injection point)
jwk, jku, x5u – links to external keys (verification key substitution vector)

In payload look for claims for modification: sub, role, isAdmin, username, user_id, exp. Payload is not encrypted – it is base64url-coding, not encryption. Encryption is JWE (JSON Web Encryption) and the standard JWT in JWS (JSON Web Signature) format only signs the data. Anyone reads the contents of the token without a key.

For intelligence automation Launch jwt_tool <token> – the tool will output decoded header and payload, indicate the algorithm and tell potential attack vectors. Full scanning mode jwt_tool <token> -M at drives all known checks in one run. Start it first while manually picking the app.

From the point of view of MITRE ATT & CK, the operation of JWT falls under the T1606 Forge Web Credentials (Credential Access) equipment – the attacker creates fake credentials for unauthorized access. The motivation is simple: successful forgery JWT = complete bypass authentication = access to any account or privileges. On the CTF it is a flag; in the real world - compromise of the entire authorization system.
Attack alg none JWT: Bypassing the Token Signature

The simplest and most frequent vulnerability of JWT on CTF sites. Significance "alg": "none" by specification means Unsecured JWT – token without signature. If the server accepts such tokens in production mode, the signature can simply be thrown out, and the claims are rewritten as you like.

According to PortSwigger Web Security Academy, the vulnerability arises from the fact that the server trusts the value alg from the token itself to choose the verification method. Fundamental defect: The security decision is made based on the data that the client controls.
Step-by-step manual operation

Four steps:

Decode the header from base64url and replace alg on none. Be sure to try the register options: None, NONE, nOnE – part of the libraries filters only lowercase writing.
Decodify payload and change target claims: "role": "user" → "role": "admin", or "isAdmin": false → "isAdmin": true.
Code both objects back to base64url. It is base64url, not standard base64 - the difference in symbols (+/ → -_) and lack of padding =.
Gather a token in format <new_header>.<new_payload>. – the point at the end is mandatory, there is no signature after it.

On the CTF AquaCommerce from Intigriti, it was this technique that opened access to the administrator panel: change "role": "user" on "role": "admin" with algorithm none and an empty signature. After receiving admin access in administrative panel discovered SSTI in Jinja2, through which the participants achieved RCE.

Through jwt_tool everything is done by one team: jwt_tool <token> -X a. Flag -X a launches the attack alg:none and generates several token variants with a different case of the word “none”.
Why libraries pass none

As PortSwigger notes, many JWT libraries provide two methods: verify() and decode(). The developers confuse them and call them decode() instead of verify(), completely disabling signature verification. The second scenario is that the library rejects correctly none, but the developer did not limit the white list of valid server-side algorithms. Both options are a consequence A05:2021 Security Misconfiguration by OWASP.

Frequent beginner error (and on CTF devours a lot of time): forget to leave the finishing point after payload. The token should have a format header.payload. – with a point, not header.payload without her. Without a point, the servers reject the token as syntactically invalid. The attack doesn't work for a formal reason, not because of protection.
Brute force secret JWT: hashcat and vocabulary attacks

If alg:none not passed — check the durability of the secret for the HMAC signature (HS256/HS384/HS512). On CTF, weak secrets are constantly found: secret, password, 123456, application name, line from task description. This JWT signature vulnerability falls under A02:2021 Cryptographic Failures by OWASP.

For overkill – hashcat with 16500 mode specially sharpened under JWT:

hashcat -m 16500 jwt.txt -a 0 /usr/share/wordlists/rockyou.txt

In the file jwt.txt place the full token (all three parts through the points), -a 0 – vocabulary attack. On the video card RTX 3070 overkill on rockyou (14 million records) takes several minutes.

Alternative – jwt_tool <token> -C -d rockyou.txt. jwt_tool has a built-in dictionary of popular JWT secrets — team jwt_tool <token> -C without specifying the file will drive it in seconds. For old schoolers, John the Ripper works: john jwt.txt --format=HMAC-SHA256.

After finding the secret, reassemble the token with the changed claims and the correct signature. In jwt_tool: jwt_tool <token> -T -S hs256 -p '<found_secret>' – flag -T opens an interactive editor, claims -S hs256 setting the algorithm, -p - a secret found.

Typical error: try brute force for RS256. RSA keys are not sorted with a dictionary – they are fixed-length cryptographic pairs (2048/4096 bits). Brute force is applicable only to symmetric algorithms (HS256/HS384/HS512) with text secrets. If you see RS256, go to the next section.
JWT Confusion attack: substitution RS256 on HS256

Algorithm Confusion (Key Confusion) is one of the most elegant attacks on JWT token vulnerabilities. The essence: the server uses RS256 (asymmetrical – signature by private key, check public), and the attacker changes alg on HS256 (symmetrical – one key for signature and verification) and signs the token with a public server key as a HMAC secret. Beautiful, right?
Mechanics at the library level

With RS256, the server stores a public key to verify the signature. If the library does not control that the claimed algorithm corresponds to the expected type of key, it takes the same public key — but now uses it as an HMAC secret for HS256. RSA public key is information available to any client. The attacker gains full control over the “secret” of the signature and can stamp valid tokens with arbitrary claims.
Step-by-Step Operation

Find the public server key. Typical locations: /.well-known/jwks.json, /jwks.json, /api/keys, HTTP response headers, frontend JavaScript files. On CTF, the key is usually open – this is part of the job.

Convert the key to PEM. If the key is received in JWK format (JSON object with fields n, e, kty), use OpenSSL or jwt_tool to convert to PEM file.

Sign the token with the algorithm swapped:

jwt_tool <token> -X k -pk public.pem

Flag -X k Launches Key Confusion Attack. The tool will automatically change alg on HS256 and will sign the token with the contents of the file public.pem as an HMAC secret.

Send the token to the server. If the library is vulnerable, the signature will be verified.

The nuance on which they get stuck: some libraries expect a key in a strictly defined format - PEM with line transfers \n, without them, in pure base64 or with a certain line ending. If the attack does not work on the first attempt, try the formatting options. Check if the PEM file has an extra string transfer character at the end. On the CTF, this little thing devours more time than the attack itself.

Algorithm confusion only works when the server allows switching between the algorithm families (RSA → HMAC). If the server side is tightly fixed RS256 through a white list and the library ignores alg from the client token - will not pass.
Kid injection JWT: from path traversal to SQLi

Parameter kid (Key ID) in JWT header manipulation is one of the most dangerous and undervalued vectors. By specification kid – just a string-identifier of the key, but implementations often use it as a path to the file, the SQL query parameter or the command argument. It's turning kid in a universal injection point.
Path traversal through the kid

If the server reads the key from the file along the path specified in the kid, you can send it to a file with known (or empty) contents:

{
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../dev/null"
}

/dev/null returns empty data. Sign the token with an empty line ("") – the signature will coincide with the “key” read from /dev/null. Alternatives: /proc/sys/kernel/hostname or any file with predictable content if you know it ahead.

Team via jwt_tool: jwt_tool <token> -I -hc kid -hv "../../../../dev/null" -S hs256 -p "". Here -I – injection mode, -hc kid – target parameter in header, -hv – the substituted value, -p "" - an empty signature secret.

According to PortSwigger, the kid path traversal is one of the most common injections in the JWT header. The attack matches A03:2021 Injection by OWASP.
SQLi through the kid

When the meaning kid used in a SQL query to obtain a key from the database (SELECT key FROM keys WHERE kid = '<kid_value>'), the SQL injection vector opens. Typical Payload: "kid": "1' UNION SELECT 'controlled-secret' -- ". The request will return the line controlled-secret instead of the real key, and the token is signed by this value.

As Red Team Pentesting describes, SQLi through kid allows you to control the verification key – this is a session hijacking through JWT in its purest form. The attacker generates valid tokens for any user of the system.
Command injection through a kid

In rare cases, the significance kid transmitted to the system command (called external script to receive the key). Standard pails work: "kid": "key1 | cat /flag.txt" or "kid": "key1; id". On CTF, this vector is less common path traversal and SQLi, but it is worth checking – especially when other injections did not produce results.
Counterfeiting JWT Token via JWK and JKU Injection

JWK (JSON Web Key) and JKU (JWK Set URL) are the header settings through which the token can carry verification key information. If the server trusts these parameters from the incoming token without additional validation, the attacker puts its own key and signs the token with it.
JWK Injection and CVE-2018-0114

The attacker generates its own pair of RSA keys, signs the token with its private key and embeds the public key directly into the header through the parameter jwk. The vulnerable library takes this built-in key for verification – and the signature passes. In fact, the token itself brings the key that it should be checked. Absurd, but it works.

Canonical example — CVE-2018-0114 in the Cisco node-jose library (npm-pack node-jose, versions up to 0.11.0). According to NVD, the vulnerability has CVSS 7.5 (HIGH), the CVSS vector:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N — network operation, with low complexity, without authentication and without user interaction, with high integrity impact. The root of the problem is CWE-347 (Improper Verification of Cryptographic Signature): The library followed the JWS standard, allowing the transfer of JWK to the header, and trusted this key without restrictions.

According to EPSS, the probability of operating CVE-2018-0114 within 30 days is 42.65% (percentile 98.63% – top 5% of all CVEs), CISA classifies status as “PoC available, automatable”. The public PoC is available in the zi0Black/POC-CVE-2018-0114 repository on GitHub (26 stars).

Through jwt_tool, the JWT token forge with JWK injection is executed by the command jwt_tool <token> -X i – flag -X i generates a new pair of keys, signs the token, and embeds the JWK in the header automatically.
JKU Injection

Instead of embedding the key, the attacker specifies the URL to its server via the parameter jku. The server accesses the specified address, downloads a set of keys (JWKS) and uses one of them for verification. The attacker hosts its JWKS file with a public key corresponding to the private one that the token is signed.

On CTF is implemented through Burp Collaborator, ngrok or local Python server: install "jku": "https://attacker.com/.well-known/jwks.json" in the header, you sign the token with your private key, you place the public key according to the specified URL in the standard format JWKS. If the server does not restrict domains for jku - the fake is passing. Side effect: This is also the SSRF vector (A10:2021 on OWASP) – the server makes a request for an arbitrary URL controlled by the client.
 
Top Bottom