查看原文
其他

原创 | Mybatis-plus框架常见SQL注入场景

tkswifty SecIN技术平台 2022-06-18
点击上方蓝字 关注我吧

01

关于Mybatis-plus



MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。


02

常见SQL注入场景



与SpringDataJpa类似,mybatis-plus提供了相关的funciton进行sql的操作,例如like("name","tks")——>name like '%tks%',同时也很贴心的考虑到了SQL注入问题,对绝大部分场景进行了预编译处理。但是类似动态表名、orderby这种需要拼接的场景在实际开发中还是需要额外的注意。


2.1 条件构造器Wrapper


条件构造器Wrapper可以用于复杂的数据库操作:大于、小于、模糊查询等等。



比较常用的是QueryWrapper和UpdateWrapper:

  • Wrapper :条件构造抽象类,最顶端父类,抽象类中提供4个方法
  • AbstractWrapper :用于查询条件封装,生成 sql 的 where 条件
  • AbstractLambdaWrapper :Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。
  • LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
  • LambdaUpdateWrapper :Lambda 更新封装Wrapper
  • QueryWrapper :Entity 对象封装操作类,不是用lambda语法
  • UpdateWrapper :Update 条件封装,用于Entity对象更新操作


2.1.1 配置方法


首先配置mapper:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.baomidou.mybatisplus.samples.wrapper.entity.User;
public interface UserMapper extends BaseMapper<User> {}

然后直接调用相关的api进行操作即可,例如查询name为“admin”的用户:
QueryWrapper<User> wrapper = new QueryWrapper<>();wrapper.eq("name","admin")userMapper.selectOne(wrapper); // 查询一个数据。查询多个使用List或者Map

传统的mybaits框架对于in范围查询和like模糊查询需要做额外的处理:

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

in范围查询的话需要在进行同条件多值查询的时候,可以使用MyBatis自带的循环指令foreach来解决SQL语句动态拼接的问题:
<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>

比较便利的是,mybatis-puls已经考虑到sql注入的影响,相关wrapper的function已进行了相关的预编译处理。例如mybatis常见的like和in注入场景,均进行了预编译处理,例如如下例子:

  • like模糊查询
QueryWrapper<User> qw = new QueryWrapper<>();qw.select("id","name").like("name", "jack");List<User> plainUsers = userMapper.selectList(qw);

打印相关的查询log,可以看到相关查询已使用?进行预编译处理:



  • in范围查询
QueryWrapper<User> qw = new QueryWrapper<>();qw.select("id","name").in("age","10","30");List<User> plainUsers = userMapper.selectList(qw);

打印相关的查询log,可以看到相关查询已使用?进行预编译处理:


2.1.2 常见注入场景


以QueryWrapper为例,由于部分场景存在动态sql的场景,官方文档已进行风险提醒:


PS:
boolean condition参数表示该条件是否加入最后生成的sql中

如下是审计过程中发现的比较常见的存在注入风险的点,也是对官方文档的一个补充


apply


该方法可用于数据库函数的调用:
apply(String applySql, Object... params)apply(boolean condition, String applySql, Object... params)

例如:
apply("date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")--->date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")

注意:动态入参的params对应前面applySql内部的{index}部分。这样可以进行预编译,防止SQL注入问题。


例如下面的例子,未通过动态入参params进行预编译处理:
List<User> plainUsers4 = userMapper.selectList(new QueryWrapper<User>() .apply("role_id = 2"));List<User> lambdaUsers4 = userMapper.selectList(new QueryWrapper<User>().lambda() .apply("role_id = 2"));

打印相关的查询log,可以看到相关的输入role_id直接拼接到sql语句中,并未做预编译处理:


使用params进行预编译处理:
List<User> plainUsers5 = userMapper.selectList(new QueryWrapper<User>() .apply("role_id = {0}",2));List<User> lambdaUsers5 = userMapper.selectList(new QueryWrapper<User>().lambda() .apply("role_id = {0}",2));

打印相关的查询log。通过{index}标记的部分最终进行了预编译处理:


last

该函数用于无视优化规则直接将内容拼接到 sql 的最后,一般在排序场景会用到:
last(String lastSql)last(boolean condition, String lastSql)

例如下面的例子,直接将order by排序相关语句拼接进行拼接,如果相关内容用户可控,那么会存在Sql注入风险:
.last("order by " + StringUtil.isEmpty(queryParam.getColumns()) + " " + queryParam.getSort()));
exists/notExists

这两个场景同样的是进行sql的拼接,如果相关内容用户可控,那么会存在Sql注入风险:


  • 拼接 EXISTS ( sql语句 )
exists(String existsSql)exists(boolean condition, String existsSql)

  • 拼接 NOT EXISTS ( sql语句 )
notExists(String notExistsSql)notExists(boolean condition, String notExistsSql)

例如如下例子,若exists中的内容用户可控,则会存在注入风险:
QueryWrapper<User> qw = new QueryWrapper<>();qw.select("id","name").exists("select id from table where role_id = 2");




having

用于Having查询,一般用配合groupby在对分组统计函数进行过滤的场景中:
having(String sqlHaving, Object... params)having(boolean condition, String sqlHaving, Object... params)

跟apply一样,动态入参的params对应前面applySql内部的{index}部分。这样可以进行预编译防止SQL注入问题。例如如下的例子:
QueryWrapper<User>().select("id","name").groupBy("name").having(true, "role_id>{0}",2));

Order By

Order by无疑是SQL注入的常客了。

  • 原始orderby
orderBy(boolean condition, boolean isAsc, R... columns)

  • ORDER BY 字段, ... DESC
orderByDesc(R... columns)orderByDesc(boolean condition, R... columns)

  • ORDER BY 字段, ... ASC
orderByDesc(R... columns)orderByDesc(boolean condition, R... columns)

因为Order by排序时不能进行预编译处理,所以相关内容用户可控的话会存在sql注入风险。

例如如下H2 database的例子:
QueryWrapper<User> qw = new QueryWrapper<>();qw.select("id","name").orderByAsc(str);

str用户可控,那么这里尝试带外请求dnslog,成功接收到带外请求:


group By

主要用于用于结合聚合函数,根据一个或多个列对结果集进行分组。
groupBy(R... columns)groupBy(boolean condition, R... columns)

例如如下H2 database的例子:
QueryWrapper<User> qw = new QueryWrapper<>();qw.select("id","name").groupBy(str);

str用户可控,那么这里尝试带外请求dnslog,成功接收到带外请求:


insql/notinsql

虽然mybatis-plus提供的in/not in方法进行处理,但是无法满足对子查询结果进行范围查询的场景。此时提供了如下方法:

通过sql 注入方式调用in/not in 方法:
  • notInSql
notInSql(boolean condition, R column, String inValue);notInSql(R column, String inValue)

  • inSql
inSql(R column, String inValue)inSql(boolean condition, R column, String inValue)

例如如下H2 database的例子:
userMapper.selectList(new QueryWrapper<User>().inSql(str, "select id from role where id = 2"));

str用户可控,那么这里尝试带外请求dnslog,成功接收到带外请求:



同理,除了column字段以外,inValue字段可控的情况下也会存在注入风险。


2.2 使用 Wrapper 自定义SQL(特殊的预编译场景)


虽然mybatis-plus提供了很多函数使用,但是不一定能满足所有的业务需要,此时Wrapper提供了自定义SQL场景,虽然跟传统的mybatis一样使用$进行注解,但是实际上ew已经做了预编译处理。同样的也支持注解&xml配置。

  • 注解方式
@Select("select * from mysql_data ${ew.customSqlSegment}")List<MysqlData> getAll(@Param(Constants.WRAPPER) Wrapper wrapper);

  • xml方式
<select id="getAll" resultType="MysqlData">SELECT * FROM mysql_data ${ew.customSqlSegment}</select>

2.3 其他


除此之外,官方文档中还推荐使用
com.github.pagehelper进行分页处理:
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>3.7.5</version></dependency>

因为Orderby排序时不能进行预编译处理,所以在使用插件时需要额外注意如下function,同样会存在SQL注入风险:

com.github.pagehelper.Page
主要是setOrderBy(java.lang.String)方法

com.github.pagehelper.page.PageMethod

主要是startPage(int,int,java.lang.String)方法

com.github.pagehelper.PageHelper

主要是startPage(int,int,java.lang.String)方法


03

安全加固建议



上述场景中,除了apply、having是因为方法使用不当导致注入以外,其他场景更多的类似动态拼接,可以参考如下方法进行安全加固:

1.在代码层使用白名单验证方式,如设置表名白名单,如果输入不再白名单范围内则设置为一个默认值如user;
2.在代码层使用间接引用方式,如限制用户输入只能为数字1、2,当输入1时映射到user,为2时映射到product,其他情况均映射到一个默认值例如product;
3.使用sdk对用户输入进行安全检查。


04参考资料


https://www.cnblogs.com/nongzihong/p/12661446.html
https://github.com/baomidou/mybatis-plus-samples

相关推荐




原创 | 沙盒逃逸之seccomp学习
原创 | 堆的tcache利用
原创 | 堆的off-by-one利用
原创 | vulnhub: Momentum:1


你要的分享、在看与点赞都在这儿~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存