查看原文
其他

原创 | MyBatisPlus通用IService的注入场景

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


关于Mybatis plus通用IService


通常来说,我们会将具体的业务逻辑封装在 service 层中(一般会有个 interface 类以及具体的实现)。而MyBatis-Plus为我们提供了一个IService 接口,里面封装了通用 Service CRUD 操作。


1.1   相关依赖

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-extension</artifactId> <version>3.4.3</version></dependency>

1.2   使用方法


首先根据对应的数据库表创建对应的实体类:
@Datapublic class User { private Long id; private String name; private Integer age; private String email;}

然后创建UserMapper接口,并继承BaseMapper接口,就可以使用Mapper内置的各种CRUD方法了:
@Mapperpublic interface UserMapper extends BaseMapper<User> {
}

最后继承 IService 创建 Serivice 接口,并创建对应的实现类,这样便可以使用 Servive 的内置的各种 CRUD 方法了:
public interface IUserService extends IService<User> {}

@Servicepublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}

UserServiceImpl.java使用了BaseMapper或者BaseMapper的子类用于实现CURD等功能,实际上也就是在BaseMapper功能的基础上封装了一些方法。实际上功能的实现还是要依赖于BaseMapper

完成上述配置后就可以调用IService封装的方法了,例如使用page方法进行分页操作:
@Autowired IUserService userService;
@Test void lambdaPageTest() { LambdaQueryChainWrapper<User> wrapper2 = userService.lambdaQuery(); wrapper2.like(User::getName, "a"); userService.page(new Page<>(1, 10), wrapper2.getWrapper()).getRecords().forEach(System.out::print); }


通用IService中的注入场景


同样的,通用IService中也会存在使用不当导致的SQL注入风险。下面列举常见的场景。


2.1   分页查询


IService 接口提供了 page/pageMaps 方法实现分页查询,同时会自动识别是何种数据库,然后自动拼接相应分页语句(若是 mysql 则自动通过 limit 分页,若是 oracle 则自动通过 rownum 进行分页)。


2.1.1   漏洞原理


查看其具体方法实现,实际上还是调用了BaseMapper里的分页方法

/** * 无条件翻页查询 * * @param page 翻页对象 * @see Wrappers#emptyWrapper() */ default <E extends IPage<T>> E page(E page) { return page(page, Wrappers.emptyWrapper()); }
/** * 翻页查询 * * @param page 翻页对象 * @param queryWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.query.QueryWrapper} */ default <E extends IPage<T>> E page(E page, Wrapper<T> queryWrapper) { return getBaseMapper().selectPage(page, queryWrapper); } /** * 翻页查询 * * @param page 翻页对象 * @param queryWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.query.QueryWrapper} */ default <E extends IPage<Map<String, Object>>> E pageMaps(E page, Wrapper<T> queryWrapper) { return getBaseMapper().selectMapsPage(page, queryWrapper); }
/** * 无条件翻页查询 * * @param page 翻页对象 * @see Wrappers#emptyWrapper() */ default <E extends IPage<Map<String, Object>>> E pageMaps(E page) { return pageMaps(page, Wrappers.emptyWrapper()); }

因为本质上还是调用了BaseMapper的selectPageselectMapsPage 方法,传递参数 Page 即自动分页 。所以同样的,若使用不当会存在对应的安全问题。

例如如下case,以调用page方法为例,实现了通用的IService,然后调用page方法进行分页操作:
@Autowired IUserService userService;
@RequestMapping(value = "/getPageByJson") public String getPage(@RequestBody Page page) { IPage userList =userService.page(page, new QueryWrapper<User>().lambda().eq(User::getId, 1)); ...... }

直接传递
com.baomidou.mybatisplus.extension.plugins.pagination.Page

结合Spring自动绑定,会存在SQL注入风险,更详细的分析可以参考
https://sec-in.com/article/1088

不同版本对应的风险参数:
  • ascs/descs
  • List<OrderItem> orders


2.1.2   利用方式


利用方式也比较简单,根据不同mybatis-plus版本操作对应的参数进行注入即可:


打印日志成功注入并执行1/0,触发SQL Error:


高版本主要操作orders集合,同理:


打印日志成功注入并执行1/0,触发SQL Error:


2.2   链式调用(查询与更新)


IService 接口支持链式操作。例如提供了一个 query 方法,该方法返回 QueryChainWrapper 对象。我们可以使用该对象实现链式查询,避免每次都创建 QueryWrapper (条件构造器)对象。
/** * 以下的方法使用介绍: * * 一. 名称介绍 * 1. 方法名带有 query 的为对数据的查询操作, 方法名带有 update 的为对数据的修改操作 * 2. 方法名带有 lambda 的为内部方法入参 column 支持函数式的 * * 二. 支持介绍 * 1. 方法名带有 query 的支持以 {@link ChainQuery} 内部的方法名结尾进行数据查询操作 * 2. 方法名带有 update 的支持以 {@link ChainUpdate} 内部的方法名为结尾进行数据修改操作 * * 三. 使用示例,只用不带 lambda 的方法各展示一个例子,其他类推 * 1. 根据条件获取一条数据: `query().eq("column", value).one()` * 2. 根据条件删除一条数据: `update().eq("column", value).remove()` * */
/** * 链式查询 普通 * * @return QueryWrapper 的包装类 */ default QueryChainWrapper<T> query() { return new QueryChainWrapper<>(getBaseMapper()); }
/** * 链式查询 lambda 式 * <p>注意:不支持 Kotlin </p> * * @return LambdaQueryWrapper 的包装类 */ default LambdaQueryChainWrapper<T> lambdaQuery() { return new LambdaQueryChainWrapper<>(getBaseMapper()); }
/** * 链式更改 普通 * * @return UpdateWrapper 的包装类 */ default UpdateChainWrapper<T> update() { return new UpdateChainWrapper<>(getBaseMapper()); }
/** * 链式更改 lambda 式 * <p>注意:不支持 Kotlin </p> * * @return LambdaUpdateWrapper 的包装类 */ default LambdaUpdateChainWrapper<T> lambdaUpdate() { return new LambdaUpdateChainWrapper<>(getBaseMapper()); }

2.2.1   漏洞原理


以其中的QueryChainWrapper为例,其提供的方法和 QueryWrapper  (条件构造器)方法基本是一致的。

那么也就是说条件构造器对应Method直接sql拼接的风险方法,链式查询对象也是存在的。

例如QueryWrapper的apply(),实际上是父类AbstractWrapper的方法:
public abstract class AbstractWrapper<T, R, Children extends AbstractWrapper<T, R, Children>> extends Wrapper<T> implements Compare<Children, R>, Nested<Children, Children>, Join<Children>, Func<Children, R> { ...... @Override public Children apply(boolean condition, String applySql, Object... value) { return doIt(condition, APPLY, () -> formatSql(applySql, value)); }}

同理,包装类QueryChainWrapper 的父类AbstractChainWrapper同样提供了apply方法(本质上通过getWrapper方法获取了条件构造器 ,然后再调用条件构造器的apply方法):
public abstract class AbstractChainWrapper<T, R, Children extends AbstractChainWrapper<T, R, Children, Param>, Param> extends Wrapper<T> implements Compare<Children, R>, Func<Children, R>, Join<Children>, Nested<Param, Children> { @Override public Children apply(boolean condition, String applySql, Object... value) { getWrapper().apply(condition, applySql, value); return typedThis; } @SuppressWarnings("rawtypes") public AbstractWrapper getWrapper() { return (AbstractWrapper) wrapperChildren; }}

2.2.2   利用方式


实际上只需要补全sql上下文,利用sql拼接即可利用。举例说明:

  • 查询query

例如如下case,通过query方法得到QueryChainWrapper,然后使用orderByAsc方法进行排序(该排序方法跟所有Orderby排序一样,都是动态拼接的):

@RequestMapping(value = "/getPage") public String getPage(String sort,Integer UserId) { List<User> userList = userService.query().eq("id", UserId).orderByAsc(sort).list(); ...... }

因为orderByAsc是直接进行sql拼接的,所以直接写入对应的sql语句即可:


打印日志成功注入并执行1/0,触发SQL Error:



  • 更新update

通过UserId来更新username,通过调用setSql()方法来设置username(存在sql直接拼接的操作):
@RequestMapping(value = "/updateUserName") public String getPage1(String username,Integer UserId) { boolean isTrue = userService.update().eq("id", UserId).setSql("name='"+username+"'").update(); if(isTrue){ ...... } ......

所以这里可以通过'||'补全setSql的上下文,然后利用拼接符里可以执行表达式的特点来进行利用(案例中是H2数据库):

  
打印日志成功注入并执行1/0,触发SQL Error:


其他query和update的函数同理,只要对应的method存在sql拼接,即可利用。


2.2.3   相关函数


因为本质上通过getWrapper方法获取了条件构造器,然后调用对应的方法进行操作,所以常见的风险函数是类似的。这里统计了一些,方便审计时使用:
apply(String applySql, Object... params)apply(boolean condition, String applySql, Object... params)last(String lastSql)last(boolean condition, String lastSql)exists(String existsSql)exists(boolean condition, String existsSql)notExists(String notExistsSql)notExists(boolean condition, String notExistsSql)having(String sqlHaving, Object... params)having(boolean condition, String sqlHaving, Object... params)orderBy(boolean condition, boolean isAsc, R... columns)orderByDesc(R... columns)orderByDesc(boolean condition, R... columns)orderByAsc(R... columns)orderByAsc(boolean condition, R... columns)groupBy(R... columns)groupBy(boolean condition, R... columns)notInSql(boolean condition, R column, String inValue);notInSql(R column, String inValue)inSql(R column, String inValue)inSql(boolean condition, R column, String inValue)setSql(String sql)setSql(boolean condition, String sql)

PS:apply、having跟条件构造器的一样,是可以通过动态入参params进行预编译处理来防止 SQL 注入问题的:
apply("role_id = {0}",2)

安全加固建议


上述场景中,除了apply、having是因为方法使用不当导致注入以外,其他场景更多的类似动态拼接,可以考虑对用户输入进行安全检查。对于Page分页,遵循最小化原则,尽量不要直接传递com.baomidou.mybatisplus.extension.plugins.pagination.Page,避免Spring自动绑定导致的注入风险。


参考资料


https://baomidou.com/guide/crud-interface.html#service-crud-%E6%8E%A5%E5%8F%A3


相关推荐



原创 | Digvuln Tricks之JS泄露全到后台越权
原创 |【胖哈勃的七月公开赛】NewSql
原创 | 记一次完成的钓鱼实战
原创 | AMSI 浅析及绕过

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

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

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