MyBatis 默认是支持OGNL 表达式的,尤其是在动态SQL中,通过OGNL 表达式可以灵活的组装 SQL 语句,从而完成更多的功能。在特定的情况下可能会存在RCE的风险。
0x01引言
前段时间看了一道CTF题目ezsql
看完文章后有一些疑惑:
带着这些疑惑,下面从mybatis的解析流程入手,分析这个case的成因并且看看能不能解决上述提出的疑惑。
0x02 mybatis封装SQL流程
提到Mybatis很自然的会想到${}和#{},看看具体是怎么解析的。
2.1 相关过程
Mybatis的工作流程首先是构建,也就是解析我们写的配置(xml,注解等),将其变成它所需要的对象。然后就是执行,通过前面的配置信息去执行对应的SQL,完成与Jdbc的交互。
简单的分析下具体的流程,案例代码如下:
对应的mapper方法:
相关的xml配置:
以mybatis 3.5.1为例,将断点下在调用的mybatis mapper方法上,简单的梳理对应的执行流程:
首先是
org.apache.ibatis.binding.MapperProxy#invoke
方法,主要的执行逻辑都在
MapperMethod
的
execute()
方法:
跟进
org.apache.ibatis.binding.MapperMethod#execute
方法,首先是SQL类型的判断(INSERT/UPDATE/DELETE/SELECT):
mapper的方法是SELECT的,具体看看SELECT的流程,这里会通过method的返回值的不同调用不同的方法。例如前面的mapper method返回值是List
,会返回多行,那么就会调用
executeForMany()
方法:
在
org.apache.ibatis.binding.MapperMethod#executeForMany
中,核心的是
sqlSession.selectList()
,具体的sql执行应该是在这里,rowBounds参数从名称上看应该是跟分页有关的:
继续跟进,通过
MappedStatement#getBoundSql()
来获取要执行的sql语句,这里应该会对相关的SQL进行组装:
这里主要是通过SqlSource来组装,继续跟进相关的代码:
sqlSource主要是个接口,有四个实现类:
-
StaticSqlSource静态SQL,DynamicSqlSource、RawSqlSource处理过后都会转成StaticSqlSource
-
DynamicSqlSource处理包含${}、动态SQL节点的
-
RawSqlSource处理不包含${}、动态SQL节点的
-
ProviderSqlSource动态SQL,看名称应该是跟类似@SelectProvider注解有关
前面xml里配置的是
${username}
,如果包含
${}
的话一般会调用DynamicSqlSource进行解析,跟进具体的代码:
在
rootSqlNode.apply(context)
会对相关的sql节点进行组装,那么就会对root节点进行遍历,调用对应class的apply方法进行解析:
这里会遍历所有的SqlNode,然后根据对应的type再次调用对应的apply方法:
例如当前mapper的节点的类型为TextSqlNode,会调用其apply方法进行进一步的解析:
继续跟进,这里调用了
org.apache.ibatis.parsing.GenericTokenParser#parse
方法进行处理,主要是删除反斜杠并处理相关的参数(${}):
最后
把${}包裹的内容提取出来
,然后调用
BindingTokenParser#handleToken
方法进行解析:
再往下跟进,这里会调用
OgnlCache.getValue
方法,从名称看应该是对sql中ognl表达式进行解析,然后替换SQL中对应的
${xxx}
:
完成对应sql的封装后,最终会调用selectList方法完成sql执行的操作,从下图中的Exception信息也可以知道该方法与数据库进行了交互:
mybatis的整个封装SQL的流程大体上就是这样子了。
#{}的解析流程类似,主要不同的是BindingTokenParser变成了ParameterMappingTokenHandler,然后在handleToken对将#{}替换成占位符?
。
0x03 可能的缺陷
结合前面的CTF题目以及对应的疑惑,这里做一个猜想,mybatis使用不当的话存在sql注入(即输入直接拼接${})的情况,那么根据上面的分析,会调用DynamicSqlSource然后通过OgnlCache进行相应的解析,
如果parseExpression方法中解析的expression是一个恶意的ognl表达式的话,那么有可能存在风险
:
那么如何找到一处可以输入${恶意ognl表达式},同时能调用DynamicSqlSource通过OgnlCache进行相应的解析的利用点呢?题目里的Provider注解有什么关联呢?
3.1 分析过程
MyBatis 默认是支持OGNL 表达式的,尤其是在动态SQL中,通过OGNL 表达式可以灵活的组装 SQL 语句,从而完成更多的功能。从MyBatis的常见使用方式开始梳理,逐个进行分析:
3.1.1 XML配置
XML配置是比较常用的一种方式。
假设xml配置如下:
根据前面的分析MyBatis处理${}的时候,会使用OGNL计算这个结果值,然后替换SQL中对应的${xxx}。
很明显在调用这个mapper method时,OGNL会计算
@java.lang.Runtime@getRuntime().exec("open /System/Applications/Calculator.app")
的结果,然后再拼接到原始的SQL中。那么对应的恶意OGNL表达式就会被执行,这里简单写一个controller调用验证猜想。
可以看到提取了${}里的内容,然后OGNL进行了解析,很明显会调用Runtime执行对应的命令:
以上是一种比较理想的情况,实际上也不会有人在xml里写恶意的ognl表达式,替换跟覆盖已有的XML也不太现实。
更常见的场景一般是如下配置:
="getUserByUserName" parameterType="String" resultMap="User">
select * from users where username like ${username}
如果整个解析顺序如下:
那么确实这里除了SQL注入以外,还可以通过OGNL注入达到上面RCE的效果。显然Mybatis的设计者在设计之初就考虑到了这一个风险,下断点调试,可以看到实际情况在解析时只会解析原有的${username},解析完毕后再把用户输入的值赋予给他。避免了RCE的利用。
3.1.2 普通注解
mybatis3提供了注解的方式,常见的有@Select、@Insert、@Update、@Delete,他们跟xml配置中对应的标签语法是类似的。这里一般sql配置是不可控的,跟xml一样没办法操作。
3.1.3 Provider注解
除了上述两种方式以外,MyBatis3提供了使用Provider注解指定某个工具类的方法来动态编写SQL。也就是题目里的注解,常见的注解有:
-
@SelectProvider
-
@InsertProvider
-
@UpdateProvider
-
@DeleteProvider
本质上Provider指定的工具类只需要返回一个SQL字符串,通过在外部定义好 sql直接引用
。
跟进下其封装SQL的过程,前面的过程都是一样的,最后都会通过
MappedStatement#getBoundSql()
来获取要执行的sql语句,进行相应的组装。
然后会调用
org.apache.ibatis.builder.annotation.ProviderSqlSource#getBoundSql
进一步组装:
相比xml配置,多了一个
org.apache.ibatis.builder.annotation.ProviderSqlSource#createSqlSource
的调用:
继续跟进,调用
org.apache.ibatis.builder.annotation.ProviderSqlSource#invokeProviderMethod
:
因为Provider注解是用户自己编辑的,从对应的参数信息可以看出来这里大致应该是解析相应的外部类,得到对应的SQL,然后返回: