之前学习web的Nginx解析漏洞就看到了FastCGI协议,今天学习一下。顺便看看有什么漏洞可以复现
Nginx解析漏洞
Nginx接受用户的http请求后生成的cgi环境变量有一SCRIPIT_NAME和PATH_INFO比如想要请求
/var/www/html/example.gif/.php那么/var/www/html/example.gif就是SCRIPT_NAME
/.php就是PATH_INFO之后我们就知道了,原本这两者应该分开,但是nginx将这两个一起交给php解释器去定位php文件
那么不存在这个文件就会报错了
包括php.ini中有一个那个选项是fix_pathinfo那么如果给的文件不存在,会去掉最后一个\之后的内容再去寻找,这就导致了解析漏洞,所以本质上其实就是配置错误的运维漏洞
想要检测也是很简单
这个漏洞会导致nginx将任意文件解析成php文件
比如web根目录下的robots.txt文件,如果我们访问robots.txt/.php
我们bp抓包后看到如果已经是Content-Type是text/html并且由php指纹
x-powered-by指定php解释器
php-fpm介绍
php作为一个服务端脚本编程语言,能够使用它制作强大功能的动态网页。一度被称之为世界上最好的编程语言。在大型企业内部,php往往以php-fpm的形式存在的。php-fpm是php官方提供的进程管理器,他是后台的一个守护进程,能够根据负载情况,管理不定数量的子进程。其中包括php处理器,能够接受web server传来的cgi环境变量,定位解析php文件,返回html代码。
大部分情况下php-fpm会通过fastCGI协议于web server之间进行通信。主要是传送一些环境变量
FastCgi协议介绍
fastcgi协议是一个二进制流通信协议,用于web server和其他应用服务之间通信。数据包称之为记录record。分为记录头Header和记录体Body,其中头部固定8字节长度,Body由header的contentlength决定
下面可以看一下record的c语言代码
typedef struct{
/*Header*/
unsigned char version; //版本
unsigned char type; //本次record类型
unsigned char requestIdB1; //请求id,两个字节记录
unsigned char requestIdB0;
unsigned char contentLength1; //Body长度,两个字节记录
unsigned char contentLength0;
unsigned char paddingLength; //填充长度
unsigned char reserved; //保留字节
/*Body*/
unsigned char contentData [coontentLength];
unsigned char paddingData [paddingLength];
} FCGI_Record;
fastcgi记录类型
fastcgi的数据包称之为record,它的type下面几种
FCGI_PARAMS #传递应用的cgi环境变量,name-value形式,最常用的一个type
FCGI_STDIN #服务器传递给应用的标准输入
FCGI_DATA #发送过滤器处理后的数据给应用
FCGI_STDOUT #应用返回服务器的标准输出
FCGI_STDERR #应用返回服务器的标注错误
FCGI_END_REQUEST #表示结束此次请求。可以是服务器或者应用发送的
其中当type是PARAMS的时候,web server发送cgi环境变量给应用。这个cgi环境变量其实就是各种配置。
name-value形式有四种实现方式。FastCGI会根据长度自动选择合适的,减少内存消耗
typedef struct{
unsigned char nameLengthB0;
unsigned char valueLengthB0;
unsigned char nameData[nameLength];
unsigned char valueData[valuelength];
}FCGI_NameValuePair11;
typedef struct{
unsigned char nameLengthB0;
unsigned char nameLengthB1;
unsigned char nameLengthB2;
unsigned char nameLengthB3;
unsigned char valueLengthB0;
unsigned char nameData[nameLength((B3 & 0x7f)<<24)+ (B2<<16)+(B1<<8)+B0];
unsigned char valueData[valuelength];
}FCGI_NameValuePair41;
typedef struct{
unsigned char nameLengthB0;
unsigned char valueLengthB0;
unsigned char valueLengthB1;
unsigned char valueLengthB2;
unsigned char valueLengthB3;
unsigned char nameData[nameLength];
unsigned char valueData[valuelength((B3 & 0x7f)<<24)+ (B2<<16)+(B1<<8)+B0];
}FCGI_NameValuePair14;
typedef struct{
unsigned char nameLengthB0;
unsigned char nameLengthB1;
unsigned char nameLengthB2;
unsigned char nameLengthB3;
unsigned char valueLengthB0;
unsigned char valueLengthB1;
unsigned char valueLengthB2;
unsigned char valueLengthB3;
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;
其实就是看key和value长度来分1一个字节还是4个字节
name,value都小于128B的时候, 使用FCGI_NameValuePair11;
name>128B value<128B 使用FCGI_NameValuePair41;
name<128B value<128B 使用FCGI_NameValuePair14;
name>128B value>128B 使用FCGI_NameValuePair44;
常见的CGI环境变量
REQUEST_METHOD http请求方式
SCRIPT_FILENAME 用户想要访问的文件名,绝对路径
SCRIPT_NAME 用户想要访问的文件名,相对路径
QUERY_STRING http请求传递的参数 包括get和post
DOCUMENT_ROOT web根目录
REMOTE_ADDR 用户ip地址
FastCGI漏洞
FastCGI默认监听9000端口,如果它暴露出来,没有防火墙对不信任ip的隔离策略,我们就可以自己构造FastCGI协议的记录,直接发送给PHP-FPM。都不需要经过web server了。其中特别重要的是SCRIPT_FILENAME这个变量。在php-fpm某个版本之前,都是比可以指定任意后缀名文件的。
但是后来出现一个配置 security.limit_extensions = .php .php3 .php4
这时我们只能访问php后缀的文件,而且得去寻找现有的文件 这显然不是那么好使用的漏洞。怎么样能够任意代码执行呢?
我当时想到了php.ini的一个配置 auto_prepend_file:它可以指定一个文件,让访问一个php文件之前就包含它,相当于include
这一招在ctf里面很常见,我们通常会上传一个文件 .user.ini
里面写入 auto_prepend_file=/var/log/nginx/access.log 让他指定nginx的访问日志
然后访问index.php 就会自动 include('/var/log/nginx/access.log');
这样我们只需要在http请求头里
User-Agent: 便成功制成了一个后门文件
比如 CTFSHOW有一题文件上传 https://ctf.show/challenges#web169-612,感兴趣可以去试试,解题原理就是利用.user.ini自动包含日志
这时我们只能访问php后缀的文件,而且得去寻找现有的文件 这显然不是那么好使用的漏洞。怎么样能够任意代码执行呢?
我当时想到了php.ini的一个配置 auto_prepend_file:它可以指定一个文件,让访问一个php文件之前就包含它,相当于include
这一招在ctf里面很常见,我们通常会上传一个文件 .user.ini
里面写入 auto_prepend_file=/var/log/nginx/access.log 让他指定nginx的访问日志
然后访问index.php 就会自动 include('/var/log/nginx/access.log');
这样我们只需要在http请求头里
User-Agent: 便成功制成了一个后门文件
比如 CTFSHOW有一题文件上传 https://ctf.show/challenges#web169-612 感兴趣可以去试试,解题原理就是利用.user.ini自动包含日志
但是这一招确实需要一个文件上传的接口,还是有局限性的
不上传文件.user.ini我们能不能设置呢?
答案是可以的,因为FastCGI给了两个环境变量,PHP_VALUE和PHP_ADMIN_VALUE 就是用来配置php的
而且还可以额外配置一个allow_url_include这样就支持文件包含使用伪协议
如此 我们可以通过FastCGI协议直接给PHP-FPM传入环境变量
自己编写脚本写一个fpm的记录还是挺烦的,直接使用p神的脚本了,修改一下地址就行
来源:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
首先还是Docker开启一下环境
然后Nmap看看是否开启了服务
Nmap扫除了端口时开发的,却没有识别出来9000端口服务是FastCGI啊
端口就这样直接开放了,我们直接访问FastCGI访问
从脚本中可以看到,就是使用POST方式访问
然后指定访问文件/usr/local/lib/php/System.php。并且配置了包含php://input,本质就是包含了请求体里的参数,所以,将payload写进了post的请求体里面
访问后PHP-FPM就会解析对应的/usr/local/lib/php/System.php文件
从结果看,PHP-FPM返回的确实是html代码