• 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
    test.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

     

  • 相关阅读:
    linux服务器网络配置
    全面了解linux服务器
    centos selinux学习记录
    centos7使用samba共享文件
    centos7修改yum下载源为阿里源
    ubuntu14.04使用samba共享文件
    计算两个经纬度之间的距离(python算法)
    awk中的冒泡排序
    linux awk时间计算脚本
    linux shell中FS、OFS、RS、ORS图解
  • 原文地址:https://www.cnblogs.com/ermei/p/11781369.html
Copyright © 2020-2023  润新知