原文始发于先知社区(TGAO):定时任务功能点绕过黑白名单执行任意sql语句
环境
若依后台管理系统存在多种架构体系。如下
这里使用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);
}
}
配置job
和trigger
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
抽象类,新增了before
、doExecute
、after
方法,重写了父类的excute
方法(此方法采用了模版方法设计模式),其中before
方法记录任务执行开始时间,doExecute
是真正执行任务的方法(此方法交给具体的子类实现),after
方法用于将任务执行日志写入数据库中,excute
方法将before、doExecute、after
三个方法组合。
excute方法如下:
子类QuartzDisallowConcurrentExecution
与QuartzJobExecution
实现了doExecute
方法,这两个类只存在再并发支持上的区别,因此这里分析QuartzJobExecution
即可。
继续跟进invokeMethod(Job)方法
跟进invokeMethod方法,利用反射执行方法
一些细节处:
获取BeanName
获取MethodName
获取MethodParams
:从目标字符串中提取第一个(和第一个)中间的字符串。并将其以,进行分割成字符串数据
遍历分割生成的字符串数组
可以看到参数类型仅仅支持以下数据类型:String
,Boolean
,Long
,Double
,Integer
类型。
判断beanName
是否是指定格式,当beanName
是中不包括.或者仅仅包括一个.符号便符合指定格式
接着便走到如下分支
从上可分析出如下结果:
- 对象可以是spring容器中注册过的bean,也可以指定class名称
- 若是spring容器中注册过的bean,则可直接从spring容器中取出,若是指定class名称,则可以通过反射newInstance()创建对象,因此必须保证class中存在无参构造函数
- 方法不能是private修饰的方法,因为在getDeclaredMethod获取方法后,并没有执行setAccessible(true)
- 方法参数类型仅仅可以为如下类型: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
容器中的对象,则需要满足如下条件:
- 对象存在于
spring
容器中 - 方法至少不能是
private
修饰的方法 - 参数类型只能为
String
,Boolean
,Long
,Double
,Integer
类型
利用上述条件刷选方法,看是否存在可以利用的方法。可以编写脚本进行筛选,也可以借助spring actuator
手动筛选,actuator
组件会列举spring
容器中所有的对象。但由于RuoYi-fast
没有使用actuator
组件,这里简单添加一下依赖和配置即可
访问Beans
接口,获取所有的spring beans
对象。可以手工一个一个去IDEA
中去搜索类,查找符合的方法。
在JdbcTemplate
类中,发现execute
和update
方法(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语句
漏洞修复