本来只是想验证一下漏洞,但是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_bucket
和fcgi_hash_seg
这两个结构。
fcgi_hash_bucket
是一个哈希表,保存着请求的环境变量的键值对:
fcgi_hash_seg
就是保存这些键值对的地方,前面是3个8字节的指针pos
(下一个要插入的变量的位置)、end
(当前fcgi_hash_seg
的data
块的终点)、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_seg
的pos
指针的最低位
接着把这一位置零,置零前
置零后
跟进FCGI_PUTENV
函数
进入fcgi_hash_set
函数,这里向前面提到的fcgi_hash_seg
的pos
指针指向进行了两次写操作,第一次是写参数名,第二次才是要写进去的php环境变量
参数名写完之后,pos
指针往后移了0x11
,现在开始写PHP_VALUE
前面提到fcgi_hash_bucket
是一个哈希表,所以php-fpm获取环境变量时也是根据哈希来获取,所以单单把PHP_VALUE
写进去是不行的,获取的时候会判断哈希和长度,EXP的作者是fuzz出了跟它长度一样、hash也一样的变量HTTP_EBUT
,最后是需要把PHP_VALUE
覆盖到这个HTTP_EBUT
上即可。由于长度可控,所以多余的字符不会影响变量读取。上一张图可以看到str
开头并不是PHP_VALUE
,h->data->pos
开头也不是HTTP_EBUT
,那是因为str前面还有11个字节的/index.php/
,因此h->data->pos
还需要往后移11字节完成对齐,完成这个工作的,就是在它前面新添加的http头。
最后为了演示简单,我就只写了auto_prepend_file=a
这个php变量,并在html目录下新建了a文件,最终完成攻击。
还是要感叹一下大佬的运气,这得多少巧合才能触发这个异常,(m _ _)n