Mybatis下的SQL注入漏洞原理及防护方法解析

 更新时间:2022年11月22日 08:56:25   作者:2ha0yuk7on  
SQL 注入是发生在 Web 程序中数据库层的安全漏洞,是网站存在最多也是最简单的漏洞,在实际项目中,即使使用了 Mybatis 框架,但仍然有可能因为编码人员安全意识不足而导致 SQL 注入问题,这篇文章主要介绍了Mybatis下的SQL注入漏洞原理及防护方法 ,需要的朋友可以参考下

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

一、前言

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

二、SQL 注入漏洞原理

1、概述

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

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

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

2、漏洞复现

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

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

查看该页面的源代码:

<?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”,即“或真”的逻辑,导致查询出了全部用户的数据。

如果攻击者可以任意替代提交的字符串,就可以利用 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 注入漏洞。

两者的区别如下:

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

2、漏洞复现

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

若没有采用 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 注入漏洞,成功查询了所有用户数据。

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

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

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

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

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

1、模糊查询

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

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

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

于是很多安全经验不足的程序员就把 #{} 号改成了 ${},如果应用层代码没有对用户输入的内容做处理势必会产生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 注入漏洞,成功查询了所有用户数据。

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

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

2、带有 IN 谓词的查询

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

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

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

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

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

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

因此,此种情况下,安全的做法应当使用 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 用户)

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

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

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

因此,此种情况下,安全的做法应当在 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 没有问题。

到此这篇关于Mybatis下的SQL注入漏洞原理及防护方法的文章就介绍到这了,更多相关Mybatis SQL注入漏洞原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解Spring/Spring boot异步任务编程WebAsyncTask

    详解Spring/Spring boot异步任务编程WebAsyncTask

    这篇文章主要介绍了详解Spring/Spring boot异步任务编程WebAsyncTask,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-06-06
  • Java实例讲解注解的应用

    Java实例讲解注解的应用

    JAVA注解 Annotation(注解)是JDK1.5及以后版本引入的。它可以用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查。注解是以‘@注解名’在代码中存在的
    2022-06-06
  • Java中文件操作功能小结

    Java中文件操作功能小结

    这篇文章主要为大家整理了一些Java中文件操作功能的相关资料,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以了解一下
    2023-05-05
  • SpringBoot 过滤器与拦截器实例演示

    SpringBoot 过滤器与拦截器实例演示

    本文通过示例代码给大家讲解SpringBoot 过滤器与拦截器的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2021-11-11
  • java编写冒泡排序的完整示例

    java编写冒泡排序的完整示例

    这篇文章主要给大家介绍了关于java编写冒泡排序的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • DecimalFormat数字格式化 0和# 的区别及说明

    DecimalFormat数字格式化 0和# 的区别及说明

    这篇文章主要介绍了DecimalFormat数字格式化 0和# 的区别及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10
  • Spring中@Configuration注解修改的类生成代理原因解析

    Spring中@Configuration注解修改的类生成代理原因解析

    大家好,本篇文章主要讲的是Spring中@Configuration注解修改的类生成代理原因解析,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-02-02
  • jvm内存溢出解决方法(jvm内存溢出怎么解决)

    jvm内存溢出解决方法(jvm内存溢出怎么解决)

    jvm内存溢出解决方法,详细内容看下面解释
    2013-12-12
  • java中static关键字用法详解

    java中static关键字用法详解

    这篇文章主要为大家详细介绍了java中static关键字的用法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-08-08
  • java使用influxDB数据库的详细代码

    java使用influxDB数据库的详细代码

    这篇文章主要为大家介绍了java使用influxDB数据库的详细代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-07-07

最新评论