3

Mybatis下的SQL注入漏洞原理及防护方法 - 2ha0yuk7on

 1 year ago
source link: https://www.cnblogs.com/2ha0yuk7on/p/16880528.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

之前我一直认为 Mybatis 框架下已经实现预编译机制,很多东西都封装好了,应该基本上不会再有 SQL 注入问题了。近期在渗透中发现,在实际项目中,即使使用了 Mybatis 框架,但仍然有可能因为编码人员安全意识不足而导致 SQL 注入问题。出现情况还真不少,因此有了这篇文章。

二、SQL 注入漏洞原理

SQL 注入(SQL Injection)是发生在 Web 程序中数据库层的安全漏洞,是网站存在最多也是最简单的漏洞。主要原因是程序对用户输入数据的合法性没有判断和处理,导致攻击者可以在 Web 应用程序中事先定义好的 SQL 语句中添加额外的 SQL 语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步获取到数据信息。

简单地说,就是通过在用户可控参数中注入 SQL 语法,破坏原有 SQL 结构,达到编写程序时意料之外结果的攻击行为。其成因可以归结为如下原因造成的:

  1. 程序编写者在处理应用程序和数据库交互时,使用字符串拼接的方式构造 SQL 语句。
  2. 且未对用户可控参数进行足够的过滤。

2、漏洞复现

下面使用DVWA靶场来进行演示,网站架构为PHP,我们重点关注漏洞原理即可。

该页面提供了一个简单的查询功能,可以根据前端输入的用户ID来查询对应的用户信息。如图,输入 1,返回了对应 admin 用户的信息。

2957280-20221121152029788-63050904.png

查看该页面的源代码:

<?php

if( isset( $_REQUEST[ 'Submit' ] ) ) {
    // Get input
    $id = $_REQUEST[ 'id' ];

    // Check database
    $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

    // Get results
    while( $row = mysqli_fetch_assoc( $result ) ) {
        // Get values
        $first = $row["first_name"];
        $last  = $row["last_name"];

        // Feedback for end user
        echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
    }

    mysqli_close($GLOBALS["___mysqli_ston"]);
}

?>

进行代码审计可以发现,程序将前端输入的 id 参数未加任何处理,直接拼接在了 SQL 语句中,那么此时就导致了SQL注入漏洞。

若此时攻击者输入的用户ID为 1' or 1='1,则程序拼接后执行的 SQL 语句变成了:

SELECT first_name, last_name FROM users WHERE user_id = '1' or 1='1';

可见,攻击者通过单引号 ' 闭合了数据库查询语句,并且在查询条件之后构造了“或 1=1”,即“或真”的逻辑,导致查询出了全部用户的数据。

2957280-20221121152053036-1860301437.png

如果攻击者可以任意替代提交的字符串,就可以利用 SQL 注入漏洞改变原有 SQL 语句的含义,进而执行任意 SQL 命令,入侵数据库进行脱库、删库,甚至通过数据库提权获取系统权限,造成不可估量的损失。(SQL注入的场景类型非常之多,攻击手法、绕过姿势也非常多,本文不作重点讨论)

3、修复建议

一般来说,防御 SQL 注入的最佳方式就是使用预编译语句(其他防御方法还有很多,本文不作重点讨论),绑定变量。例如:

String sql = "SELECT * FROM user_table WHERE username = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, "zxd");
ResultSet results = pstmt.executeQuery();

使用预编译的 SQL 语句,SQL 语句的语义不会发生改变。在 SQL 语句中,变量用占位符 ? 表示,攻击者无法改变 SQL 的结构。

三、Mybatis 框架简介

1、参数符号的两种方式

Mybatis 的 SQL 语句可以基于注解的方式写在类方法上面,更多的是以 xml 的方式写到 xml 文件。Mybatis 中 SQL 语句需要我们自己手动编写或者用 generator 自动生成。编写 xml 文件时,MyBatis 支持两种参数符号,#{}${}

  • #{} 使用预编译,通过 PreparedStatement 和占位符来实现,会把参数部分用一个占位符 ? 替代,而后注入的参数将不会再进行 SQL 编译,而是当作字符串处理。可以有效避免 SQL 注入漏洞
  • ${} 表示使用拼接字符串,将接受到参数的内容不加任何修饰符拼接在 SQL 中。易导致 SQL 注入漏洞。

两者的区别如下:

  1. #{} 为参数占位符 ?,即 SQL 预编译。${} 为字符串替换,即 SQL 拼接。
  2. #{} 是“动态解析->预编译->执行”的过程。${} 是“动态解析->编译->执行”的过程。
  3. #{} 的变量替换是在 DBMS 中。${} 的变量替换是在 DBMS 外。
  4. 变量替换后,#{} 对应的变量自动加上引号。变量替换后,${} 对应的变量不会加上引号。

2、漏洞复现

下面以一个查询场景进行简单演示,数据库表 user_table 的表数据如下:

2957280-20221121152125276-1453790944.png

若没有采用 JDBC 的预编译模式,查询 SQL 写为:

<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
    select * from user_table where username = '${username}'
</select>

这种写法就产生了 SQL 语句的动态拼接,这样格式的参数会直接参与 SQL 语句的编译,从而不能避免SQL注入攻击。

若此时攻击者提交的参数值为 zxd' or 1='1,如下图,利用 SQL 注入漏洞,成功查询了所有用户数据。

2957280-20221121153056171-526035448.png

因此,应用 Mybatis 框架 SQL语句的安全写法(即 JDBC 预编译模式):

<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
    select * from user_table where username = #{username}
</select>

可见,此时采用 JDBC 预编译模式,即使攻击者尝试 SQL 注入攻击,也只会将参数整体作为字符串处理,有效避免了 SQL 注入问题。

2957280-20221121153106301-744787149.png

四、Mybatis 框架下的 SQL 注入问题及防护方法

还是以上节的查询场景举例,Mybatis 框架下易产生 SQL 注入漏洞的情况主要有以下三种:

1、模糊查询

在模糊查询场景下,考虑安全编码规范,使用 #{} 传入参数:

<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
	select * from user_table where username like '%#{username}%'
</select>

在这种情况下使用 #{} 程序会报错:

2957280-20221121152350176-1919450894.png

于是很多安全经验不足的程序员就把 #{} 号改成了 ${},如果应用层代码没有对用户输入的内容做处理势必会产生SQL注入漏洞。

<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
	select * from user_table where username like '%${username}%'
</select>

若此时攻击者提交的参数值为 zxd' or 1=1#,如下图,利用 SQL 注入漏洞,成功查询了所有用户数据。

2957280-20221121152423825-1621310553.png

因此,安全的写法应当使用 CONCAT 函数连接通配符

<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
	select * from user_table where username like concat('%',#{username},'%')
</select>

2957280-20221121152436748-1489428993.png

2、带有 IN 谓词的查询

在 IN 关键字之后使用 #{} 查询多个参数:

<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
	select * from user_table where username in (#{usernames})
</select>

正常提交查询参数 'zxd','hhh',因为预编译机制,系统将我们输入的字符当作了一个字符串,因此查询结果为空,不能满足业务功能需求。

2957280-20221121152654471-2091873044.png

于是很多安全经验不足的程序员就把 #{} 号改成了 ${}

<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
	select * from user_table where username in (${usernames})
</select>

攻击者提交参数值 'hhh') or 1=1#,利用 SQL 注入漏洞,成功查询了所有用户数据。

2957280-20221121152522592-1773869944.png

因此,此种情况下,安全的做法应当使用 foreach 标签

<select id="getUserFromList" resultType="user.NewUserDO">
	select * from user_table where username in
		<foreach collection="list" item="username" open="(" separator="," close=")">
			#{username}
		</foreach>
</select>

3、带有动态排序功能的查询

动态排序功能,需要在 ORDER BY 之后传入参数,考虑安全编码规范,使用 #{} 传入参数:

<select id="getUserOrder" parameterType="java.lang.String" resultType="user.NewUserDO">
	select * from user_table order by #{column} limit 0,1
</select>

提交参数 username 根据用户名字段排序。但因为预编译机制,系统将我们输入的字符当作了一个字符串,根据字符串排序是不生效的,不能满足业务功能需求。(根据用户名字段排序,此时正常应返回 root 用户)

2957280-20221121152727487-560483229.png

于是很多安全经验不足的程序员就把 #{} 号改成了 ${}

<select id="getUserOrder" parameterType="java.lang.String" resultType="user.NewUserDO">
	select * from user_table order by ${column} limit 0,1
</select>

攻击者提交参数值 username#,利用 SQL 注入漏洞,成功查询了所有用户数据。

2957280-20221121152808439-1727096247.png

因此,此种情况下,安全的做法应当在 Java 代码层面来进行解决。可以设置一个字段值的白名单,仅允许用户传入白名单内的字段。

String sort = request.getParameter("sort");
String[] sortWhiteList = {"id", "username", "password"};
if(!Arrays.asList(sortWhiteList).contains(sort)){
    sort = "id";
} 

或者仅允许用户传入索引值,代码再将索引值映射成对应字段。

String sort = request.getParameter("sort");
switch(sort){
    case "1":
        sort = "id";
        break;
    case "2":
        sort = "username";
        break;
    case "3":
        sort = "password";
        break;
    default:
        sort = "id";
        break;
} 

需要注意的是在 mybatis-generator 自动生成的 SQL 语句中,ORDER BY 使用的也是 ${},而 LIKE 和 IN 没有问题。

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK