Java 内存马类型基础与扫描切入点
这篇算是冰蝎 4.1 Agent 型内存马分析之前的铺垫。
先把常见 Java 内存马分个类,看它们一般藏在哪里、代码形态长什么样、扫描工具应该从哪些位置下手。
文里的代码会保留真实扫描时需要识别的危险调用形态,比如 ProcessBuilder。
但示例只使用固定的无害命令,不从请求参数读取命令,避免把文章写成可以直接复用的 WebShell。

目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 1. 先把内存马按位置分清楚 2. Web 组件型内存马 2.1 Filter 型 2.2 Servlet 型 2.3 Listener 型 3. Spring 生态里的内存马 3.1 Controller 型 3.2 Interceptor 型 3.3 HandlerMapping 型 4. 容器内部型内存马 4.1 Tomcat Valve 型 4.2 Tomcat Upgrade 型 5. 字节码 / Agent 型内存马 5.1 Java Agent 型 5.2 redefineClasses 型 5.3 Transformer 型 6. 扫描工具怎么落点 7. 小结
|
1. 先把内存马按位置分清楚
Java 内存马这个词很容易被说得很玄,但拆开看,其实就是一句话:
1
| 把一段恶意逻辑挂进 Java Web 请求处理链里,并且尽量不落地成普通文件
|
不同类型的内存马,区别主要在“挂到哪里”。
常见位置大概有几类:
1 2 3 4
| Web 组件层:Filter / Servlet / Listener 框架路由层:Spring Controller / Interceptor / HandlerMapping 容器内部层:Tomcat Valve / Upgrade / Pipeline JVM 字节码层:Java Agent / Instrumentation / redefineClasses
|
扫描时不能只问“有没有新 Filter”。Filter 型当然常见,但不是全部。像冰蝎 4.1 的 Agent 链,就更偏向 JVM 字节码层。
2. Web 组件型内存马
Web 组件型是最容易理解的一类。它们直接混进 Servlet 规范的请求处理链里。
2.1 Filter 型
Filter 本来就是用来拦请求的,所以它也很适合被滥用。请求进来以后,Filter 可以先看 URI、Header、参数,再决定是否继续往后走。
正常情况下,Filter 常用来做这些事:
1 2 3 4 5 6
| 登录校验 权限判断 编码处理 日志记录 跨域处理 请求耗时统计
|
它会排在 Servlet 前面,请求还没进入真正业务代码时就能先拿到 request 和 response。这也是它容易被当成内存马注入点的原因:位置靠前、覆盖面广、能拦截大量路径,还能在命中特定条件后直接写响应并中断后续链路。
实验室里的惰性样例可以长这样:
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
| import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;
public class LabMarkerFilter implements Filter { @Override public void init(FilterConfig filterConfig) { }
@Override public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response;
if ("/lab/filter-marker".equals(req.getRequestURI())) { runFixedLabCommand();
resp.setHeader("X-Lab-Marker", "filter"); resp.getWriter().write("filter marker");
return; }
chain.doFilter(request, response); }
@Override public void destroy() { }
private void runFixedLabCommand() throws IOException { if (System.getProperty("os.name").toLowerCase().contains("win")) { new ProcessBuilder("notepad.exe").start(); } else { new ProcessBuilder("true").start(); } } }
|
这个例子用的是固定命令,不从请求里取命令参数。这里要看的不是功能本身,而是 Filter 型内存马常见的几个形态:
1 2 3 4 5 6
| 实现 Filter 重写 doFilter 判断请求特征 命中特征后提前返回 命中后触发 ProcessBuilder / Runtime 等敏感调用 否则继续 chain.doFilter
|
扫描时可以重点看:
1 2 3 4
| 运行时 Filter 列表里有没有来源异常的 Filter FilterClass 是否来自非应用正常路径 doFilter 里是否有 Header / URI / 参数触发逻辑 是否出现 Base64 / AES / defineClass / 反射执行链
|
2.2 Servlet 型
Servlet 型更直接,它就是挂一个新的请求处理入口。
Servlet 原本就是 Java Web 里处理请求的基础单元。无论上层是 JSP、Spring MVC,还是别的框架,底层都绕不开 Servlet 体系。
正常业务里的 Servlet 一般负责:
1 2 3 4 5
| 接收 HTTP 请求 读取参数 / Body 处理业务逻辑 写 HTTP 响应 维护 URL 到处理类的映射
|
它之所以能成为内存马注入点,是因为只要运行时把一个新的 Servlet 实例和 URL 映射塞进容器,请求就能直接打到这个实例上。相比 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
| import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;
public class LabMarkerServlet extends HttpServlet { @Override protected void doPost( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException { if ("/lab/servlet-marker".equals(req.getRequestURI())) { runFixedLabCommand();
resp.setHeader("X-Lab-Marker", "servlet"); resp.getWriter().write("servlet marker"); return; }
resp.sendError(404); }
private void runFixedLabCommand() throws IOException { if (System.getProperty("os.name").toLowerCase().contains("win")) { new ProcessBuilder("notepad.exe").start(); } else { new ProcessBuilder("true").start(); } } }
|
这类内存马通常会有一个映射路径。扫描时可以从 Servlet 映射表入手:
1 2 3 4
| Servlet 名称是否异常 ServletClass 是否异常 url-pattern 是否很隐蔽 实例是否没有对应的 web.xml / 注解来源
|
如果运行时能拿到 ServletContext、Tomcat StandardContext 或框架自己的映射表,就可以把“启动基线”和“当前状态”做对比。
2.3 Listener 型
Listener 不一定直接处理请求,但能监听请求创建、Session 创建、上下文初始化等事件。
Listener 原本用来监听 Web 应用生命周期和运行时事件。比如应用启动时初始化资源,Session 创建时记录在线用户,请求创建时做一些上下文准备。
常见 Listener 包括:
1 2 3 4
| ServletContextListener ServletRequestListener HttpSessionListener ServletRequestAttributeListener
|
它适合被当成注入点,是因为它不一定需要独立 URL。只要事件触发,Listener 就会被容器调用。对于 ServletRequestListener 来说,每次请求进入应用时都有机会执行逻辑,所以它更像一个隐藏在事件系统里的入口。
一个无害结构样例:
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
| import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.http.HttpServletRequest;
public class LabMarkerListener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if ("/lab/listener-marker".equals(req.getRequestURI())) { runFixedLabCommand();
req.setAttribute("lab.listener.marker", "true"); } }
@Override public void requestDestroyed(ServletRequestEvent sre) { }
private void runFixedLabCommand() { try { if (System.getProperty("os.name").toLowerCase().contains("win")) { new ProcessBuilder("notepad.exe").start(); } else { new ProcessBuilder("true").start(); } } catch (Exception ignored) { } } }
|
Listener 型的麻烦点在于,它不一定有一个明显的 URL 映射。它可能在每个请求进入时都先跑一遍。
扫描时可以看:
1 2 3 4 5
| ServletRequestListener / ServletContextListener / HttpSessionListener Listener 实例的 ClassLoader Listener 类来源路径 requestInitialized 里是否读取请求参数或 Header 是否存在动态类加载、反射、进程执行等敏感调用
|
3. Spring 生态里的内存马
Spring 应用里,很多请求不直接看 Servlet 映射,而是进 Spring MVC 的 HandlerMapping。
这类内存马的核心是:把恶意逻辑藏进 Spring 的路由体系里。
3.1 Controller 型
Controller 型很好理解,就是多出一个控制器方法。
Controller 是 Spring MVC 暴露 HTTP 接口的常规方式。正常业务里,@Controller 或 @RestController 会配合 @RequestMapping、@GetMapping、@PostMapping 把某个 URL 绑定到一个 Java 方法。
它原本负责:
1 2 3 4
| 接收路由请求 绑定参数 调用业务 Service 返回 JSON / 页面 / 文件
|
它会被当成内存马注入点,是因为 Spring 应用的路由都集中在 HandlerMapping 里。只要能在运行时注册一个新的 Controller 或 HandlerMethod,就相当于在应用里多了一条隐藏路由。
惰性样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController;
@RestController public class LabMarkerController { @PostMapping("/lab/controller-marker") public String marker() { return "controller marker"; } }
|
正常业务里 Controller 很多,所以只靠“发现一个新 Controller”没法判断。
更实际的检测思路是:
1 2 3 4
| 运行时 HandlerMapping 中的路径是否偏离基线 Controller 类是否来自临时目录、上传目录、JSP 编译目录 方法体里是否有反射、Base64、AES、defineClass 等组合 是否有非常规 Header / 参数作为触发条件
|
3.2 Interceptor 型
Interceptor 和 Filter 很像,但它位于 Spring MVC 层。
Interceptor 原本是 Spring MVC 用来在 Controller 前后插入逻辑的机制。它比 Filter 更贴近 Spring,能拿到即将执行的 handler,也更适合做业务层面的权限、审计和日志。
常见用途包括:
1 2 3 4 5
| 登录态校验 接口权限判断 审计日志 灰度标记 Controller 前后置处理
|
它能成为注入点,是因为 preHandle 会在 Controller 方法执行前触发。如果这里判断某个特殊路径或 Header,然后直接写 response 并返回 false,后面的业务 Controller 就不会再执行。
惰性样例:
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
| import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
public class LabMarkerInterceptor implements HandlerInterceptor { @Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler ) throws Exception { if ("/lab/interceptor-marker".equals(request.getRequestURI())) { runFixedLabCommand();
response.setHeader("X-Lab-Marker", "interceptor"); response.getWriter().write("interceptor marker");
return false; }
return true; }
private void runFixedLabCommand() throws Exception { if (System.getProperty("os.name").toLowerCase().contains("win")) { new ProcessBuilder("notepad.exe").start(); } else { new ProcessBuilder("true").start(); } } }
|
扫描时可以看:
1 2 3 4
| HandlerExecutionChain 里是否多出陌生 Interceptor Interceptor 类加载器是否异常 preHandle 里是否有请求特征判断 是否绕过正常 Controller 直接写 response
|
3.3 HandlerMapping 型
HandlerMapping 型更隐蔽一些。它不是简单加一个 Controller 类,而是直接往 Spring 的映射表里塞一条路由。
HandlerMapping 是 Spring MVC 查找“这个请求该交给谁处理”的核心表。浏览器请求进来以后,Spring 会根据路径、HTTP 方法、Header 等信息,在 HandlerMapping 里找到对应的 HandlerMethod。
它原本负责:
1 2 3 4
| 保存 URL 到 Controller 方法的映射 根据请求匹配 Handler 维护路径条件、方法条件、参数条件 把请求交给 HandlerAdapter 执行
|
它适合作为内存马注入点,是因为攻击者不一定需要真的新增一个普通 Controller 类。只要能改运行时映射表,就能让一个已有对象或动态对象接管某个隐藏路径。扫描时如果只看源码里的注解,很容易漏掉运行时被塞进去的映射。
检测时更适合从运行时映射表下手:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
RequestMappingHandlerMapping mapping = applicationContext .getBean(RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> methods = mapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : methods.entrySet()) { RequestMappingInfo info = entry.getKey();
HandlerMethod method = entry.getValue();
System.out.println(info + " -> " + method.getBeanType().getName()); }
|
这段代码本身是防护侧枚举逻辑,可以用来做基线:
1 2 3
| 启动时记录 HandlerMapping 运行中定期比对 发现新增路径后检查 HandlerMethod 来源
|
4. 容器内部型内存马
再往下就是容器内部。以 Tomcat 为例,请求进入应用前后,会经过 Connector、Pipeline、Valve、Wrapper 等组件。
这类内存马不一定出现在 Servlet 规范里的列表中。
4.1 Tomcat Valve 型
Valve 是 Tomcat Pipeline 里的处理节点。
Tomcat 的请求处理不是一下子就到 Servlet。请求会经过一条 Pipeline,Pipeline 里可以挂多个 Valve。Tomcat 自己也用 Valve 做访问日志、错误处理、认证等功能。
Valve 原本常用于:
1 2 3 4 5
| 访问日志 认证鉴权 Host / Context 级别处理 错误页处理 请求前后置扩展
|
它会被当成内存马注入点,是因为它比 Filter 更靠近容器底层。Valve 可以在请求进入具体 Web 应用前后执行,而且不一定会出现在 Servlet 规范的 Filter / Servlet / Listener 列表里。只查 Web 组件注册表时,很容易看不到它。
一个无害的结构样例:
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
| import org.apache.catalina.Valve; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.valves.ValveBase;
import javax.servlet.ServletException; import java.io.IOException;
public class LabMarkerValve extends ValveBase { @Override public void invoke(Request request, Response response) throws IOException, ServletException { if ("/lab/valve-marker".equals(request.getRequestURI())) { runFixedLabCommand();
response.addHeader("X-Lab-Marker", "valve"); response.getWriter().write("valve marker");
return; }
Valve next = getNext(); if (next != null) { next.invoke(request, response); } }
private void runFixedLabCommand() throws IOException { if (System.getProperty("os.name").toLowerCase().contains("win")) { new ProcessBuilder("notepad.exe").start(); } else { new ProcessBuilder("true").start(); } } }
|
Valve 型扫描可以看:
1 2 3 4
| StandardContext / StandardHost / Engine 的 Pipeline Pipeline 中 Valve 链是否和启动基线一致 Valve 类来源是否异常 invoke 方法里是否直接处理请求并提前返回
|
4.2 Tomcat Upgrade 型
Upgrade 本来用于协议升级,比如 WebSocket。滥用时可能把特殊请求导到自定义处理逻辑。
HTTP Upgrade 原本是协议升级机制。最常见的场景是 WebSocket:客户端先发一个普通 HTTP 请求,带上 Upgrade 相关 Header,服务端确认后切换到新的通信协议。
它原本负责:
1 2 3 4
| 识别 Upgrade 请求 切换协议处理器 维护升级后的连接 支持 WebSocket 等长连接场景
|
它会成为注入点,是因为它处在 Connector / ProtocolHandler 这一层,位置更偏底层。特殊 Header 命中后,请求可能不走普通 Servlet 路由,而是进入自定义 Upgrade 处理器。对于只枚举 Web 应用组件的扫描器来说,这一类比较容易漏。
这类不建议只靠字符串搜,最好结合运行时结构看:
1 2 3 4
| Connector ProtocolHandler UpgradeProtocol 列表 WebSocket / HTTP Upgrade 处理器 是否出现异常协议名或异常实现类
|
扫描时可以把它当成“容器连接层扩展点”来处理。它不在普通 Servlet 映射表里,漏扫概率比较高。
5. 字节码 / Agent 型内存马
这类和前面的组件型不一样。它不一定新增组件,而是直接改已经加载的类。
冰蝎 4.1 的 Agent 链就是这个方向:
1 2 3 4
| 找到请求入口类 生成 Hook 版字节码 拿到 Instrumentation redefineClasses 替换已加载类
|
5.1 Java Agent 型
Agent 型内存马是这篇前置内容里最应该单独拎出来的一类。因为它不一定往 Web 容器里新增 Filter、Servlet 或 Listener,而是先拿到 Instrumentation,再修改已经加载的类。
Java Agent 原本是 JVM 提供给监控、调试、性能分析和 APM 工具使用的扩展机制。像链路追踪、方法耗时统计、覆盖率采集、热更新,有不少都会用到 Agent。
它原本能做的事包括:
1 2 3 4 5 6
| JVM 启动时加载 Agent 运行中 attach 到目标 JVM 注册 ClassFileTransformer 修改类字节码 触发 retransform / redefine 采集方法调用和性能数据
|
它会被当成内存马注入点,是因为权限太靠近 JVM 核心。一旦拿到 Instrumentation,就不只是“新增一个 Web 组件”,而是可以修改已经加载的类。请求入口类、FilterChain、Tomcat 内部类都可能被重写,这也是 Agent 型内存马比普通组件型更麻烦的地方。
Java Agent 的入口一般长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import java.lang.instrument.Instrumentation;
public class LabAgentEntry { public static void premain(String agentArgs, Instrumentation inst) { install(agentArgs, inst); }
public static void agentmain(String agentArgs, Instrumentation inst) { install(agentArgs, inst); }
private static void install(String agentArgs, Instrumentation inst) { inst.addTransformer(new LabAgentTransformer(), true); } }
|
premain 是 JVM 启动时通过 -javaagent 进来的入口,agentmain 是运行中 attach 进来的入口。冰蝎这类注入链更关注后者:
1 2 3 4 5
| VirtualMachine.attach -> loadAgent -> agentmain -> 获取 Instrumentation -> addTransformer / redefineClasses
|
Agent Jar 里还会有一个 Manifest,指定 Agent 入口类:
1 2 3 4
| Manifest-Version: 1.0 Agent-Class: LabAgentEntry Can-Redefine-Classes: true Can-Retransform-Classes: true
|
真正要盯的是这些点:
1 2 3 4 5 6
| 运行中 attach 到当前 JVM loadAgent 加载临时 Jar agentmain 被调用 Instrumentation 被保存或继续传递 addTransformer / redefineClasses / retransformClasses 目标类命中 Servlet / FilterChain / Tomcat 核心链路
|
为了让扫描器能识别真实危险调用,Agent 内部的 Hook 逻辑也可能包含固定命令触发点。下面这个方法本身不接收外部参数,只用来展示扫描时应该抓到的 ProcessBuilder 形态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class LabAgentCommandMarker { public static void runFixedLabCommand() { try { if (System.getProperty("os.name").toLowerCase().contains("win")) { new ProcessBuilder("notepad.exe").start(); } else { new ProcessBuilder("true").start(); } } catch (Exception ignored) { } } }
|
如果这段逻辑被插进 HttpServlet.service()、ApplicationFilterChain.doFilter()、ServletStubImpl.execute() 这类入口方法里,就已经不是普通 Agent 插桩了。
5.2 redefineClasses 型
防护侧可以用一个很小的示意代码理解它的落点:
redefineClasses 本来是给调试器、热修复、Agent 插桩用的能力。它允许在 JVM 运行时,把某个已经加载的类替换成新的字节码版本。
正常用途里,它可能出现在:
1 2 3 4 5
| APM 方法增强 在线诊断工具 热补丁 测试覆盖率工具 运行时调试工具
|
它被滥用的原因也很直接:不需要新增 Filter,不需要新增 Servlet,只要把原来的入口类改掉,请求还是走原来的类名和调用链,但方法体已经变了。外面看映射表可能一切正常,真正异常藏在字节码里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import java.lang.instrument.ClassDefinition; import java.lang.instrument.Instrumentation;
public class RedefineShape { public static void redefine( Instrumentation instrumentation, Class<?> targetClass, byte[] newBytes ) throws Exception { ClassDefinition definition = new ClassDefinition(targetClass, newBytes);
instrumentation.redefineClasses(definition); } }
|
这个例子没有提供 Instrumentation 获取方式,也没有提供可用的替换字节码。这里只看形态:
1 2 3 4
| ClassDefinition 目标 Class 新字节码 Instrumentation.redefineClasses
|
真正值得警惕的是目标类是谁。
如果被重定义的是业务普通类,可能是 APM、热更新或调试工具。如果被重定义的是这些入口类,就要认真看了:
1 2 3 4
| javax.servlet.http.HttpServlet jakarta.servlet.http.HttpServlet weblogic.servlet.internal.ServletStubImpl org.apache.catalina.core.ApplicationFilterChain
|
Agent 也可能通过 ClassFileTransformer 在类加载或重转换时改字节码。
Transformer 原本是 Java Agent 用来“拦截类加载并改字节码”的接口。类加载时,JVM 会把原始 class 字节数组交给 Transformer,Transformer 可以返回修改后的字节码。
正常场景里,Transformer 常用于:
1 2 3 4 5
| 方法耗时插桩 日志增强 链路追踪 安全探针 代码覆盖率统计
|
它能成为注入点,是因为它正好站在“类字节码进入 JVM”这条路径上。如果 Transformer 命中了 Servlet、FilterChain、Controller、Tomcat Valve 等关键类,就可以在这些类的方法里插入额外逻辑。
结构大概是:
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
| import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
public class LabMarkerTransformer implements ClassFileTransformer { @Override public byte[] transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer ) throws IllegalClassFormatException { if ("javax/servlet/http/HttpServlet".equals(className)) { return classfileBuffer; }
return null; } }
|
这段只是展示 Transformer 的形态,没有修改字节码。
扫描时可以盯:
1 2 3 4
| 是否加载了 javaagent 是否出现自定义 ClassFileTransformer 是否发生 retransformClasses / redefineClasses Transformer 是否命中 Servlet / FilterChain / Tomcat 核心类
|
6. 扫描工具怎么落点
内存马类型很多,但扫描工具不用一开始就全做满。可以按“越靠近请求入口,优先级越高”的思路来排。
第一层,先查 Web 组件:
1 2 3 4 5
| FilterRegistration ServletRegistration Listener 列表 Spring HandlerMapping Interceptor 链
|
第二层,查容器内部扩展点:
1 2 3
| Tomcat Pipeline / Valve Wrapper / Context 内部结构 UpgradeProtocol
|
第三层,查字节码和 JVM 行为:
1 2 3 4 5
| 关键入口类 hash Instrumentation.redefineClasses ClassFileTransformer ClassLoader#defineClass JSP 编译目录中的异常类
|
最后再做特征组合,而不是靠单点判断:
1 2 3 4 5 6 7 8 9
| 请求路径判断 只处理 POST 读取请求体 Base64 / AES 反射 setAccessible defineClass newInstance / equals 直接写 response 提前 return
|
单个点不一定有问题,组合在一起才有价值。
比如下面这种组合就很值得报警:
1 2 3 4 5
| 入口类被 redefine + 入口方法里出现路径匹配 + 请求体解密 + defineClass 动态加载 + equals / 反射触发执行
|
7. 小结
Java 内存马可以先按挂载位置来理解:
1 2 3 4
| 组件型:Filter / Servlet / Listener 框架型:Controller / Interceptor / HandlerMapping 容器型:Valve / Upgrade / Pipeline 字节码型:Agent / Transformer / redefineClasses
|
前几类更像“多挂了一个处理节点”,Agent 型更像“把原来的节点改了”。
所以做扫描工具时,不能只看运行时注册表。注册表能抓一批,但抓不到全部。
更稳的做法是把三件事结合起来:
1 2 3
| 运行时组件枚举 关键入口类字节码完整性 敏感 JVM 行为监控
|
这样再回头看冰蝎 4.1 的 Agent 注入链,就会更清楚:它不是新增一个普通 Web 组件,而是借助 Instrumentation 把请求入口类改成了带 Hook 的版本。