NodeJS-DNS模块DoS:CVE-2020-8277分析

漏洞概述

node这个dos洞是dns模块解析ttl时限定了数组大小导致的。当node的dns模块发出dns请求后,恶意dns server返回ttl数量大于256就会造成数组越界。之前误以为很多ttl的意思是多次cname,跟代码后发现只要返回一条包含很多ip的a记录,即可触发漏洞。

环境搭建

之前一直没怎么自己搭dns-rebinding的服务器,这里自己搭建,记录一下过程。

Step1: python dns server
这里用kunkun的,python2跑一下就行,注意vps的话可能需要先systemctl stop systemd-resolved,防止53端口占用

Step2: 设置域名解析
这里先配置cloudflare,生效快而且定义很简单。
添加两条记录就行。A记录指向跑python dns server的vps,NS记录指向A记录。
-w1459

然后配置域名的dns server地址为图中cloudflare提供的两个dns server地址。
之前是用的国内的腾讯阿里云啥的,改了dns server后,生效都非常慢,于是这里去namesilo买了。设置都不用改,只需要改一下dns server就行。

-w342

随手测一下dns-rebinding,没问题。这里环境就先算是能用了。

漏洞分析

漏洞commit,可以看到对naddrs值大于naddrttls的值是进行了限制。

1
2
3
4
https://github.com/nodejs/node/commit/a81aa37944a6b3efad49c15bbb62cbd1522631f4
Original commit message:
If there are more ttls returned than the maximum provided by the requestor, then the
*naddrttls response would be larger than the actual number of elements in the addrttls array.

根据描述也能大概看出,漏洞原因是返回的ttl数量大于nodejs中设置的ttl数组的容量,导致数组越界。
node中处理dns相关的逻辑使用的是c-ares库,我们跟一下c代码看看问题出在哪里。

-w651

往上跟,addrttls和naddrttls是调用时传入的,找一下调用:
-w820

在node对c-ares的调用中,传入了node里自定义的addrttls和naddrttls,而addrttls的是个256长度的数组。。。naddrttls自然时256了,当naddrs大于256时,赋值给naddrttls,就造成了数组越界。往下一看就有根据naddrttls长度去访问。。

-w397

-w640

addrttls存的是cname和a记录的ttl,因此这里只要让返回的ttls数量足够多就行。而DNS记录是只能返回一个ttl的,这怎么能让他ttl超过256个呢?我们去看一下c-ares的ares_parse_a_reply实现。

首先naddrs的值,可以看到由next链表长度决定。ai是一个ares_addrinfo结构体,由ares__parse_into_addrinfo2将输入buf解析得到。

-w362

进入ares__parse_into_addrinfo2,可以看到一个for循环根据ancount大小,将dns返回的ares_addrinfo_node插到ares_addrinfo.node链表尾。

-w520

那ancount的大小就是关键了。根据dns header结构,这个ancount是header中answer count的值,也就是相应的记录数量。这里我们改一下dns server让它返回256+的answer就行。

这里还挺有意思的,之前没看源码,以为要cname256次,就搭了俩dns server,互相cname,结果发现域名成环就不能继续cname了。于是又在域名前面random了一下,才可以cname256次,结果没卵用。。

-w458

-w623

现在可以response超过256的answer了,按道理在node里http.get一下应该就完了,结果。。咋断都断不下,死活不触发。调了一下node的源码,发现ares_init完了以后,就再也没进过ares。。。人都傻了

-w1440

后来再仔细看代码,发现node在注册回调时,只有QueryWrap相关的操作才会触发漏洞函数。再看看node,在dns.js中,只有resolve系列的函数才绑定了query系列,因此,这个漏洞只有dns.resolve4(host)这种类似的形式才能触发。。。着实鸡肋

-w753

-w726

看完怎么触发,后边就简单了,随便写个demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var express = require('express');
const http = require('http');
const request = require('request');
var dns = require('dns');

var app = express()

app.get('/', function (req, res) {
res.send('hello world')
})

app.get('/test',function(req, res){
dns.resolve4('a.lfytest.buzz', function (err, addresses, family) {
console.log('IP:%s,Protocal:%s', addresses, family);
});
})

var server = app.listen(8089, function () {
var host = server.address().address
var port = server.address().port
console.log("http://%s:%s", host, port)
})

跑一下,然后搭建一个dns server返回超过256个ip就行。但是这里我遇到了一个问题,我用之前的dns server改大返回的answer数量,node却爆了connect error,这让我很懵逼。。

查了一下资料,根据rfc(回头一定好好啃rfc…),当udp包大于512字节时,会转为tcp方式去连接。而我们的dns server就监听了udp的53,也没有tcp的处理逻辑,自然就connect error了。

DNS协议从UDP切换到TCP的过程如下:

1
2
3
4
1、客户端向服务器发起UDP DNS请求;
2、如果服务器发现DNS响应数据超过512字节,则返回UDP DNS响应中置truncated位,告知客户端改用TCP进行重新请求;
3、客户端向服务器发起TCP DNS请求;
4、服务器返回TCP DNS响应。

因此我在cloudfare里加了700多个a记录。。。这里就是dig的流量
-w1256

这样就成功断下来啦!但不知道为什么这里AddrTTLToArray进去之前,addrttls的长度时256,进去后变长了,导致我加了很多a记录才成功
-w1439

最后放个成功的图:
-w951

总结

这个洞本身有没有用呢?
毫无疑问,没有。
但我还是想去调试一下他,哪怕花了接近两天。一个原因是我对dns相关的知识并不是很熟想去看看,另一个原因很想学一下这种协议造成的dos是什么思路挖到的。这看一下,虽然就是个数组越界,但还是要知识面足够广,协议理解够深刻,才能处理各个bug,看到别人看不到的洞。

Proudly powered by Hexo and Theme by Hacker
© 2023 LFY