Java代码审计(SQL注入漏洞)

Java代码审计(SQL注入漏洞)

java代码审计

java 代码审计整理,目录如下

一、sql注入
二、命令执行
三、文件上传
四、xss
五、目录遍历
六、CSRF
七、XXE
八、SSRF
九、url跳转
十、不安全反序列化
十一、fastjson
十二、log4j
十三、未授权
十四、代码审计实战
 

一、sql注入

1、通过一些关键字可以定位到SQL语句附近

Statement
createStatement
PrepareStatement
like '%${
in (${
select
update
insert
 

(一)、JDBC的SQL注入

1、常规漏洞

实操–搭建springboot

a961260ff720240711232927

ece3e2885d20240711232945

配置数据库

ece3e2885d20240711233139

打开 src/main/resources/application.properties 配置文件,将以下数据库连接信息添加至配置中

#访问端口号
server.port=7070
#数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/java_sec-code?AllowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

2e84f26a2220240711233112

在 src\main\java\com\example\demo\jdbcinjection 下新建一个名为 JdbcDynamicController 的 Java Class。

package com.example.sql.jdbc;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.sql.*;

@RestController
@RequestMapping("/jdbcsql")
public class JdbcDynamicController {
    private static String driver = "com.mysql.cj.jdbc.Driver";
    //使用的是MySQL数据库的JDBC驱动,其类名是 "com.mysql.cj.jdbc.Driver"。这个驱动类负责与MySQL数据库建立连接。
    //用于从配置文件中读取属性值
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String user_name;
    @Value("${spring.datasource.password}")
    private String password;

    @RequestMapping("/dynamic")
    public String jdbcdynamic(@RequestParam("id") String id) throws ClassNotFoundException, SQLException {
        StringBuilder result = new StringBuilder();
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user_name, password);
        Statement statement = conn.createStatement();
        String sql = "select * from user where user_id = '" + id + "'";
        ResultSet rs = statement.executeQuery(sql);
        //遍历结果集
        while (rs.next()) {
            String rsUsername = rs.getString("user_name");
            String rsPassword = rs.getString("password");
            String info = String.format("%s: %s\n", rsUsername, rsPassword);
            result.append(info);
        }
        rs.close();
        conn.close();
        return result.toString();



    }
}
 

9a7df41be220240711231013

测试

http://127.0.0.1:7070/jdbcsql/dynamic?id=3
 

bf6da70aec20240711231056

19d36b8e9920240711231113

漏洞修复

在 src\main\java\com\example\demo\jdbcinjection 下新建一个名为 JdbcPrepareStatement 的 Java Class

package com.example.sql.jdbc;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.sql.*;

@RestController
@RequestMapping("/jdbcsqld")
public class JdbcPrepareStatement {
    private static String driver = "com.mysql.cj.jdbc.Driver";
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String user_name;
    @Value("${spring.datasource.password}")
    private String password;

    @RequestMapping("/preSec")

    public String jdbcPreSec(@RequestParam("id") String id) throws ClassNotFoundException, SQLException {
        if (id == null || id.trim().isEmpty()) {
            return "Invalid id";
        }

        StringBuilder result = new StringBuilder();
        try {
            Class.forName(driver);
            Connection conn = DriverManager.getConnection(url, user_name, password);
            String sql = "select * from user where user_id=?";
            PreparedStatement preparedStatement = conn.prepareStatement(sql);
            preparedStatement.setString(1, id);

            ResultSet rs = preparedStatement.executeQuery();
            while (rs.next()) {
                String resUsername = rs.getString("user_name");
                String resPassword = rs.getString("password");
                String info = String.format("%s: %s\n", resUsername, resPassword);
                result.append(info);
            }
        } catch (Exception e) {
            // Log or handle the exception here
            return "Error occurred: " + e.getMessage();
        }

        return result.toString();
    }

    @RequestMapping("/preNot")
    public String jdbcPreNot(@RequestParam("id") String id) throws SQLException, ClassNotFoundException {
        StringBuilder result = new StringBuilder();
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user_name, password);
        //还是直接进行了拼接 无效
        String sql = "select * from user where user_id = '" + id + "'";
        PreparedStatement preparestatement = conn.prepareStatement(sql);
        ResultSet rs = preparestatement.executeQuery();
        while (rs.next()) {
            String reUsername = rs.getString("user_name");
            String resPassword = rs.getString("password");
            String info = String.format("%s: %s\n", reUsername, resPassword);
            result.append(info);
        }
        rs.close();
        conn.close();
        return result.toString();

    }

}
 

正确的预编译代码效果:

http://127.0.0.1:7070/jdbcsqld/preNot?id=1%27or%201=1%23
 

85744647f620240711231135

错误的预编译代码效果:

http://127.0.0.1:7070/jdbcsqld/preSec?id=1%27or%201=1%23
 

4b20ad6c4120240711231150

2、order by注入

order by 语句用于对结果集进行排序。 order by 语句后面需要是字段名或者字段位 置。 在使用 PreparedStatement 预编译时,会将传递任意参数使用单引号包裹进而变为了字符串。 如果使用预编译方式执行 order by 语句,设置的字段名会被人为是字符串,而不再是字段名。 因此,在使用 order by 时,就不能使用 PreparedStatement 预编译了

新建一个名为 jdbcOrderby 的Java Class, 并键入以下代码

package com.example.sql.jdbc;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.sql.*;

@RestController
@RequestMapping("/jdbcsqlorderby")
public class jdbcOrderby {
    private static String driver = "com.mysql.jdbc.Driver";
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String user_name;
    @Value("${spring.datasource.password}")
    private String password;
    @RequestMapping("/PreOrderby")
    public String jdbcOrderby(@RequestParam("id") String id) throws
            ClassNotFoundException, SQLException {
        StringBuilder result = new StringBuilder();
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user_name, password);
        String sql = "select * from user" + " order by " + id;
        PreparedStatement preparestatement = conn.prepareStatement(sql);
        ResultSet rs = preparestatement.executeQuery();
        while (rs.next()) {
            String reUsername = rs.getString("user_name");
            String resPassword = rs.getString("password");
            String info = String.format("%s: %s\n", reUsername, resPassword);
            result.append(info);
        }
        rs.close();
        conn.close();
        return result.toString();
    }
}
 

测试

http://127.0.0.1:7070/jdbcsqlorderby/PreOrderby?id=if(1=1,sleep(1),1)
 

e941fe294f20240711231207

漏洞修复

package com.example.sql.jdbc;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.sql.*;

@RestController
@RequestMapping("/jdbcsqlorderbysec")
public class jdbcOrderbysec {
    private static String driver = "com.mysql.jdbc.Driver";
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String user_name;
    @Value("${spring.datasource.password}")
    private String password;

    @RequestMapping("/PreOrderbysec")
    public String jdbcOrderby(@RequestParam("id") String id) {
        StringBuilder result = new StringBuilder();
        String sql = "select * from user order by ?";

        try (Connection conn = DriverManager.getConnection(url, user_name, password);
             PreparedStatement preparestatement = conn.prepareStatement(sql)) {

            preparestatement.setString(1, id);
            ResultSet rs = preparestatement.executeQuery();

            while (rs.next()) {
                String reUsername = rs.getString("user_name");
                String resPassword = rs.getString("password");
                String info = String.format("%s: %s\n", reUsername, resPassword);
                result.append(info);
            }

        } catch (SQLException e) {
            e.printStackTrace(); // or log the exception
            // Handle the SQL exception appropriately
            return "Error occurred: " + e.getMessage();
        }

        return result.toString();
    }
}
 

使用 PreparedStatement来安全地处理参数

b7f660071520240711231309

3、like注入

package com.example.sql.jdbc;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RestController;
import java.sql.*;

@RestController
@RequestMapping("/jdbcsqllike")
public class jdbcsqllike {
    private static String driver = "com.mysql.jdbc.Driver";
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String user_name;
    @Value("${spring.datasource.password}")
    private String password;

    @RequestMapping("/vul")
    public String jdbclike(@RequestParam("id") String id) throws ClassNotFoundException, SQLException {
        StringBuilder result = new StringBuilder();
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user_name, password);

        // Vulnerable code using concatenated string
        String sql = "SELECT * FROM user WHERE user_name LIKE '%" + id + "%'";

        PreparedStatement preparestatement = conn.prepareStatement(sql);
        ResultSet rs = preparestatement.executeQuery();

        while (rs.next()) {
            String reUsername = rs.getString("user_name");
            String resPassword = rs.getString("password");
            String info = String.format("%s: %s\n", reUsername, resPassword);
            result.append(info);
        }

        rs.close();
        conn.close();
        return result.toString();
    }
}
 

测试

http://127.0.0.1:7070/jdbcsqllike/vul?id=d
 

e2e0f9bfa820240711231330

http://127.0.0.1:7070/jdbcsqllike/vul?id=a
 

5cdbc1c3e520240711231345

漏洞修复

package com.example.sql.jdbc;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RestController;
import java.sql.*;

@RestController
@RequestMapping("/jdbcsqllikesec")
public class jdbcsqllikesec {
    private static String driver = "com.mysql.jdbc.Driver";
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String user_name;
    @Value("${spring.datasource.password}")
    private String password;

    @RequestMapping("/secure")
    public String jdbclikeSecure(@RequestParam("id") String id) {
        StringBuilder result = new StringBuilder();

        try (Connection conn = DriverManager.getConnection(url, user_name, password)) {
            // Secure code using parameterized query
            String sql = "SELECT * FROM users WHERE user_name LIKE ?";
            try (PreparedStatement preparestatement = conn.prepareStatement(sql)) {
                preparestatement.setString(1, "%" + id + "%");
                ResultSet rs = preparestatement.executeQuery();

                while (rs.next()) {
                    String reUsername = rs.getString("user_name");
                    String resPassword = rs.getString("password");
                    String info = String.format("%s: %s\n", reUsername, resPassword);
                    result.append(info);
                }
            }
        } catch (SQLException e) {
            e.printStackTrace(); // Log or handle the exception appropriately
            return "Error occurred: " + e.getMessage();
        }

        return result.toString();
    }
}

 
http://127.0.0.1:7070/jdbcsqllikesec/secure?id=a

166c7952b720240711231402

(二)、Mybatis的SQL注入

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎 所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映 射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

在Mybatis中拼接SQL语句有两种方式:一种是占位符 #{} ,另一种是拼接符 ${} 。

占位符 #{} :对传入的参数进行预编译转义处理。类似JDBC中的 PreparedStatement 。

拼接符 ${} :对传入的参数不做处理,直接拼接,进而会造成SQL注入漏洞。

#{} 可以有效防止SQL注入漏洞。 ${} 则无法防止SQL注入漏洞。 因此在我们对JavaWeb整合Mybatis系统进行代码审计时,应着重审计SQL语句拼接的地方。 除非开发人员的粗心对拼接语句使用了 ${} 方式造成的SQL注入漏洞。

在Mybatis中有几种场景是不能使用预编译方式的,比如: order by 、 in , like 。

1、${}和#{}

在MyBatis中,${}#{}都用于在SQL语句中进行参数替换,但它们的工作方式不同,对于SQL注入有不同的影响。

  1. ${}(字符串替换):

    • 在MyBatis中,${}用于简单的字符串替换。参数的值直接插入到SQL语句中,没有额外的处理。
    • 如果输入没有得到适当的验证或清理,这可能导致SQL注入。
    <!-- 使用 ${} 的示例 -->
    <select id="getUserById" resultType="User">
        SELECT * FROM users WHERE id = ${userId}
    </select>
    
     

如果 userId 没有得到适当的验证,攻击者可以提供一个值来修改查询,从而导致SQL注入。

// Java代码中的使用示例
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("userId", "1 OR 1=1");
User user = sqlSession.selectOne("getUserById", paramMap);
 
  • 在这个例子中,如果 userId 没有得到适当的验证,它可能导致SQL注入,查询变为 SELECT * FROM users WHERE id = 1 OR 1=1,这将总是返回结果。

  • #{}(预处理语句):

    • 在MyBatis中,#{}用于预处理语句中的参数替换。MyBatis会处理值,确保它们被适当地转义和清理。
    • #{}${}更安全,有助于防止SQL注入。
    <!-- 使用 #{} 的示例 -->
    <select id="getUserById" resultType="User">
        SELECT * FROM users WHERE id = #{userId}
    </select>
    
     
// Java代码中的使用示例
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("userId", "1 OR 1=1");
User user = sqlSession.selectOne("getUserById", paramMap);
 
  1. 在这种情况下,MyBatis将以一种防止SQL注入的方式处理参数替换,生成的查询将是 SELECT * FROM users WHERE id = ?

总结:

  • 尽可能使用 #{} 进行参数替换,以防止SQL注入。MyBatis会处理这些值,确保它们被适当地转义。
  • 避免使用 ${},除非你确信输入已经得到适当的验证和清理,因为它会直接替换值到SQL语句中。

在使用用户输入时,请谨慎对待并在将其用于SQL语句之前验证/清理它们,以防止安全漏洞,如SQL注入。

mybatis中有些地方不能使用预编译的,这种场景下就容易出现sql注入漏洞:

动态 SQL 中的表名、列名:如果在动态 SQL 中使用 ${} 来表示表名、列名等标识符,因为这些标识符是在 SQL 解析阶段确定的,无法使用预编译参数来替换。
动态 SQL 中的 SQL 语句片段:例如在 <sql> 或 <selectKey> 等元素中使用 ${},这些片段是在 SQL 解析阶段确定的,也无法使用预编译参数来替换。
动态 SQL 中的 ORDER BY 字段:如果在 ORDER BY 子句中使用 ${} 来表示排序字段,因为排序字段是在 SQL 解析阶段确定的,同样无法使用预编译参数来替换。
LIKE 操作中的模糊查询字符串:如果在 LIKE 操作中使用 ${} 来表示模糊查询的字符串,因为这个字符串是直接拼接到 SQL 语句中的,不会被预编译。
 

2、orderby 注入

在 MyBatis 中,Order By 注入是一种常见的 SQL 注入攻击类型。这种攻击通常发生在使用动态 SQL 语句时,特别是当使用字符串拼接来构建 Order By 子句时。为了防止 Order By 注入,我们通常建议使用 #{} 来处理动态参数。

假设有一个 MyBatis 映射文件,其中有一个动态 SQL 语句用于构建 Order By 子句:

// 由于使用#{}会将对象转成字符串,形成order by "user" desc造成错误,因此很多研发会采用${}来解决,从而造成SQL注入

@GetMapping("/vul/order")
public List<User> orderBy(String field, String sort) {
    return userMapper.orderBy(field, sort);
}

// xml方式
<select id="orderBy" resultType="com.best.hello.entity.User">
    select * from users order by ${field} ${sort}
</select>

// 注解方式
@Select("select * from users order by ${field} desc")
List<User> orderBy2(@Param("field") String field);
             
 

漏洞修复- 排序映射

<select id="orderBySafe" resultType="com.best.hello.entity.User">
    select * from users
    <choose>
        <when test="field == 'id'">
            order by id desc
        </when>
        <when test="field == 'user'">
            order by user desc
        </when>
        <otherwise>
            order by id desc
        </otherwise>
    </choose>
</select>
 

3、like注入

<select id="list" resultType="com.itheima.pojo.Emp">
	select *
	from emp
	<where>
		<if test="name!=null and name!=''">
			name like '% $iname} %'

		</if>
		<if test="gender!=null">
			gender = #{gender}
		</if>
		<if test="begin!=null and end!=null">
			entrydate between #{begin} and #{end}
		</if>
	</where>
	order by update_time desc
</select>
 

安全写法:使用concat将%%与预编译组合起来。

<select id="list" resultType="com.itheima.pojo.Emp">
	select *
	from emp
	<where>
		<if test="name!=null and name!=''">

			name like concat('%',#{name},'%')
		</if>
		<if test="gender!=null">
			gender = #{gender}
		</if>
		<if test="begin!=null and end!=null">
			entrydate between #{begin} and #{end}
		</if>
	</where>
	order by update _time desc
</select>
© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容