【Java代码审计】SQL注入篇

发布时间:2023年12月20日

1.Java执行SQL语句的几种方式

1、JDBC Statement执行SQL语句

java.sql.Statement是Java JDBC下执行SQL语句的一种原生方式,执行语句时需要通过拼接来执行。若拼接的语句没有经过过滤,将出现SQL注入漏洞

驱动注册完成后,实例化Statement对象,SQL语句为select * from users where id = '" + id + "',通过拼接的方式传入id的值

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 id = '" + id + "'";
log.info("[vul] 执行SQL语句: " + sql);
ResultSet rs = stmt.executeQuery(sql);

2、PreparedStatement执行SQL语句

PreparedStatement是继承statement的子接口,包含已编译的SQL语句。PreparedStatement会预处理SQL语句,SQL语句可具有一个或多个IN参数。IN参数的值在SQL语句创建时未被指定,而是为每个IN参数保留一个问号(?)作为占位符。每个问号的值,必须在该语句执行之前通过适当的setXXX方法来提供。如果是int型则用setInt方法,如果是string型则用setString方法

PreparedStatement预编译的特性使得其执行SQL语句要比Statement快,SQL语句会编译在数据库系统中,执行计划会被缓存起来,使用预处理语句比普通语句更快。PreparedStatement预编译还有另一个优势,可以有效地防止SQL注入攻击,其相当于Statement的升级版

String sql = "select * from users where id = ?";
PreparedStatement st = conn.prepareStatement(sql);
st.setString(1, id);
ResultSet rs = st.executeQuery();

3、MyBatis执行SQL语句

MyBatis是一个Java持久化框架,它通过XML描述符或注解把对象与存储过程或SQL语句关联起来,它支持自定义SQL、存储过程以及高级映射

MyBatis框架底层已经实现了对SQL注入的防御,但存在使用不当的情况下,仍然存在SQL注入的风险

①MyBatis注解存储SQL语句

在这里插入图片描述

②MyBatis映射存储SQL语句

在这里插入图片描述


2.Java SQL注入

SQL语句参数直接动态拼接

在常见的场景下SQL注入是由SQL语句参数直接动态拼接的

例如如下代码:

String sql = "select * from users where id = '" + id + "'";
log.info("[vul] 执行SQL语句: " + sql);
ResultSet rs = stmt.executeQuery(sql);

while (rs.next()) {
    String res_name = rs.getString("user");
    String res_pass = rs.getString("pass");
    String info = String.format("查询结果 %s: %s", res_name, res_pass);
    result.append(info);
}

一个普通的查询,会输出账户名和密码:

http://localhost:8888/SQLI/JDBC/vul1?id=1

在这里插入图片描述

但倘若我们使用引号闭合SQL语句,并使用updatexml构造一个恶意的报错语句,就可以执行任何我们想要执行的SQL命令:

http://127.0.0.1:8888/SQLI/JDBC/vul1?id=1' and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)--%20+

在这里插入图片描述

防御方法,可以采用黑名单过滤的方式(误杀比较严重)

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

预编译依然采用拼接

使用PrepareStatement执行SQL语句是因为预编译参数化查询能够有效地防止SQL注入。那么是否能将使用Statement执行SQL语句的方式丢弃掉,使用PrepareStatement执行SQL语句防止SQL注入?

答案是否定的,很多开发者因为个人开发习惯的原因,没有按照PrepareStatement正确的开发方式进行数据库连接查询,在预编译语句中使用错误编程方式,那么即使使用了SQL语句拼接的方式,同样也会产生SQL注入漏洞

例如:

String sql = "select * from users where id = " + id;
log.info("[vul] 执行SQL语句: " + sql);
PreparedStatement st = conn.prepareStatement(sql);
ResultSet rs = st.executeQuery();

while (rs.next()) {
    String res_name = rs.getString("user");
    String res_pass = rs.getString("pass");
    String info = String.format("查询结果%n %s: %s%n", res_name, res_pass);
    result.append(info);
}

一个普通的查询,会输出账户名和密码:

http://127.0.0.1:8888/SQLI/JDBC/vul2?id=1

在这里插入图片描述

但倘若我们使用引号闭合SQL语句,就可以构造恶意的payload来获取用户表的所有数据:

http://127.0.0.1:8888/SQLI/JDBC/vul2?id=2%20or%201=1

在这里插入图片描述

防御方法,是采用占位符的方式执行SQL命令:

public String safe1(String id) {
    String sql = "select * from users where id = ?";
    PreparedStatement st = conn.prepareStatement(sql);
    st.setString(1, id);
    ResultSet rs = st.executeQuery();
}

order by注入

在有些特殊情况下不能使用PrepareStatement,比较典型的就是使用order by子句进行排序。order by子句后面需要加字段名或者字段位置,而字段名是不能带引号的,否则就会被认为是一个字符串而不是字段名。PrepareStatement是使用占位符传入参数的,传递的字符都会有单引号包裹,“ps.setString(1,id)”会自动给值加上引号,这样就会导致order by子句失效

例如:

在这里插入图片描述

因为order by只能使用字符串拼接的方式,当使用“String sql="SELECT*FROM user"+"order by"+id”进行id参数拼接时,就出现了SQL注入漏洞。id参数传入的值为“String id="2 or 1=1"”,因为存在SQL注入漏洞,故当执行完成后会将所有的user表中信息输出

防御方法是执行严格的过滤或使用类似Mybatis的排序映射

%和_模糊查询

在Java预编译查询中不会对%_进行转义处理,而%_刚好是like查询的通配符,如果没有做好相关的过滤,就有可能导致恶意模糊查询,占用服务器性能,甚至可能耗尽资源,造成服务器宕机

如图,当传入的username为“"%user%"”时,通过动态调试发现数据库在执行时并没有将%进行转义处理,而是作为通配符进行查询的

在这里插入图片描述

对于此攻击方式最好的防范措施就是进行过滤,此类攻击场景大多出现在查询的功能接口中,直接将%进行过滤就是最简单和有效的方式

MyBatis中使用存在风险的语法

#{}在底层实现上使用“?”作为占位符来生成PreparedStatement,也是参数化查询预编译的机制,这样既快又安全。${}将传入的数据直接显示生成在SQL语句中,类似于字符串拼接,可能会出现SQL注入的风险

示例:

在这里插入图片描述

${id}不会进行SQL参数化查询,如果传入的数据没有经过过滤就有可能出现SQL注入,设置传入的id的值为“1 and 1=2 union select 1,database(),3”,所以输出了SQL注入后数据库的数据信息:

在这里插入图片描述

另外,在前面order by注入中已经讲到,order by子句不能使用参数化查询的方式,只能使用字符拼接的方式,而在MyBatis中#{}是进行参数化查询的,如果在MyBatis的order by子句中使用#{},则order by子句会失效,所以要使用order by子句只能使用${}

例如,一个风险代码如下:

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

一个正常的排序访问,会输出按照field排序的结果:

http://127.0.0.1:8888/SQLI/MyBatis/vul/order?field=id&sort=desc

在这里插入图片描述

倘若输入恶意的payload,则可以导致注入:

http://127.0.0.1:8888/SQLI/MyBatis/vul/order?field=id&sort=desc,abs(111111)

防御方法是执行严格的过滤或使用Mybatis的排序映射

例如,如下是一个良好的解决order by注入的查询:

<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>

还有一种情况,上面提到的%_模糊查询,MyBatis的like子句中使用#{}程序会报错,例如:“select*from users where name like'%#{user}%'”;为了避免报错只能使用${},例如:“select*from users where name like'%${user}%'”;但${}可能会存在SQL注入漏洞,要避免SQL注入漏洞就要进行过滤

例如,下面是一个存在SQL注入的代码:

@Select("select * from users where user like '%${q}%'")
List<User> search(String q);

防御这种类型的攻击,一种有效的方式是强制数据类型,使用 ${} 本身是存在注入的,但由于强制使用Integer或long类型导致注入无效(无法注入字符串)

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

当然,最有效的方式,还是使用 #{} 安全编码,不过为了语法的正确性,要采用CONCAT函数进行拼接语句

@Select("select * from users where user like CONCAT('%', #{user}, '%')")
List<User> queryByUser(@Param("user") String user);

另外,MyBatis的in子句中使用#{}参数化查询,会将“select * from users where name in (#{user})”转变为“select * from users where name like (''user1','user2','user3','user4'')”,这样把“‘user1’,‘user2’,‘user3’,‘user4’”当作一个整体,偏离了原来的程序设计逻辑,无法查到数据,所以也存在上述的问题


3.Java常规注入代码审计思路

我们可以总结出下面这些常见的关键字,通过这些关键字便可快速地定位到SQL语句的附近,进而进行有针对性的审计:

在这里插入图片描述

例如我们搜索Statement,发现如下代码片段:

 Statement stmt = conn.createStatement();
 String sql = "select * from users where id = '" + id + "'";
 log.info("[vul] 执行SQL语句: " + sql);
 ResultSet rs = stmt.executeQuery(sql);

id是用户传入的参数,现在我们回看source点,看看对于id有没有什么过滤,以及过滤能否绕过:

追踪我们controller的视图,查看MvcConfig类:

在这里插入图片描述

进入视图文件,发现id是直接传入的方式,没有任何的过滤和全局过滤器:

在这里插入图片描述

注入产生!


4.二次注入代码审计

1、与常规注入一样,通过搜索SQL关键字定位至存在SQL语句的文件

在这里插入图片描述

2、跟进“UserMapper.java”文件,可以发现:其中定义了大量SQL语句,但大多数使用了#号的安全写法。通过搜索可以发现:以下语句使用了不安全的$

在这里插入图片描述

3、通过搜索调用栈,在UserService.java中可以找到其对应的调用

在这里插入图片描述

4、通读代码可以发现其逻辑为:从session中取出username,随后拼入SQL语句进行查询。我们接着查找session的调用,便能找到其赋值依据。最终在login逻辑中成功地找到了session的赋值过程

在这里插入图片描述

5、这里可以看到username的值来源于user.getUsername(),也就是说,username的值是通过登录时输入用户名获取的,由于前面存在if逻辑判断,因此此处取到的应是成功登录后的用户名。那么我们可以接着寻找注册逻辑,以便对漏洞进行利用

在这里插入图片描述

6、注册逻辑直接调用UserMapper进行入库操作,并没有对用户名进行过滤。同时入库时采用的是#号的安全写法,最后会通过预编译执行SQL语句

在这里插入图片描述

7、这里存在的注入为二次注入,而我们想要触发该漏洞则需先注册一个存在注入语句的用户名进行登录,随后通过触发info逻辑进行二次注入。通过查看逻辑可以知道:info是通过路由/info进行触发的

在这里插入图片描述

8、首先,注册账号名为“'union select user(),2,3#”的用户名

在这里插入图片描述

9、登录后触发info逻辑

在这里插入图片描述

文章来源:https://blog.csdn.net/Gherbirthday0916/article/details/134953282
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。