Java代码审计之SQL注入——Mybatis框架

  • A+

Mybatis简介

  MyBatis是支持定制化SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis可以对配置和原生Map使用简单的 XML 或注解,将接口和Java 的POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。

定位框架

  直接查看相关的依赖即可:
xml
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>

常见模式

  涉及到两个常用的符号:$、#
  通过配置文件:
* \$将传入的数据直接显示生成在sql中(直接拼接),一般用于传入数据库对象,例如传入表名和列名,还有排序时使用order by动态参数时也会使用$等。
xml
<select id="getUser" parameterType="String" resultType="com.codeaudit.sqlinject.mybatis.User">
select * from user order by ${_parameter} desc
</select>

* 使用#将传入的数据都当成一个字符串,类似预编译。
xml
<select id="searchUser" parameterType="String" resultType="com.codeaudit.sqlinject.mybatis.User">
select * from user where name like CONCAT('%',#{_parameter},'%')
</select>

  通过注解:
  MyBatis3提供了新的基于注解的配置。常见如下:
[email protected](${sql})
[email protected](${sql})
[email protected]($sql)
delete(${sql})

  但是#和$的意义跟配置文件的方式是一样的,#的作用主要是替换预编译语句(PrepareStatement)中的占位符?,\$是直接的SQL拼接,例如下面就是使用预编译的方式进行insert操作:
java
@Insert("insert into cluster_manager(cluster_name, cluster_time, cluster_address, cluster_access_user, cluster_access_passwd) values(#{clusterName}, #{clusterTime}, #{clusterAddress}, #{clusterAccessUser}, #{clusterAccessPasswd})")

常见SQL注入漏洞产生场景

  正常情况下的增删改查是不需要用到\$进行参数注解的的,直接使用$类似于直接拼接,会产生SQL注入问题:
xml
<select id="getUser" parameterType="String" resultType="com.codeaudit.sqlinject.mybatis.User">
select * from user order by ${_parameter} desc
</select>

  例如针对上述的配置我们尝试进行SQL盲注,相关的payload成功拼接并且执行了:

图片.png
  但是在进行审计的时候不是简单的搜索对应的配置文件,定位\$关键字就可以认为存在SQL注入了,一些特殊场景类似涉及到动态表名和列名时,只能使用“${xxx}”这样的参数格式。所以,这样的参数需要我们在代码中手工进行处理来防止注入。需要在审计中额外关注。
  主要是以下场景:
* like模糊查询
* Orderby排序
* 范围查询in
* 动态表名/列名

  也就是说,在进行相关审计时,可以通过下列的关键字快速定位相关的配置,然后查看相关的参数是否用户可控,然后快速定位SQL注入问题:
${
like
in(
order by

  定位到了相关场景后,检查是否存在相关的安全措施即可,以下是相关详情:

like模糊查询

  使用#预编译的方式进行注解的话结果如下,会发生异常:

mybatis_1.png
  应该在mapperxml配置中用sql的内置函数进行拼接,拼接后再采用#预编译的方式进行查询:
xml
<select id="searchUser" parameterType="String" resultType="com.codeaudit.sqlinject.mybatis.User">
select * from user where name like '%'||'#param#'||'%' #Oracle
select * from user where name like CONCAT('%',#param#,'%') #mysql
select * from user where name like '%'+#param#+'%' #mssql
</select>

Order by排序

  使用#预编译的方式进行注解的话无法进行正常的排序操作,与实际业务相悖。
  正确的方法如下:
* 在代码层使用白名单验证方式,如限制orderBy参数的值只能为id、name,如果输入不再白名单范围内则设置为一个默认值如name
* 在代码层使用间接引用方式,如限制用户输入只能为数字1、2,当输入1时映射到id,为2时映射到name,其他情况均映射到一个默认值例如name
* 在mapper配置中配置白名单,例如:
xml
<select id="getUser" parameterType="String" resultType="com.codeaudit.sqlinject.mybatis.User">
select * from user
<choose>
<when test="_parameter=='id' or _parameter='name'">
order by ${_parameter} desc
</when>
<otherwise>
order by name
</otherwise>
</choose>
</select>

范围查询in

  同样的使用#预编译的方式进行注解的话无法进行正常的范围查询操作,与实际业务相悖。
  正确的方式是,在进行同条件多值查询的时候,可以使用MyBatis自带的循环指令foreach来解决SQL语句动态拼接的问题:

mybatis_6.png
  例如,预编译的方式传入一个数组:
xml
<select id="searchUser" parameterType="String" resultType="com.tk.codeAudit.sqlinject.mybatis.User">
select * from user where id in
<foreach collection="array" index="index" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</select>

  在满足业务的情况下,又看到了我们熟悉的占位符?,防止了SQL注入的产生。

WX202003171309132x.png

动态表名/列名

  使用#预编译的方式无法正常的传递表名进行查询:

mybatis_8.png
  具体的方式可以参考Orderby场景,同样的也可以设置白名单或者间接引用方式进行防护。

其他安全措施

  除了上述场景外,最后还要考虑类似过滤器等安全防护措施,有可能存在相关SQL关键字被拦截导致注入点无法使用。
PS:在设计过滤器规则时,要考虑应用的数据库类型,github上的mysql规则对于oracle不一定适用:

WX202003171336592x.png