SSRF漏洞修复方案

SSRF在服务端代码中属于常见的漏洞,关于SSRF漏洞的介绍及攻击方式在网络上都有详细的资料,这两篇就写的非常详细了:

SSRF安全指北 - 博客 - 腾讯安全应急响应中心

SSRF Tips | xl7dev

目前互联网企业中的项目,主要以Java项目为主,我们知道在Java语言下,SSRF漏洞的利用场景一般比较有限,很少能通过该漏洞进行文件读取等操作,一般来说能进行漏洞利用的场景如下:

  1. 通过访问内网地址对内网服务进行探测、盲打
  2. CRLF注入
  3. 利用该接口作为DDOS攻击的一个发起点(可限制访问频率来解决,本文暂不讨论)

因此本文只针对内网探测类的SSRF攻击类型来进行防御修复讨论,由于在不同的业务场景下,制定一套相对通用的SSRF修复方案时是存在一定困难的,一般会产生两种不同的场景:

  1. 服务器需要访问的资源可以限制在一定范围之内,比如我们自己的oss服务器域名
  2. 服务器需要访问的资源无法设限,必须要访问任意的网络资源

我们根据不同的业务场景,设定了3种修复方案,其中前两种分别对应以上两个业务场景,第三种作为最终的解决方案,但实施起来比较复杂,目前无法实现,仅作为探索。

修复方案代码仅基于Java编写

1. 可对域名添加白名单的修复方案

/*
1. ssrf可增加白名单的修复方案,检查域名非法字符,白名单限制二级域名
 */
public boolean ssrfFilterSecondaryDomain(String externalUrl) throws MalformedURLException {
 
    URL url = new URL(externalUrl);
    String domain = url.getHost();
    if (!domainFilter(domain)){
        return false;
    }
    String secondaryDomain = getSecondaryDomain(domain);
 
    List domainWhiteList = new ArrayList<>();
    domainWhiteList.add("yuanfudao.com");
    domainWhiteList.add("fbcontent.cn");
 
    for (Iterator it = domainWhiteList.iterator(); it.hasNext();) {
        String value = it.next();
        if(secondaryDomain.equals(value)){
            return true;
        }
    }
    return false;
}
 
private static String getSecondaryDomain(String domain) {
    String[] domains = domain.split("\\.");
    return domains[domains.length - 2] + "." + domains[domains.length - 1];
}
 
private static boolean domainFilter(String domain) {
    String regex = "[^a-zA-Z0-9\\.-]";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(domain);
    return !matcher.find();
}

这种场景修复起来较为简单,把服务器需要访问的资源限制在一定范围之内即可。

对url中host部分先进行域名有效字符的校验,防止使用反斜线等特殊字符进行绕过;再基于白名单对二级域名进行校验。

例如上述代码提供的ssrfFilter方法就可以对url进行一次过滤,符合规则返回true,可以使服务器该url进行访问,反之则不能访问。

其中的domainWhiteList部分可以由开发人员根据业务需求指定。

2. 无法添加白名单的修复方案

/*
2. 无法添加白名单的修复方案
    检测非法字符CRLF
    ->通过域名解析ip
    ->判断ip是否为内网地址
    ->固定url中的host部分为该ip,同时设置headers中的host为域名,防止某些站点检测host不为域名时不能访问
    ->请求不跟随302跳转
 可解决大部分ssrf问题
 */
public URLConnection httpSecureAccess(String externalUrl) throws IOException {
 
    URL exurl = new URL(externalUrl);
    String host = exurl.getHost();
    String protocol = exurl.getProtocol();
    // 判断是否存在非法特殊字符
    String decode = URLDecoder.decode(externalUrl, "utf-8");
    if (decode.contains("\r\n")){
        //System.out.println("Illegal characters CRLF error");
        throw new IllegalArgumentException();
    }
    // 判断ip是否为内网,目前只判断ipv4
    InetAddress[] addresses = InetAddress.getAllByName(host);
    List ips = new ArrayList<>();
    for (int i = 0; i < addresses.length; i++) {
        if(addresses[i] instanceof Inet4Address) {
            if (ipIsInner(addresses[i].getHostAddress())) {
                //System.out.println("Illegal address error");
                throw new IllegalArgumentException();
            } else {
                ips.add(addresses[i].getHostAddress());
            }
        }
    }
    // 若该域名对应多个ip则随机取值,同时重新拼接URL,设置host为域名,同时不跟随302跳转
    Collections.shuffle(ips);
    String ip = ips.get(0);
    String newurl = externalUrl.replaceFirst(host, ip);
    System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
    URL url = new URL(newurl);
    // http和https请求分别处理,其他协议直接拒绝
    if (protocol.equals("https")) {
 
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
        connection.setInstanceFollowRedirects(false);
        connection.setRequestMethod("GET");
        connection.setRequestProperty("Host", host);
        connection.setHostnameVerifier(new TrustAnyHostnameVerifier());
        return connection;
    }
    else if (protocol.equals("http")) {
 
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setInstanceFollowRedirects(false);
        connection.setRequestMethod("GET");
        connection.setRequestProperty("Host", host);
        return connection;
    }
    else {
        //System.out.println("Illegal protocol error");
        throw new IllegalArgumentException();
    }
}
 
static class TrustAnyHostnameVerifier implements HostnameVerifier {
    public boolean verify(String hostname, SSLSession session){
        return true;
    }
}
 
static List ipFilterRegexList = new ArrayList<>();
static {
 
    Set ipFilter = new HashSet();
    ipFilter.add("^10\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])"
            + "\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])" + "\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
    ipFilter.add("^172\\.(1[6789]|2[0-9]|3[01])\\" + ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\"
            + ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
    ipFilter.add("^192\\.168\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\"
            + ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
    ipFilter.add("127.0.0.1");
    ipFilter.add("0.0.0.0");
    ipFilter.add("localhost");
 
    for (String reg : ipFilter) {
        ipFilterRegexList.add(Pattern.compile(reg));
    }
}
 
public static boolean ipIsInner(String ip) {
    for (Pattern reg : ipFilterRegexList) {
        Matcher matcher = reg.matcher(ip);
        if (matcher.find()){
            return true;
        }
    }
    return false;
}

由于业务需求导致无法添加域名的白名单时,可从多个角度对url进行限制,尽量防止攻击的发生。

观察公司业务发现http请求的类大都使用的是HttpURLConnection,因此上述代码也以该类作为演示,其他类具体使用到的方法可能略有不同,但修复思路是相同的。

这种情况下的修复思路如下:

  1. 限制访问方法只能使用GET
  2. 限制访问的协议只能是http或https,其他协议直接拒绝访问
  3. 检测特殊字符CRLF,存在则直接拒绝访问
  4. 设置setInstanceFollowRedirects属性为false,禁止跟随302跳转;禁止跳转的目的是防止攻击者利用跳转访问内网,从而绕过之后的黑名单校验
  5. 解析url中域名对应的ipv4地址:
    1. 该地址先过黑名单校验,不允许是内网地址,利用正则表达式对ip进行过滤
    2. 使用ip对url中的域名进行替换,访问资源时都以ip进行访问;这样可以防御DNS Rebinding类的攻击

使用ip替换域名这一操作会导致访问资源出现一些问题,比如某些站点会对headers中的host进行校验,如果host不是域名则访问会被拒绝;以及https证书校验的过程也会受阻。

以上问题的解决方案:

  1. 使用setRequestProperty方法设置host头为域名,这样使用ip访问资源则不会受阻;但必须先设置System.setProperty("sun.net.http.allowRestrictedHeaders", "true"),才可以使host字段被修改,否则不能生效,上述代码中都有所体现
  2. https请求时使用setHostnameVerifier方法直接信任该域名的证书(这样直接信任证书不确定会不会带来其他安全问题,后续再进行研究讨论)

上述代码提供的httpSecureAccess方法,输入为url,如果该url符合规范则返回一个URLConnection类的connection连接,开发人员可以直接使用这个connection进行资源读取等操作;如果url不符合规范则直接抛出异常。

这部分修复方案可以保证绝大多数情况下对SSRF进行防御,但由于对http请求进行了较多的修改,可能会产生其他问题,因此还要经过实践验证。

3. 更安全的修复方案

/*
3. 更安全的修复方案,通过代理服务器访问外网资源,且该服务器对内网隔离。可更彻底的解决ssrf问题,但修复成本较高。
 */
public HttpURLConnection httpProxyAccess(String externalUrl) throws IOException {
 
    String proxyHost = "10.x.x.x";
    int proxyPort = 8080;
 
    String proxyUser = "";
    String proxyPass = "";
 
    InetSocketAddress isa = new InetSocketAddress(proxyHost, proxyPort);
    Proxy proxy = new Proxy(Proxy.Type.HTTP, isa);
    Authenticator.setDefault(new CustomAuthenticator(proxyUser, proxyPass));
 
    URL url = new URL(externalUrl);
    HttpURLConnection connection = (HttpURLConnection)url.openConnection(proxy);
    connection.setInstanceFollowRedirects(false);
    connection.setRequestMethod("GET");
    return connection;
}
 
static class CustomAuthenticator extends Authenticator {
    private String user = "";
    private String password = "";
 
    public CustomAuthenticator(String user, String password) {
        this.user = user;
        this.password = password;
    }
 
    protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication(user, password.toCharArray());
    }
}

最安全的修复方案是增加服务器集群,专门用来对需要访问资源的服务进行代理。

可分为内网集群和外网集群, 例如需要访问外网资源的服务走外网的代理集群,且该集群对办公内网隔离。

这样一来所有访问资源的请求都由专用的服务器集群发出,避免了内网探测的风险;一旦有利用SSRF进行任意文件读取的场景,也不会对生产环境的服务器本身造成危害。

但是该方案操作难度较大,我们现在的情况还不具备设置服务器集群作为代理的条件,因此仅作为一种思路进行讨论。

你可能感兴趣的:(Web安全,java)