前言
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 版本中复现,结果被成功拦截