PHP-fpm 远程代码执行漏洞(CVE-2019-11043)源码分析
一、漏洞复现
1、搭建docker环境(yum install docker-re)
2、拉取镜像
配置docker-compose.yml文件,并拉取镜像
docker-compose up -d
version: '2' services: nginx: image: nginx:1 volumes: - ./www:/usr/share/nginx/html - ./default.conf:/etc/nginx/conf.d/default.conf depends_on: - php ports: - "8080:80" php: image: php:7.1.32-fpm volumes: - ./www:/var/www/html
default.conf
server { listen 80 default_server; listen [::]:80 default_server; root /usr/share/nginx/html; index index.html index.php; server_name _; location / { try_files $uri $uri/ =404; } location ~ [^/]\.php(/|$) { fastcgi_split_path_info ^(.+?\.php)(/.*)$; include fastcgi_params; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_index index.php; fastcgi_param REDIRECT_STATUS 200; fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT /var/www/html; fastcgi_pass php:9000; } }
二、源码分析
static void init_request_info(void) { fcgi_request *request = (fcgi_request*) SG(server_context); //文件绝对路径 char *env_script_filename = FCGI_GETENV(request, "SCRIPT_FILENAME"); //env_path_translated值和env_script_filename值一样 char *env_path_translated = FCGI_GETENV(request, "PATH_TRANSLATED"); char *script_path_translated = env_script_filename; char *ini; int apache_was_here = 0; /* some broken servers do not have script_filename or argv0 * an example, IIS configured in some ways. then they do more * broken stuff and set path_translated to the cgi script location */ if (!script_path_translated && env_path_translated) { script_path_translated = env_path_translated; } /* initialize the defaults */ SG(request_info).path_translated = NULL; SG(request_info).request_method = NULL; SG(request_info).proto_num = 1000; SG(request_info).query_string = NULL; SG(request_info).request_uri = NULL; SG(request_info).content_type = NULL; SG(request_info).content_length = 0; SG(sapi_headers).http_response_code = 200; if (script_path_translated) { const char *auth; //获取request请求中的参数 char *content_length = FCGI_GETENV(request, "CONTENT_LENGTH"); char *content_type = FCGI_GETENV(request, "CONTENT_TYPE"); char *env_path_info = FCGI_GETENV(request, "PATH_INFO"); char *env_script_name = FCGI_GETENV(request, "SCRIPT_NAME"); ... if (CGIG(fix_pathinfo)) { struct stat st; char *real_path = NULL; char *env_redirect_url = FCGI_GETENV(request, "REDIRECT_URL"); char *env_document_root = FCGI_GETENV(request, "DOCUMENT_ROOT"); char *orig_path_translated = env_path_translated; char *orig_path_info = env_path_info; char *orig_script_name = env_script_name; char *orig_script_filename = env_script_filename; int script_path_translated_len; ... if (script_path_translated && //script_path_translated_len是请求uri_path中第一个斜杠前的内容:如http://127.0.0.1/index.php/test,则变量的值为/var/www/html/index.php的长度 (script_path_translated_len = strlen(script_path_translated)) > 0 && (script_path_translated[script_path_translated_len-1] == '/' || #ifdef PHP_WIN32 script_path_translated[script_path_translated_len-1] == '\\' || #endif (real_path = tsrm_realpath(script_path_translated, NULL)) == NULL) ) { //字符串复制 char *pt = estrndup(script_path_translated, script_path_translated_len); //url的长度取决于nginx的配置当请求url,http://127.0.0.1/index.php/123%0atest.php。script_path_translated来自于nginx的配置,为/var/www/html/index.php/123\ntest.php int len = script_path_translated_len; ... int ptlen = strlen(pt); int slen = len - ptlen; //request中path_info的长度,此参数值可控 int pilen = env_path_info ? strlen(env_path_info) : 0; int tflag = 0; char *path_info; if (apache_was_here) { /* recall that PATH_INFO won't exist */ path_info = script_path_translated + ptlen; tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0)); } else { //在c语言中,char *变量,加一个int数字,是一个使指针指向的地址偏移 path_info = env_path_info ? env_path_info + pilen - slen : NULL; tflag = (orig_path_info != path_info); }
下面的代码进行举例分析:
path_info = env_path_info ? env_path_info + pilen - slen : NULL;
替换成类似代码:由此可以看出,下面的代码在c语言中可以起到偏移char *首地址的#include <stdio.h>
int main() { char *a = "aaaaaaaa"; char *b = a-2; printf("%s",b);
//path_info[0]此地址对应的是path_info的首地址。根据fpm代码,则是可操作堆上任意数据置为0,那我们就可以把_fcgi_data_seg结构体的char* pos置零
FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info); old = path_info[0]; path_info[0] = 0; //orig_script_name变量不为空 if (!orig_script_name || strcmp(orig_script_name, env_path_info) != 0) { if (orig_script_name) {
FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);//进入此函数
}
php源码中FCGI_PUTENV函数
#define FCGI_PUTENV(request, name, value) \ fcgi_quick_putenv(request, name, sizeof(name)-1, FCGI_HASH_FUNC(name, sizeof(name)-1), value)
查看fcgi_quick_putenv函数
char* fcgi_quick_putenv(fcgi_request *req, char* var, int var_len, unsigned int hash_value, char* val) { if (val == NULL) { fcgi_hash_del(&req->env, hash_value, var, var_len); return NULL; } else { return fcgi_hash_set(&req->env, hash_value, var, var_len, val, (unsigned int)strlen(val)); } }
查看fcgi_hash_set函数,其中fcgi_request结构体为
//此为fcgi_request的机构体 struct _fcgi_request { int listen_socket; int tcp; int fd; int id; int keep; #ifdef TCP_NODELAY int nodelay; #endif int ended; int in_len; int in_pad; fcgi_header *out_hdr; unsigned char *out_pos; unsigned char out_buf[1024*8]; unsigned char reserved[sizeof(fcgi_end_request_rec)]; fcgi_req_hook hook; int has_env; fcgi_hash env;//此处是request->env存放的位置,存储的是nginx配置的ENV全局变量,在fcgi_hash_set中的变量名为h };
现在请联系之前,path_info[0]=0这段代码,这段代码表示我们可以随意在栈上的任意位置置为0。现在也就是说,传入fcgi_hash_set函数中的fcgi_hash *h我们可以修改这个变量中的任意位置为0,
static char* fcgi_hash_set(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, char *val, unsigned int val_len) { unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK; fcgi_hash_bucket *p = h->hash_table[idx]; while (UNEXPECTED(p != NULL)) {
//php全局变量名称hash_value相等,p中的var值以传入的固定的全局变量名称开头就可以,以及全局变量名称长度相同,则可覆盖到php其他全局变量的值,hash函数如下
/***
*memcmp是比较内存区域buf1和buf2的前count个字节。该函数是按字节进行比较的
*memcmp(p->var, var, var_len)这段函数是比较p->var中的值是否是以var的值开头
*#define FCGI_HASH_FUNC(var, var_len) \
*(UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \
*(((unsigned int)var[3]) << 2) + \
*(((unsigned int)var[var_len-2]) << 4) + \
*(((unsigned int)var[var_len-1]) << 2) + \
*var_len)
*/
if (UNEXPECTED(p->hash_value == hash_value) && p->var_len == var_len && memcmp(p->var, var, var_len) == 0) { p->val_len = val_len; p->val = fcgi_hash_strndup(h, val, val_len); return p->val; } p = p->next; } if (UNEXPECTED(h->buckets->idx >= FCGI_HASH_TABLE_SIZE)) { fcgi_hash_buckets *b = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets)); b->idx = 0; b->next = h->buckets; h->buckets = b; } p = h->buckets->data + h->buckets->idx; h->buckets->idx++; p->next = h->hash_table[idx]; h->hash_table[idx] = p; p->list_next = h->list; h->list = p; p->hash_value = hash_value; p->var_len = var_len;
//进入fcgi_hash_strndup此函数 p->var = fcgi_hash_strndup(h, var, var_len); p->val_len = val_len; p->val = fcgi_hash_strndup(h, val, val_len); return p->val; }
进入fcgi_hash_strndup此函数
static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len) { char *ret; if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) { unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE; fcgi_data_seg *p = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + seg_size); p->pos = p->data; p->end = p->pos + seg_size; p->next = h->data; h->data = p; }
//写入数据 ret = h->data->pos; memcpy(ret, str, str_len); ret[str_len] = 0; h->data->pos += str_len + 1; return ret; }
到现在,全局变量的值已经可控的了。
如何使用修改全局变量导致远程代码执行,可参考poc