反序列化打内存马知一二
字数 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)链的后半部分
  • 两种执行方式
    1. TemplatesImpl动态加载字节码
    2. InvokerTransformer反射调用链
  • 局限性
    • 执行路径固定,无法中途交互或修改
    • 返回值无法供后续代码使用
    • 需要处理访问限制(如通过反射setAccessible(true)
    • 无法直接获取请求上下文(request/response),因此不适合直接用于内存马注入

3. 动态代码上下文执行 (Dynamic Code Context Execution)

这是实现内存马注入的关键:让恶意代码在程序运行时动态执行,从而:

  • 获取当前执行环境的上下文(如Servlet容器的requestresponse
  • 实现与Web容器的交互
  • 达到“无文件落地”的隐蔽效果

核心技术:动态类加载机制

1. 类加载器 (ClassLoader) 基础

Java类加载过程:

  1. 加载:根据全限定名定位并读取类字节码
  2. 链接:转换为JVM可执行格式
  3. 初始化:执行静态变量赋值和静态代码块(注意:此时不会触发构造函数

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 \]

`开头

  • 加载过程
    1. 去除`

\[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());
    }
}

总结

反序列化注入内存马的核心原理可概括为:"在运行期间使程序通过其动态特性加载任意字节码"

关键技术点

  1. 理解单向执行链与动态执行的差异:前者只能执行固定代码,后者可获取上下文
  2. 掌握类加载机制:特别是defineClass方法的各种公开访问方式
  3. 区分初始化时机:静态代码块 vs 构造函数在不同加载器中的表现
  4. 适应环境限制:JDK版本、依赖存在性、模块系统限制等
  5. 处理大小限制:通过压缩技术处理大尺寸内存马字节码

实践建议

  • 根据目标环境选择合适的技术方案(TemplatesImpl/BCEL/Groovy/表达式)
  • 精心设计内存马结构,确保在目标初始化机制下能正确执行
  • 考虑绕过现代JD安全限制的技术方案
  • 测试不同环境下的兼容性和稳定性

通过掌握这些核心技术原理,安全研究人员可以更深入地理解Java反序列化漏洞的高级利用方式,并有效防御这类攻击。

反序列化漏洞注入内存马核心技术原理详解 概述 本文档深入剖析通过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 方法: 功能 :将字节数组转换为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$$ 前缀 解码BCEL格式字节码 通过 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表达式注入 基本利用 内存马注入Payload 3. SpEL表达式注入 (Spring环境) 基本利用链 内存马注入 高版本JDK注意事项 Module限制 :需要绕过JDK9+的模块访问限制 参考方案 :使用MethodHandle等技术绕过 大字节码处理 :如内存马过大,需进行GZIP压缩+Base64编码 利用IOUtils的简化方案 如果环境中存在 org.apache.commons.io.IOUtils : 字节码压缩脚本示例 总结 反序列化注入内存马的核心原理可概括为: "在运行期间使程序通过其动态特性加载任意字节码" 。 关键技术点 理解单向执行链与动态执行的差异 :前者只能执行固定代码,后者可获取上下文 掌握类加载机制 :特别是 defineClass 方法的各种公开访问方式 区分初始化时机 :静态代码块 vs 构造函数在不同加载器中的表现 适应环境限制 :JDK版本、依赖存在性、模块系统限制等 处理大小限制 :通过压缩技术处理大尺寸内存马字节码 实践建议 根据目标环境选择合适的技术方案(TemplatesImpl/BCEL/Groovy/表达式) 精心设计内存马结构,确保在目标初始化机制下能正确执行 考虑绕过现代JD安全限制的技术方案 测试不同环境下的兼容性和稳定性 通过掌握这些核心技术原理,安全研究人员可以更深入地理解Java反序列化漏洞的高级利用方式,并有效防御这类攻击。