对PHP-FPM RCE漏洞(CVE-2019-11043)的调试和分析

Posted by kuron3k0 on October 29, 2019

本来只是想验证一下漏洞,但是Emil Lerner大佬的EXP一直用不了,LoRexxar大佬的分析省略了一些关键信息也看不懂(好吧主要还是太菜了),只能自己动手调一遍了,还好90sec的maple大佬分析的很详细,不然感觉调不出来。。

0x00 漏洞成因

万恶之源,就是Nginx处理url的正则有个bug,在解析的时候把path_info置空了

0x01 漏洞利用

首先是Nginx这个点,加了换行然后就匹配不了,很像之前CTF题里面的情况,于是也下了Nginx源码调试了一下,发现正则处理是调用的libpcre.so,PHP调用正则时也是用的这个库

这里直接用PHP来模拟Nginx的处理过程。这是不带%0A的字符串,可以正常匹配

%0A在字符串中间的时候,libpcre会直接不匹配。

在结尾的时候又可以匹配了。

具体实现可能跟pcre正则的状态机有关,这里就不再深入了。

开始调PHP-FPM,这里我下载的是7.3.10的版本的源码,为了进行调试,编译的时候需要加上enable debug的选项

./configure --prefix=/root/php7.3.10 --enable-phpdbg-debug --enable-debug --enable-fpm CFLAGS="-g3 -gdwarf-4"

Nginx的配置

    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 /opt/nginx-src/nginx-branches-stable-1.10/html$fastcgi_script_name;
        fastcgi_param  DOCUMENT_ROOT /opt/nginx-src/nginx-branches-stable-1.10/html;
        fastcgi_pass 127.0.0.1:9000;
    }

一切准备就绪之后,用gdb连上php-fpm,漏洞函数init_request_info打上断点,然后在burp发包,现在已经进到这个函数

获取Nginx传过来的PATH_INFO参数,可以看到是值是空的

这里涉及到几个参数

path_info赋值,因为env_path_info指针的指向虽然是空的,但env_path_info的值是指针的地址,而且pilen是0,所以path_info得到的是env_path_info指针向上偏移slen的值

这里有个关键的点就是fcgi_hash_bucketfcgi_hash_seg这两个结构。

fcgi_hash_bucket是一个哈希表,保存着请求的环境变量的键值对:

fcgi_hash_seg就是保存这些键值对的地方,前面是3个8字节的指针pos(下一个要插入的变量的位置)、end(当前fcgi_hash_segdata块的终点)、next(下一个fcgi_hash_seg的位置),后面的data就是连续的键值对,fcgi_hash_bucket中的指向就在这里:

当一个fcgi_hash_seg到达一定大小后,再插入变量会重新分配一个fcgi_hash_seg,所以通过调整其他参数,可以看到前面提到的env_path_info刚好是位于一个新块的开头

path_info指针减去了len('PHP_VALUE%0Aauto_prepend_file=a;;;;')之后,刚好指向了fcgi_hash_segpos指针的最低位

接着把这一位置零,置零前

置零后

跟进FCGI_PUTENV函数

进入fcgi_hash_set函数,这里向前面提到的fcgi_hash_segpos指针指向进行了两次写操作,第一次是写参数名,第二次才是要写进去的php环境变量

参数名写完之后,pos指针往后移了0x11,现在开始写PHP_VALUE

前面提到fcgi_hash_bucket是一个哈希表,所以php-fpm获取环境变量时也是根据哈希来获取,所以单单把PHP_VALUE写进去是不行的,获取的时候会判断哈希和长度,EXP的作者是fuzz出了跟它长度一样、hash也一样的变量HTTP_EBUT,最后是需要把PHP_VALUE覆盖到这个HTTP_EBUT上即可。由于长度可控,所以多余的字符不会影响变量读取。上一张图可以看到str开头并不是PHP_VALUEh->data->pos开头也不是HTTP_EBUT,那是因为str前面还有11个字节的/index.php/,因此h->data->pos还需要往后移11字节完成对齐,完成这个工作的,就是在它前面新添加的http头。

最后为了演示简单,我就只写了auto_prepend_file=a这个php变量,并在html目录下新建了a文件,最终完成攻击。

还是要感叹一下大佬的运气,这得多少巧合才能触发这个异常,(m _ _)n

0x02 Reference