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

 

 

posted @ 2019-11-02 14:41  ermei  阅读(554)  评论(0编辑  收藏  举报