环境

若依后台管理系统存在多种架构体系。如下

这里使用RuoYi-fast v4.7.3(前后端不分离)来分析定时任务功能点处如何绕过黑白名单,执行任意的sql语句

Quartz组件

RuoYi-fast使用Quartz作为定时任务组件,但由于本文是重点分析定时任务处产生漏洞原因,因此仅简单写个Quartz使用demo,更多的使用可百度获取。
创建springboot项目,导入如下依赖:

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
  </dependency>
</dependencies>

编写job,需要继承org.quartz.Job,如下继承org.quartz.Job抽象子类QuartzJobBean

package com.example.quartz.job;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateTimeJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        String msg = (String) context.getJobDetail().getJobDataMap().get("msg");
        System.out.println("current time :"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "---" + msg);
    }
}

配置jobtrigger

package com.example.quartz.config;

import com.example.quartz.job.DateTimeJob;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzConfig {
    // 配置 job
    @Bean
    public JobDetail printTimeJobDetail(){
        return JobBuilder.newJob(DateTimeJob.class)//PrintTimeJob我们的业务类
                .withIdentity("DateTimeJob")//可以给该JobDetail起一个id
                //每个JobDetail内都有一个Map,包含了关联到这个Job的数据,在Job类中可以通过context获取
                .usingJobData("msg", "Hello Quartz")//关联键值对,当触发定时任务时,可从上下文中获取此键值对
                .storeDurably()//即使没有Trigger关联时,也不需要删除该JobDetail
                .build();
    }

    // 配置 trigger:触发规则
    @Bean
    public Trigger printTimeJobTrigger() {
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?");
        return TriggerBuilder.newTrigger()
                .forJob(printTimeJobDetail())//关联上述的JobDetail
                .withIdentity("quartzTaskService")//给Trigger起个名字
                .withSchedule(cronScheduleBuilder)
                .build();
    }
}

启动项目,定时任务成功执行

漏洞分析

定时任务执行逻辑

定时任务代码在com.ruoyi.project.monitor.job包下:
Job接口是真正干活的,所要实现的业务处理,都会继承此接口重写excute方法
RuoYi-fast中定义的Job体系如下:

查看AbstractQuartzJob抽象类,新增了beforedoExecuteafter方法,重写了父类的excute方法(此方法采用了模版方法设计模式),其中before方法记录任务执行开始时间,doExecute是真正执行任务的方法(此方法交给具体的子类实现),after方法用于将任务执行日志写入数据库中,excute方法将before、doExecute、after三个方法组合。

excute方法如下:

子类QuartzDisallowConcurrentExecutionQuartzJobExecution实现了doExecute方法,这两个类只存在再并发支持上的区别,因此这里分析QuartzJobExecution即可。

继续跟进invokeMethod(Job)方法

跟进invokeMethod方法,利用反射执行方法

一些细节处:
获取BeanName

获取MethodName

获取MethodParams:从目标字符串中提取第一个(和第一个)中间的字符串。并将其以,进行分割成字符串数据

遍历分割生成的字符串数组

可以看到参数类型仅仅支持以下数据类型:StringBooleanLongDoubleInteger类型。
判断beanName是否是指定格式,当beanName是中不包括.或者仅仅包括一个.符号便符合指定格式

接着便走到如下分支


从上可分析出如下结果:

  1. 对象可以是spring容器中注册过的bean,也可以指定class名称
  2. 若是spring容器中注册过的bean,则可直接从spring容器中取出,若是指定class名称,则可以通过反射newInstance()创建对象,因此必须保证class中存在无参构造函数
  3. 方法不能是private修饰的方法,因为在getDeclaredMethod获取方法后,并没有执行setAccessible(true)
  4. 方法参数类型仅仅可以为如下类型:String,Boolean,Long,Double,Integer

定时任务添加/修改逻辑

由于定时任务的新增和修改逻辑相似,因此这里仅仅分析定时任务的添加。
查看:com.ruoyi.project.monitor.job.controller.JobController#addSave

可以看到在添加定时任务前,进行了黑白名单的判断。当通过了上述条件后,则执行com.ruoyi.project.monitor.job.service.JobServiceImpl#insertJob,代码如下,先将定时任务相关字段存入数据库中,然后使用Quartz创建定时任务

跟进com.ruoyi.project.monitor.job.util.ScheduleUtils#createScheduleJob,创建定时任务

成功创建定时任务后,便可等待任务触发或者立即执行,便会走到上一节<<定时任务执行逻辑>>中代码
然后再回来看看黑白名单:
黑名单:

白名单:
虽然有个白名单,不但一个有趣的现象是,RuoYi官方在对黑白名单进行判断的时候,存在遗漏,导致漏洞利用。
代码如下:

红框中的代码很眼熟,就是如下代码


可以发现代码仅仅对全限定类名(class)进行了检测。但是没有对spring容器中的对象进行白名单检测。
只需要在spring容器中找到一个可以利用的对象,即可以绕过黑名单检测,又可以逃过白名单的检测。

寻找spring容器中可利用对象

<<定时任务执行逻辑>>小结已经分析出,定时任务若是调用spring容器中的对象,则需要满足如下条件:

  1. 对象存在于spring容器中
  2. 方法至少不能是private修饰的方法
  3. 参数类型只能为StringBooleanLongDoubleInteger类型
    利用上述条件刷选方法,看是否存在可以利用的方法。可以编写脚本进行筛选,也可以借助spring actuator手动筛选,actuator组件会列举spring容器中所有的对象。但由于RuoYi-fast没有使用actuator组件,这里简单添加一下依赖和配置即可


    访问Beans接口,获取所有的spring beans对象。可以手工一个一个去IDEA中去搜索类,查找符合的方法。

    JdbcTemplate类中,发现executeupdate方法(public方法),参数为String,符合前面分析的所需要的条件。

    execute方法可以执行任意sql语句。不过在截取方法参数值时,是从目标字符串中提取第一个(和第一个)中间的字符串,若目标字符串为:jdbcTemplate.execute("insert into sys_user_role values(7,7);"),则方法参数值为"insert into sys_user_role values(7,7,具体可在<<定时任务执行逻辑>>一节中找到代码论证。下一节分析如何绕过这个截取方式

使用预处理和hex编码绕过对方法参数的截取

方法参数值是通过从目标字符串第一个(和第一个)中间的字符串获取的,那么只要保证参数值内容中不出现)即可。
可以使用mysql预处理和hex编码使参数值内容中不出现)。可以参考2019年的强网杯【随便注】一题。
比如,以执行insert into sys_user_role values(7,7);为例。
首先将insert语句进行hex编码:

设置变量,值为hex编码:

定义预处理语句:

执行预处理语句:

数据成功插入:

依次将上述sql语句填入jdbcTemplate.execute方法中,如下payload中没有)符号。并且每次修改定时任务时,立即执行任务。

在后台修改任务:目标字符串依次如上

选中立即执行

观察数据库,成功执行insert语句

漏洞修复

点击收藏 | 2 关注 | 1
登录 后跟帖