本文最后更新于 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 )
黑名单+大小写强制转换过滤 通过黑名单过滤来预防注入
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
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; return jdbctemplate.queryForMap(sql); }
又是对输入的字段直接进行拼接,形成了 sql 注入
payload
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) ; } <select id="orderBy" resultType="com.best.hello.entity.User" > select * from users order by ${field} ${sort} </select>
可以看到代码中调用了 userMapper
接口中的 orderBy
方法,传入参数 file
和 sort
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>
可以发现用户输入的值只能是 id
或 user
时拼接字段执行其规则,其余直接限制了输出
比如当输入 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+_]+" ); }public static boolean isValidSort (String sort) { return "desc" .equalsIgnoreCase(sort) || "asc" .equalsIgnoreCase(sort); }
字段名只允许通过数字字母下划线加号,顺序排序只允许 asc
和 dsc
搜索注入 由于在 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
这时候,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); }@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
类型,导致无法注入