前言
Ruoyi 的 v4.7.6 是 2022 年 12 月 16 日发布的一个版本,而任意文件下载漏洞实际上 12 月底的时候就已经爆出了,也陆续有一些文章在写这个漏洞,但是 Ruoyi 一直没有更新修复。
上月中旬(2023 年 5 月),Ruoyi 更新了 v4.7.7 版本,通过加固了白名单限制,修复了该漏洞。
记得及时更新昂!
Ruoyi v4.7.7
更新日志:v4.7.7
更新之后,可以看到任意文件下载的 payload 已经被限制

Ruoyi v4.7.6 任意文件下载漏洞复现
代码下载&部署
- 贴上 v4.7.6 版本的链接:Ruoyi v4.7.6
运行 Ruoyi,新建/修改定时任务
ruoYiConfig.setProfile("D:\\")

看一下我 D 盘下的文件,以这个 123.txt为例

手动触发定时任务

访问 common/download/resource 接口获取文件
http://localhost:8081/common/download/resource?resource=/profile/123.txt
关于为什么要访问这个 url,文件名前为什么要加 /profile/ 后面也会有详解

Ruoyi 的漏洞史
这里主要针对本次这个任意文件下载漏洞来说。
实际上这个漏洞,并不是 v4.7.5 … v4.7.6 的过程中出现的,它其实很早之前就出现了。Ruoyi 的定时任务功能在最初上线不久就被爆出了远程代码执行漏洞
后期进行过多次修复
- v4.6.2 — 定时任务屏蔽 rmi 远程调用
- v4.7.0 — 定时任务屏蔽 ldap 远程调用; 定时任务屏蔽 http(s) 远程调用
- v4.7.1 — 定时任务屏蔽违规字符
- v4.7.3 — 定时任务目标字符串验证包名白名单;定时任务屏蔽违规的字符
- v4.7.4 — 定时任务检查 Bean 包名是否为白名单配置
- v4.7.6 — 定时任务违规的字符
从代码审计的角度来看漏洞是如何被发现的
本次漏洞的爆发点其实还是在【定时任务】与【文件下载】功能,根本原因还是定时任务违规字符校验不完善,被绕过的问题。
-
先了解一下 Ruoyi 定时任务功能的作用和原理。
- Ruoyi 默认提供了三个定时任务的示例(红框中的三个)。分别调用目标字符串
ryTask.ryNoParams、ryTask.ryParams('ry')、ryTask.ryMultipleParams('ry', true, 2000L, 316.50D, 100)。

- 这三个调用字符串,特征很明显,分别在调用一个对象的无参方法、有参方法、多参方法
- Ruoyi 默认提供了三个定时任务的示例(红框中的三个)。分别调用目标字符串
-
在源码中找到
ryTask这个对象进行确认(在 idea 中直接连按两次shift搜索ryTask即可找到)

-
尝试点击【执行一次】,发现控制台中成功输出内容,说明方法被正确调用

-
这里简单科普一下这个
@Component("ryTask")是什么,为什么可以通过ryTask.xxx调用这个方法?@Component是Java Spring中的一个注解,其作用就是定义Spring管理类,简而言之就是,被@Component注解标记的类,将交给Spring框架来自动管理。
像Web开发过程中最常见的@Controller、@Service、@Repository、@Configuration等等其实都是@Component的扩展。
@Component("ryTask")括号中的ryTask是定义的 Bean 的 id,如果不写的话,默认是短类名(类名首字母小写)@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Controller { @AliasFor( annotation = Component.class ) String value() default ""; }@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Configuration { @AliasFor( annotation = Component.class ) String value() default ""; boolean proxyBeanMethods() default true; } -
看到这儿,就会萌生出一个大胆的猜测,是否可以利用【定时任务】调用任意被
@Controller、@Component、@Configuration等注解标记的接口呢? -
创建一个测试类,用
@Controller注解标记尝试一下(修改完代码要重启服务)@Controller public class JobTest { public String hello(String words) { System.out.println("hello" + words); // 在这里打个断点试试 return words; } } -
添加定时任意,尝试调用测试类


进入了断点,并成功输出了helloruoyi,由此验证,上面的猜想是正确的

-
走读代码,整理出那些被
@Component注解及其扩展注解标记的类,并且可能涉及到配置、越权、非公开方法之类的接口为什么主要关注 配置、越权、非公开方法 这三类呢?
因为正常的接口本身就是有权限的,即使可以从这里调用执行,从安全角度讲,也没有很大的意义。
而这三类就不一样了(主要是这三类,不代表只有这三类)- 配置文件是涉及到全局,如果被调用影响到,很可能会影响到其他用户,就会存在风险;
- 还有某些涉及到权限管控的接口,从这个定时任务这个入口调用,就有可能绕过原本的鉴权;
- 非公开的接口,简单举个例子,有些接口,可能不在
@Controller中,那么就无法通过正常的 HTTP 请求从 Web 端去调用,但是却可以通过这里的定时任务间接调用执行,从而造成风险。
相关的配置接口,还是挺多的,就不全部粘贴了,感兴趣的可以自己去整理一下试试玩儿,或许还有漏洞呢。
这里就先贴出本次要讨论的这个漏洞相关的配置 —— 全局配置类RuoYiConfig.java
全局配置类共有 6 个 set 方法,本次漏洞利用的就是setProfile方法// 全局配置 ruoYiConfig.setName(String name) ruoYiConfig.setVersion(String version) ruoYiConfig.setCopyrightYear(String copyrightYear) ruoYiConfig.setDemoEnabled(String demoEnabled) ruoYiConfig.setProfile(String profile) ruoYiConfig.setAddressEnabled(String addressEnabled) -
ruoYiConfig.setProfile的作用setProfile方法和profile属性,以及配置文件中的ruoyi.profile的关系,就不多说了,这些属于Java基础从方法内的注释结合配置文件中的注释,不难看出,
profile实际上是系统中文件的保存路径。
Windows下的默认值是D:/ruoyi/uploadPath,Linux下的默认值是/home/ruoyi/uploadPath


-
文件保存的默认目录可以被修改,那,被修改之后呢?还需要找一个访问这个目录下内容的方法。
走读代码,在
CommonController.java中,找到一个resourceDownload的方法
该方法内,先是进行了非法路径的检查

话说,这个目录穿越的检查,只检查..符号,怎么感觉能绕过呢?

同时里面还有一个白名单(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION)校验

然后执行
RuoYiConfig.getProfile()从RuoYiConfig中获取了Profile的值,并与传入的资源名称进行组合形成完整路径后进行下载。完美~~~
-
至此,万事俱备,开始整活儿
-
按照前 9 步的分析,我目前测试机是
Windows机器,所以编写payload为ruoYiConfig.setProfile("D:\\"),尝试将文件默认路径设置为D盘根目录

-
点击执行,手动触发定时任务

-
构造下载链接,访问 D 盘中的资源
String localPath = RuoYiConfig.getProfile(); // 刚才被篡改的地址。 现在应该是 D:// String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);Constans.RESOURCE_PREFIX的值如下:/** * 资源映射路径 前缀 */ public static final String RESOURCE_PREFIX = "/profile";StringUtils.substringAfter方法的源码如下:public static String substringAfter(String str, String separator) { if (isEmpty(str)) { return str; } else if (separator == null) { return ""; } else { // 从传入的路径中获取到分隔符 /profile 的位置 int pos = str.indexOf(separator); // 截取 /profile 所在位置的后面的内容。例如 /profile/1.txt 被截取之后就是 /1.txt return pos == -1 ? "" : str.substring(pos + separator.length()); } }所以,如果想下载
D://123.txt,那么最终构造的url就应该是http://localhost:8081/common/download/resource?resource=/profile/123.txt -
下载成功

漏洞修复
截止发文日(2023.05.25),Ruoyi 官方已经于 2023.4.14 日针对 v4.7.6 版本的任意文件下载漏洞进行了修复。

通过Compare v4.7.7 和 v4.7.6 的代码变动情况,可以看出,官方的修复方案如下:

尝试在 v4.7.7 版本中复现,结果被成功拦截



















