一、Java中的sql注入
SQL注入漏洞是一种常见的网络安全漏洞,它允许攻击者通过在应用程序的输入字段中插入恶意的SQL代码,从而执行未经授权的数据库操作。Java应用程序中,SQL注入通常发生在使用不安全的字符串拼接构建SQL查询时。
二、利用条件
1、用户传入参数可控
2、参数与数据库有交互
三、JDBC执行SQL
在JDBC下有两种方法执行SQL语句,分别是Statement和PrepareStatement。
Statement执行sql
String id = "1";
String sql = "SELECT * FROM user WHERE id = " + id;
pstmt = conn.createStatement();
ex = pstmt.executeQuery(sql);
Statement采用字符串拼接的方式去执行sql,参数可控并且能够执行我们的恶意sql代码,这里id值为1,应该执行后返回id为1的那一行数据,当我们将id值改一下
String id = "1 or 1=1";
String sql = "SELECT * FROM user WHERE id = " + id;
pstmt = conn.createStatement();
ex = pstmt.executeQuery(sql);
此时sql语句变成了SELECT * FROM user WHERE id =1 or 1=1
,or 1=1始终为真,因此整个条件id = 1 or 1=1
始终为真,这时会返回表中所有的数据。因此,JDBC使用Statement是不安全的,需要额外的去做过滤。
PreparedStatement执行sql
与Statement的区别在于PrepareStatement会对SQL语句进行预编译,预编译的好处不仅在于在一定程度上防止了sql注入,还减少了sql语句的编译次数,提高了性能,其原理是先去编译sql语句,无论最后输入为何,预编译的语句只是作为字符串来执行,而SQL注入只对编译过程有破坏作用,执行阶段只是把输入串作为数据处理,不需要再对SQL语句进行解析,因此解决了注入问题。
String sql = "SELECT * FROM user WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, id);
ResultSet ex = pstmt.executeQuery();
使用?
作为占位符告诉数据库SQL语句的结构,?
传进来的是参数而不是SQL语句,因此攻击者即使传入恶意的sql语句也只会被当成数据执行,而不会被当成sql语句去执行。
但是,并不是使用了PreparedStatement就代表不会存在sql注入,如果还是拼接参数,而不是使用参数化查询,一样会有sql注入。
如下:
String id = "1 OR 1=1";
String sql = "SELECT * FROM user WHERE id = " + id;
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet ex = pstmt.executeQuery();
但是预编译并不能解决所有sql注入,有些sql语句是没办法进行预编译的。
like注入
如果使用上面的预编译使用?
占位符,在like语句中会报错,它会被视为字面字符串'%?%'
,查询将查找用户名中包含?
的记录
String sql = "select * from user where UserName like '%?%'";
如果使用拼接的方式,虽然不报错,但是一样有sql注入
String sql = "select * from user where userName like '%" + UserName + "%'";
正确的写法
String sql = "select * from user where UserName like ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "%" + UserName + "%");
ResultSet ex = pstmt.executeQuery();
使用pstmt.setString(1, "%" + UserName + "%");
将用户输入的值安全地设置为参数,同时在参数值中添加了%
,以实现模糊匹配。
% 和 _模糊 查询
% 和 _ 是like查询的通配符,如果没有做相关的过滤,则会导致恶意的模糊查询,大量占用服务器cpu资源
%
是多字符通配符,它可以匹配任意数量的字符,包括零个字符,在查询中使用%,它会匹配任意位置的任意字符序列
_
是一个单字符通配符,它只匹配一个单个字符
举例:
SELECT * FROM users WHERE username LIKE 'admin%';
这个查询将返回所有以“admin”开头的用户名,例如admin、administrator
SELECT * FROM users WHERE username LIKE 'a_d';
这个查询将返回所有以“a”开头,后面跟着一个字符,最后以“d”结尾的用户名,例如abd、acd
in注入
in
语句在SQL中用于指定多个可能的值,但如果没有适当的输入验证和参数化,可能会导致SQL注入问题
String userInput = "1, 2, 3); DROP TABLE users; --";
String sql = "SELECT * FROM users WHERE user_id IN (" + userInput + ")";
在in当中使用拼接而不使用占位符做预编译的原因是,很多时候无法确定userInput
里含有多少个对象
拼接导致存在sql注入
SELECT * FROM users WHERE user_id IN (1, 2, 3); DROP TABLE users; --);
对传入的对象进行处理,确定对象的个数,然后增加同量的占位符?
来预编译
String sql = "SELECT * FROM users WHERE user_id IN (?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 1);
pstmt.setInt(2, 2);
order by 注入
order by 是将结果按照指定列排序,使用order by语句时是无法使用预编译的,是因为order by 后面需要加字段名或者字段的位置,而字段名不能带引号,如果使用预编译会被PreapareStatement强制加上单引号,导致order by 失效,应该手动过滤,或者采用白名单机制。
下面是使用白名单的正确的例子
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Arrays;
import java.util.List;
public class UserQuery {
public void getUsers(String sortColumn) throws Exception {
// 定义允许的排序字段
List<String> allowedColumns = Arrays.asList("username", "age");
// 检查用户输入的字段名是否在白名单中
if (!allowedColumns.contains(sortColumn)) {
throw new IllegalArgumentException("Invalid sort column");
}
// 使用预编译语句
String sql = "SELECT * FROM users ORDER BY " + sortColumn; // 这里是安全的
try (Connection conn = /* 获取数据库连接 */;
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("username"));
}
}
}
}
利用
order by利用一般使用盲注和报错,配合case when 语句和if语句
select * from users order by if null(null,id);
select * from users order by (case when (1=1) then user else id end );
select * from users order by extractvalue(1,(select concat(0x7e,user())));
select * from users order by updatexml(1,(select concat(0x7e,user())),1);
四、Mybatis执行SQL
Mybatis与JDBC类似,一样是拼接和预编译两种方式,\#{}类似于预编译, $ {}类似于拼接
Mapper设置
例如
select * from users where username = ${username} and password = ${password}
select * from users where username = #{username} and password = #{password}
like注入
在这种情况下使用 #{}
程序会报错:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUser">
select * from user_table where username like '%${username}%'
</select>
为了方便,开发者将 #{}
改成了 ${}
,从而导致sql注入
正确的写法是使用 concat
函数连接通配符
select * from user_table where username like concat('%',#{username},'%')
in注入
由于使用预编译,系统将输入的字符视为字符串,无法满足需求,因此将 #{}
改成了 ${}
,导致 SQL 注入
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUser">
select * from user_table where username in (${usernames})
</select>
正确写法应该用foreach
标签,来处理多个值
<select id="getUser" parameterType="java.util.List" resultType="user.NewUser">
SELECT * FROM user_table WHERE username IN
<foreach item="username" collection="usernames" open="(" separator="," close=")">
#{username}
</foreach>
</select>
order by注入
由于预编译机制,系统将输入的字符视为字符串,导致根据字符串排序无效,因此将 #{}
改成了 ${}
,从而引发 SQL 注入
<select id="UserOrder" parameterType="java.lang.String" resultType="user.NewUser">
select * from user_table order by ${column} limit 0,1
</select>
正确的做法应该,添加白名单或者是用预定义的字符
<select id="getUserOrder" parameterType="java.lang.String" resultType="user.NewUser">
SELECT * FROM user_table
ORDER BY
<choose>
<when test="column == 'username'">username</when>
<when test="column == 'created_at'">created_at</when>
<otherwise>username</otherwise>
</choose>
LIMIT 0, 1
</select>
总结
SQL 注入主要与函数的选择和写法密切相关,提取相应的关键字可以大大提高代码审计的效率。以下是提取的关键字列表:
like '%${
in (${
order by ${
Statement
PreparedStatement
createStatement
可以考虑把这些关键字写个规则放入代码审计工具里面(如:Fortify)
最后,作为java代码审计的初学者,文章中可能会存在问题,欢迎指出,后续也会坚持更新Java代码审计相关的文章