若依后台管理系统存在多种架构体系。如下
这里使用RuoYi-fast v4.7.3(前后端不分离)来分析定时任务功能点处如何绕过黑白名单,执行任意的sql语句
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
是中不包括.或者仅仅包括一个.符号便符合指定格式
接着便走到如下分支
从上可分析出如下结果:
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
,创建定时任务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
,具体可在<<定时任务执行逻辑>>
一节中找到代码论证。下一节分析如何绕过这个截取方式(
和第一个)
中间的字符串获取的,那么只要保证参数值内容中不出现)
即可。mysql
预处理和hex
编码使参数值内容中不出现)。可以参考2019年的强网杯【随便注】一题。insert into sys_user_role values(7,7);
为例。insert
语句进行hex
编码:sql
语句填入jdbcTemplate.execute
方法中,如下payload
中没有)
符号。并且每次修改定时任务时,立即执行任务。