java-sec-code-master搭建&&审计

java-sec-code-master搭建

源码地址:

https://github.com/JoyChou93/java-sec-code

搭建环境:

IDEA

apache-maven-3.9.1

apache-tomcat-9.0.105

JDK 1.8

MySQL 5.7.26

数据库:小皮面板自带的MySQL,导入sql文件

  1. 在项目结构里面,然后填上你的jdk1.8的bin目录

    image-20250827162302231

  2. 直接创建数据库

    我使用的是phpstudy

    image-20250827162426689

  3. 然后把 create_db.sql文件的连接数据库的好密码改为自己的

    (记得到导入数据库配置文件 create_db.sql)

  4. 然后配置一下tomcat

    去官网下载tomcat,解压出来就是这样的

    image-20250827162630334

  5. 然后在设置里的这个地方添加这个

    image-20250827162731912

  6. maven使用IDEA自带就行

    (也可以自己下载)

    和之前一样,使用 IDEA 打开项目,执行 mvn clean install,然后运行主类即可启动。

    运行application.java(成功后会显示这个)

    image-20250827163223030

  7. 运行成功之后,下次直接使用这个就可以启动啦

    image-20250827163153762

  8. 直接登录就好了

    image-20250827163341209

java-sec-code-master审计

使用IDEA搭建了java-sec-code-master

(使用时,要打开phpstudy运行mysql)

命令注入

src\main\java\org\joychou\controller\CommandInject.java

  1. 存在三个方法

    1
    2
    3
    4
    @GetMapping("/codeinject")
    @GetMapping("/codeinject/host")
    @GetMapping("/codeinject/sec")
    //codeinject/sec是安全方法(其它两个存在漏洞)
    • /codeInject

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @GetMapping("/codeinject")
          public String codeInject(String filepath) throws IOException {
      
              //String[] cmdList = new String[]{"cmd", "-c", "dir -la " + filepath};
              String[] cmdList = new String[]{"cmd.exe", "/c", "dir  " + filepath};
              ProcessBuilder builder = new ProcessBuilder(cmdList);
              builder.redirectErrorStream(true);
              Process process = builder.start();
              return WebUtils.convertStreamToString(process.getInputStream());
          }

      filepath并未做任何的过滤,直接拼接

      在windows中使用&符号拼接cmd命令

      由于是web服务应用,使用&符号拼接url命令

    • payload

      localhost:9000/codeinject?filepath=.%26ipconfig

      (页面返回主机配置)

    • /codeinject/host

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @GetMapping("/codeinject/host")
          public String codeInjectHost(HttpServletRequest request) throws IOException {
      
              String host = request.getHeader("host");
              logger.info(host);
              //String[] cmdList = new String[]{"cmd", "-c", "curl " + host};
              String[] cmdList = new String[]{"cmd.exe", "/c", "dir  " + host};
              ProcessBuilder builder = new ProcessBuilder(cmdList);
              builder.redirectErrorStream(true);
              Process process = builder.start();
              return WebUtils.convertStreamToString(process.getInputStream());
          }

      http的请求头host(无过滤)

    • payload

      抓包之后,在数据包中修改 host:localhost&ipconfig

      (返回包显示主机信息)

    • codeinject/sec(不行)

      原因:(存在过滤)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
       @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[]{"cmd", "-c", "dir -la " + filterFilePath};
              String[] cmdList = new String[]{"cmd.exe", "/c", "dir  " + filterFilePath};
              ProcessBuilder builder = new ProcessBuilder(cmdList);
              builder.redirectErrorStream(true);
              Process process = builder.start();
              return WebUtils.convertStreamToString(process.getInputStream());
          }
      }
    • 追踪cmdFilter

      1
      2
      3
      4
      5
      6
      public static string cmdFilter(string input){
          if(!FILTER PATTERN.matcher(input).matches()){
              return null;
          }
      return input;
      }
    • 查看FILTER PATTERN(常量)

      1
      private static final Pattern FILTER PATTERN = Pattern.compile("^[a-zA-Z0-9 /\\.-]+$");

      FILTER PATTERN过滤(大小写,特殊字符)

      出现命令注入时,有特殊字符返回null

    • 所以不行

RCE

\src\main\java\org\joychou\controller\Rce.java

(已经展示了命令执行过程)

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
@Slf4j
@RestController
@RequestMapping("/rce")
public class Rce {

    @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();
    }

Runtime.getRuntime().exec(),ProcessBuilder、通过yaml加载恶意Java对象进行命令执行,通过groovyShell进行命令执行

执行之后执行结果推送回前端显示

SQL注入

\src\main\java\org\joychou\controller\SQLI.java

定义的方法:

1
2
3
4
5
6
7
8
9
10
jdbc_sqli_vul(漏洞方法)
jdbc_sqli_sec(安全方法)
jdbc_ps_vuln
mybatisVuln01(漏洞方法)
mybatisVuln02(漏洞方法)
mybatisVuln03(漏洞方法)
mybatisSec01(安全方法)
mybatisSec02(安全方法)
mybatisSec03(安全方法)
mybatisOrderBySec04(安全方法)

jdbc_sqli_vul

  1. 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
    /**
         * <p>Sql injection jbdc vuln code.</p><br>
         *
         * <a href="http://localhost:8080/sqli/jdbc/vuln?username=joychou">http://localhost:8080/sqli/jdbc/vuln?username=joychou</a>
         */
        @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 + "'";    //直接拼接語句存在sql漏洞
                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();
        }

    直接拼接語句存在sql漏洞(对于参数username进行带入数据库查询)

  2. payload

    username=1’ or ‘1’=’1
    url编码:username=1%27%20or%20%271%27=%271

    最终的payload:sqli/jdbc/vuln?username=1%27%20or%20%271%27=%271

jdbc_sqli_sec

  1. 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
    /**
     * <p>Sql injection jbdc security code by using {@link PreparedStatement}.</p><br>
     *
     * <a href="http://localhost:8080/sqli/jdbc/sec?username=joychou">http://localhost:8080/sqli/jdbc/sec?username=joychou</a>
     */
    @RequestMapping("/jdbc/sec")
    public String jdbc_sqli_sec(@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.");
       
            // fix code
            String sql = "select * from users where username = ?";
            
            PreparedStatement st = con.prepareStatement(sql);//使用了PreparedStatement接口来访问数据库   
            
            st.setString(1, username);
       
            logger.info(st.toString());  // sql after prepare statement
            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();
    }
    1
    PreparedStatement st = con.prepareStatement(sql);//使用了PreparedStatement接口来访问数据库
    • PreparedStatement 接口是 Java 中用于执行预编译 SQL 语句的关键接口,它继承自 Statement 接口,主要用于执行参数化 SQL 语句。使用 PreparedStatement 可以有效防止 SQL 注入攻击,并且在执行多次相同结构的 SQL 语句时能提高性能。

      防护sql:

      1. 使用预编译:使用?(占位符)传递参数,数据库自动处理参数转义

        解释:对于php使用?传入的未限制是数字还是字符;但是java中使用?传入限制只能是数字,如果不是数字,那么会加以过滤(转义/加入特殊字符)

        eg:

        用户输入 字符串拼接结果 预编译处理结果
        1 WHERE id = '1' WHERE id = 1
        1 OR 1=1 WHERE id = '1 OR 1=1'(逻辑被篡改) WHERE id = '1 OR 1=1'(纯字符串)
        1'; DROP TABLE users WHERE id = '1'; DROP TABLE users(表被删除) WHERE id = '1\'; DROP TABLE users'(安全)
      2. 存在对于特殊字符的转义

      3. 禁用动态 SQL 拼接:避免在代码中手动拼接 SQL,尤其是包含用户输入的部分。若必须使用动态 SQL,需严格验证输入。

  2. 所以java中:
    不只order by,凡是字符串但又不能加引号的位置都不能参数化;包括sql关键字、库名表名字段名函数名等等”,(在有些SQL语句中还是会必然使用到拼接的方式)

    所以不可注入

jdbc_ps_vuln

也是使用预处理(同上)

mybatisVuln01 02 03

\src\main\java\org\joychou\controller\SQLI.java

  1. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @GetMapping("/mybatis/sec01")
       public User mybatisSec01(@RequestParam("username") String username) {
           return userMapper.findByUserName(username);
       }
    @GetMapping("/mybatis/sec02")
       public User mybatisSec02(@RequestParam("id") Integer id) {
           return userMapper.findById(id);
       }
     @GetMapping("/mybatis/sec03")
       public User mybatisSec03() {
           return userMapper.OrderByUsername();
       }
  2. 查看map类

    \src\main\resources\mapper\UserMapper.xml

    02

    1
    2
    3
    4
    5
     <select id="findByUserNameVuln02" parameterType="String" resultMap="User">
            select * from users where username like '%${_parameter}%'
        </select>
    
    #使用like进行拼接sql语句${_parameter}是 MyBatis 的字符串替换,会直接将参数内容拼接到 SQL 中

    解决:

    1
    2
    3
    4
    5
    <select id="findByUserNameSafe" parameterType="String" resultMap="User">
        select * from users where username like concat('%',#{_parameter},'%')
    </select>
    
    #使用concat('%',#{_parameter},'%')的方式进行查询

    03

    1
    2
    3
    4
    5
    6
    7
    8
     <select id="findByUserNameVuln03" parameterType="String" resultMap="User">
            select * from users
            <if test="order != null">
                order by ${order} asc
            </if>
        </select>
    
    #${}使用的是拼接的方式导致的漏洞产生

    解决:

    如果是order by后的占位,则最好根据情况通过if-elseif-else来分情况实现。

    mybatisOrderBySec04

    1

    01

    \target\classes\org\joychou\mapper\UserMapper.class

    1
    2
    3
    4
    5
    6
    7
    8
    public interface UserMapper {
        @Select({"select * from users where username = #{username}"})
        User findByUserName(@Param("username") String var1);
    
        @Select({"select * from users where username = '${username}'"})
        List<User> findByUserNameVuln01(@Param("username") String var1);
    
    #${}使用的是拼接的方式导致的漏洞产生

    解决:

    正确的使用方法应该是使用#号来进行占位

mybatisOrderBySec04

  1. 1
    2
    3
    4
    @GetMapping("/mybatis/orderby/sec04")
    public List<User> mybatisOrderBySec04(@RequestParam("sort") String sort) {
        return userMapper.findByUserNameVuln03(SecurityUtil.sqlFilter(sort));//存在过滤函数
    }
  2. 追溯sqlFilter

    \src\main\java\org\joychou\security\SecurityUtil.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
        * 过滤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;
       }

    FILTER_PATTERN:

    1
    private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
  3. 过滤mybatis中order by不能用#的情况。
    严格限制用户输入只能包含a-zA-Z0-9_-.字符

    @param sql sql
    @return 安全sql,否则返回null

SSTI服务器模板注入

\src\main\java\org\joychou\controller\SSTI.java

主要是使用Velocity组件

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
 @RestController
@RequestMapping("/ssti")
public class SSTI {

    /**
     * SSTI of Java velocity. The latest Velocity version still has this problem.
     * Fix method: Avoid to use Velocity.evaluate method.
     * <p>
     * http://localhost:8080/ssti/velocity?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22open%20-a%20Calculator%22)
     * Open a calculator in MacOS.
     *
     * @param template exp
     */
    @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);
    }
}

主要是Velocity组件模板·存在漏洞

参考文章:https://xz.aliyun.com/news/14795

路径遍历

  1. \src\main\java\org\joychou\controller\PathTraversal.java

    1
    2
    3
    4
    5
    6
    7
    /**
         * http://localhost:8080/path_traversal/vul?filepath=../../../../../etc/passwd
         */
        @GetMapping("/path_traversal/vul")
        public String getImage(String filepath) throws IOException {
            return getImgBase64(filepath);
        }

    输入路径之后,filepath 参数直接传递给 getImgBase64 方法,未做任何验证,只存在一个basa64编码(getImgBase64)

  2. 追溯getImgBase64

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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.";
            }
        }
  3. payload:http://localhost:9000/path_traversal/vul?filepath=d:/test.txt

    读取D盘下的测试文件(发现可以成功读取内容)

解决:(过滤)

  1. 使用白名单过滤、
  2. 使用 Path 和 normalize() 方法(根目录怕拼接)
    • 使用 Path.resolve() 而非字符串拼接。
    • 使用 normalize() 方法规范化路径,防止 ../ 绕过。

文件上传

\src\main\java\org\joychou\controller\FileUpload.java

  1. 无过滤

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

    上传文件 未判断文件的类型、扩展名等信息,未对生成文件的文件名进行重置

    ​ 直接将文件上传到文件保存目录中

  2. payload

    直接上传

解决:

  1. 使用白名单校验扩展名(如 jpg, png, pdf)
  2. 双重验证 MIME 类型(file.getContentType())
  3. 对上传图片实际内容的判断,如果图片可以正常读取,就判断其为允许上传文件,否则上传失败。
  4. 生成安全文件名(设置成随机字符拼接…)(防止路径遍历和注入)
  • 如果上传文件名是路径(../是向上跳转,最后会出现路劲遍历的漏洞)
  • 攻击者知道服务器上已有重要文件(比如其他用户上传的合法文件 avatar.jpg),可以故意使用相同的文件名上传恶意文件(攻击文件),直接覆盖原文件。后续用户访问执行恶意代码

XSS

\src\main\java\org\joychou\controller\XSS.java

    1. 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      /**
           * Vuln Code.
           * ReflectXSS
           * http://localhost:8080/xss/reflect?xss=<script>alert(1)</script>
           *
           * @param xss unescape string
           */
          @RequestMapping("/reflect")
          @ResponseBody
          public static String reflect(String xss) {
              return xss;
          }

      参数未做过滤,直接显示在前端页面

    2. payload

      直接在url中编写xss脚本即可

      1
      localhost:9000/xss/reflect?xss=<script>alert(1)</script>
    1. 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
      /**
          * Vul Code.
          * StoredXSS Step1
          * http://localhost:8080/xss/stored/store?xss=<script>alert(1)</script>
          *
          * @param xss unescape string
          */
         @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漏洞

      ​ store方法将未经过滤的参数直接存储于cookie中

      ​ show方法在cookie中将存储的漏洞参数直接显示在页面上

    2. payload

      1
      写入<script>alert(1)</script>

解决:

  1. 输入转义(重要)
  2. 限制长度

java-sec-code-master搭建&&审计
http://example.com/2025/10/27/java-sec-code-master搭建&&审计/
作者
Piggy Sprint
发布于
2025年10月27日
许可协议