反序列化打内存马知一二
字数 2547 2025-09-23 19:27:46
反序列化漏洞注入内存马核心技术原理详解
概述
本文档深入剖析通过Java反序列化漏洞注入内存马(Memory Shell)的核心技术原理。关键在于理解如何利用Java的动态特性,在程序运行时动态加载并执行恶意字节码,实现“无文件落地”的攻击。
核心概念解析
1. Java语言特性:编译-解释型语言
Java并非纯编译或纯解释型语言,而是编译-解释型语言:
- 编译期:
.java源代码通过javac编译为字节码(.class文件) - 运行期:JVM加载并解释执行字节码
这种特性限制了直接执行动态代码的能力,但提供了其他动态执行机制。
2. 单向代码执行链 (One-Way Code Execution Chain)
- 典型代表:Commons Collections (CC)链的后半部分
- 两种执行方式:
TemplatesImpl动态加载字节码InvokerTransformer反射调用链
- 局限性:
- 执行路径固定,无法中途交互或修改
- 返回值无法供后续代码使用
- 需要处理访问限制(如通过反射
setAccessible(true)) - 无法直接获取请求上下文(request/response),因此不适合直接用于内存马注入
3. 动态代码上下文执行 (Dynamic Code Context Execution)
这是实现内存马注入的关键:让恶意代码在程序运行时动态执行,从而:
- 获取当前执行环境的上下文(如Servlet容器的
request和response) - 实现与Web容器的交互
- 达到“无文件落地”的隐蔽效果
核心技术:动态类加载机制
1. 类加载器 (ClassLoader) 基础
Java类加载过程:
- 加载:根据全限定名定位并读取类字节码
- 链接:转换为JVM可执行格式
- 初始化:执行静态变量赋值和静态代码块(注意:此时不会触发构造函数)
2. 关键方法:defineClass
ClassLoader.defineClass方法:
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
- 功能:将字节数组转换为Class对象
- 参数:
name: 类的二进制名称(如com.example.MaliciousClass)b: 存储类数据的字节数组(通常是.class文件内容)off: 起始偏移量len: 数据长度
- 限制:该方法为
protected,需要找到可公开访问的替代方案
具体技术实现方案
方案一:利用TemplatesImpl加载字节码
技术原理
- 位置:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl - 优势:JDK自带,无需额外依赖
- 关键代码:在
TemplatesImpl类的getTransletInstance()方法中(约559行)会调用newInstance()实例化对象 - 要求:内存马类必须提供无参构造函数,因为
newInstance()要求无参构造
利用方式
将内存马字节码嵌入反序列化链中(如CC链、K1/K2链等),通过TemplatesImpl加载执行
方案二:BCEL ClassLoader (JDK8u251前有效)
技术原理
- 位置:
com.sun.org.apache.bcel.internal.util.ClassLoader - 触发条件:类名以`
\[BCEL \]
`开头
- 加载过程:
- 去除`
\[BCEL \]
前缀 2. 解码BCEL格式字节码 3. 通过createClass`方法解析并返回Class对象
重要注意事项
- 仅触发静态代码块:BCEL加载不会调用构造函数,只执行静态初始化块
- 内存马设计:必须将核心逻辑写在静态代码块中而非构造函数内
- 典型应用:Fastjson反序列化漏洞的不出网利用
方案三:Groovy ClassLoader (需外部依赖)
技术原理
- 位置:
groovy.lang.GroovyClassLoader - 特性:继承自
URLClassLoader,提供public的defineClass方法 - 初始化:无参构造函数从线程上下文获取最顶层类加载器
优势
可直接访问public的defineClass方法,便于嵌入反序列化利用链
表达式与脚本引擎组合利用
1. 脚本引擎 (Script Engine)
- 核心类:
javax.script.ScriptEngineManager - 方法:
getEngineByName()获取引擎,eval()执行脚本 - 现状:JDK15+移除了"Nashorn"引擎,需注意环境兼容性
2. EL表达式注入
基本利用
// 通过反射执行命令
${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null), 'calc')}
内存马注入Payload
// 加载字节码并实例化
${''.getClass().forName('java.lang.ClassLoader').getMethod('defineClass', ''.getClass(), [].class.getClass(), Integer.TYPE, Integer.TYPE).invoke(Thread.currentThread().getContextClassLoader(), byteArray, 0, byteArray.length).newInstance()}
3. SpEL表达式注入 (Spring环境)
基本利用链
T(java.lang.Runtime).getRuntime().exec('calc')
内存马注入
// 需要与org.springframework.expression同包
T(org.springframework.expression.ExpressionParser).parseExpression(...)
高版本JDK注意事项
- Module限制:需要绕过JDK9+的模块访问限制
- 参考方案:使用MethodHandle等技术绕过
- 大字节码处理:如内存马过大,需进行GZIP压缩+Base64编码
利用IOUtils的简化方案
如果环境中存在org.apache.commons.io.IOUtils:
// 解压并加载字节码
T(org.apache.commons.io.IOUtils).toString(T(java.util.zip.GZIPInputStream).newInstance(T(java.io.ByteArrayInputStream).newInstance(T(java.util.Base64).getDecoder().decode('BASE64编码的压缩字节码')), "UTF-8")
字节码压缩脚本示例
// 示例压缩代码
import java.util.zip.*;
import java.util.Base64;
public class Compressor {
public static String compress(byte[] data) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(bos);
gzip.write(data);
gzip.close();
return Base64.getEncoder().encodeToString(bos.toByteArray());
}
}
总结
反序列化注入内存马的核心原理可概括为:"在运行期间使程序通过其动态特性加载任意字节码"。
关键技术点
- 理解单向执行链与动态执行的差异:前者只能执行固定代码,后者可获取上下文
- 掌握类加载机制:特别是
defineClass方法的各种公开访问方式 - 区分初始化时机:静态代码块 vs 构造函数在不同加载器中的表现
- 适应环境限制:JDK版本、依赖存在性、模块系统限制等
- 处理大小限制:通过压缩技术处理大尺寸内存马字节码
实践建议
- 根据目标环境选择合适的技术方案(TemplatesImpl/BCEL/Groovy/表达式)
- 精心设计内存马结构,确保在目标初始化机制下能正确执行
- 考虑绕过现代JD安全限制的技术方案
- 测试不同环境下的兼容性和稳定性
通过掌握这些核心技术原理,安全研究人员可以更深入地理解Java反序列化漏洞的高级利用方式,并有效防御这类攻击。