Barb’hack 2022: Leveraging PHP Local File Inclusion to achieve universal RCE

For the third consecutive time, the French city of Toulon hosted the French southernmost hacking event known as Barb’hack. We – two of Wavestone security auditors – have had the opportunity to attend the conference and participate in the Capture-the-Flag (CTF) event during the night.

 

Context

The CTF featured many challenges across many categories (reverse, binary exploitation, crypto, forensics, etc.), but one of the web application challenges kept us busy for long. The challenge presented itself as a simple PHP web application with multiple pages, and the user could switch between them by changing the ?p= GET parameter available. This usually results in a Local File Inclusion (LFI) vulnerability, with the backend PHP code being one of:

<?php

include $_GET['p'];
include 'includes/' . $_GET['p'];
include $_GET['p'] . '.php';

?>

These codes (and all derivatives) allow users to include almost any file from the server hosting the application and to which the web server service account (usually www-data) has access. In many cases, malicious users can exfiltrate data, leak the application source code, unveil secrets and passwords, etc. But in few specific ones, it is also possible to achieve Remote Code Execution (RCE). Over the years, the number of techniques on which one could rely to transform an LFI into an RCE grew in size, with the following examples:

  • Abusing the PHP_SESSION_UPLOAD_PROGRESS (Orange)
  • Abusing arbitrary data in PHP sessions (RCE Security)
  • Abusing nginx’s temporary files (Hacktricks)
  • Using phpinfo(), php://input, zlib://compress, etc.

One common element about all these techniques is that they all rely on (at least) an additional requirement. If not present, the LFI cannot be converted into RCE, and the pentester gets sad.

 

The usual trick

The web application we had under scrutiny was unfortunately so simple that all of these techniques did not work. We tried to exfiltrate interesting files from the server (/etc/passwd, Apache/nginx virtual host configuration, process environment, etc.) but nothing interested could be found.

Using this technique, it is not possible at first to exfiltrate PHP source files, since they are executed when they enter the include or require statement. However, it is possible to rely on the php:// stream and its filter function to apply a Base64 encoding before including the file, therefore changing the active content into innocent plaintext. For example: http://webapp/?p=php://filter/convert.base64-encode/resource=index.php.

Though this trick worked, it only showed that there was not interesting content or flag within the available source code. Time to dig deeper!

 

Universal PHP LFI to RCE

After many minutes hours of research, we finally came across this recent article (2 months) by Hacktricks, that explained how the same php://filter trick could be used (in combination with other encoding filters) to produce arbitrary content. This allows for generating a Base64-encoded minimalist webshell, which can be decode by a final convert.base64-decode filter into active PHP content.

But exactly how is generated this arbitrary content, from uncontrolled sources? The first thing to notice is that the exploit requires knowing the path of a file with read access (such as /etc/passwd), but the content of the file is almost irrelevant (it only needs some printable characters in the file).

The whole exploit leverages the special convert.iconv.UTF8.CSISO2022KR encoding filter. Its particularity is that it prepends the output string with \x1b$)C, therefore generating some semi-known content (there will always be the character “C”). Then, it uses the convert.base64-decode filter (which is extremely tolerant on characters not in the Base64 set) to remove the unprintable part of the string, followed by convert.base64-encode to restore our uppercase “C”. Finally, if the Base64 encoding produced equal signs (which could disturb the behaviour of subsequent operations), they can be removed with the convert.iconv.UTF8.UTF7 filter.

The same way we can now produce the “C” character, the authors of the exploit managed to find chaining of encodings that can produced any character from the Base64 set, most importantly prepending a user-controlled string. By combining all the filter chains for all characters for the known Base64-encoded webshell string (in reverse order), the exploit generates said string, followed by lots of (printable) garbage. The final convert.base64-decode filter decodes the webshell (and the garbage), and the include() or require() statement executes it!

 

Proof of Concept

What better testing environment than a clean and up-to-date docker container. Let’s build our Dockerfile:

FROM debian:latest

RUN apt update --fix-missing && \
apt upgrade -y && \
apt install -y apache2 libapache2-mod-php php WORKDIR /var/www/html VOLUME ["/var/www/html"] ENV APACHE_RUN_USER www-data ENV APACHE_RUN_GROUP www-data ENV APACHE_LOG_DIR /var/log/apache2 ENV APACHE_PID_FILE /var/run/apache2.pid ENV APACHE_RUN_DIR /var/run/apache2 ENV APACHE_LOCK_DIR /var/lock/apache2 RUN mkdir -p $APACHE_RUN_DIR $APACHE_LOCK_DIR $APACHE_LOG_DIR EXPOSE 80 ENTRYPOINT [ "/usr/sbin/apache2" ] CMD ["-D", "FOREGROUND"]

Let’s also prepare our vulnerable PHP file:

<?php include $_GET['p']; ?>

And finally build and test it:

root @ server $ docker build .
...
Successfully built 23dc284ec248

root @ server $ docker run --rm -p 11111:80 --mount type=bind,source=$(pwd)/www,target=/var/www/html 23dc284ec248

root @ server $ curl 'http://localhost:11111/?p=/etc/passwd'
root:x:0:0:root:/root:/bin/bash
...
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin

Finally, we can slightly adapt Hacktricks’ script to target our local URL and use a different parameter:

root @ server $ python3 attack.py | hexdump -C | less

00000000  75 69 64 3d 33 33 28 77  77 77 2d 64 61 74 61 29  |uid=33(www-data)|
00000010  20 67 69 64 3d 33 33 28  77 77 77 2d 64 61 74 61  | gid=33(www-data|
00000020  29 20 67 72 6f 75 70 73  3d 33 33 28 77 77 77 2d  |) groups=33(www-|
00000030  64 61 74 61 29 0a 0a 06  ef bf bd 0a 50 dc 9b ef  |data).......P...|
00000040  bf bd ef bf bd 0e ef bf  bd 0e ef bf bd 0e ef bf  |................|
00000050  bd 0e ef bf bd ef bf bd  ef bf bd ef bf bd 0e ef  |................|
00000060  bf bd dc 9b ef bf bd ef  bf bd 0e ef bf bd d8 9a  |................|
00000070  5b ef bf bd d8 98 5c ef  bf bd 02 ef bf bd 18 59  |[.....\........Y|
00000080  5b 5b db 8e ef bf bd 0e  ef bf bd 4e ef bf bd 4e  |[[.........N...N|
....

 

Preventing

There are many ways one can prevent a malicious user from turning a (not so) benign LFI into a full-blown RCE:

<?php

// Do not use this!
while(strpos($payload, 'filter')!==FALSE) { $payload = str_replace('filter', '', $payload); } 


// Slightly better, but still...
$payload = './' . $payload;


// Leverage builtin functions!
assert(stream_wrapper_unregister('php'));

?>

 

That’s all folks!

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top