JAVA内存马查杀

本文最后更新于 2025年12月7日 下午

初步排查

  1. 流量异常:
    • 出现未知路径的 HTTP 请求(如 /shell, /cmd, /x.jsp 等)。
    • 非业务时间有大量请求或异常 User-Agent。
    • 请求体中包含 Base64 编码、命令执行特征(如 cmd=whoami)。
  2. 日志异常:
    • 访问日志中出现可疑 URL 路径。
    • Tomcat/Catalina 日志中无对应 JSP 文件却能访问。
  3. 进程/端口异常:
    • Java 进程监听了非预期端口。
    • 出现反向连接

静态分析

根据不同类型内存马的注入路径或特征反向排查

Servlet API 类内存马

(1)Filter 内存马

排查

原理:通过 ServletContext.addFilter() 动态注册 Filter,拦截所有请求。

排查思路:

  • 检查运行时注册的 Filter 是否包含未在代码注解/web.xml 中声明的。
  • 关注 Filter 实现类是否来自非业务包(如随机类名、无 package、含 shell 字样)。
  • Filter的doFilter方法中有恶意代码

工具排查

https://arthas.aliyun.com/arthas-boot.jar

1
java -jar .\arthas-boot.jar

注意选择web进程

1
2
3
#查看 Mbean 的信息,查看异常Filter节点
mbean | grep "Filter"
mbean |findstr "Filter"

1
sc *.Filter   //搜索符合pattern的Filter,只能找到通过jsp执行生成的内存马

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
vmtool --action getInstances --className org.apache.catalina.core.StandardContext
//确定web程序下标
@StandardContext[][
@StandardContext[StandardEngine[Catalina].StandardHost[localhost].StandardContext[/manager]],
@StandardContext[StandardEngine[Catalina].StandardHost[localhost].StandardContext[]],
//ROOT下标为1,#ctx = instances[1]

vmtool --action getInstances \
--className org.apache.catalina.core.StandardContext \
--limit 2 \
--express '
#ctx = instances[1],
#servletContext = #ctx.getServletContext(),
#registrations = #servletContext.getFilterRegistrations(),
#registrations != null ? #registrations.keySet().toArray() : new java.lang.Object[]{"getFilterRegistrations() returned null"}
' -x 3

//列出所有filter
@Object[][
@String[com.example.Myfilter],
@String[Tomcat WebSocket (JSR356) Filter],
]

sc *com.example.Myfilter*
//得到类
org.apache.jsp.shell2_jsp$Shell2Servlet
//看是否有恶意代码
jad org.apache.jsp.shell2_jsp

反编译可疑类可以查看是否有恶意代码

1
jad org.apache.jsp.shell_jsp

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 先获取 StandardContext 实例
vmtool --action getInstances --className javax.servlet.Filter


# 调用 findFilterDefs() 获取所有 Filter 定义(Map<String, FilterDef>)
vmtool --action invokeMethod \
--className org.apache.catalina.core.StandardContext \
--object 0x7a3b4c5d(替换为实际的) \
--methodName findFilterDefs \
--paramTypes '' \
--json


输出 所有 Filter 的真实类名 然后反编译分析

清理

重启web应用(不影响其他tomcat中的web应用)

但是需要重启受害的web服务

登录tomcat管理页面/manager/html

1
# 找到你的应用(/),点击 "Undeploy" → 再 "Deploy"

不关闭web服务的情况下清理

需要tomcat热加载jsp(默认)

将下面jsp放到web目录

filter_menshell_clean.jsp(通过remove()方法删除数组中的对应项,删除后索引会改变,所以需要一个一个删除,不能选中多个)

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.*" %>
<%@ page import="java.util.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<% ServletContext servletCtx=request.getServletContext(); StandardContext ctx=null; try { Field
appctx=servletCtx.getClass().getDeclaredField("context"); appctx.setAccessible(true); Object
ac=appctx.get(servletCtx); Field stdctx=ac.getClass().getDeclaredField("context");
stdctx.setAccessible(true); ctx=(StandardContext) stdctx.get(ac); } catch (Exception e) {
ctx=(StandardContext) servletCtx.getAttribute("org.apache.catalina.CONTEXT"); } if (ctx==null) {
out.println("<h2>Error: Cannot get StandardContext</h2>");
return;
}

Field configField = ctx.getClass().getDeclaredField("filterConfigs");
configField.setAccessible(true);
Map<String, Object> filterConfigs = (Map<String, Object>) configField.get(ctx);

Map<String, String> filters = new LinkedHashMap<>();
if (filterConfigs != null) {
for (String name : filterConfigs.keySet()) {
try {
Object fc = filterConfigs.get(name);
Field f = fc.getClass().getDeclaredField("filter");
f.setAccessible(true);
Filter filter = (Filter) f.get(fc);
filters.put(name, filter.getClass().getName());
} catch (Exception ex) {
filters.put(name, "[ERROR: " + ex.getMessage() + "]");
}
}
}

String[] toRemove = request.getParameterValues("remove");
if (toRemove != null && toRemove.length > 0) {
List<String> removed = new ArrayList<>();
for (String name : toRemove) {
if (filters.containsKey(name)) {
try {
filterConfigs.remove(name);
try {
Field defsField = ctx.getClass().getDeclaredField("filterDefs");
defsField.setAccessible(true);
Map
<?, ?> defs = (Map
<?, ?>) defsField.get(ctx);
if (defs instanceof Map) {
((Map) defs).remove(name);
}
} catch (Exception ignored) {}
removed.add(name);
} catch (Exception e) {
// ignore
}
}
}
// FIXED: No multi-line strings!
out.println("<h3>Deleted Filters:</h3>");
out.println("<ul>");
for (String r : removed) {
out.println("<li>" + r + "</li>");
}
out.println("</ul>");
out.println("<p><a href='?'>Back</a></p>");
return;
}
%>
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<title>Filter Manager</title>
<style>
body {
font-family: Consolas, monospace;
margin: 20px;
background: #f5f5f5;
}

h2 {
color: #1976d2;
}

table {
border-collapse: collapse;
width: 100%;
margin: 15px 0;
}

th,
td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}

input[type=checkbox] {
transform: scale(1.3);
margin-right: 8px;
}

button {
padding: 8px 16px;
background: #d32f2f;
color: white;
border: none;
cursor: pointer;
font-weight: bold;
}

button:hover {
background: #b71c1c;
}
</style>
</head>

<body>
<h2>Filter Manager - Manual Removal</h2>

<% if (filters.isEmpty()) { %>
<p>No filters found.</p>
<% } else { %>
<form method="post">
<table>
<thead>
<tr>
<th>Select</th>
<th>Filter Name</th>
<th>Class Name</th>
</tr>
</thead>
<tbody>
<% for (String name : filters.keySet()) { %>
<tr>
<td><input type="checkbox" name="remove"
value="<%= name %>"></td>
<td>
<%= name %>
</td>
<td>
<%= filters.get(name) %>
</td>
</tr>
<% } %>
</tbody>
</table>
<button type="submit"
onclick="return confirm('Delete selected filters?')">Delete
Selected</button>
</form>
<% } %>

<hr>
<p style="color: gray; font-size: 12px;">Delete this
file after use.</p>
</body>

</html>

通过反射列出所有filter实例

1
2
3
4
5
6
7
8
9
10
11
JSP/Servlet
↓ (1) getServletContext()
javax.servlet.ServletContext ← 接口,运行时是 ApplicationContextFacade
↓ (2) 反射读取 "context" 字段
org.apache.catalina.core.ApplicationContext ← 真实上下文包装类
↓ (3) 反射读取 "context" 字段
org.apache.catalina.core.StandardContext ← 核心!Web 应用的容器实现
↓ (4) 反射读取 "filterConfigs" 字段
java.util.Map<String, ApplicationFilterConfig>
↓ (5) 遍历每个 ApplicationFilterConfig
→ 反射读取其 "filter" 字段 → 得到 Filter 实例

选择已经确定为内存马的filter删除

filterConfigs 里删除内存马添加的filter,

也可以通过arthas来做,原理是一样的

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
//列出所有 Filter 实例,识别可疑项
vmtool --action getInstances --className javax.servlet.Filter

//预期输出(关键线索):
@Filter[][
@Myfilter[com.example.Myfilter@283cc9cc],
@WsFilter[org.apache.tomcat.websocket.server.WsFilter@42a28ca7],
@CsrfPreventionFilter[org.apache.catalina.filters.CsrfPreventionFilter@6c54ab75],
@HttpHeaderSecurityFilter[org.apache.catalina.filters.HttpHeaderSecurityFilter@1629399c],
@WsFilter[org.apache.tomcat.websocket.server.WsFilter@5895e03c],
@Shellfilter[org.apache.jsp.shell_jsp$Shellfilter@e84883d] ← 可疑!
]

//列出所有 ClassLoader
classloader --list
//在输出中查找类似以下结构:
<hash值> <实例数> ... ParallelWebappClassLoader
context: /your-app-path
或通过其子加载器反推(如 JasperLoader 的 parent)
org.apache.jasper.servlet.JasperLoader@xxxxxx ... ParallelWebappClassLoader
Parent 是 AppClassLoader 或 URLClassLoader
记录其 hash 值(如 a1b2c3d4)——这就是你要用的 -c 参数

//验证 ClassLoader 是否有效
ognl -c <你的hash> "new java.lang.String('OK')"
若返回 "OK",说明 hash 可用。

//清除指定 Filter(核心命令)
//将 <你的hash> 替换为第二步获取的值,<FilterName> 替换为第一步记下的简单类名(如 Shellfilter):
ognl -c <你的hash> "#c=@java.lang.Thread@currentThread().getContextClassLoader(),#f=#c.getClass().getSuperclass().getDeclaredField(\"resources\"),#f.setAccessible(true),#r=#f.get(#c),#g=#r.getClass().getDeclaredField(\"context\"),#g.setAccessible(true),#ctx=#g.get(#r),#fcField=#ctx.getClass().getDeclaredField(\"filterConfigs\"),#fdField=#ctx.getClass().getDeclaredField(\"filterDefs\"),#fcField.setAccessible(true),#fdField.setAccessible(true),#fc=(java.util.Map)#fcField.get(#ctx),#fd=(java.util.Map)#fdField.get(#ctx),#fc.remove(\"<FilterName>\"),#fd.remove(\"<FilterName>\")"

//验证是否清除成功
ognl -c <你的hash> "#c=@java.lang.Thread@currentThread().getContextClassLoader(),#f=#c.getClass().getSuperclass().getDeclaredField(\"resources\"),#f.setAccessible(true),#r=#f.get(#c),#g=#r.getClass().getDeclaredField(\"context\"),#g.setAccessible(true),#ctx=#g.get(#r),#h=#ctx.getClass().getDeclaredField(\"filterConfigs\"),#h.setAccessible(true),#fc=(java.util.Map)#h.get(#ctx),#fc.keySet()"

⚠️ 注意:

已经在执行的请求不受影响(Filter 实例可能还在内存中)

新请求将不再触发该 Filter 这不是“卸载类”,只是断开容器对它的引用

也就是说如果已经通过内存马执行反弹shell,删除filter不会断开反弹shell连接

(2)Servlet 内存马

排查

原理:通过 ServletContext.addServlet() 动态注册 Servlet,绑定特定路径(如 /cmd)。

排查思路:

  • 检查运行时注册的 Servlet 名称和映射路径。
  • 对比源码/Web.xml,确认是否多出未知 Servlet。
  • 检查 Servlet 的 service/doGet/doPost 方法中是否存在可疑行为
1
2
3
4
5
6
7
8
9
#查看 Mbean 的信息,查看异常Filter节点
mbean | grep "Filter"
mbean |findstr "Filter"

//或者
sc *.Servlet //只能找到通过jsp执行生成的内存马

//然后反编译可疑类分析
jad org.apache.jsp.shell2_jsp

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
# 先获取 StandardContext 实例(通常可通过已知的 ServletContext 间接获取)
vmtool --action getInstances --className org.apache.catalina.core.StandardContext

# 调用 findChild() 或遍历 children 获取所有 Wrapper(即 Servlet 封装)
# 更推荐直接调用 findServlets()(若存在)或 getChildNames() + findChild()
# 但 Tomcat 中常用方式是通过 findChildren() 获取所有子容器(包括 Servlet)

vmtool --action invokeMethod \
--className org.apache.catalina.core.StandardContext \
--object 0x7a3b4c5d \
--methodName findChildren \
--paramTypes '' \
--json
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
vmtool --action getInstances --className org.apache.catalina.core.StandardContext
//确定web程序下标
@StandardContext[][
@StandardContext[StandardEngine[Catalina].StandardHost[localhost].StandardContext[/manager]],
@StandardContext[StandardEngine[Catalina].StandardHost[localhost].StandardContext[]],
//ROOT下标为1,#ctx = instances[1]

vmtool --action getInstances \
--className org.apache.catalina.core.StandardContext \
--limit 2 \
--express '
#ctx = instances[1],
#servletContext = #ctx.getServletContext(),
#registrations = #servletContext.getServletRegistrations(),
#registrations != null ? #registrations.keySet().toArray() : new java.lang.Object[]{"getServletRegistrations() returned null"}
' -x 3

//列出所有filter
@Object[][
@String[Shell2Servlet],
@String[default],
@String[com.example.MyServlet],
@String[jsp],
]

sc *Shell2Servlet*
//得到类
org.apache.jsp.shell2_jsp$Shell2Servlet
//看是否有恶意代码
jad org.apache.jsp.shell2_jsp

清理

在允许重启web服务的情况下可以通过tomcat后台或者命令行来重启受害的web服务

或者不重启web服务,通过jsp热加载

clean_servlet_memshell.jsp(通过remove()方法删除数组中的对应项,删除后索引会改变,所以需要一个一个删除,不能选中多个)

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.*" %>
<%@ page import="java.util.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Container" %>

<%
ServletContext servletCtx = request.getServletContext();
StandardContext ctx = null;
try {
Field appctx = servletCtx.getClass().getDeclaredField("context");
appctx.setAccessible(true);
Object ac = appctx.get(servletCtx);
Field stdctx = ac.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
ctx = (StandardContext) stdctx.get(ac);
} catch (Exception e) {
ctx = (StandardContext) servletCtx.getAttribute("org.apache.catalina.CONTEXT");
}
if (ctx == null) {
out.println("<h2>Error: Cannot get StandardContext</h2>");
return;
}

// 使用 findChildren() 获取所有 Wrapper(Servlet)
Method findChildrenMethod = ctx.getClass().getMethod("findChildren");
Container[] wrappers = (Container[]) findChildrenMethod.invoke(ctx);

// 构建 servletInfo: name -> className
Map<String, String> servletInfo = new LinkedHashMap<>();
for (Container wrapper : wrappers) {
try {
String name = wrapper.getName();
Method getServletClassMethod = wrapper.getClass().getMethod("getServletClass");
String className = (String) getServletClassMethod.invoke(wrapper);
servletInfo.put(name, className != null ? className : "[No Class]");
} catch (Exception ex) {
servletInfo.put(wrapper.getName(), "[ERROR: " + ex.getMessage() + "]");
}
}

// 处理删除
String[] toRemove = request.getParameterValues("remove");
if (toRemove != null && toRemove.length > 0) {
List<String> removed = new ArrayList<>();
Method removeChildMethod = null;
try {
removeChildMethod = ctx.getClass().getMethod("removeChild", Container.class);
} catch (NoSuchMethodException e) {
// fallback to superclass
removeChildMethod = ctx.getClass().getSuperclass().getMethod("removeChild", Container.class);
}
removeChildMethod.setAccessible(true);

for (String name : toRemove) {
if (servletInfo.containsKey(name)) {
try {
// 找到对应的 Wrapper
Container target = null;
for (Container w : wrappers) {
if (name.equals(w.getName())) {
target = w;
break;
}
}
if (target != null) {
// 调用 removeChild
removeChildMethod.invoke(ctx, target);
removed.add(name);
}
} catch (Exception e) {
// ignore
}
}
}

out.println("<h3>Deleted Servlets:</h3>");
out.println("<ul>");
for (String r : removed) {
out.println("<li>" + r + "</li>");
}
out.println("</ul>");
out.println("<p><a href='?'>Back</a></p>");
return;
}
%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Servlet Manager (Tomcat 9+ Compatible)</title>
<style>
body { font-family: Consolas, monospace; margin: 20px; background: #f5f5f5; }
h2 { color: #1976d2; }
table { border-collapse: collapse; width: 100%; margin: 15px 0; }
th, td { border: 1px solid #ccc; padding: 10px; text-align: left; }
input[type=checkbox] { transform: scale(1.3); margin-right: 8px; }
button { padding: 8px 16px; background: #d32f2f; color: white; border: none; cursor: pointer; font-weight: bold; }
button:hover { background: #b71c1c; }
</style>
</head>
<body>
<h2>Servlet Manager - Manual Removal (Tomcat 9+)</h2>

<% if (servletInfo.isEmpty()) { %>
<p>No servlets found.</p>
<% } else { %>
<form method="post">
<table>
<thead>
<tr>
<th>Select</th>
<th>Servlet Name</th>
<th>Class Name</th>
</tr>
</thead>
<tbody>
<% for (String name : servletInfo.keySet()) { %>
<tr>
<td><input type="checkbox" name="remove" value="<%= name %>"></td>
<td><%= name %></td>
<td><%= servletInfo.get(name) %></td>
</tr>
<% } %>
</tbody>
</table>
<button type="submit" onclick="return confirm('Delete selected servlets?')">Delete Selected</button>
</form>
<% } %>

<hr>
<p style="color: gray; font-size: 12px;">⚠️ Delete this file after use!</p>
</body>
</html>

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
# 列出所有 javax.servlet.Servlet 实例
vmtool --action getInstances --className javax.servlet.Servlet

//预期输出(关键线索):
@Servlet[][
@DefaultServlet[org.apache.catalina.servlets.DefaultServlet@1a2b3c4d],
@JspServlet[org.apache.jasper.servlet.JspServlet@5e6f7g8h],
@MyShellServlet[com.example.MyShellServlet@9i0j1k2l], ← 可疑!
@Shell[org.apache.jsp.shell_jsp$Shell@abc12345] ← 高危!
]


# 列出所有 ClassLoader
classloader --list
//在输出中查找你的 Web 应用对应的 ParallelWebappClassLoader,例如:
a1b2c3d4 1 ... ParallelWebappClassLoader
context: /myapp
//或通过子加载器反推(如 JasperLoader 的 parent):
org.apache.jasper.servlet.JasperLoader@xxxxxx
parent -> ParallelWebappClassLoader@a1b2c3d4
//验证 hash 是否有效:
ognl -c a1b2c3d4 "new java.lang.String('OK')"
# 若返回 "OK",说明可用


ognl -c <你的hash> '
#c=@java.lang.Thread@currentThread().getContextClassLoader(),
#f=#c.getClass().getSuperclass().getDeclaredField("resources"),
#f.setAccessible(true),
#r=#f.get(#c),
#g=#r.getClass().getDeclaredField("context"),
#g.setAccessible(true),
#ctx=#g.get(#r),

// 获取 children 字段(Map<String, Wrapper>)
#childrenField=#ctx.getClass().getSuperclass().getDeclaredField("children"),
#childrenField.setAccessible(true),
#children=(java.util.Map)#childrenField.get(#ctx),

// 获取 servletMappings 字段(Map<String, String>)
#mappingsField=#ctx.getClass().getSuperclass().getDeclaredField("servletMappings"),
#mappingsField.setAccessible(true),
#mappings=(java.util.Map)#mappingsField.get(#ctx),

// 要删除的 Servlet 名称(注意:不是类名!是注册时的 name)
#servletName="<ServletName>",

// 从 children 中移除 Wrapper
#wrapper=#children.remove(#servletName),

// 从 servletMappings 中移除所有指向该 name 的映射
#it=#mappings.entrySet().iterator(),
#removedMappings={},
{
while (#it.hasNext()) {
#entry=#it.next(),
if (#entry.value.equals(#servletName)) {
#it.remove()
}
}
},

"Removed servlet: " + #servletName + ", wrapper=" + (#wrapper != null ? "OK" : "NOT_FOUND")
'

(3)Listener 内存马

排查

原理:较少见,但可通过 ServletContext.addListener() 注册 ServletContextListenerServletRequestListener,在请求前后执行恶意逻辑。

排查思路:

  • Listener 通常在应用启动时注册,动态添加较难触发,但仍需检查。
  • 重点看 ServletRequestListener(listener内存马中唯一能命令执行的并且无法回显只能外带),可拦截请求/响应。
1
2
3
4
sc *ServletRequestListener*
sc *ServletContextListener*
sc *HttpSessionListener*
//只能找到通过jsp执行生成的内存马

用clean_listener_memshell.jsp列出所有listener的实现类

然后对可疑类用arthas反编译排查看是否有恶意代码

清理

tomat管理页面重启web应用

或者

clean_listener_memshell.jsp(通过remove()方法删除数组中的对应项,删除后索引会改变,所以需要一个一个删除,不能选中多个)

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.*" %>
<%@ page import="java.util.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>

<%
ServletContext servletCtx = request.getServletContext();
StandardContext ctx = null;
try {
Field appctx = servletCtx.getClass().getDeclaredField("context");
appctx.setAccessible(true);
Object ac = appctx.get(servletCtx);
Field stdctx = ac.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
ctx = (StandardContext) stdctx.get(ac);
} catch (Exception e) {
ctx = (StandardContext) servletCtx.getAttribute("org.apache.catalina.CONTEXT");
}
if (ctx == null) {
out.println("<h2>Error: Cannot get StandardContext</h2>");
return;
}

Field listenersField = null;
try {
listenersField = ctx.getClass().getDeclaredField("applicationEventListenersList");
} catch (NoSuchFieldException e) {
listenersField = ctx.getClass().getSuperclass().getDeclaredField("applicationEventListenersList");
}
listenersField.setAccessible(true);
@SuppressWarnings("unchecked")
List<Object> listeners = (List<Object>) listenersField.get(ctx);

Map<Integer, String> listenerInfo = new LinkedHashMap<>();
for (int i = 0; i < listeners.size(); i++) {
Object listener = listeners.get(i);
listenerInfo.put(i, listener != null ? listener.getClass().getName() : "[NULL]");
}

// ✅ 正确声明 toRemove
String[] toRemove = request.getParameterValues("remove");
if (toRemove != null && toRemove.length > 0) {
List<String> removed = new ArrayList<>();
for (String idxStr : toRemove) {
try {
int idx = Integer.parseInt(idxStr);
if (listenerInfo.containsKey(idx)) {
listeners.remove(idx);
removed.add("Index " + idx + " (" + listenerInfo.get(idx) + ")");
}
} catch (Exception ex) {
// ignore invalid index
}
}

out.println("<h3>Deleted Listeners:</h3>");
out.println("<ul>");
for (String r : removed) {
out.println("<li>" + r + "</li>");
}
out.println("</ul>");
out.println("<p><a href='?'>Back</a></p>");
return;
}
%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Listener Cleaner</title>
<style>
body { font-family: Consolas, monospace; margin: 20px; background: #f9f9f9; }
h2 { color: #c62828; }
table { border-collapse: collapse; width: 100%; margin: 15px 0; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
input[type=checkbox] { transform: scale(1.2); }
button { padding: 8px 16px; background: #d32f2f; color: white; border: none; cursor: pointer; }
</style>
</head>
<body>
<h2>ServletRequestListener Memory Shell Remover</h2>

<% if (listenerInfo.isEmpty()) { %>
<p>No listeners found.</p>
<% } else { %>
<form method="post">
<table>
<thead>
<tr>
<th>Select</th>
<th>Index</th>
<th>Class Name</th>
</tr>
</thead>
<tbody>
<% for (Integer idx : listenerInfo.keySet()) { %>
<tr>
<td><input type="checkbox" name="remove" value="<%= idx %>"></td>
<td><%= idx %></td>
<td><%= listenerInfo.get(idx) %></td>
</tr>
<% } %>
</tbody>
</table>
<button type="submit" onclick="return confirm('Confirm deletion?')">Delete Selected</button>
</form>
<% } %>

<hr>
<p style="color: red; font-size: 12px;">⚠️ Delete this file immediately after use!</p>
</body>
</html>
1
读取 Tomcat StandardContext 中的字段:applicationEventListenersList(包含实现了 ServletContextListener、HttpSessionListener、ServletRequestListener 等接口的监听器)

Spring 类内存马

(1)Spring Controller 内存马

排查

原理:通过操作 RequestMappingHandlerMapping 动态注册新的 @RequestMapping 映射。

排查思路:

  • 检查 Spring MVC 的 HandlerMappings 中是否存在未定义的 URL 映射。
  • 恶意 Controller 通常无源码、类名随机、方法含 eval/exec

arthas

1
2
3
4
5
6
7
8
9
vmtool --action getInstances --classLoaderHash 5e481248 --className org.springframework.web.servlet.DispatcherServlet --express '
instances[0].getWebApplicationContext()
.getBean("requestMappingHandlerMapping")
.getHandlerMethods()
.keySet()
.toArray()
'

输出所有路由,查看是否存在可疑路由
1
sc *Controller*

1
jad com.example.demo.InjectMemshellController

反编译分析可疑类

清理

重启jvm(根治)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ognl -x 5 '
#ds=@org.springframework.web.servlet.DispatcherServlet@4fc142ec,
#ctx=#ds.getWebApplicationContext(),
#mapping=#ctx.getBean("requestMappingHandlerMapping"),
#field=#mapping.getClass().getDeclaredField("handlerMethods"),
#field.setAccessible(true),
#handlerMethods=(java.util.Map)#field.get(#mapping),
#iter=#handlerMethods.entrySet().iterator(),
#removed=0,
while(#iter.hasNext()){
#entry=#iter.next(),
#method=#entry.value.getMethod(),
#className=#method.declaringClass.name,
// 👇 修改这里的条件,匹配你的可疑类名
if(#className.contains("Object$") || #className.contains("Evil") || #className.startsWith("com.attacker")){
#iter.remove(),
#removed++,
println("Removed: " + #className + " -> " + #entry.key.getPatternsCondition().getPatterns())
}
},
"Total removed: " + #removed
'

(2)Spring Interceptor 内存马

排查

原理:向 HandlerMappingWebMvcConfigurer 动态添加 Interceptor,拦截请求。

排查思路:

  • 检查 HandlerExecutionChain 中的 interceptors。
  • 拦截器类若非项目代码,可疑。
  • 寻找可疑路由
  • 查看流量日志
1
sc *Interceptor*

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
vmtool --action getInstances \
--classLoaderHash 5e481248 \
--className org.springframework.web.servlet.DispatcherServlet \
--express '
#m = instances[0].getWebApplicationContext().getBean("requestMappingHandlerMapping"),
#m.getClass().name + " -> " +
#m.getClass().superclass.name + " -> " +
#m.getClass().superclass.superclass.name + " -> " +
#m.getClass().superclass.superclass.superclass.name
'
//输出
@String[org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping -> org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping -> org.springframework.web.servlet.handler.AbstractHandlerMethodMapping -> org.springframework.web.servlet.handler.AbstractHandlerMapping]


vmtool --action getInstances \
--classLoaderHash 5e481248 \
--className org.springframework.web.servlet.DispatcherServlet \
--express '
#m = instances[0].getWebApplicationContext().getBean("requestMappingHandlerMapping"),
#f = #m.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("adaptedInterceptors"),
#f.setAccessible(true),
#list = #f.get(#m),
#list.{class.name}
' -x 3

//这里用了 .getSuperclass().getSuperclass().getSuperclass()(三次),正好到达 AbstractHandlerMapping。

依旧jad反编译可疑类分析

清理

重启jvm(根治)

arthas删除adaptedInterceptors 中的恶意Interceptor

1
2
3
4
5
6
7
8
9
10
11
12
vmtool --action getInstances \
--classLoaderHash 5e481248 \
--className org.springframework.web.servlet.DispatcherServlet \
--express '
#m = instances[0].getWebApplicationContext().getBean("requestMappingHandlerMapping"),
#f = #m.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("adaptedInterceptors"),
#f.setAccessible(true),
#list = #f.get(#m),
#bad = #list.{? #this.class.name == "com.example.demo.InterceptorMemShell$1"}[0],
#bad != null ? #list.remove(#bad) : false,
#bad != null ? "Removed" : "Not found"
' -x 3

Tomcat Valve 内存马

排查

原理:Valve 是 Tomcat 特有组件,位于请求处理管道中(类似 Filter 但更底层)。攻击者通过 Host/Engine/Context.addValve() 注入。

排查思路:

  • 同样在web日志中看是否有可疑路由或者可疑header、参数
  • 需直接检查 Tomcat 内部结构。
1
2
3
获取当前 Web 应用的 StandardContext;
通过 context.getPipeline().getFirst() 遍历整个 Valve 链;
打印每个 Valve 的类名,反编译识别可疑项

tomcat

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
sc -d org.apache.catalina.core.ApplicationContextFacade
//得到 classLoaderHash 4361bd48

vmtool --action getInstances \
--classLoaderHash 4361bd48 \
--className org.apache.catalina.core.StandardContext \
--limit 1 \
--express '
#ctx=instances[0],#v=#ctx.getPipeline().getFirst(),#l=new java.util.ArrayList(),
#v!=null?#l.add(#v.getClass().getName()+"|"+#v.getClass().getClassLoader()):null,
#v=#v!=null?#v.getNext():null,
#v!=null?#l.add(#v.getClass().getName()+"|"+#v.getClass().getClassLoader()):null,
#v=#v!=null?#v.getNext():null,
#v!=null?#l.add(#v.getClass().getName()+"|"+#v.getClass().getClassLoader()):null,
#v=#v!=null?#v.getNext():null,
#v!=null?#l.add(#v.getClass().getName()+"|"+#v.getClass().getClassLoader()):null,
#v=#v!=null?#v.getNext():null,
#v!=null?#l.add(#v.getClass().getName()+"|"+#v.getClass().getClassLoader()):null,
#l.isEmpty()?#l.add("none"):null,#l
' -x 3

//得到
@ArrayList[
@String[org.apache.catalina.authenticator.NonLoginAuthenticator|java.net.URLClassLoader@4361bd48],
@String[org.apache.jsp.shell4_jsp$1|org.apache.jasper.servlet.JasperLoader@543c3416],
@String[org.apache.catalina.core.StandardContextValve|java.net.URLClassLoader@4361bd48],
]

或者热加载jsp

清理

重启jvm

热加载jsp

clean_valve_memshell.jsp

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.*" %>
<%@ page import="java.util.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Valve" %>

<%
ServletContext servletCtx = request.getServletContext();
StandardContext ctx = null;
try {
Field appctx = servletCtx.getClass().getDeclaredField("context");
appctx.setAccessible(true);
Object ac = appctx.get(servletCtx);
Field stdctx = ac.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
ctx = (StandardContext) stdctx.get(ac);
} catch (Exception e) {
ctx = (StandardContext) servletCtx.getAttribute("org.apache.catalina.CONTEXT");
}
if (ctx == null) {
out.println("<h2>Error: Cannot get StandardContext</h2>");
return;
}

// 获取 Pipeline 的 first Valve
Valve firstValve = ctx.getPipeline().getFirst();
List<Valve> valveList = new ArrayList<>();
Valve current = firstValve;
while (current != null) {
valveList.add(current);
try {
current = current.getNext();
} catch (Exception e) {
break; // 防止 getNext() 抛异常
}
}

// 处理删除请求
String[] toRemove = request.getParameterValues("remove");
if (toRemove != null && toRemove.length > 0) {
List<String> removed = new ArrayList<>();

// 注意:Valve 链是单向链表,不能直接按 index remove
// 所以我们重建整个链(跳过要删除的)
List<Valve> newChain = new ArrayList<>();
for (int i = 0; i < valveList.size(); i++) {
boolean shouldRemove = false;
for (String idxStr : toRemove) {
try {
if (i == Integer.parseInt(idxStr)) {
shouldRemove = true;
removed.add("Index " + i + " (" + valveList.get(i).getClass().getName() + ")");
break;
}
} catch (NumberFormatException ignored) {}
}
if (!shouldRemove) {
newChain.add(valveList.get(i));
}
}

// 重建 Pipeline
// ========== 重建 Pipeline(使用反射调用 setFirst)==========
Class<?> pipelineClass = ctx.getPipeline().getClass(); // 应该是 StandardPipeline
Method setFirstMethod = null;
try {
setFirstMethod = pipelineClass.getDeclaredMethod("setFirst", Valve.class);
} catch (NoSuchMethodException e) {
// 兼容旧版?尝试父类
setFirstMethod = pipelineClass.getMethod("setFirst", Valve.class);
}
setFirstMethod.setAccessible(true);

if (newChain.isEmpty()) {
setFirstMethod.invoke(ctx.getPipeline(), (Object) null);
} else {
Valve head = newChain.get(0);
Valve prev = head;
for (int i = 1; i < newChain.size(); i++) {
prev.setNext(newChain.get(i));
prev = newChain.get(i);
}
prev.setNext(null); // 确保结尾
setFirstMethod.invoke(ctx.getPipeline(), head);
}

out.println("<h3>Deleted Valves:</h3>");
out.println("<ul>");
for (String r : removed) {
out.println("<li>" + r + "</li>");
}
out.println("</ul>");
out.println("<p><a href='?'>Back</a></p>");
return;
}
%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Tomcat Valve Cleaner</title>
<style>
body { font-family: Consolas, monospace; margin: 20px; background: #f9f9f9; }
h2 { color: #c62828; }
table { border-collapse: collapse; width: 100%; margin: 15px 0; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
input[type=checkbox] { transform: scale(1.2); }
button { padding: 8px 16px; background: #d32f2f; color: white; border: none; cursor: pointer; }
.suspicious { background-color: #ffebee; color: #c62828; font-weight: bold; }
</style>
</head>
<body>
<h2>Tomcat Valve Memory Shell Remover</h2>

<% if (valveList.isEmpty()) { %>
<p>No valves found.</p>
<% } else { %>
<form method="post">
<table>
<thead>
<tr>
<th>Select</th>
<th>Index</th>
<th>Class Name</th>
<th>ClassLoader</th>
</tr>
</thead>
<tbody>
<%
for (int i = 0; i < valveList.size(); i++) {
Valve v = valveList.get(i);
String className = v != null ? v.getClass().getName() : "[NULL]";
String classLoader = v != null ? v.getClass().getClassLoader().toString() : "null";
boolean isSuspicious = v != null &&
!classLoader.contains("Catalina") &&
!classLoader.contains("common") &&
classLoader.contains("WebAppClassLoader");
%>
<tr <%= isSuspicious ? "class='suspicious'" : "" %>>
<td><input type="checkbox" name="remove" value="<%= i %>"></td>
<td><%= i %></td>
<td><%= className %></td>
<td><%= classLoader %></td>
</tr>
<% } %>
</tbody>
</table>
<button type="submit" onclick="return confirm('Confirm deletion of selected valves?')">Delete Selected</button>
</form>
<% } %>

<hr>
<p style="color: red; font-size: 12px;">⚠️ Delete this file immediately after use!</p>
</body>
</html>

MBean 内存马

排查

原理:通过 ManagementFactory.getPlatformMBeanServer().registerMBean() 注册恶意 MBean,可通过 JMX 远程调用执行命令。

排查思路:

  • MBean 本身不处理 HTTP 请求,但可作为后门入口(需开启 JMX)。
  • http异常流量,web日志
  • 检查是否存在可疑 MBean(如含 shellexec 的 ObjectName)。
  • 排查可疑JMX流量
1
sc *MBean   //容易找到jsp生成的内存马

springboot/tomcat

1
ognl '@java.lang.management.ManagementFactory@getPlatformMBeanServer().queryNames(null, null).toArray()'

1
2
3
4
5
6
ognl '
#mbs=@java.lang.management.ManagementFactory@getPlatformMBeanServer(),
#obj=#mbs.getObjectInstance(new javax.management.ObjectName("DefaultDomain:type=Service,name=MetricsCollector")),
#obj.getClassName()
'
//获取可疑项DefaultDomain:type=Service,name=MetricsCollector的类
1
2
3
ognl '@java.lang.management.ManagementFactory@getPlatformMBeanServer().queryNames(null, null).toArray()' | grep -v 'Catalina\|java.lang\|JMImplementation\|j2eeType\|com.sun.management'

//linux快速去除标准mbean,列出可疑mbean

tomcat热加载下面的clean_mbean_memshell.jsp来排查可疑的类,反编译看是否有恶意代码

以下域名(Domain)和类型是 Tomcat / JVM 自带的,属于合法组件:

Domain 说明
Catalina Tomcat 核心组件(Connector、Context、Valve、Servlet、Filter 等)
java.lang JVM 内置监控(内存、线程、GC、类加载等)
java.nio NIO 缓冲池
com.sun.management HotSpot 诊断命令(如 HotSpotDiagnostic
, DiagnosticCommand
JMImplementation JMX 自身代理
Users Tomcat 用户数据库(tomcat-users.xml
中定义)
java.util.logging 日志管理

清理

clean_mbean_memshell.jsp

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.management.ManagementFactory" %>
<%@ page import="javax.management.MBeanServer" %>
<%@ page import="javax.management.ObjectName" %>
<%@ page import="javax.management.MBeanInfo" %>
<%@ page import="java.util.*" %>

<%!
private String escapeHtml(String s) {
if (s == null) return "";
return s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#x27;");
}
%>

<%
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
Set<ObjectName> mbeans = mbs.queryNames(null, null);

// 存储 { objectName -> className }
Map<String, String> mbeanMap = new LinkedHashMap<>();
for (ObjectName name : mbeans) {
String objNameStr = name.toString();
String className = "Unknown";
try {
MBeanInfo info = mbs.getMBeanInfo(name);
className = info.getClassName();
} catch (Exception e) {
className = "[Error: " + e.getClass().getSimpleName() + "]";
}
mbeanMap.put(objNameStr, className);
}

// 处理删除请求(只传 ObjectName)
String[] toRemove = request.getParameterValues("remove");
if (toRemove != null && toRemove.length > 0) {
List<String> removed = new ArrayList<>();
for (String nameStr : toRemove) {
try {
ObjectName objName = new ObjectName(nameStr);
if (mbs.isRegistered(objName)) {
mbs.unregisterMBean(objName);
removed.add(nameStr);
}
} catch (Exception ex) {
// ignore
}
}

out.println("<h3>Deleted MBeans:</h3>");
out.println("<ul>");
for (String r : removed) {
out.println("<li>" + escapeHtml(r) + "</li>");
}
out.println("</ul>");
out.println("<p><a href='?'>Back</a></p>");
return;
}
%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>MBean Cleaner (with Class)</title>
<style>
body { font-family: Consolas, monospace; margin: 20px; background: #f9f9f9; }
h2 { color: #c62828; }
table { border-collapse: collapse; width: 100%; margin: 15px 0; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; word-break: break-all; }
input[type=checkbox] { transform: scale(1.2); }
button { padding: 8px 16px; background: #d32f2f; color: white; border: none; cursor: pointer; }
.classname { color: #2e7d32; font-weight: bold; }
</style>
</head>
<body>
<h2>MBean Memory Shell Remover (with Class Name)</h2>

<% if (mbeanMap.isEmpty()) { %>
<p>No MBeans found.</p>
<% } else { %>
<form method="post">
<table>
<thead>
<tr>
<th>Select</th>
<th>ObjectName</th>
<th>Class Name</th>
</tr>
</thead>
<tbody>
<% for (Map.Entry<String, String> entry : mbeanMap.entrySet()) {
String objName = entry.getKey();
String className = entry.getValue(); %>
<tr>
<td><input type="checkbox" name="remove" value="<%= escapeHtml(objName) %>"></td>
<td><%= escapeHtml(objName) %></td>
<td class="classname"><%= escapeHtml(className) %></td>
</tr>
<% } %>
</tbody>
</table>
<button type="submit" onclick="return confirm('Confirm deletion of selected MBeans?')">Delete Selected</button>
</form>
<% } %>

<hr>
<p style="color: red; font-size: 12px;">⚠️ Delete this file immediately after use!</p>
<p style="font-size: 12px; color: #555;">
💡 Tip: Use Arthas command: <code>jad &lt;ClassName&gt;</code> to decompile suspicious classes.
</p>
</body>
</html>

Agent 内存马

排查

原理:通过 -javaagent 启动参数,或利用 Attach API(如 com.sun.tools.attach.VirtualMachine)动态加载 agent,在字节码层面插桩(如修改 java.lang.ProcessBuilder.start)。

排查思路:

  • Agent 不在应用 ClassLoader 中,常规方式无法看到。
  • 重点检查 JVM 启动参数和已加载的 agent。
  • http流量、web日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 反编译 ApplicationFilterChain 查看是否有恶意代码
jad org.apache.catalina.core.ApplicationFilterChain

jps -v
# 或
ps aux | grep tomcat
#看是否包含 -javaagent:xxx.jar(但高级攻击者会清除参数)

#监控异常网络连接(辅助判断)
#Agent 内存马通常会回连 C2:
# 查看 Tomcat 进程的网络连接
lsof -p $(pgrep -f catalina) | grep ESTABLISHED
# 或
netstat -antp | grep $(pgrep -f catalina)

清理

重启jvm,然后禁止attach jvm

1
2
3
4
5
# Linux (catalina.sh)
CATALINA_OPTS="$CATALINA_OPTS -XX:+DisableAttachMechanism"

:: Windows (catalina.bat)
set "CATALINA_OPTS=%CATALINA_OPTS% -XX:+DisableAttachMechanism"

补充

Arthas 本质是通过 Java Agent Attach 机制注入到目标 JVM 的。而像 冰蝎(Behinder)、哥斯拉(Godzilla)的一些高级 WebShell,会主动 破坏 Attach 机制(删除.java_pid<pid>导致无法attach jvm),以阻止使用 Arthas等工具进行分析。

这种情况下需要通过dump内存的方法去排查,毕竟内存马始终存在于内存。

1
jmap -dump:format=b,file=<filename> <pid>


JAVA内存马查杀
http://example.com/2025/12/07/JAVA内存马查杀/
作者
J_0k3r
发布于
2025年12月7日
许可协议
BY J_0K3R