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!