上一篇FPM提到了FastCGI协议,在这里粗略的整理下

协议分析

请求头

web服务器和FastCGI程序每传输一个消息,首先会传输一个8字节固定长度的消息头,这个消息头记录了随后要传输的这个消息的 类型,长度等等属性,消息头的结构体如下(go语言):

1
2
3
4
5
6
7
8
type header struct {
	Version       uint8 //版本
	Type          uint8 //操作类型
	Id            uint16 //请求id
	ContentLength uint16 //内容长度
	PaddingLength uint8 //填充字节的长度
	Reserved      uint8 //保留字节
}

Type字段用来说明每次所发送消息的类型,其具体值可以为如下

type值具体含义
1在与php-fpm建立连接之后发送的第一个消息中的type值就得为1,用来表明此消息为请求开始的第一个消息
2异常断开与php-fpm的交互
3在与php-fpm交互中所发的最后一个消息中type值为此,以表明交互的正常结束
4在交互过程中给php-fpm传递环境参数时,将type设为此,以表明消息中包含的数据为某个name-value对
5web服务器将从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm,这种消息的type就得设为5
6php-fpm给web服务器回的正常响应消息的type就设为6
7php-fpm给web服务器回的错误响应设为7
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const (
	FCGI_BEGIN_REQUEST uint8 = iota + 1
	FCGI_ABORT_REQUEST
	FCGI_END_REQUEST
	FCGI_PARAMS
	FCGI_STDIN
	FCGI_STDOUT
	FCGI_STDERR
	FCGI_DATA
	FCGI_GET_VALUES
	FCGI_GET_VALUES_RESULT
	FCGI_UNKNOWN_TYPE
	FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
)

PaddingLength:填充长度的值,为了提高处理消息的能力,我们的每个消息大小都必须为8的倍数,此长度表示在消息的尾部填充的长度

FastCGI采用大端序,所以如果收到消息头信息为0x01010FFF00080000

说明verison=1 type=1 id=4095 content length=8 padding length = 0

请求体

对于 FCGI_BEGIN_REQUEST 和  FCGI_END_REQUEST 消息类型,fastcgi协议分别定义了一个结构体如下,而对于其他类型的消息体,没有专门结构体与之对应,消息体就是普通的二进制数据。

1
2
3
4
5
type FCGI_BeginRequestBody struct {
    Role 		uint16
    Flags 		uint8
    Reserved 	uint8
}

role字段占2个字节,用来说明我们对php-fpm发起请求时,我们想让php-fpm为我们扮演什么角色,其常见的3个取值如下:

role值具体含义名字
1最常用的值,php-fpm接受我们的http所关联的信息,并产生个响应FCGI_RESPONDER
2php-fpm会对我们的请求进行认证,认证通过的其会返回响应,认证不通过则关闭请求FCGI_AUTHORIZER
3过滤请求中的额外数据流,并产生过滤后的http响应FCGI_FILTER

flags 是一个8位掩码, web服务器可以利用该掩码告知FastCGI程序在处理完一个请求后是否关闭socket连接 (最初协议的设计者可能还预留了该掩码的其他作用,只是目前只有这一个作用)

flags & FCGI_KEEP_CONN 的值为1,则FastCGI程序请求结束不关闭连接,为0 则关键连接

其中 FCGI_KEEP_CONN 是一个宏,定义如下:

1
const FCGI_KEEP_CONN uint8 = 1
1
2
3
4
5
type FCGI_EndRequestBody struct {
	AppStatus      uint32
	ProtocolStatus uint8
	Reserved       uint8
}

AppStatus是应用级别的状态码。每个角色记录其对appStatus的使用情况

ProtocolStatus 是协议级别的状态码,可能的protocolStatus值是:

ProtocolStatus值具体含义名字
1请求的正常结束,典型的应该都是该个值FCGI_REQUEST_COMPLETE
2拒绝新的请求。当Web服务器通过一个连接将并发请求发送到每个连接一次处理一个请求的应用程序时,会发生这种情况FCGI_CANT_MPX_CONN
3拒绝新的请求。当应用程序耗尽某些资源(例如数据库连接)时会发生这种情况FCGI_OVERLOADED
4拒绝新的请求。当Web服务器指定了应用程序未知的角色时,会发生这种情况FCGI_UNKNOWN_ROLE

还有种要说的就是当type=FCGI_PARAMS,传递环境参数时FCGI_PARAMS以键值对的方式传送,键和值之间没有’=‘,每个键值对之前会分别用1或4个字节来标识键和值的长度 例如:x0B\x02SERVER_PORT80\x0B\x0ESERVER_ADDR199.170.183.42上面的长度是用十六进制表示的 \x0B = 11 正好为SERVER_PORT的长度, \x02 = 2 为80的长度

消息发送流程

1、web服务器向FastCGI程序发送一个8字节 type=FCGI_BEGIN_REQUEST的消息头和一个8字节 FCGI_BeginRequestBody 结构的消息体,标志一个新请求的开始

2、web服务器向FastCGI程序发送一个8字节 type=FCGI_PARAMS 的消息头 和一个消息头中指定长度的FCGI_PARAMS类型消息体

3、根据FCGI_PARAMS消息的长度可能重复步骤 2 多次,最终发送一个8字节 type=FCGI_PARAMS 并且 contentLength 高位和低位都为0的消息头标志FCGI_PARAMS消息发送结束

4、以和步骤2、3相同的方式 发送FCGI_STDIN消息

5、FastCGI程序处理完请求后,以和步骤2、3相同的方式,发送FCGI_STDOUT消息和FCGI_STDERR消息返回给服务器

6、FastCGI程序发送一个type=FCGI_END_REQUEST的消息头和一个8字节FCGI_EndRequestBody结构的消息体,标志此次请求结束


FastCGI远程利用

前提

php >= 5.3.3

php-fpm监听9000端口

9000端口开放在外网或存在ssrf漏洞

利用

伪装成webserver,与php-fpm通过fastcgi进行交互,执行构造好的恶意代码

因为能够通过设置FASTCGI_PARAMS,利用PHP_ADMIN_VALUEPHP_VALUE去动态修改php的设置

1
2
3
env["REQUEST_METHOD"] = "POST"
env["PHP_VALUE"] = "auto_prepend_file = php://input"
env["PHP_ADMIN_VALUE"] = "allow_url_include = On\ndisable_functions = \nsafe_mode = Off"

再通过FCGI_STDIN传递要执行的php代码,利用执行php://input,然后在POST的内容中写入我们的php代码,这样就可以直接任意代码执行了

ssrf则配合gopher协议食用

exp

这里附上原作者的exp https://github.com/wofeiwo/webcgi-exploits/tree/master/php/Fastcgi

(这远程利用文章原作者原来在乌云发的


参考

https://blog.csdn.net/zhang197093/article/details/78914509 Author:zhang197093

https://blog.csdn.net/Shreck66/article/details/50355729 Author:Shreck66

https://fastcgi-archives.github.io/FastCGI_Specification.html

https://github.com/wofeiwo/webcgi-exploits/blob/master/php/Fastcgi/php-fastcgi-remote-exploit.md Author:wofeiwo