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


    一、前言

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

    二、SQL 注入漏洞原理

    1、概述

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

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

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

    2、漏洞复现

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

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

    image

    查看该页面的源代码:

    
    
    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( '
    ' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
    '
    ); // Get results while( $row = mysqli_fetch_assoc( $result ) ) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "
    ID: {$id}
    First name: {$first}
    Surname: {$last}
    "
    ; } 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”,即“或真”的逻辑,导致查询出了全部用户的数据。

    image

    如果攻击者可以任意替代提交的字符串,就可以利用 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 的表数据如下:

    image

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

    image

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

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

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

    image

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

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

    1、模糊查询

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

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

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

    image

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

    image

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

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

    image

    2、带有 IN 谓词的查询

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

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

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

    image

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

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

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

    image

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

    image

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

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

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

    image

    因此,此种情况下,安全的做法应当在 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__

  • 本文作者: 2ha0yuk7on
  • 本文链接: https://www.cnblogs.com/2ha0yuk7on/p/16880528.html
  • 关于博主: 更多文章欢迎访问我的CSDN博客:2ha0yuk7on.的博客_CSDN博客
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    Qt学习总结之布局管理
    C++流插入运算符和流提取运算符的重载
    深入理解MySQL索引底层数据结构
    C++设计模式之模板方法Template Method
    苹果电脑双系统正确打开方式,虚拟机已经Out了
    DHCP与TCP的简单解析
    Linux 命令【8】:ssm
    不降逼格的情况下降价格!Chromebook Plus让创新延续和计算机重生
    【数据结构】带头双向循环链表的实现(C语言)
    Linux入门
  • 原文地址:https://www.cnblogs.com/2ha0yuk7on/p/16880528.html