千家信息网

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

发表于:2024-10-02 作者:千家信息网编辑
千家信息网最后更新 2024年10月02日,本篇内容主要讲解"PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"PHP-FPM在Nginx特定
千家信息网最后更新 2024年10月02日PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析

本篇内容主要讲解"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 length10            int pilen = env_path_info ? strlen(env_path_info) : 0;  //Path info 长度 011            int tflag = 0;12            char *path_info;1314            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            }2425            if (tflag) {26                if (orig_path_info) {27                char old;2829                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"1112    // 经过重新计算处理后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")1617    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;1112    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;1718    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;2425    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) + 409642        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    }1920    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确定 index24        fcgi_hash_bucket *p = h->hash_table[idx];  //获取原有hash_table中的对应值2526        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) {3031                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        }3738        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        }4445        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;5152        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    }5960    static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len)61    // 实际操作request->env->data,进行数据写入。62    {63        char *ret;6465        if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) {66        //如果准备写入的数据长度大于当前指向的fcgi_hash_seg大小,则向前插入新的fcgi_hash_seg67                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            }7475            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_EBUTPHP_VALUE在长度和hash_value方面相等,且PHP_VALUE会在后续处理中被尝试读取(ini = FCGI_GETENV(request, "PHP_VALUE");)。满足条件4的要求。

除此之外,鉴于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"1112    // 经过重新计算处理后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 ")1617    path_info = env_path_info + pilen - slen;  // pilen < slen, 即偏移为-N

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

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

漏洞利用

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=\"\"", //指定加载的extension11    }

影响范围

在文初提到的配置下,该漏洞影响以下版本的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特定配置下任意代码执行漏洞举例分析"有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

位置 漏洞 变量 配置 指向 环境 长度 代码 指针 错误 偏移 影响 分析 数据 条件 处理 控制 文件 结构 相同 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 国家法律网络安全防骗法 数据库立即执行定时清除任务 网络安全监测分析师 天津工业软件开发代理价钱 肇庆应用软件开发订制 深圳头等舱互联网科技 怎么看哪个是服务器的管理口 天津库存管理软件开发公司 数据库考研信息 北京团建无忧科技互联网 我的世界方块云服务器英雄 网络安全校园日发言稿 信杨软件开发 网络安全在我身边的名人警句 枣庄管理系统软件开发 社交软件分享功能数据库设计 装了安全狗 服务器卡多了 优惠云服务器 网络安全手抄报 小学 简单 云服务器可以玩暗黑破坏神3吗 一个网络技术公司有多少人 安卓微信已删除好友数据库 枝江市软件开发技术 贵州创新网络技术服务单价 魔兽世界主宰之剑服务器在哪里 服务器一般使用什么语言 四川计算机基础数据库系统 校企携手拧紧网络安全阀 数据库部分字段相同模糊匹配 山西网络技术服务项目
0