0x00 传统SSRF过滤
传统SSRF过滤器的方式大致是以下几个步骤:
(1) 获取到输入的URL,从该URL中提取host
(2) 对该host进行DNS解析,获取到解析的IP
(3) 检测该IP是否是合法的,比如是否是私有IP等
(4) 如果IP检测为合法的,则进入curl的阶段发包
乍一看,这种过滤方式似乎没有什么问题。
我们从DNS解析的角度看,该检测方式一共有两次,第一次是步骤2中对该host进行DNS解析,第二次是使用CURL发包的时候进行解析。这两次DNS解析是有时间差的,我们可以使用这个时间差进行绕过。
0x01 DNS Rebinding绕过
DNS Rebinding不是啥新技术了,之前有人用这个技术来做SOP绕过,我们先来看一下具体流程。
攻击者需要自己持有一个域名,然后将这个域名解析指向自己的DNS Server,在该server上写个解析服务,每次返回不同的解析结果。
比如:
你可以看到,这个解析服务每次返回的结果不同,第一次请求DNS查询,结果返回的是101.191.60.117,是一个合法的公网IP,但是第二次请求时,变成了私有IP 10.36.5.215。注意到,这两条记录的ttl都是0,这是为了防止有DNS服务器对解析结果进行缓存。
说到这里,基本就可以道出绕过原理了。从传统SSRF过滤思路来看,DNS解析一共分两次,其中第一次是至关重要的有效性检测,第二次则是具体发起的请求。我们利用DNS Rebinding技术,在第一次校验IP的时候返回一个合法的IP,在真实发起请求的时候,返回我们真正想要访问的内网IP即可。
该脚本的代码片段如下:
0x02 IP双重绑定绕过
深入探索一下,一般PHP在获取IP的时候通常是使用gethostname或者dns_get_record这俩函数。
我在一个域名下,同时绑定两个IP,来看看这两个函数是怎么处理的。
然后执行一下 dns_get_record看看:
如果使用的是gethostname来获取IP,则只会返回一个,返回哪个IP是随机的。
但是Curl在访问这种域名的时候,由于绑定的是两个IP,curl会尝试访问每一个IP,最终返回有效的那个。比如我一个域名绑定了两个IP,一个是1.1.1.1(80端口关闭),一个是2.2.2.2(80端口开放),在curl这个域名的时候,会返回2.2.2.2的请求结果。
如果SSRF过滤逻辑使用的是gethostname或者只获取了dns_get_record返回数组的第一个元素,那么就会存在被绕过的风险。
0x03 实战中的问题
事实上,基于DNS Rebinding的绕过方式在实战中可能会遇到一些问题。
问题一是DNS缓存的问题,即使我们在前面实现的时候设置了TTL为0,但是有些公共DNS服务器,比如114.114.114.114还是会把记录进行缓存,完全不按照标准协议来,遇到这种情况是无解的。但是8.8.8.8是严格按照DNS协议去管理缓存的,如果设置TTL为0,则不会进行缓存,从效果上来看,每次dig都会跑去我们的NS服务器上去查询一遍。
问题二是DNS迭代查询和递归查询的问题,往往这边发起攻击,DNS服务器会收到很多不同IP的查询请求,无法确定与受害服务器相关的来源IP是哪个。为此我一共实现了3版解析脚本,第一版很容易想到,首先对来源IP进行搜集,保存在文件中,然后真实发起请求的时候基于IP列表进行解析,但是后来发现还是很多莫名其妙的来源IP过来。但是仔细查看这些IP,发现都是某个B段或者C段的,很固定,因此第二版是基于IP段过滤,但是又有这种解析flag标志位交替不准确的问题。
最终,我实现一个时间窗口,用这个时间窗口去返回解析内容,比如前5s返回结果1,后5s返回结果2,对于时间窗口的具体值,需要探测阶段进行统计和尝试。
相关的代码不公开,有兴趣的可以自己实现以下,不是很难。