Sketching the situation
Let’s suppose we do a server post-breach analysis and manage to state the following:
- external access is possible only through a web application, and the web server is running with the privileges of an unprivileged user,
- the application is out-of-date and contains publicly known RCE vulnerability (remote code execution),
- the access_log, error_log files have the appropriate permissions and there are no signs of vulnerability in them.
In this situation, can we think that the attack vector was different? It turns out that not necessarily, because it is possible that the attacker has obtained root and modified the logs. But what if the server is properly configured and we exclude such a possibility? It might seem that you should look elsewhere, but I asked myself: What if the queries were not logged in? The question may seem absurd, but I have taken the subject seriously.
Searching for answers on the Internet
The first step was research on the Internet, and the only point of reference was the question on StackOverflow. The author asked about the possibility, in which the server does not log some queries. However, no answer was given.
Searching for answers on your own
I proceeded to take action myself: I went to the Apache documentation to learn more about the access_log file. The first sentence turned out to be interesting:
The server access log records all requests processed by the server.
For verification, I checked the nginx documentation and there:
NGINX writes information about client requests in the access log right after the request is processed.
We can conclude from this that logs are saved only for queries that have been processed. This also confirms the sample entry:
1 |
127.0.0.1 - - [13/Jul/2018:07:14:12 +0000] "GET /index.php HTTP/1.1" 200 29 |
Number 200 means status, so try to find a situation where the server will stop processing and will not return any response.
Testing environment
We choose the easiest and probably the most popular configuration of the web server: Apache + PHP, where Apache uses prefork MPM and communicates with PHP using the built-in module.
Consider various ways to terminate the code processing:
- syntax error
- throwing an exception
- exit ()
- die ()
- tiggererror () trigger_error ()
- __haltcompiler()
- killing the process (eg by itself)
For each method, a file is prepared, for example, 01_syntaxerror.php:
1 2 3 |
<?php asd echo "This line should not be executed!"; |
The whole is closed in Docker containers, with redirection of access_log, error_log to standard output. To this scripts are added that allow full testing automation. The code used is available in the public repository andrzej1_1/rce-weblogs-bypass.
Tests
Running the run.sh script returns. the following logs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
apache_prefork-mod_php_1 | Testing started apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 01_syntaxerror.php apache_prefork-mod_php_1 | [Sat Jul 14 11:18:04.230715 2018] [php7:emerg] [pid 14] [client 127.0.0.1:33806] PHP Parse error: syntax error, unexpected 'echo' (T_ECHO) in /srv/http/01_syntaxerror.php on line 3 apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:04 +0000] "GET /01_syntaxerror.php HTTP/1.1" 500 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 02_exception.php apache_prefork-mod_php_1 | [Sat Jul 14 11:18:05.239377 2018] [php7:error] [pid 17] [client 127.0.0.1:33808] PHP Fatal error: Uncaught Exception: error in /srv/http/02_exception.php:2\nStack trace:\n#0 {main}\n thrown in /srv/http/02_exception.php on line 2 apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:05 +0000] "GET /02_exception.php HTTP/1.1" 500 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 03_exit.php apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:06 +0000] "GET /03_exit.php HTTP/1.1" 200 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 04_die.php apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:07 +0000] "GET /04_die.php HTTP/1.1" 200 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 05_triggererror.php apache_prefork-mod_php_1 | [Sat Jul 14 11:18:08.262572 2018] [php7:error] [pid 18] [client 127.0.0.1:33814] PHP Fatal error: Cannot divide by zero in /srv/http/05_triggererror.php on line 2 apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:08 +0000] "GET /05_triggererror.php HTTP/1.1" 500 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 06_haltcompiler.php apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:09 +0000] "GET /06_haltcompiler.php HTTP/1.1" 200 - apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 07_selfkill.php apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | # Testing 99_base.php apache_prefork-mod_php_1 | 127.0.0.1 - - [14/Jul/2018:11:18:12 +0000] "GET /99_base.php HTTP/1.1" 200 29 apache_prefork-mod_php_1 | ------------ apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | apache_prefork-mod_php_1 | All files has been tested! |
As you can see, most of the scripts left a trail behind them, and only 07_selfkill.php did not result in the creation of any entry. Here are its contents:
1 2 3 |
<?php exec('kill ' . getmypid()); echo "This line should not be executed!"; |
The code operation is simple: the kill command is executed on the current process.
Further tests
We managed to execute a script that did not leave any trace, however, it is worth checking if the behavior will be identical for other configurations. There are many different ways to run a server that supports PHP, because we have a choice of different servers and interfaces for communication. In the case of Apache, we can additionally choose different processing modules (MPMs), of which the prefork, worker and event are stable. Due to the great possibilities, we will test the most popular of them:
- Apache (prefork MPM) + mod_php (this configuration was already tested)
- Apache (prefork MPM) + FastCGI
- Apache (worker MPM) + FastCGI
- Apache (event MPM) + FastCGI
- Apache (prefork MPM) + FPM
- Apache (worker MPM) + FPM
- Apache (event MPM) + FPM
- nginx + FastCGI
- nginx + FPM
A separate container has been created for each configuration, and running the run.sh script will test each one and generate results in the form of log files.
However, after running the tests and reviewing the results, it turns out that all configurations outside the first one are “resistant” to the considered methods.
Conclusions
The tests show that the configuration of Apache with the prefork module and mod_php allows to bypass the login of queries in the case of RCE vulnerability. It is certainly not a common situation, but it is worth knowing that such a “trick” exists.