Hello-Java-Sec:SQLI

本文最后更新于 2025年4月18日 凌晨

从零开始的 Java 代码审计,项目地址:Hello-Java-Sec:https://github.com/j3ers3/Hello-Java-Sec

JDBC

Statement 注入

Statement 主要用于执行普通的 SQL语句

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String vul1(String id) {
StringBuilder result = new StringBuilder();
String sql = "select * from users where id = '" + id + "'";

try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);

Statement stmt = conn.createStatement();

log.info("[vul] 执行SQL语句: {}", sql);
ResultSet rs = stmt.executeQuery(sql);

...
}
}

首先创建了一个 StringBuilder 对象 result,构造了一个 sql 查询语句,加载 JDBC 驱动,创建了 Statement 对象执行了 SQL 语句

但是 SQL 语句直接将用户输入拼接到语句中并没有进行过滤之类的操作,导致了注入产生

payload:

1
?id=1' and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)--%20+

sql 语句变成了这样,与前面的单引号直接闭合了形成一个完整的查询语句去修改 XML 数据,利用报错将当前数据库用户通过拼接字符串显示出来

1
select * from users where id = '1' and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)--%20+ '

黑名单+大小写强制转换过滤

通过黑名单过滤来预防注入

1
2
3
4
5
6
7
8
9
public static boolean checkSql(String content) {
String[] black_list = {"'", ";", "--", "+", ",", "%", "=", ">", "*", "(", ")", "and", "or", "exec", "insert", "select", "delete", "update", "count", "drop", "chr", "mid", "master", "truncate", "char", "declare", "sleep"};
for (String s : black_list) {
if (content.toLowerCase().contains(s)) {
return true;
}
}
return false;
}

PrepareStatement

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String vul2(String id) {
StringBuilder result = new StringBuilder();
String sql = "select * from users where id = " + id;

try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);

log.info("[vul] 执行SQL语句: {}", sql);
PreparedStatement st = conn.prepareStatement(sql);
ResultSet rs = st.executeQuery();
...
}
}

虽然采用了 prepareStatement 方法进行了预编译,但并没有使用 ? 进行占位,所以本质上还是 sql 进行了语句拼接,产生了 sql 注入

payload

1
?id=2 or 1=1

sql 语句变成了这样,直接查询 id =2 的结果

1
select * from users where id = 2 or 1=1

使用 ? 占位

? 占位处理

1
2
3
4
5
6
7
8
public String safe1(String id) {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);
String sql = "select * from users where id = ?";
PreparedStatement st = conn.prepareStatement(sql);
st.setString(1, id);
ResultSet rs = st.executeQuery();
}

已经查询不到全部数据了

JdbcTemplate

JdbcTemplate 是 Spring 对 JDBC 的封装,供了直接编写查询的方法

spring JdbcTemplate 类的方法

方法 说明
public int update(String query) 用于插入,更新和删除记录。
public int update(String query,Object … args) 用于通过使用给定参数的PreparedStatement插入,更新和删除记录。
public void execute(String query) 用于执行DDL查询。
public T execute(String sql, PreparedStatementCallback action) 通过使用PreparedStatement回调执行查询。
public T query(String sql, ResultSetExtractor rse) 用于使用ResultSetExtractor获取记录。
public List query(String sql, RowMapper rse) 用于使用RowMapper获取记录。

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Map<String, Object> vul3(String id) {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl(db_url);
dataSource.setUsername(db_user);
dataSource.setPassword(db_pass);

JdbcTemplate jdbctemplate = new JdbcTemplate(dataSource);

String sql = "select * from users where id = " + id;

// 安全代码:使用参数化查询,避免SQL注入风险
// String sql = "select * from users where id = ?";

return jdbctemplate.queryForMap(sql);
}

又是对输入的字段直接进行拼接,形成了 sql 注入

payload

1
?id=1/2/3

sql 语句就变成了这样,直接拼接

1
select * from users where id = 1

强制数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
public Map<String, Object> safe4(Integer id) {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl(db_url);
dataSource.setUsername(db_user);
dataSource.setPassword(db_pass);

JdbcTemplate jdbctemplate = new JdbcTemplate(dataSource);

String sql_vul = "select * from users where id = " + id;

return jdbctemplate.queryForMap(sql_vul);
}

如果参数类型为 boolean、byte、short、int、long、float、double 等 sql 语句就无法拼接字符串

代码中会把传入的参数强制转换成 Integer 类型,所以如果是输入了像单引号 ' 这种字符,就会导致类型转换失败,比如当我输入了一个 1' 就会出如下报错

ESAPI框架

The OWASP Enterprise Security API,一款开源 Web 应用程序安全控制库,简单来说就是为了编写出更加安全的代码而设计出来的一些API,方便使用者调用,从而方便的编写安全的代码

1
2
3
4
5
6
7
8
public String safe3(String id) {
Codec<Character> oracleCodec = new OracleCodec();

Statement stmt = conn.createStatement();
String sql = "select * from users where id = '" + ESAPI.encoder().encodeForSQL(oracleCodec, id) + "'";

ResultSet rs = stmt.executeQuery(sql);
}

encodeForSQL 方法还对输入的特殊字符进行了转义处理,比如单引号 ' 会被转义为 "

正则过滤

比如说只匹配字母和数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String safe5(String name) {
StringBuilder result = new StringBuilder();
String pattern = "^[a-zA-Z0-9]+$";
boolean isValid = Pattern.matches(pattern, name);

if (isValid) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);
Statement stmt = conn.createStatement();
String sql = "select * from users where user = '" + name + "'";
ResultSet rs = stmt.executeQuery(sql);
} else {
return "非法正则匹配!";
}
}

name 的值必须要等于一个数据库中有的数据,有效预防了注入

MyBatis

order by 注入

漏洞分析

由于使用 ${} 直接拼接数据,从而造成SQL注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/vul/order")
public List<User> orderBy(String field, String sort) {
return userMapper.orderBy(field, sort);
}

// 不安全的注解写法
public interface UserMapper {
@Select("select * from users order by ${field} ${sort}")
List<User> orderBy(@Param("field") String field, @Param("sort") String sort);
}

// 不安全的XML映射写法
<select id="orderBy" resultType="com.best.hello.entity.User">
select * from users order by ${field} ${sort}
</select>

可以看到代码中调用了 userMapper 接口中的 orderBy 方法,传入参数 filesort

userMapper 接口的 orderBy 方法通常实现在 MyBatis 的 XML 映射文件或注解中

注解形式

可以发现在 mapper 文件夹下的 UserMapper 接口中,orderBy 方法在注解中的定义使用了 ${} 语法

${} 是一个简单的 String 替换,字符串是什么,解析就是什么,假如传的参数是 id (假设 id是 String 类型),对于 order by #{id} ,对应的 sql 语句就是 order by "id",对于 order by ${id},对应的 sql 语句则是 order by id,因此在这里传入的 field=3&sort=desc,abs(111111)

会被完整插入语句中,然后生成

1
select * from users order by 3 desc,abs(111111)

从而得到数据库中的数据

XML 文件形式

同样 oberBy 方法在 xml 文件中的映射使用了 ${} 语法,找到 UserMapper.xml 文件

采用了 ${} 语法,造成了注入

排序映射

采用排序映射的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@GetMapping("/safe/order")
public List<User> orderBySafe(String field) {
return userMapper.orderBySafe(field);
}

<mapper namespace="com.best.hello.mapper.UserMapper">
<select id="orderBySafe" resultType="com.best.hello.entity.User">
select * from users
<choose>
<!-- 根据字段选择排序方式,避免 SQL 注入 -->
<when test="field == 'id'">
order by id desc
</when>
<when test="field == 'user'">
order by user desc
</when>
<otherwise>
<!-- 默认排序方式,防止不合法输入 -->
order by id asc limit 1
</otherwise>
</choose>
</select>
</mapper>

可以发现用户输入的值只能是 iduser 时拼接字段执行其规则,其余直接限制了输出

比如当输入 field=idd 时,只显示出 id=1 和 2 的数据表

order by 正则过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public List<User> orderBySafeRe(String field, String sort) {
if (Security.isValidOrder(field) && Security.isValidSort(sort)) {
return userMapper.orderBy2(field, sort);
} else {
return userMapper.orderBy2("id", "desc");
}
}

// 正则表达式验证排序字段是否仅包含数字、字母、加号或下划线
public static boolean isValidOrder(String content) {
return content.matches("[0-9a-zA-Z+_]+");
}

// 白名单验证排序顺序,只允许 "desc" 和 "asc"
public static boolean isValidSort(String sort) {
return "desc".equalsIgnoreCase(sort) || "asc".equalsIgnoreCase(sort);
}

字段名只允许通过数字字母下划线加号,顺序排序只允许 ascdsc

搜索注入

由于在 LIKE 查询中使用 '%${user}%' 导致 SQL 注入

1
2
3
4
5
6
7
8
@GetMapping("/vul/search")
public List<User> searchVul(@RequestParam("user") String user) {
return userMapper.searchVul(user);
}

// 不安全的注解
@Select("select * from users where user like '%${q}%'")
List<User> search(String q);

漏洞分析

发现输入的值被复制给参数 user,然后使用 searchVul() 方法进行操作,跟进 searchVul() 方法

searchVul() 方法采用了 '%${user}%' 方式进行语句拼接,因为使用了 ${} 语法,所以输入并没有经过任何处理

payload

1
t%' or 1=1 and '%'='

这时候,sql 语句就变成了

1
select * from users where user like '%t%' or 1=1 and '%'='%'

#{} 参数化

1
2
3
4
5
6
7
8
@GetMapping("/safe/search")
public List<User> searchSafe(String user) {
return userMapper.searchSafe(user);
}

// 使用 MyBatis 注解的安全查询方法
@Select("select * from users where user like CONCAT('%', #{user}, '%')")
List<User> queryByUser(@Param("user") String user);

可以看到采用了 CONCAT 来进行模糊查询,拼接两个 %

强制数据类型

1
2
3
4
5
6
7
@GetMapping("/safe/id/{id}")
public List<User> queryById(@PathVariable Integer id) {
return userMapper.queryByIdAsInterger(id);
}

@Select("select * from users where id = ${id}")
List<User> queryByIdAsInterger(@Param("id") Integer id);

强制使用 Integer类型,导致无法注入


Hello-Java-Sec:SQLI
http://example.com/2025/04/18/Hello-Java-Sec-SQLI/
作者
butt3rf1y
发布于
2025年4月18日
许可协议