【Java代码审计】SQL注入篇

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

1、JDBC Statement执行SQL语句

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

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

Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://192.168.88.20:3306/iwebsec?&useSSL=false&server Timezone=UTC", "root", "root");
String id = "2";
String sql = "select * from user where id = " + id;
ps = conn.createStatement();
rs = ps.executeQuery(sql);
while (rs.next()) {
    System.out.println("id:" + rs.getInt("id") + "   username : " + rs.getString("username") + "   password:" + rs.
            getString("password"));
}

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 id = "1";
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://192.168.88.20:3306/iwebsec?&useSSL= false&serverTimezone=UTC", "root", "root");
String sql = "SELECT * FROM user WHERE id = ?";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
preparedStatement.setString(1, id);
rs = preparedStatement.executeQuery();
while (rs.next()) {
    System.out.println("id : " + rs.getInt("id") + " username : " + rs.getString("username") + " password :
            "+rs.getString("password"));}
}

3、MyBatis执行SQL语句

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

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

①MyBatis注解存储SQL语句

public interface BlogMapper {
    @Select("select * from Blog where id = #{id}")
    Blog selectBlog(int id);
}

②MyBatis映射存储SQL语句

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3- mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
    <select id="selectBlog" parameterType="int" resultType="Blog">select * from Blog where id = #{id}
    </select>
</mapper>

2.Java SQL注入

SQL语句参数直接动态拼接

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

例如如下代码:

public String vul1(String id) {

    StringBuilder result = new StringBuilder();

    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 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);
        }

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

    } catch (Exception e) {
        return e.toString();
    }
}

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

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

再次触发payload,显示检测到注入行为:

在这里插入图片描述

预编译依然采用拼接

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

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

例如:

public String vul2(String id) {

    StringBuilder result = new StringBuilder();

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

        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);
        }

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

    } catch (Exception e) {
        return e.toString();
    }
}

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

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();
}

再次触发payload,攻击行为被拦截:

在这里插入图片描述

order by注入

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

例如:

Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://192.168.88.20:3306/iwebsec?&useSSL=false&server Timezone=UTC", "root", "root");
String id = "2 or 1=1";
String sql = "SELECT * FROM user " + " order by " + id;
System.out.println(sql);
PreparedStatement preparedStatement = conn.prepareStatement(sql);
rs = preparedStatement.executeQuery();

因为order by只能使用字符串拼接的方式,当使用“String sql = "SELECT * FROM user" + "order by" + id”进行id参数拼接时,就出现了SQL注入漏洞

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

%和_模糊查询

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

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

在这里插入图片描述

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

MyBatis中使用存在风险的语法

使用了不安全的${}

#{}在底层实现上使用“?”作为占位符来生成PreparedStatement,也是参数化查询预编译的机制,这样既快又安全

${}将传入的数据直接显示生成在SQL语句中,类似于字符串拼接,可能会出现SQL注入的风险

示例:

<mapper namespace="com.mybatis.userMapper">
    <select id="getUser" resultType="com.mybatis.sql.User"> select * from user where id=#{id}
    </select>
</mapper>

${id}不会进行SQL参数化查询,如果传入的数据没有经过过滤就有可能出现SQL注入

示例:

<mapper namespace="com.mybatis.userMapper">
    <select id="getUser" resultType="com.mybatis.sql.User"> select * from user where id=${id}
    </select>
</mapper>

MyBatis order by注入

在前面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)

页面的返回没有报错,证明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 模糊查询注入

上面提到的%_模糊查询,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);

此时我们搜索恶意的payload:test%' union select 1,user(),@@version_compile_os--

黑客成功获取了数据库用户和操作系统信息:

在这里插入图片描述

防御这种类型的攻击,一种有效的方式是强制数据类型,使用 ${} 本身是存在注入的,但由于强制使用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 子句注入

在 MyBatis 的 in 子句中使用#{}会将多个参数当作一个整体,这偏离了原来的程序设计逻辑,无法查到数据

<mapper namespace="com.mybatis_orderby.userMapper">
    <select id="getUser" resultType="com.mybatis_orderby.sql.User"> select * from users where name in (#{user})
    </select>
</mapper>

为了避免这个问题,只能使用${}

<mapper namespace="com.mybatis_orderby.userMapper">
    <select id="getUser" resultType="com.mybatis_orderby.sql.User"> select * from users where name in (${user})
    </select>
</mapper>

但是${}使用的是字符串拼接的方式很有可能会存在 SQL 注入漏洞


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

Java 语言本身是一种强类型语言,因此在寻找 SQL 注入漏洞的过程中,可以首先找到所有包含 SQL 语句的点,随后观察传参类型是否是 String 类型,只有当传参类型是String 类型时我们才可能进行 SQL 注入

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

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

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

在这里插入图片描述

  • 34
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

世界尽头与你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值