利用DNS缓存和TLS协议将受限SSRF变为通用SSRF
本文首发于先知社区
前言
这是今年BlackHat上的一个议题:When TLS Hacks You,作者是latacora的Joshua Maddux
议题提出了一个新的ssrf攻击思路,利用DNS重绑定技术和https的优化算法(TLS Session resumption)的缺陷,将受限的SSRF变为通用的SSRF。攻击方法与之前的SNI injection类似,但是这次利用的是TLS的一种特性,而不是在特定实现中的bug。
本文是对When TLS Hacks You
这一议题的一些简单总结和分析,如有错误或不当之处请大佬们多多包涵。
背景知识
SSRF
原理
其形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制,导致攻击者可以传入任意的地址来让后端服务器对其发起请求,并返回对该目标地址请求的数据。
危害
- 内网、本地端口扫描,获取开放端口信息
- 主机信息收集,web应用指纹识别,获取服务banner信息
- 根据识别出的应用针对性的发送payload攻击,例如struts2
- 攻击内网和本地的应用程序及服务。
- 穿越防火墙
- 利用file协议读取本地文件,比如
file:///etc/passwd
常用协议
协议名称 | 简介 |
---|---|
Gopher协议 | 攻击内部应用的主力军 |
Dict协议 | 端口探测,版本信息收集 |
ftp协议 | 探测是否存在ftp |
http协议 | 探测是否存在ssrf |
file协议 | 读取本地文件 |
注:jdk1.7后java不再支持gopher
防御手段
- 禁止跳转
- 过滤返回信息
- 禁用不需要的协议
- 设置URL白名单或者限制内网IP
- 限制请求的端口为http常用的端口
- 统一错误信息
在请求资源前先访问DNS服务器判断是否为内网IP
DNS rebinding
针对上图防御手段的一种绕过
TTL
TTL是英语Time-To-Live的简称,意思为一条域名解析记录在DNS服务器中的存留时间。当各地的DNS服务器接受到解析请求时,就会向域名指定的NS服务器发出解析请求从而获得解析记录;在获得这个记录之后,记录会在DNS服务器中保存一段时间,这段时间内如果再接到这个域名的解析请求,DNS服务器将不再向NS服务器发出请求,而是直接返回刚才获得的记录;而这个记录在DNS服务器上保留的时间,就是TTL值。
它表示DNS记录在DNS服务器上缓存的时间,数值越小,修改记录各地生效的时间越快。
DNS解析
- DNS区域是一个层次结构的空间, 根域名服务器->子域名服务器->二代子域名服务器
- DNS查询方式: 递归和迭代
递归
迭代
解析流程
- 检查浏览器缓存中是否缓存过该域名对应的IP地址
- 如果在浏览器缓存中没有找到IP,那么将继续查找本机系统(如HOSTS文件)是否缓存过IP
- 向本地域名解析服务系统(路由器或者内网DNS服务)发起域名解析的请求
- 开始递归查询运营商dns -> 根域名服务器 -> 顶级域名(gTLD)服务器 -> 我们设置NS域名服务器。(这一步很重要,我们可以递归向下设置TTL的值)
- NS返回IP地址给本地服务器,本地服务器缓存解析结果(TTL)
- 解析结果返回给用户,建立TCP通信
DNS重绑定
当我们发起域名解析请求的时候,第一次访问会返回一个ip地址A,但是当我们发起第二次域名解析请求的时候,却会返回一个不同于A的ip地址B。
攻击思路是基于这种SSRF防御思路的基础上的,检查逻辑是第一次DNS查询请求确定host是不是内网IP,第二次请求的时候存在一个小间隔,导致了解析的差异性。
尝试
如下图设置好域名的A记录及NS记录,指定DNS服务器为自己搭建的
在vps上运行代码启动DNS服务(嫖的大佬的代码实现)
# -*- coding:utf-8 -*-
from twisted.internet import reactor, defer
from twisted.names import client, dns, error, server
record={}
class DynamicResolver(object):
def _doDynamicResponse(self, query):
name = query.name.name
if name not in record or record[name]<1:
ip="106.56.229.29" #这个ip只要是外网ip就行
else:
ip="127.0.0.1"
if name not in record:
record[name]=0
record[name]+=1
print name+" ===> "+ip
answer = dns.RRHeader(
name=name,
type=dns.A,
cls=dns.IN,
ttl=0, # 这里设置DNS TTL为 0
payload=dns.Record_A(address=b'%s'%ip,ttl=0)
)
answers = [answer]
authority = []
additional = []
return answers, authority, additional
def query(self, query, timeout=None):
return defer.succeed(self._doDynamicResponse(query))
def main():
factory = server.DNSServerFactory(
clients=[DynamicResolver(), client.Resolver(resolv='/etc/resolv.conf')]
)
protocol = dns.DNSDatagramProtocol(controller=factory)
reactor.listenUDP(53, protocol)
reactor.run()
if __name__ == '__main__':
raise SystemExit(main())
可以看到代码用twisted实现了dns服务,并且设置了TTL为0
效果如下
TLS session resumption
TLS shake
一个完整的 TLS 握手需要两次
- Client 发送 ClientHello;Server 回复 ServerHello
- Client 回复最终确定的 Key,Finished;Server 回复 Finished
- 握手完毕,Client 发送加密后的 HTTP 请求;Server 回复加密后的 HTTP 响应
这一过程的花费是 2RTT(Round-Trip-Time),为了减少这一开销,可以用TLS session resumpition将密钥缓存起来,下次建立链接时直接使用。
优化方案 | 原理 |
---|---|
session id | 服务端记住会话状态,客户端发带session id的client hello,服务端返回之前存储的SSL会话 |
session ticket | 客户端记住会话状态,服务端记住用于加密返回给客户端的ticket的密钥 |
PSK | 客户端和服务器第一次建立会话时,会生成一个PSK(pre-shared key)。服务器会用ticket key去加密 PSK,作为Session Ticket返回 |
session id
的相关数据是存放在 server 端,session ticket
是存放在 client 端, 在这个攻击手法中 session id
只能存放 32 byte 的 payload,session ticket
则能放更多字节的 payload。
https协议的实现(CURL)
在curl的实现代码中只检查了域名、端口和协议。而没有检查IP,因此可以用DNS重绑定的手段。
攻击方式
将上述三者结合,利用DNS缓存和TLS协议将受限SSRF变为通用SSRF。
实际漏洞案例
- Youtrack — CVE-2019-12852JetBrains YouTrack 服务的SSRF
- Nextcloud — 一个分享功能造成的SSRF,使用TLS重绑定攻击本地的memcached
攻击面
此种攻击手法需要三个条件,大部分受限的SSRF,带外通信的TLS session,本地端口上运行的应用。
SSRF攻击点
- OIDC discovery(sometimes)
- Webpush
- Webmention
- Apple Pay Web
- In browsers, just phishing people (Then we call it CSRF)
- Wificaptive portals
- SSDP
- SVG conversion
- URL-based XXE
- Scraping
- Webhooks
- PDF renderers with images enabled
TLS session缓存情况
易受攻击的内网应用
攻击流程
Demo:Phishing->CSRF->RCE
作者给了一个Demo。假设受害者是一个使用django.core.cache的项目的开发者,并且使用了memcached。受害者在Chrome等易受影响的浏览器中浏览电子邮件。受害者有一定安全意识,不会下载邮件中的附件。
攻击者会制作一封钓鱼邮件
邮件中的图片标签指向攻击者准备好的网站
然后弹计算器
整个攻击流程如图所示
- 攻击者制作钓鱼邮件,内容标签会向
https://ssltest.jmaddux.com:11211
(攻击者准备的网站)发起请求 - 受害者打开邮件,内容开始加载并请求域名
- 受害者客户端向
ssltest.jmaddux.com
的NS记录指定的DNS服务器请求DNS解析,DNS服务由攻击者搭建的DNS Server提供 - DNS服务器返回正常对应的TLS Server地址,并且设置TTL为0
- 客户端发送Client Hello
- 服务端返回Server Hello,并在包中设置payload(添加在
session id
/session ticket
/psk
字段中) - 进行后续的TLS握手
- 握手完成后进行http通信时,网站返回301跳转到
https://ssltest.jmaddux.com:11211
- 由于之前设置TTL为0,客户端再次向DNS服务器询问网站IP地址
- 此时DNS服务器返回
127.0.0.1
- 由于TLS会话重用的优化算法,客户端会读取带有payload的SSL会话缓存访问
127.0.0.1:11211
- payload加载完成RCE
未来工作
作者在讲述未来工作时提出了要建设更好的测试基础设施
- Alternating DNS Server:实现DNS重绑定
- Custom TLS:提供https服务,并传输payload
- Just netcat:监控受害者与Custom TLS的交互流量,判断攻击状态
github项目:https://github.com/jmdx/TLS-poison
防御手段
作者提出了三种防御手段
-
改变缓存的key值
- 现在是(
hostname
,port
),修改为(hostname
,port
,ip_addr
)更好 - 对于大型TLS部署的问题
- 可以修改为(
hostname
,port
,addr_type(ip_addr)
) - 类似于[CORS and RFC1918](https://wicg.github.io/cors rfc1918/)
- 可以修改为(
- 现在是(
-
禁用带外通信时的TLS session resumption
配置
libcurl: CURLOPT_SSL_SESSIONID_CACHE=false
firefox: security.ssl.disable_session_identifiers=true
Tor browser: disabled by default
Java, Nodejs, Chrome, others: no option
- WEB APP
- web app不能禁止TLS session resumption
- 关注类似 webhooks, apple pay 的应用
- 对出站请求设置代理进行监控(比如smokescreen工具)
- 阻止未进行身份校验的内部TCP内容运行,特别是带换行符的内容
其实从其他方面也能防御,比如杜绝DNS重绑定,第一次解析后直接使用解析返回的ip替换域名访问url
总结
这个议题技术覆盖比较全面,攻击手法也很新颖,作者在介绍的过程中也提出了对安全与性能之间平衡的思考:https本来是防止中间人攻击提出的安全协议,却因为对此的优化算法使其变得易受攻击。安全开销与时间开销如何取舍?
无论是议题本身还是作者挖掘漏洞的方法,思考问题的全面性,都有很多值得我们学习的地方。
作者最后也提出了一些总结:
- 过去可能不太重视https在SSRF中的应用,这个攻击方式证明TLS对SSRF来说是有用的
- 随时关注了解最新的技术手段对于打破常规是很有帮助的
- 我们需要认真考虑TLS会话重用的利弊
参考
https://github.com/jmdx/TLS-poison
https://portswigger.net/daily-swig/when-tls-hacks-you-security-friend-becomes-a-foe
https://securityboulevard.com/2020/08/def-con-safe-mode-joshua-madduxs-when-tls-hacks-you/
https://defcon.org/html/defcon-safemode/dc-safemode-speakers.html#Maddux
https://xz.aliyun.com/t/7495
https://wiki.jikexueyuan.com/project/openresty/ssl/session_resumption.html
https://mp.weixin.qq.com/s/GT3Wlu_2-Ycf_nhWz_z9Vw
https://mp.weixin.qq.com/s/-NABL-xz1Allxr6SKGiYcQ