MybatisPuls分页插件中的SQL注入 – 作者:SecIN技术社区

作者:tkswifty

来源:SecIN社区

实际业务场景中经常需要执行limit分页语句,返回部分数据。物理分页只返回部分数据占用内存小,能够获取数据库最新的状态,实时性比较强,一般适用于数据量比较大,数据更新比较频繁的场景。

MybatisPlus插件提供了相应的支持。使用PaginationInnerInterceptor插件可以十分方便的完成相应的功能

简介| MyBatis-Plus

一、关于PaginationInnerInterceptor

PaginationInnerInterceptor作为plus的分页插件,提供了通用的参数进行统一配置。可以很方便的完成分页的业务逻辑。

1.1 相关依赖

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

1.2 Springboot集成

主要是通过拦截器的方式配置分页插件:

@Configuration
public class MyBatisPlusConfig {
 
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
}

新版本改用了MybatisPlusInterceptor,配置如下:

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
}

定义好mapper后就可以直接调用对应的方法进行操作了。

最常用的是直接继承BaseMapper接口,无需编写mapper.xml文件,即可获得CRUD功能。其中mybatis-plus提供的分页方法是selectPageselectMapsPage,其中一个入参为com.baomidou.mybatisplus.extension.plugins.pagination.page,以下是具体的function定义:

/**
   * 根据 entity 条件,查询全部记录(并翻页)
   *
   * @param page     分页查询条件(可以为 RowBounds.DEFAULT)
   * @param queryWrapper 实体对象封装操作类(可以为 null)
   */
  <E extends IPage<T>> E selectPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
  
   /**
   * 根据 Wrapper 条件,查询全部记录(并翻页)
   *
   * @param page     分页查询条件
   * @param queryWrapper 实体对象封装操作类
   */
  <E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

可以看到,直接调用对应的function即可完成相关的业务需求:

@GetMapping("/userList")
    public String userList(@RequestParam(value = "pn", defaultValue = "1")Integer pn, Model model) {

        // 分页查询数据
        Page<User> userPage = new Page<>(pn, 2); //pn:第几页 size:每页记录条数
        Page<User> page = userService.page(userPage, null);

        model.addAttribute("users", page);

        long current = page.getCurrent(); // 当前页码
        long pages = page.getPages();     // 总页数
        long total = page.getTotal();     // 总记录数

        ......
    }

另外,对于使用xml和注解自定义的mapper方法,传递参数 Page 即自动分页,且参数必须放在第一位。

二、分页参数page

具体的分页是通过配置Page对象相关的参数实现的。同时,不同阶段的版本com.baomidou.mybatisplus.extension.plugins.pagination.page实现是不一样的。主要区别在于Orderby排序字段上。

2.1 ascs/descs

mybatis-plus-extension-3.1.1及以下版本

通过控制ascs或者descs来进行分页排序(3.1.1版本):

public class Page<T> implements IPage<T> {
    private static final long serialVersionUID = 8545996863226528798L;
    private List<T> records;
    private long total;
    private long size;
    private long current;
    private String[] ascs;
    private String[] descs;
    private boolean optimizeCountSql;
    private boolean isSearchCount;

    ......
    }

2.2 List<OrderItem> orders

mybatis-plus-extension-3.1.2及以上版本

通过List集合orders来进行分页排序(3.4.3版本),也可以通过setAsc、setDescs方法来配置,但是相关方法已弃用:

public class Page<T> implements IPage<T> {
  private static final long serialVersionUID = 8545996863226528798L;
  
  protected List<T> records = Collections.emptyList();
  
  protected long total = 0L;
  
  protected long size = 10L;
  
  protected long current = 1L;
  
  protected List<OrderItem> orders = new ArrayList<>();
  
  protected boolean optimizeCountSql = true;
  
  protected boolean searchCount = true;
  
  protected String countId;
  
  protected Long maxLimit;
  
  public void setOrders(List<OrderItem> orders) {
    this.orders = orders;
  }
  ......
}

orders集合封装了OrderItem,在OrderItem中包含了排序分页所需要的字段内容,默认为asc正序排序:

public class OrderItem implements Serializable {
  private static final long serialVersionUID = 1L;

  /**
   * 需要进行排序的字段
   */
  private String column;
  /**
   * 是否正序排列,默认 true
   */
  private boolean asc = true;

  public static OrderItem asc(String column) {
    return build(column, true);
  }

  public static OrderItem desc(String column) {
    return build(column, false);
  }
  ......
}

三、PaginationInnerInterceptor使用不当导致的SQL注入

跟所有的框架插件一样,只要涉及到数据库交互,使用不当就会导致SQL注入的安全风险。

3.1 Orderby场景下的SQL注入

前面提到了分页中会存在Orderby的使用,因为Orderby动态查询没办法进行预编译,所以不经过安全检查的话会存在注入风险。PaginationInnerInterceptor主要是通过设置com.baomidou.mybatisplus.extension.plugins.pagination.page对象里的属性来实现orderby的,主要是以下函数的调用,因为直接使用sql拼接,所以需要对进行排序的列名进行安全检查:

page.setAsc();
page.setDesc();
page.setAscs();
page.setDescs();
page.setOrders();
page.addOrder();

3.2 Spring自动绑定导致的潜在SQL注入风险

3.2.1 漏洞原理

在 Spring框架中,提交请求的数据是通过方法形参来接收的。从客户端请求的 key/value 数据,经过参数绑定,将 key/value 数据绑定到 Controller 的形参上,然后在 Controller 就可以直接使用该形参。

在实际开发场景中,开发人员可能通过直接传递com.baomidou.mybatisplus.extension.plugins.pagination.page来实现业务,例如下面的例子:

@RequestMapping(value = "/getPage")
    public String getPage(Page page) {
        Page<User> userList =userMapper.selectPage(page, new QueryWrapper<User>().lambda().eq(User::getId, 1));
      ......
    }

查看Page的属性,直接在request请求时候传入size和current两个参数就可以完成对应的分页需求了,前端的确也是这么设计的:

public class Page<T> implements IPage<T> {
    private static final long serialVersionUID = 8545996863226528798L;
    protected List<T> records; //分页对象记录列表
    protected long total; //总记录跳数
    protected long size; //每页显示条数
    protected long current; //当前页码
    protected List<OrderItem> orders; //排序信息
    protected boolean optimizeCountSql; //自动优化 COUNT SQL【 默认:true 】
    protected boolean isSearchCount; //进行 count 查询 【 默认: true 】
    protected boolean hitCount; //设置是否命中count缓存
    protected String countId; //MappedStatement 的 id
    protected Long maxLimit; //最大每页分页数限制,优先级高于分页插件内的 maxLimit
    ......
}

根据spring自动绑定的特性,若此时加入orders参数的传递,同样的后端会进行对应的实体封装,最终带入到sql查询中,同时因为order by场景下MybatisPlus并没有相关的安全措施,会导致SQL注入风险。

3.2.2 利用方式

以上面的代码为例,看看具体的利用方式。同时前面也提到了不同版本com.baomidou.mybatisplus.extension.plugins.pagination.page对象里排序相关的属性会不一样,这里结合常见的reqeust提交方式分情况讨论(主要是普通的post和json请求):

根据自动绑定的特性,可以将page对象中相关的属性为参数(前面第二节提到的ascs、descs以及List<OrderItem> orders),追加在request中,尝试利用,这里以H2 database为例,通过注入恶意sql请求dnslog来进行验证:

ascs/descs

以descs为例,其参数类型是String[],直接传递即可:

wKg0C2C68JqAMA9JAABW3CGcgrE629.png

通过print相关的sql日志可以看到追加的内容id,1/0成功引入到sql查询中,同时返回除0错误,说明SQL注入利用成功:

wKg0C2C68NyAciKcAAB9S1qa0Qo702.png

这里有个需要注意的点是,因为descs的数据类型是是String[],逗号会自动进行分隔,在实际利用的时候会比较麻烦

List<OrderItem> orders

因为自动绑定的内容是List,在传参时可以通过entity[index].attribute的方式进行传递:

wKg0C2C67WiAM5pmAABm5dmc2wA126.png

wKg0C2C67TmAUjFHAABN84MuIAg175.png

除了上述的情况,经常会有通过@RequestBody注解来传递JSON内容的情况:

@RequestMapping(value = "/getPageByJson")
    public String getPage1(@RequestBody Page page) {
            Page<User> userList =userMapper.selectPage(page, new QueryWrapper<User>().lambda().eq(User::getId, 1));
      ......
    }

ascs/descs

以descs为例,其参数类型是String[],可以通过[content,content]的方式进行传递,跟application/x-www-form-urlencoded不一样,因为相关字段内容用“”圈定了,所以上下文的逗号不会相互影响:

wKg0C2C68jCAVOMoAABydyxlTk720.png

wKg0C2C68haAZGjPAABNpV9BFEw982.png

List<OrderItem> orders

通过前面查看源码,可以知道orders参数的类型是List集合。

@RequestBody可以通过如下方式来自动绑定list集合:

wKg0C2C680mARDQnAABtwBR1scg056.png

wKg0C2C68yqAMBa0AABcIvSfxrQ899.png

同样的print相关日志,可以看到恶意的sql语句成功写入:

wKg0C2C69GqAQFC6AACBiMVbees267.png

3.2.3 其他

有时候用api级别的方法不能满足分页的需求,比如多张表关联查询的时候,这个时候就需要自定义分页了。对于使用xml和注解自定义的mapper方法,传递参数 Page 即自动分页(且参数必须放在第一位),同样存在类似的问题。例如下面的例子:

public interface UserMapper extends BaseMapper<User> {

     @Select({"< script>",
    "SELECT id, name, age, email FROM user where id=#{id}</script>"})
     IPage<User> queryPageById(Page<?> page, @Param("id")long id);

}

同理,在controller直接调用mapper对应的function即可:

@RequestMapping(value = "/getPage")
    public String getPage(Page page,long id) {
        IPage<User> userlist = userMapper.queryPageById(page,id);
        ....
    }

因为直接传递了Page实体,所以同样的可以直接在相关的参数写入恶意sql语句,通过自动绑定即可利用,数据库为h2 database,这里通过请求dnslog进行验证:

wKg0C2C67ZeAdbZfAABnCtRkKRY238.png

可以看到dnslog成功记录到请求,sql注入利用成功:

wKg0C2C66qqAXxkOAABDcAm3o747.png

综上所述,由于MybatisPlus分页插件的使用方法不当,导致了SQL注入风险。

四、其他

对于上述情况,在实际开发时需要额外的注意。可以通过如下方法来规避自动绑定导致的SQL注入问题。

可以修改Controller层,仅仅接受用户可以控制的参数,若需要使用orderby排序相关的参数,加入相关的安全检查即可:

@GetMapping("/userList")
    public String userList(@RequestParam(value = "pn", defaultValue = "1")Integer pn, Model model) {

        // 分页查询数据
        Page<User> userPage = new Page<>(pn, 2); //pn:第几页 size:每页记录条数
        Page<User> page = userService.page(userPage, null);
        ......

}

也可以通过设置@InitBinder可以绑定的白名单,但是例如List<OrderItem> orders不是简单的类型,需要定义相应的Editor进行处理。

来源:freebuf.com 2021-06-25 10:23:49 by: SecIN技术社区

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论