Use DNS Rebinding to Bypass IP Restriction
0x00 前言
在不久前的一个渗透测试中,我遇到一个客户自己实现的代理。这个代理可以用来翻墙,但是由于这个代理搭建在客户内网中,所以同样可以访问内网资源。
报告给客户后,客户予以修复。在之后的复测中,访问内网资源的时候返回 403,只能请求非黑名单 IP 段中的地址。
>>> export all_proxy=http://u:p@proxy_server:1234
>>> curl 10.0.0.1 -v
* Rebuilt URL to: 10.0.0.1/
* Trying proxy_server...
* TCP_NODELAY set
* Connected to proxy_server (proxy_server) port 1234 (#0)
* Proxy auth using Basic with user 'u'
> GET http://10.0.0.1/ HTTP/1.1
> Host: 10.0.0.1
> Proxy-Authorization: Basic dTpw
> User-Agent: curl/7.51.0
> Accept: */*
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 403 Forbidden
< Content-Type: text/plain; charset=utf-8
< X-Content-Type-Options: nosniff
< Date: Mon, 11 Dec 2016 13:10:23 GMT
< Content-Length: 43
<
Request URL http://10.0.0.1/ is forbidden.
于是利用一般性的绕过方式,比如:
- http://baidu.com@10.0.0.1
- http://test.loli.club (ip: 10.0.0.1)
- 301 / 302 Redirect
- file:///etc/passwd
- gopher protocol
- ftp protocol
等一系列姿势都以失败告终。于是开始思考其验证 IP 的具体方式,尝试绕过 IP 限制请求内网。
0x01 IP 验证方式
一般来说,验证 IP 是否在范围的方式如下图所示。
获取到请求的地址后,如果为域名的话,则通过 DNS 解析的方式获取到真实的 IP 地址,如果直接是 IP 地址的话,则直接对比是否在指定的 IP 段内。
比如如上的 test.loli.club 请求获得的 IP 地址为 10.0.0.1,黑名单 IP 段为 10.0.0.0/8,则会提示拒绝访问。
一般来说这种验证没有什么问题,但是通过 DNS Rebinding 技术来进行攻击的话,就可以轻而易举地绕过这个 IP 限制。
0x02 DNS Rebinding
上图所示的验证方法是存在问题的。服务器从获得请求的 URL 开始,到利用 URL 的 Hostname 获取到 IP 地址,再从判断 IP 地址到请求 URL 之间,是有一个时间差的。利用这个时间差,我们可以做一些事情。
众所周知,DNS 返回的数据包中存在一个 TTL(Time-To-Live),也就是域名解析记录在 DNS 服务器上的缓存时间。如果两次 DNS 请求的时间大于 TTL 的大小的话,那么就会重新进行一次 DNS 解析请求。
如果我们在第一次请求 DNS 解析时返回一个不在黑名单里面的 IP 地址,然后在第二次服务端请求 URL 的时候,让服务器再请求一次 DNS 解析,这次解析到黑名单内的地址,且没有任何验证,利用个短暂的时间差来绕过验证。
我们把 DNS 服务器的 TTL 设置为 0,这样就可以有足够的时间来让服务器再次请求 DNS 服务器而导致绕过 IP 黑名单限制。
0x03 攻击配置
要进行攻击首先需要一个域名,然后配置一个 NS 记录,指向攻击者配置的 DNS 服务器。
在 DNS 服务器上搭建一个 DNS 服务,核心代码如下:
测试请求 1.asf.loli.club:
两次 DNS 请求的结果不同。测试在实际环境中可以绕过 IP 验证。由于保密原因就不再提供真实环境的测试图片,但是实际上已经成功请求其内网的 gitlab、kms 等关键服务了。
0x04 攻击面
- CSRF/XSS 窃取用户数据
- 绕过 SSRF IP 限制
- 绕过代理 IP 限制
0x05 缓解措施
利用第一次请求解析的 IP 来进行后续的 HTTP/HTTPS 请求即可。
def dns_resolve(hostname):
...
def check_ip(ip):
...
url = input()
ip = dns_resolve(urlparse(url.hostname))
if not check_ip(ip):
return '403 Forbidden', 403
data = requests.get(ip, headers={'Host': url.hostname})
return data.content, data.status_code