MyBatis Plus 防御机制浅析(一)
2024-1-9 17:43:37 Author: www.freebuf.com(查看原文) 阅读量:10 收藏

1. 问题背景

本文起源于一个生产问题。我们在生产环境用户信息表的create_by字段看到了来自用户侧的输入,且未过滤特殊字符(注意到存在符号”+”),怀疑可能成为SQL注入点。

开发大佬反馈,该列数据的插入/更新使用了MyBatis-Plus。但MyBatis-Plus是什么?以下引用官方介绍:

What is MyBatis-Plus?

MyBatis-Plus is an powerful enhanced toolkit of MyBatis for simplify development. This toolkit provides some efficient, useful, out-of-the-box features for MyBatis, use it can effectively save your development time.

那么,为MyBatis提供便捷实现的MyBatis-Plus在方便开发者的同时是否可能引入SQL注入?本文将从该生产实例回答以下这个问题:

  • MyBatis Plus如何防御SQL注入;

2. 测试环境准备

为了不影响生产数据,使用官方提供的mybatis-plus-sample-crud快速搭建本地测试环境。

mysql版本:8.0.35

mybatis-plus-spring-boot3-starter版本:3.5.4

3. 问题分析

1)确认生产数据来源

根据表格“create_by”字段搜索源代码,发现使用了MyBatis Plus的注解,该基于iBatis实现(详见第4节)。该字段来自用户传入的request参数,且未做任何过滤。

MemberInfo类定义了数据库字段create_by对应的属性:

查看引用,InfoMessagesSAOImpl.java不涉及数据库操作,注意MemberServiceImpl.java的214行、1910行:

1704792614_659d1226ee759e80906eb.png!small

分别在220/1915使用了mybatis-plus的insert接口;

1704792623_659d122fd6aa038729c0b.png!small1704792623_659d122fd98846b82833b.png!small

insert接口来自mybatisplus1704792658_659d1252b76f2f4d1f522.png!small?1704792657391

2)测试环境复现

由于生产使用了insert方法,故本地测试也用insert并观察是否存在注入。

测试数据表如下:

1704792727_659d1297ab627191cc44a.png!small?1704792726378

接下来,直接在mysql的console内直接执行含有注入payload的语句:

insert into sys_user (id, name, age, email) values (124,'Jasdf',42,'[email protected]');/**/DROP/**/TABLE/**/sys_user;/**/--#',25,'[email protected]');

可以发现恶意语句被执行,sys_user表被删除:

1704792732_659d129c6e523050ff0be.png!small

重新建表,并将payload放入参数`name`中,观察MyBatisPlus如何处理恶意payload:

Jasdf',42,'[email protected]');/**/DROP/**/TABLE/**/sys_user;/**/--#

1704792889_659d1339e796207ea728b.png!small?1704792888642

Application.xml添加配置,打印MyBatis-Plus构造后的SQL语句便于观察

# MyBatis-Plus 配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志

注意到payload经过MyBatis-Plus处理后依然是String类型:

1704792944_659d13705082dd4e0da1f.png!small?1704792943089

Jasdf',42,'[email protected]');/**/DROP/**/TABLE/**/sys_user;/**/--# (String)

检查数据:

1704792954_659d137ad4e4670f00ebf.png!small?1704792953349

Payload作为完整的String被插入了数据库,说明注入未成功。

4. 防御方式解析

在第3节中,我们的注入未成功,下面通过debug方式展示mybatis-plus对攻击者的payload做了什么处理。

在SqlSourceBuilder.java的

ParameterMappingTokenHandler handler处下断点,可以看到原始语句首先被解析为为模板+数据,其中模板为:

`INSERT INTO sys_user  ( id, name, age, email )  VALUES (  #{id}, #{name}, #{age}, #{email}  )`

1704792975_659d138f71fc58de591ae.png!small?1704792974597

熟悉mybatis的同学都知道,相较于${value}不参与预编译的形式,#{value}形式的变量最终将调用preparedStatement来实现预编译。对于预编译的SQL语句,mysql将以开发者预定的参数类型构造sql语句,换句话说,不会将payload理解为新的SQL语句。

1704793036_659d13cc9e2d1da557908.png!small?1704793035530

PrepareStatement调用:

1704793048_659d13d86d2b100c6be6b.png!small?1704793047190

开始预编译:

1704793059_659d13e35db105f5bf899.png!small?1704793058126

handler.parameterize对预编译语句进行变量初始化,首先是long类型的id,通过ibatis的setLong方法处理(由于SQL注入重点在于String类型,本文不进一步测试Int或者long类型的处理方式,仅关注String类型):

1704793100_659d140c4e724d7652548.png!small?1704793099877

1704793118_659d141ed764c627e09c5.png!small?1704793117854

然后是String类型的name:

1704793128_659d14285c7116bfb3696.png!small?1704793127845

1704793144_659d1438bb09b5fe1a968.png!small?1704793143778

在boundSql对象中将模板SQL语句和参数对象一一对应

1704793156_659d14445226d58fcf1c4.png!small?1704793155849

typeHandler.setParameter将‘name’和payload进行绑定

1704793170_659d14523919d9524345a.png!small

来到关键方法。Name作为string类型,setNonNullParameter对其赋值,调用了ps.setString(i, parameter);方法,该方法判断String类型的变量内的特殊符号是否需要转义:

1704793196_659d146c7f29174d44ef1.png!small?1704793195344

1704793205_659d147551857ab68b5d9.png!small?1704793203975

可以发现攻击payload中含有的‘,将在此处被转义。除此之外,’\u0000’,’\n’,’\r’,…等符号也会被转义,视情况在特殊符号后添加\’,\n,\r,\Z等:

1704793211_659d147b560db1ed5692b.png!small?1704793210193

1704793219_659d14834b2b1f0d408e2.png!small

最终,经过预编译后,即将送入MySQL的stmt对象变为:

1704793227_659d148b8cf9ca3ab2b99.png!small?1704793226475

1704793296_659d14d0497ad5c800828.png!small?1704793295062

INSERT INTO sys_user  ( id, name, age, email )  VALUES (  1744564513751109634, 'Jasdf'',42,''[email protected]'');/**/DROP/**/TABLE/**/sys_user;/**/--#', 3, '[email protected]'  )

对比原始payload:

Jasdf',42,'[email protected]');/**/DROP/**/TABLE/**/sys_user;/**/--#

Jasdf'',42,''[email protected]'');/**/DROP/**/TABLE/**/sys_user;/**/--#

由此回答第一个问题,String类型的payload经过preparedStatement处理后,原有可能导致sql语句被截断的单引号’后添加了一个’作为转义,避免了SQL注入的发生。本文以“insert”语句为例,对于Update\Select\Create语句内的setString类型变量皆是如此,这里不再赘述。

竟有如此方便的MyBatis-Plus,不需要人工介入即可完成参数的预处理,那是不是只要使用它就可以远离SQL注入高枕无忧了呢?事实上并不能,且听下回分解。

5. 参考

  1. https://github.com/baomidou/mybatis-plus-samples

文章来源: https://www.freebuf.com/articles/web/389261.html
如有侵权请联系:admin#unsafe.sh