| 项目 | 内容 |
|---|---|
| 文档名称 | Java 内存马扫描与清理工具设计文档 |
| 项目代号 | memshell-scanner |
| 适用场景 | 应急响应、主机排查、Java Web 运行时安全检查、内存马取证与清理 |
| 目标环境 | Tomcat、Spring MVC、Spring Boot、部分兼容 Jetty / Undertow / WebLogic / WebSphere 的扩展架构 |
| 核心语言 | Java |
| 推荐 JDK | JDK 8 / 11 / 17 / 21 |
| 输出格式 | Console / JSON / HTML / Markdown |
| 默认模式 | 只读扫描 |
| 高危操作 | 清理运行时组件、注销 Spring Mapping、移除 Filter / Servlet / Listener / Valve |
Java 内存马通常不是传统落地文件型 WebShell,而是攻击者利用反序列化、JNDI 注入、表达式注入、后台命令执行、组件漏洞等入口,在 Java Web 容器或框架运行时动态注册恶意组件。
常见类型包括:
- Tomcat Filter 型内存马
- Tomcat Servlet 型内存马
- Tomcat Listener 型内存马
- Tomcat Valve 型内存马
- Spring Controller 型内存马
- Spring Interceptor 型内存马
- Java Agent / Instrumentation 型内存马
- ClassLoader / defineClass 动态加载型内存马
- WebSocket / Upgrade 协议型内存马
- 反射修改正常业务对象型内存马
传统文件查杀重点检查磁盘上的 JSP、JAR、CLASS 文件,但内存马可能只存在于 JVM 内存、Web 容器运行时注册表、Spring HandlerMapping、ClassLoader 已加载类中。因此需要一种能够进入目标 JVM 内部观察运行时状态的扫描工具。
本工具目标是实现一个 Java 内存马扫描与清理工具,具备以下能力:
- 发现目标 Java 进程中的异常运行时组件。
- 枚举 JVM 已加载类并识别可疑类。
- 枚举 Tomcat 运行时 Filter、Servlet、Listener、Valve。
- 枚举 Spring MVC 运行时 Controller、HandlerMapping、Interceptor。
- 对可疑对象进行评分并输出原因。
- 导出 JSON / HTML / Markdown 报告。
- 支持按对象 ID 精准清理,不做默认自动删除。
- 清理前保存快照和证据。
- 清理后自动复扫确认。
- 支持后续扩展 Jetty、Undertow、WebLogic、WebSphere。
本工具不做以下事情:
- 不生成内存马。
- 不提供攻击载荷。
- 不提供绕过安全产品的能力。
- 不自动清空所有运行时组件。
- 不默认修改业务类字节码。
- 不替代完整主机应急响应,只作为 Java 运行时排查工具。
推荐采用双 JAR 架构:
memshell-attach.jar # 外部启动器,负责 attach 到目标 JVM
memshell-agent.jar # Java Agent,被加载进目标 JVM 内部执行扫描和清理
采用 agentmain 阻塞执行 + 文件落盘 方案:
agentmain在目标 JVM 内同步执行全部逻辑,执行完毕后退出,attach 端等待返回- 扫描报告、证据文件、清理计划均写入本地文件系统
- attach 端通过
VirtualMachine.loadAgent()传入命令参数,agent 解析后执行对应逻辑 - 不使用 Socket 通信,避免容器网络 namespace 隔离导致连接失败
clean 命令拆成两步执行,无需双向通信:
第一步:clean --id <id> --dry-run → 生成清理计划文件,人工审阅
第二步:clean --id <id> --confirm → 读取计划文件,交互确认后执行
整体流程:
安全人员执行命令
↓
memshell-attach.jar
↓
Attach 到目标 Java 进程 PID
↓
loadAgent 加载 memshell-agent.jar(传入命令参数)
↓
agentmain 在目标 JVM 内同步执行
↓
扫描 JVM / Tomcat / Spring / ClassLoader
↓
报告写入本地文件
↓
人工审阅报告
↓
dry-run 生成清理计划
↓
人工确认(终端输入 yes)
↓
按 ID 精准清理
↓
复扫验证
memshell-scanner/
├── pom.xml
├── README.md
├── docs/
│ ├── design.md
│ ├── usage.md
│ └── risk-control.md
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/memshell/
│ │ ├── attach/
│ │ │ ├── AttachMain.java
│ │ │ ├── JvmProcessLister.java
│ │ │ └── AttachPermissionChecker.java
│ │ ├── agent/
│ │ │ ├── MemShellAgent.java
│ │ │ ├── AgentContext.java
│ │ │ └── AgentCommandDispatcher.java
│ │ ├── scanner/
│ │ │ ├── Scanner.java
│ │ │ ├── ClassScanner.java
│ │ │ ├── ClassLoaderScanner.java
│ │ │ ├── TomcatScanner.java
│ │ │ ├── SpringScanner.java
│ │ │ ├── ServletScanner.java
│ │ │ ├── ListenerScanner.java
│ │ │ ├── ValveScanner.java
│ │ │ └── InterceptorScanner.java
│ │ ├── cleaner/
│ │ │ ├── Cleaner.java
│ │ │ ├── CleanPlan.java
│ │ │ ├── TomcatCleaner.java
│ │ │ ├── SpringCleaner.java
│ │ │ └── RollbackManager.java
│ │ ├── detector/
│ │ │ ├── Finding.java
│ │ │ ├── FindingType.java
│ │ │ ├── RiskScore.java
│ │ │ ├── RuleEngine.java
│ │ │ ├── BytecodeKeywordRule.java
│ │ │ ├── CodeSourceRule.java
│ │ │ ├── ClassNameRule.java
│ │ │ ├── RuntimeRegistryRule.java
│ │ │ └── BaselineDiffRule.java
│ │ ├── report/
│ │ │ ├── Report.java
│ │ │ ├── JsonReportWriter.java
│ │ │ ├── HtmlReportWriter.java
│ │ │ └── MarkdownReportWriter.java
│ │ ├── snapshot/
│ │ │ ├── RuntimeSnapshot.java
│ │ │ ├── TomcatSnapshot.java
│ │ │ ├── SpringSnapshot.java
│ │ │ └── ClassSnapshot.java
│ │ └── util/
│ │ ├── ReflectUtil.java
│ │ ├── ClassUtil.java
│ │ ├── JsonUtil.java
│ │ ├── HashUtil.java
│ │ └── SafeExecutor.java
│ └── resources/
│ ├── META-INF/
│ │ └── MANIFEST.MF
│ ├── rules/
│ │ ├── default-rules.yml
│ │ └── whitelist.yml
│ └── templates/
│ └── report.html
└── target/
├── memshell-attach.jar
└── memshell-agent.jar
负责从外部连接目标 JVM。
核心职责:
- 枚举当前机器 Java 进程。
- 根据 PID attach 到目标 JVM。
- 加载 agent JAR。
- 传入执行模式参数。
- 打印 attach 状态。
- 处理权限错误。
主要类:
AttachMain
JvmProcessLister
AttachPermissionChecker
典型命令:
java -jar memshell-attach.jar <pid> ./memshell-agent.jar scanAgent 模块是真正进入目标 JVM 内部执行逻辑的部分。
核心入口:
public static void agentmain(String args, Instrumentation inst)职责:
- 接收 attach-client 传入的命令。
- 初始化扫描上下文。
- 调用不同扫描器。
- 聚合扫描结果。
- 写出报告。
- 执行清理命令。
- 清理后复扫验证。
负责采集运行时数据。
包括:
| 扫描器 | 作用 |
|---|---|
| ClassScanner | 扫描 JVM 已加载类 |
| ClassLoaderScanner | 扫描 ClassLoader 树 |
| TomcatScanner | 扫描 Tomcat StandardContext |
| ServletScanner | 扫描 Servlet / Wrapper |
| ListenerScanner | 扫描 Listener |
| ValveScanner | 扫描 Pipeline / Valve |
| SpringScanner | 扫描 Spring Mapping / Bean / Interceptor |
| InterceptorScanner | 扫描 Spring HandlerInterceptor |
负责把原始运行时对象转换成风险结果。
核心职责:
- 规则匹配。
- 风险评分。
- 原因解释。
- 白名单过滤。
- 基线对比。
- 输出 Finding 对象。
负责精准清理指定对象。
清理对象包括:
- Tomcat Filter
- Tomcat Servlet
- Tomcat Listener
- Tomcat Valve
- Spring Mapping
- Spring Interceptor
- Spring Bean
- 可疑 ClassFileTransformer
原则:
- 默认不清理。
- 必须指定 Finding ID。
- 清理前导出证据。
- 清理前生成 CleanPlan。
- 清理后复扫。
- 失败后尽量回滚。
- 无法安全清理时只给出重启建议。
负责输出报告。
支持格式:
- console
- JSON
- HTML
- Markdown
报告应包含:
- 目标进程信息
- JVM 信息
- 容器类型
- Spring 信息
- 扫描时间
- 可疑对象列表
- 风险评分
- 证据
- 清理建议
- 清理结果
- 复扫结果
java -jar memshell-attach.jar list输出示例:
PID MainClass
12345 org.apache.catalina.startup.Bootstrap
22333 com.example.Application
java -jar memshell-attach.jar 12345 ./memshell-agent.jar scanjava -jar memshell-attach.jar 12345 ./memshell-agent.jar scan --format json --output /tmp/memshell-report.jsonjava -jar memshell-attach.jar 12345 ./memshell-agent.jar scan --format html --output /tmp/memshell-report.htmljava -jar memshell-attach.jar 12345 ./memshell-agent.jar scan --target tomcatjava -jar memshell-attach.jar 12345 ./memshell-agent.jar scan --target springjava -jar memshell-attach.jar 12345 ./memshell-agent.jar list-runtimejava -jar memshell-attach.jar 12345 ./memshell-agent.jar baseline --output /opt/baseline/java-runtime-baseline.jsonjava -jar memshell-attach.jar 12345 ./memshell-agent.jar scan --baseline /opt/baseline/java-runtime-baseline.jsonjava -jar memshell-attach.jar 12345 ./memshell-agent.jar clean --id finding-filter-a3f92c1djava -jar memshell-attach.jar 12345 ./memshell-agent.jar verify --id finding-filter-a3f92c1d采集字段:
{
"className": "com.example.Abc",
"classLoader": "org.apache.catalina.loader.ParallelWebappClassLoader",
"codeSource": "file:/opt/tomcat/webapps/app/WEB-INF/classes/",
"interfaces": [
"javax.servlet.Filter"
],
"superClass": "java.lang.Object",
"hash": "sha256:xxxx",
"suspiciousKeywords": [
"java/lang/Runtime",
"ProcessBuilder"
]
}重点关注:
- 实现 Filter / Servlet / Listener / Valve / HandlerInterceptor 的类
- CodeSource 为空的类
- 来源在
/tmp、/var/tmp、上传目录中的类 - 类名随机
- ClassLoader 异常
- 字节码包含高危 API
- 非业务包名
- 非依赖白名单包名
采集字段:
{
"type": "tomcat-filter",
"filterName": "DebugFilter",
"filterClass": "com.example.DebugFilter",
"urlPatterns": ["/*"],
"dispatcherTypes": ["REQUEST"],
"codeSource": null,
"classLoader": "WebappClassLoaderBase",
"fromWebXml": false,
"runtimeOnly": true
}重点关注:
- runtimeOnly 为 true
- URL Pattern 为
/* - filterName 随机
- filterClass 来源异常
- filter 排序靠前
- FilterConfig 存在但部署描述文件中不存在
- 字节码存在命令执行、反射、类加载、加解密特征
采集字段:
{
"type": "tomcat-servlet",
"servletName": "ApiServlet",
"servletClass": "com.example.ApiServlet",
"mappings": ["/api/debug"],
"loadOnStartup": -1,
"codeSource": null,
"runtimeOnly": true
}重点关注:
- 源码中不存在的 Servlet Mapping
- 可疑路径
- 类来源为空
- Wrapper 动态创建
wasCreatedDynamicServlet相关线索
采集字段:
{
"type": "tomcat-listener",
"listenerClass": "com.example.ReqListener",
"listenerObject": "com.example.ReqListener@1234",
"listenerKind": "ServletRequestListener",
"codeSource": null,
"runtimeOnly": true
}重点关注:
- ServletRequestListener
- HttpSessionListener
- ServletContextListener
- 动态添加但配置文件无记录
- Listener 中存在命令执行或反射逻辑
采集字段:
{
"type": "tomcat-valve",
"valveClass": "com.example.DebugValve",
"containerLevel": "Context",
"pipelineIndex": 0,
"codeSource": null
}重点关注:
- 非 Tomcat 默认 Valve
- 非 APM / 业务白名单 Valve
- CodeSource 为空
- 插入 Context / Host / Engine Pipeline
- 字节码存在高危 API
采集字段:
{
"type": "spring-mapping",
"pattern": "/api/debug",
"methods": ["GET", "POST"],
"handlerClass": "com.example.DebugController",
"handlerMethod": "run",
"beanName": "debugController",
"codeSource": null,
"runtimeOnly": true
}重点关注:
- HandlerMapping 中存在,但源码或基线中不存在
- Controller Bean 非正常包名
- HandlerMethod 所属类 CodeSource 为空
- Mapping 路径伪装
- Handler 方法内部存在高危 API
采集字段:
{
"type": "spring-interceptor",
"interceptorClass": "com.example.DebugInterceptor",
"includePatterns": ["/*"],
"excludePatterns": [],
"codeSource": null,
"runtimeOnly": true
}重点关注:
- 匹配范围过大
- 动态添加
- 非业务包名
- CodeSource 为空
- preHandle 中存在高危 API
采用分值累计机制。
| 规则 | 分值 |
|---|---|
| 实现 Filter / Servlet / Listener / Valve / HandlerInterceptor | +3 |
| CodeSource 为空 | +3 |
| CodeSource 位于临时目录 | +3 |
| 字节码包含 Runtime.exec / ProcessBuilder | +4 |
| 字节码包含 defineClass / ClassLoader#defineClass | +3 |
| 字节码包含 setAccessible / getDeclaredField | +2 |
| 字节码包含 Cipher / AES / Base64 | +2 |
| 运行时存在但 web.xml / 注解 / 基线不存在 | +4 |
URL Pattern 为 /* |
+2 |
| Filter 排序靠前 | +2 |
| 类名随机或高熵 | +1 |
| 非业务包名且非依赖白名单 | +2 |
| ClassLoader 异常 | +2 |
| Mapping 路径伪装成静态资源或健康检查 | +2 |
| 命中白名单 | -5 |
| 来源为已知 APM / 正常 Agent | -4 |
风险等级:
0 - 3 低风险
4 - 6 可疑
7 - 9 高危
10+ 严重
示例:
{
"id": "finding-filter-a3f92c1d",
"type": "tomcat-filter",
"name": "DebugFilter",
"className": "com.example.Abc123",
"score": 12,
"level": "critical",
"reasons": [
"runtime only filter",
"url pattern is /*",
"codeSource is null",
"bytecode contains ProcessBuilder"
]
}packageWhitelist:
- "org.springframework."
- "org.apache.catalina."
- "org.apache.tomcat."
- "org.apache.coyote."
- "com.fasterxml.jackson."
- "ch.qos.logback."
- "org.slf4j."
- "com.alibaba.druid."businessPackages:
- "com.company."
- "cn.company."
- "com.example.app."agentWhitelist:
- "arthas"
- "skywalking"
- "pinpoint"
- "elastic-apm"
- "opentelemetry"
- "jrebel"codeSourceWhitelist:
- "/opt/app/"
- "/opt/tomcat/webapps/"
- "/usr/local/tomcat/lib/"很多正常 Java 应用也会动态生成类,例如:
- Spring CGLIB
- Hibernate Proxy
- MyBatis Mapper Proxy
- ByteBuddy
- JDK Proxy
- APM Agent
- Arthas
- JRebel
如果只依赖关键字,很容易误报。因此推荐在应用刚启动、确认干净时生成一份运行时基线。
{
"app": "example-app",
"pid": 12345,
"time": "2026-05-18T15:00:00+09:00",
"jvm": {
"javaVersion": "17.0.10",
"javaHome": "/usr/lib/jvm/java-17"
},
"tomcat": {
"filters": [],
"servlets": [],
"listeners": [],
"valves": []
},
"spring": {
"mappings": [],
"interceptors": [],
"beans": []
},
"classes": []
}扫描时对比:
- 新增 Filter
- 新增 Servlet Mapping
- 新增 Listener
- 新增 Valve
- 新增 Spring Mapping
- 新增 Interceptor
- 新增可疑类
- 原有组件 className 变化
- 原有 class hash 变化
一个 Tomcat 进程可以部署多个 Web 应用,每个应用对应一个独立的 StandardContext,拥有各自独立的 Filter、Servlet、Listener 注册表。扫描器必须枚举所有 Context,不能只扫描一个。
推荐遍历路径:从 Engine 向下遍历
Server
└── Service
└── Engine
└── Host(可能多个 Host)
└── StandardContext(可能多个 Context)
通过 MBeanServer 查询所有 Catalina:type=Context MBean,可获取所有已注册 Context 的引用:
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
Set<ObjectName> names = mbs.queryNames(new ObjectName("Catalina:type=Context,*"), null);也可从已加载类中找到 StandardEngine 实例,递归遍历 getChildren()。
多 Context 扫描原则:
- 每个 Context 独立扫描,Finding ID 中包含 Context path 信息
- 报告按 Context 分组输出
- 基线生成时也按 Context 分组存储
可能路径(按优先级):
- MBeanServer 查询所有 Tomcat Context MBean(推荐,兼容性最好)
- 从已加载类中找到 StandardEngine,递归 getChildren()
- 从当前线程上下文 ClassLoader 反射获取 WebResourceRoot
- 从 ServletContextFacade 反射获取 ApplicationContext,再获取 StandardContext
- 从当前请求线程栈中定位容器对象
推荐实现多个 Provider:
StandardContextProvider
├── MBeanContextProvider # 主路径:MBeanServer 枚举所有 Context
├── EngineTraversalProvider # 次要路径:从 Engine 递归遍历
├── ClassLoaderContextProvider
├── ServletContextProvider
└── ThreadStackContextProvider
采集来源:
StandardContext.findFilterDefs()StandardContext.findFilterMaps()StandardContext.findFilterConfig(name)- 反射读取
filterConfigs - 反射读取
filterDefs - 反射读取
filterMaps
输出:
- filterName
- filterClass
- urlPatterns
- dispatcherTypes
- initParameters
- filter instance class
- codeSource
- classLoader
- runtimeOnly
- score
采集来源:
StandardContext.findChildren()- Wrapper
- servletClass
- servletName
- mappings
- loadOnStartup
- instance
- codeSource
采集来源:
applicationEventListenersapplicationLifecycleListenersapplicationListeners- Listener 实例对象
- Listener 类名
采集来源:
context.getPipeline()pipeline.getFirst()valve.getNext()
需要识别:
- StandardContextValve
- ErrorReportValve
- AccessLogValve
- RemoteIpValve
- 自定义 Valve
可能方式:
- 从 WebApplicationContextUtils 获取。
- 从 DispatcherServlet 获取 WebApplicationContext。
- 从已加载 BeanFactory / ApplicationContext 静态引用中定位。
- 从 Spring Boot Actuator 相关对象中定位。
- 从当前 ClassLoader 和 ServletContext 中定位。
重点 Bean:
RequestMappingHandlerMapping
BeanNameUrlHandlerMapping
SimpleUrlHandlerMapping
RouterFunctionMapping
WelcomePageHandlerMapping
主要关注:
- RequestMappingHandlerMapping#getHandlerMethods()
- AbstractHandlerMethodMapping#unregisterMapping()
- HandlerMethod#getBeanType()
- HandlerMethod#getMethod()
重点对象:
- AbstractHandlerMapping 的 adaptedInterceptors
- MappedInterceptor
- HandlerInterceptor
- WebMvcConfigurer 注册对象
采集字段:
- interceptorClass
- includePatterns
- excludePatterns
- order
- codeSource
- classLoader
关注:
- BeanDefinition 是否存在
- singletonObjects 中是否存在
- beanClassName 是否异常
- factoryBeanName 是否异常
- bean 来源是否为空
- 是否 runtime only
字节码扫描不对所有已加载类执行,而是分两阶段进行,避免在类数量庞大的应用中产生严重性能影响。
第一阶段:初筛(无字节码读取)
对所有已加载类按以下条件过滤,命中任意一条进入字节码精扫:
| 优先级 | 初筛条件 |
|---|---|
| 1 | 实现 Filter / Servlet / Listener / Valve / HandlerInterceptor |
| 2 | CodeSource 为空 |
| 3 | 运行时存在于容器注册表,但基线中不存在 |
| 4 | 类名高熵(随机字符,熵值超过阈值) |
| 5 | 非业务包名且非白名单包名 |
第二阶段:字节码精扫
仅对初筛命中的类读取字节码,执行关键字匹配和 hash 计算。
好处:
- 典型 Tomcat 应用加载数千个类,但真正可疑的通常只有几十个
- 扫描速度快,应急场景下可用
- 后续可通过
--deep参数对全量类执行字节码扫描
可尝试从以下位置读取类字节码:
clazz.getResourceAsStream("/a/b/C.class")- CodeSource 文件路径
- ClassLoader resource
- Instrumentation transformer 临时 dump
- 已知 class dump 工具扩展
高危关键字:
java/lang/Runtime
getRuntime
exec
java/lang/ProcessBuilder
start
defineClass
getDeclaredField
getDeclaredMethod
setAccessible
javax/crypto/Cipher
java/util/Base64
sun/misc/BASE64Decoder
org/apache/catalina/core/StandardContext
org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping
注意:
- 单独命中 Base64 不代表恶意。
- 单独命中反射不代表恶意。
- 要结合 Web 组件类型、CodeSource、运行时注册表和基线对比。
对类字节码计算:
- SHA-256
- MD5 可选,只用于兼容旧平台
- 文件路径 hash
- className hash
清理是高危操作,必须遵守:
- 默认不执行清理。
- 必须指定 Finding ID。
- 必须先生成清理计划。
- 必须先保存证据。
- 必须支持 dry-run。
- 必须执行后复扫。
- 无法安全回滚时明确提示重启。
- 不提供 clean-all 默认能力。
java -jar memshell-attach.jar 12345 ./memshell-agent.jar clean --id finding-filter-a3f92c1d --dry-runjava -jar memshell-attach.jar 12345 ./memshell-agent.jar clean --id finding-filter-a3f92c1d --confirm执行 --confirm 时,agent 将清理计划打印到终端,要求操作者明确输入 yes 后才真正执行:
[!] You are about to clean:
Type : tomcat-filter
Name : DebugFilter
Class : com.example.Abc123
Pattern : /*
Score : 12
Context : /app
Steps:
1. backup filterDef / filterMap / filterConfig
2. call filter.destroy()
3. remove from filterConfigs / filterDefs / filterMaps
4. re-scan to verify removal
Rollback supported: YES
Type "yes" to confirm (anything else cancels):
输入非 yes 字符串时取消操作,不做任何修改。
此设计强制操作者在清理前完整阅读清理计划,防止手滑将错误 ID 与 --confirm 组合执行。
{
"findingId": "finding-filter-a3f92c1d",
"type": "tomcat-filter",
"target": "DebugFilter",
"steps": [
"backup filterDef",
"backup filterMap",
"backup filterConfig",
"call filter.destroy",
"remove filterConfig",
"remove filterDef",
"remove filterMap",
"verify filter not exists"
],
"rollbackSupported": true
}需要处理:
- filterConfigs
- filterDefs
- filterMaps
- Filter 实例 destroy
- URL Pattern 映射
清理步骤:
- 根据 findingId 找到 filterName。
- 导出 FilterConfig、FilterDef、FilterMap。
- 调用 Filter.destroy()。
- 从 filterConfigs 中移除。
- 从 filterDefs 中移除。
- 从 filterMaps 中移除对应映射。
- 重新扫描确认不存在。
- 如果失败,尝试恢复快照。
注意:
- 不同 Tomcat 版本 filterMaps 数据结构不同。
- Tomcat 8 / 9 / 10 的 javax / jakarta 包名不同。
- 如果对象结构不兼容,停止清理并建议重启。
需要处理:
- Wrapper
- servletMappings
- children
- 实例销毁
- mapping 删除
清理步骤:
- 找到 Wrapper。
- 导出 Wrapper 元数据。
- 调用 unload / destroy。
- 删除 servlet mapping。
- 从 children 中移除 Wrapper。
- 复扫。
需要处理:
- applicationEventListeners
- applicationLifecycleListeners
- applicationListeners
清理步骤:
- 定位 Listener 实例。
- 判断 Listener 类型。
- 从数组或列表中移除。
- 复扫。
需要处理:
- Pipeline
- Valve 链表
清理步骤:
- 定位目标 Valve。
- 保存前后 Valve 链关系。
- 从 Pipeline 移除。
- 复扫。
Spring MVC 可以通过 HandlerMapping 注销 mapping。
清理步骤:
- 找到 RequestMappingHandlerMapping。
- 根据 pattern / methods / handlerMethod 找到 RequestMappingInfo。
- 调用 unregisterMapping。
- 复扫确认 Mapping 消失。
- 如果对应 Bean 也是 runtime only,进入 Bean 清理流程。
清理对象:
- MappedInterceptor
- HandlerInterceptor
- adaptedInterceptors
- mappedInterceptors
步骤:
- 找到对应 interceptor 实例。
- 保存列表快照。
- 从列表中移除。
- 复扫。
清理对象:
- BeanDefinition
- singletonObjects
- earlySingletonObjects
- singletonFactories
- disposableBeans
步骤:
- 判断是否 runtime only。
- 判断是否仍被 Mapping 或 Interceptor 引用。
- 销毁 Bean。
- 从 BeanFactory 中移除。
- 复扫。
清理前必须导出:
evidence/
├── finding-filter-a3f92c1d/
│ ├── finding.json
│ ├── class-info.json
│ ├── bytecode.bin
│ ├── bytecode.sha256
│ ├── runtime-object.json
│ ├── clean-plan.json
│ └── before-snapshot.json
证据内容:
- 类名
- CodeSource
- ClassLoader
- URL Pattern
- Handler Mapping
- 字节码 hash
- 可疑关键字
- 运行时对象摘要
- 清理前快照
{
"scanId": "scan-20260518-150000",
"target": {
"pid": 12345,
"mainClass": "org.apache.catalina.startup.Bootstrap",
"javaVersion": "17.0.10",
"os": "Linux"
},
"summary": {
"totalFindings": 3,
"critical": 1,
"high": 1,
"medium": 1,
"low": 0
},
"findings": [
{
"id": "finding-filter-a3f92c1d",
"type": "tomcat-filter",
"level": "critical",
"score": 12,
"name": "DebugFilter",
"className": "com.example.Abc123",
"reasons": [
"runtime only filter",
"codeSource is null",
"urlPattern is /*"
],
"recommendation": "manual confirm then clean by id"
}
]
}[+] Scan finished
PID: 12345
Container: Tomcat 9.x
Spring: detected
[CRITICAL] finding-filter-a3f92c1d
Type: tomcat-filter
Name: DebugFilter
Class: com.example.Abc123
Pattern: /*
Score: 12
Reason:
- runtime only filter
- codeSource is null
- bytecode contains ProcessBuilder
Action:
- run clean --id finding-filter-a3f92c1d --dry-run first
默认只读扫描:
java -jar memshell-attach.jar 12345 ./memshell-agent.jar scan禁止默认清理。
清理必须满足:
- 指定 ID。
- 风险等级至少 high。
- 用户传入
--confirm。 - 清理前成功保存证据。
- 清理计划生成成功。
- 目标对象当前仍存在。
工具不提供:
clean-all
delete-all-filter
delete-all-mapping
remove-all-listener
force-clear-context
如需强制模式,必须隐藏在开发配置中,并默认禁用。
清理 Filter / Valve / Interceptor 时,目标 JVM 可能正在处理并发请求,直接从注册表中移除对象可能导致正在执行 filter chain 的线程出现 NPE 或 ConcurrentModificationException。
处理策略:
- 不在清理时主动等待请求排空(应急场景下不可预期)
- 清理操作使用原子性替换:先构造新的 filterConfigs / filterMaps 副本,移除目标对象后通过反射一次性替换,减小并发窗口
- 清理后在报告中提示:如果应用负载较高,建议在低峰期执行清理或在清理后重启服务以彻底消除风险
- 不提供锁定请求处理的能力:强行暂停 Tomcat 线程池超出工具职责范围
agentmain 执行完毕后,agent JAR 中的类仍然留在目标 JVM 的 ClassLoader 中。对长期运行的应用多次 attach 会持续累积类。
处理策略:
- agent 内部类尽量精简,不持有静态状态,减少内存占用
- agentmain 执行完毕前主动将所有静态引用置 null,触发 GC
- 在文档和 README 中明确说明:不建议对同一进程频繁 attach,生产环境排查完毕后建议在维护窗口重启服务
- 不提供 agent 卸载能力(JVM 不支持动态卸载 agent)
Spring Boot 内嵌 Tomcat 的 ClassLoader 结构与独立 Tomcat 不同:
| 差异点 | 标准 Tomcat | Spring Boot fat jar |
|---|---|---|
| ClassLoader | WebappClassLoaderBase | LaunchedURLClassLoader |
| CodeSource | 指向 webapps 目录 | 指向 fat jar 内部路径(jar:file:/app.jar!/BOOT-INF/...) |
| Context 获取 | MBeanServer 或 Engine 遍历 | 优先从 DispatcherServlet 或 SpringApplication 获取 EmbeddedWebApplicationContext |
扫描器在判断 CodeSource 是否可信时,需要识别 fat jar 内部路径格式,不能将 jar:file:/... 路径误判为异常来源。
白名单中需要增加 fat jar 路径模式:
codeSourceWhitelist:
- "/opt/app/"
- "/opt/tomcat/webapps/"
- "/usr/local/tomcat/lib/"
- "jar:file:" # Spring Boot fat jar 内部路径前缀获取 ApplicationContext 时,优先从 SpringApplication 静态引用或 EmbeddedWebApplicationContext 入手,而不是依赖 WebApplicationContextUtils(后者在内嵌容器场景下可能无法直接使用)。
| JDK | 支持情况 | 注意事项 |
|---|---|---|
| JDK 8 | 支持 | 需要 tools.jar |
| JDK 11 | 支持 | Attach API 位于 jdk.attach 模块 |
| JDK 17 | 支持 | 模块访问限制更严格,需动态注入 --add-opens |
| JDK 21 | 支持 | 动态 Agent 加载默认警告,需 -XX:+EnableDynamicAgentLoading 或接受警告 |
| JRE-only | 不推荐 | 可能无法 attach |
JDK 17+ 对反射访问私有字段有严格限制,直接调用 setAccessible(true) 会抛出 InaccessibleObjectException。扫描器大量依赖反射读取 filterConfigs、filterDefs、applicationEventListeners 等私有字段,必须处理此问题。
处理策略:两层降级
第一层:Agent 加载时动态注入 --add-opens
agentmain 入口处通过 Instrumentation 和反射调用 jdk.internal.misc.Unsafe 或直接操作模块系统,动态为必要包添加 --add-opens,无需修改目标进程启动参数:
需要开放的模块:
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.management/sun.management=ALL-UNNAMED
具体实现参考 Arthas 的 ModuleUtils 做法,通过 Instrumentation.redefineModule() 动态添加 opens。
第二层:MBean / 公开 API 兜底
动态注入失败,或特定字段在当前 JDK 版本不可访问时,降级走 MBeanServer 查询:
反射路径失败 → MBeanServer 查询 Tomcat Context MBean
→ 通过公开 API(findFilterDefs / findChildren)获取部分信息
→ 在报告中标记 partial_scan,说明受限字段
原则:局部反射失败不阻断整体扫描,只影响该字段的采集完整性。
| 版本 | 包名 |
|---|---|
| Tomcat 7 / 8 / 9 | javax.servlet |
| Tomcat 10 / 11 | jakarta.servlet |
扫描器需要同时识别:
javax.servlet.Filter
jakarta.servlet.Filter
javax.servlet.Servlet
jakarta.servlet.Servlet
javax.servlet.ServletRequestListener
jakarta.servlet.ServletRequestListener
| 容器 | 第一阶段 |
|---|---|
| Tomcat | 完整支持 |
| Spring Boot 内嵌 Tomcat | 完整支持 |
| Jetty | 预留扩展 |
| Undertow | 预留扩展 |
| WebLogic | 预留扩展 |
| WebSphere | 预留扩展 |
常见错误:
原因:
- 权限不足
- 目标 JVM 禁用 attach
- 不是同一用户
- 容器 namespace 隔离
- JDK 不完整
/tmp/.java_pid<PID>不可访问
处理:
- 输出原因。
- 提示使用同用户或 root。
- 提示检查 JDK。
- 提示容器场景需进入同 namespace。
原因:
- Manifest 缺少 Agent-Class
- agentmain 方法不存在
- JDK 版本不兼容
- 模块访问限制
处理:
- 输出具体异常。
- 提示检查 Manifest。
- 提示加
--add-opens或使用对应 JDK。
原因:
- 容器版本不兼容
- 反射字段不存在
- SecurityManager 限制
- ClassLoader 隔离
处理:
- 局部失败不影响整体扫描。
- 每个 scanner 独立 try-catch。
- 报告中记录 unsupported reason。
原因:
- 对象已不存在
- 对象结构不兼容
- 清理中业务正在访问
- 无法安全修改内部结构
处理:
- 停止后续步骤。
- 输出失败步骤。
- 尝试回滚。
- 建议重启服务。
rules/default-rules.yml:
risk:
critical: 10
high: 7
suspicious: 4
keywords:
command:
- "java/lang/Runtime"
- "getRuntime"
- "exec"
- "java/lang/ProcessBuilder"
classloader:
- "defineClass"
- "ClassLoader"
reflection:
- "getDeclaredField"
- "getDeclaredMethod"
- "setAccessible"
crypto:
- "javax/crypto/Cipher"
- "java/util/Base64"
runtime:
suspiciousUrlPatterns:
- "/*"
- "/favicon.ico"
- "/health"
- "/actuator/health"
- "/static/*"rules/whitelist.yml:
businessPackages:
- "com.company."
- "cn.company."
trustedCodeSource:
- "/opt/app/"
- "/opt/tomcat/webapps/"
- "/usr/local/tomcat/lib/"
trustedAgents:
- "arthas"
- "skywalking"
- "opentelemetry"
- "pinpoint"Finding ID 采用对象特征 hash 生成,格式为 finding-<type>-<hash8位>。
hash 输入字段:type + className + urlPattern/mappingPath,对这三个字段拼接后取 SHA-256 前 8 位(十六进制)。
好处:
- 同一个内存马跨多次扫描 ID 保持不变,clean 命令中的 ID 可直接复用
- 清理后复扫时,能通过 ID 缺失直接确认对象已消失
- 基线对比时,新增 Finding 意味着新 ID 出现
示例:finding-filter-a3f92c1d、finding-spring-mapping-b71e04ca
public class Finding {
private String id; // finding-<type>-<sha256前8位>
private String type;
private String level;
private int score;
private String name;
private String className;
private String codeSource;
private String classLoader;
private List<String> reasons;
private Map<String, Object> evidence;
private String recommendation;
}public class RuntimeComponent {
private String type;
private String name;
private String className;
private Object runtimeObject;
private String codeSource;
private String classLoader;
private Map<String, Object> attributes;
}public class CleanPlan {
private String findingId;
private String componentType;
private String targetName;
private List<String> steps;
private boolean rollbackSupported;
private Map<String, Object> backup;
}目标:
- Attach 到目标 JVM
- Agent 加载成功
- 枚举已加载类
- 识别 Web 组件接口
- 输出 className / CodeSource / ClassLoader
- JSON 报告
目标:
- 定位 StandardContext
- 枚举 Filter
- 枚举 Servlet
- 枚举 Listener
- 枚举 Valve
- 输出风险评分
目标:
- 12 条评分规则统一接管 score/level/reasons
- 内置 framework / APM / CodeSource 白名单
- 支持
--whitelist <file>用户追加白名单 - 支持
--explain输出 ruleHits - 将 FakeFilter / FakeServlet / FakeInterceptor 升至 high/critical
- 将 WsFilter / StandardContextValve 等框架组件降回 low
目标:
- 字节码扫描规则接入现有 ScoringRule
- ASM 9.7 读取目标类字节码
- 5 条字节码评分规则接入现有 ScoringRule
- FakeFilter / FakeServlet / FakeInterceptor 注入项全部提升到 critical
目标:
- 复用 ScanReport JSON 作为基线文件
- 支持
scan --baseline <file> - 新增
baseline-new评分规则(+4) - Summary 输出
baselineNewCount/baselineMatchedCount - Listener / Interceptor Finding ID 跨 JVM 重启稳定
- 归档 v0.5 干净基线和注入后 E2E 样例报告
目标:
- 清理 Tomcat Filter(已完成)
- dry-run 生成清理计划与证据包(已完成)
- confirm 严格二次确认后执行清理(已完成)
- 清理前快照与清理结果落盘(已完成)
- 清理后复扫与 verify-result 输出(已完成)
- Tomcat Servlet / Listener / Valve 清理(后续版本)
- Spring Mapping / Interceptor 清理(后续版本)
v0.6 E2E 证据已归档到 docs/superpowers/specs/v0.6-clean-flow-evidence/。在 FakeFilter 注入场景中,清理前 finding 为 critical,执行 clean --confirm 后 clean-result.success=true、verifiedDisappeared=true,随后 verify --id 输出 stillPresent=false。
目标:
- confirm 阶段强制比对持久化 plan 与新生成 plan(findingId/filterClass/score/forced)
- --force 三方一致性校验(persisted ≡ fresh ≡ confirmFlag)
- Phase D 步骤标签区分 destroy-ran / no-release-method / destroy-threw
- 新增 EXIT_PLAN_STALE=3 退出码
- 新增 PlanReconciler + PlanStaleException
目标:
- 新增 TomcatServletCleaner / TomcatListenerCleaner / TomcatValveCleaner
- CleanPlan schema 抽象:targetName/targetClass + details
- CleanerRegistry:type 到 Cleaner factory 查表(exact + prefix 匹配)
- AbstractTomcatCleaner:共享 Phase A/D/E 模板
- RollbackStrategy 接口:替换单一 RollbackManager;每个 Cleaner 自带 strategy
- PlanReconciler:filterClass → targetClass 字段重命名
- MemHunterAgent:polymorphic dispatch via CleanerRegistry
- 197 单元/集成测试覆盖 4 类 Cleaner 全 Phase + dispatch + 旧 v0.6 plan 拒绝
目标:
- 新增 SpringMappingCleaner(官方 unregisterMapping)/ SpringInterceptorCleaner (adaptedInterceptors 副本替换)
- 提取容器无关的 AbstractCleaner;AbstractTomcatCleaner / AbstractSpringCleaner 继承它
- CleanerRegistry:ContextKind 路由(TOMCAT / SPRING)
- MemHunterAgent:dual-context dispatch(同时定位 Tomcat StandardContext + Spring ApplicationContext)
- CleanExecutionException.didMutate:精确 rolledBack 标志(修 v0.7 Valve nit)
- 单元/集成测试覆盖两类 Spring Cleaner 全 Phase + dispatch
目标:
- 真实 test-target 跑通 6 类内存马清理 E2E(filter / servlet / listener / valve
/ spring-mapping / spring-interceptor),证据归档到
docs/superpowers/specs/v0.8-clean-flow-evidence/ - FakeValveInjector 在 JDK 17 下 setAccessible(true) 修复(catalina 包模块封装 导致 public 方法仍需 setAccessible 才能反射调用)
- 发现并记录两个 v0.8.2 patch 候选:
--force不能与--dry-run组合 → sub-threshold finding 无法生成 evidence bundleclean --confirm在 plan 文件不存在时仍执行清理 → PlanReconciler 审计 链有后门
目标:
- 修复 VerifyExecutor 只扫 Filter 假阴性 bug(抽 FindingLocator;VerifyExecutor 接受双 context,覆盖全部 6 类 finding)
- 删除 AgentArgs.validate 中阻断 dry-run --force 的 force-gate(一致性已由 v0.6.1 PlanReconciler 三方一致性保证)
- AttachMain 在 confirm 时若 plan 文件不存在 → 友好 stderr + 退出码 2,不触达 agent
- 新增 FindingLocator 单测(4 个),VerifyExecutor 单测扩展到 Spring 类型, AttachMain 单测覆盖 plan-missing 路径
目标:
- 同一份 agent.jar 同时支持 Spring Boot 2.7 + Tomcat 9 + javax 与 Spring Boot 3.2 +
Tomcat 10 + jakarta;新增
ServletApiBridge抽象 javax/jakarta servlet API - 4 处 scanner / 规则字符串字面量列表镜像 jakarta 命名(DefaultWhitelist、 WebComponentDetector、TomcatListenerScanner、RuntimeOnlyRule —— 此前迭代已 接近完成,本期确认并扩展单测覆盖)
- 新增
test-target-sb32+test-target-injector-sb32模块(Spring Boot 3.2 + jakarta + JDK 17 编译目标) - 新增
integration-tests模块,sb27 / sb32 双 Maven profile,通过 ProcessBuilder fork attach.jar 子进程驱动真实 attach 流程,覆盖 6 finding type × scan / clean / verify 完整 E2E - 新增
.github/workflows/test.yml(unit + sb27/sb32 矩阵)与release.yml(tag v* 触发,上传 memhunter-agent.jar + memhunter-attach.jar) - 新增 Apache 2.0 LICENSE、CONTRIBUTING.md、Issue/PR 模板
- README 拆为双语:英文 README.md 作为入口,中文 README.zh-CN.md 作为详细文档
- 补 v0.1 → v0.8.2 共 12 个历史 release tag,v0.9 为首个走 release.yml 自动 打包的官方 release
- 参考:
docs/superpowers/specs/2026-06-01-v0.9-multiversion-opensource-design.md、docs/superpowers/plans/2026-06-01-v0.9-multiversion-opensource.md
目标:
- 新增
AgentTypeScanner(com.memhunter.agent.scanner.agent),聚合三个子检测器:TransformerScanner:反射InstrumentationImpl.mTransformerManager.mTransformers扫已注册的ClassFileTransformer,非白名单的 → findingagent-transformer(high, score=10)。白名单含 com.memhunter. 自身 + 9 个主流 APM 前缀 (Skywalking/Arthas/Pinpoint/Byte-buddy/Jacoco/Elastic APM/NewRelic/ Dynatrace/OpenTelemetry)DynamicClassScanner:扫所有已加载类,找 codeSource==null 且 (classLoader 非标准 OR 短类名)的类 → findingagent-dynamic-class(suspicious, score=4)BytecodeTamperScanner:对 5 个关键类(ApplicationFilterChain、 StandardContextValve、CoyoteAdapter、HttpServlet、DispatcherServlet)做 内存字节码(retransformClasses 捕获)vs JAR 字节码(getResourceAsStream) 对比,不一致 → findingagent-bytecode-tampered(critical, score=15)。 捕获用临时 transformer + finally removeTransformer,不污染目标 JVM
RuleEngine.evaluateOne对agent-*型 finding 早返回,保留 scanner 设置的 权威固定分数(不被规则集重算降级)- 误报降低:
TomcatServletScanner调用StandardContext.wasCreatedDynamicServlet(servlet)、TomcatFilterScanner读FilterDef.dynamic字段,给 finding 打isDynamic属性。RuntimeOnlyRule对isDynamic=false(web.xml 静态声明)早返回, 消除 Tomcat examples(HelloWorldExample/async0~3 等)的大量误报 ReflectUtil.tryInvokeWithArg新增单参数方法调用重载(continue on setAccessible failure,遍历类层级)- 实测背景:冰蝎 AgentNoFile 在 JDK 8 + Windows 注入失败(pointerLength 字段 缺失),但本期检测能力覆盖注入成功的场景
- 参考:
docs/superpowers/specs/2026-06-02-v0.10-agent-type-scanner-design.md、docs/superpowers/plans/2026-06-02-v0.10-agent-type-scanner.md
在真实独立 Tomcat 9.0.94 + JDK 8 上用真实手法(JSP 从请求线程反射注册 Filter 内存马到 ROOT webapp、后门可执行命令)实测,暴露并修复 4 个真实问题:
- 多 webapp 检测盲区(漏报核心威胁):
ClassLoadedContextProvider原先在 WebappClassLoader 策略找到任意一个 context 后就提前 return,从不执行 Engine→Host→Context 完整遍历。实测只定位到 /examples 一个,注入到 ROOT 的 ShellFilter 完全漏报。修复:两个策略都跑、seen 去重;并在looksLikeTomcatThread加入Catalina(独立 Tomcat 刚启动时 StandardEngine 仅通过 Catalina-utility-N 线程可达)。修复后定位到全部 5 个 webapp context,ShellFilter 被检出为 tomcat-filter / critical。 - BytecodeTamperScanner 100% 误报:逐字节
Arrays.equals比对内存 vs JAR 字节码,常量池/debug/属性顺序的良性差异导致 4 个关键类全报 critical(memSize == diskSize 却 byte 不等)。修复:改用 ASM 提取每方法的操作码指纹 (SKIP_DEBUG|SKIP_FRAMES),只在方法增/删/方法体改时报告。误报清零。 - attach.jar 无法在 JDK 8 运行:attach 模块编译为 Java 11 字节码(class v55), 在 JDK 8 上跑报 UnsupportedClassVersionError——而 JDK 8 是主流生产 Tomcat 运行时。 修复:attach 改用 source/target 8(非 release,否则会隐藏 com.sun.tools.attach) 产出 Java 8 字节码,一份 attach.jar 既能在 JDK 8(带 tools.jar)也能在 JDK 9+ 运行。
- 附带发现并记录:用不匹配的 JDK 版本 attach 一次会污染目标 JVM 的 attach 通道, 后续正确版本也连不上,只能重启目标 JVM 恢复(应急取证注意事项)。
BytecodeTamperScanner 检出 agent-bytecode-tampered 后,提取被注入的内容辅助应急响应:
- 新增
ConstantPoolExtractor:解析 class 字节码常量池的 CONSTANT_Utf8 字符串 (纯函数,正确处理 Long/Double 占 2 槽,tag 1-20 全覆盖) - 内存字节码常量池 减 磁盘 JAR 常量池 = 攻击者注入的新字符串(含内存马访问路径、
解密类名、payload 特征)。两个 finding 属性:
injectedStrings:启发式过滤后的可疑子集(含//*路径、长 token、高熵 Base64;L...;描述符剥出内部类名再判断,使Ljavax/crypto/Cipher;这类注入类名被保留)injectedStringsRaw:全量新增字串,不漏
- 字节码存证:被改类的内存字节码 + 磁盘原始字节码 dump 到
evidence/<finding-id>/tampered.class+original.class,供事后反编译对比;evidenceDumped属性标记是否成功;dump 失败不影响 finding 产出 evidence-dir从 MemHunterAgent → AgentTypeScanner → BytecodeTamperScanner 透传- 实战价值:应急人员可直接从 scan 报告读出内存马访问路径,立即封堵 WAF + 查访问日志溯源
- 参考:
docs/superpowers/specs/2026-06-02-v0.11-injected-content-extraction-design.md
真实靶场扫描暴露信噪比 ~1:125(252 finding / 2 真阳,132 条为 JVM 反射生成类噪音)。 检测哲学从「动态注册=可疑」转向「有恶意特征才可疑」——普通业务组件默认不报, 只报「有恶意特征」的。不依赖基线比较:真实应急环境早已被注入,没有干净基线可对比, v0.12 只靠内置知识判定。
- 新增
JvmGeneratedClasses白名单:识别 JVM 自身机制生成的类 (sun.reflect.GeneratedMethodAccessor*/GeneratedConstructorAccessor*、jdk.internal.reflect.Generated*、$$Lambda$、com.sun.proxy.$Proxy*、$Proxy\d+), DynamicClassScanner 直接放行($Proxy\d+正则预编译为静态常量,全量扫描高频调用) - 新增
BytecodeMaliceCheck公共 helper:统一高确信度字节码恶意特征判定 (Runtime.exec/ProcessBuilder.start/defineClass/Cipher.doFinal; 故意不查 反射invoke/getDeclaredMethod——业务代码太常见,误报高), DynamicClassScanner 与 BenignComponentRule 复用 DynamicClassScanner要求「无 codeSource + 非标准 loader/短名 + 恶意字节码特征」 三者俱全才报,砍掉 132 条 JVM 反射生成类噪音;字节码读不到时保守降误报 (不升级为 finding),但向ScanReport.partialErrors写一条「字节码不可读、建议人工复核」 的可观测警告,不静默丢弃信号- 新增
BenignComponentRule(负分 -10):把 codeSource 指向 webapp 正常目录 (WEB-INF/classes/jar/webapps)、类名非高熵(香农熵 ≤3.5)、字节码已读取且无恶意特征 的业务组件压到 low;agent-* 型 finding 永不抑制(RuleEngine 层 + 规则层双重防线) RuntimeOnlyRule降权 +4 → +1(动态注册降为弱信号,而非近临界信号)- 抑制需正向证据(code review 加固):
- 临时目录投放的 jar(
/tmp/、/var/tmp/)即使路径含.jar也不视为正常 webapp 来源 - 「字节码读不到」≠「字节码干净」:
BenignComponentRule仅在BytecodeMaliceCheck.hasReadableBytecode为真(确实读到字节码)且无恶意特征时才抑制; 读不到字节码的组件保留原分供人工复核
- 临时目录投放的 jar(
- 真阳全保留:JSP shell(work/Catalina codeSource)仍 high、冰蝎 redefine (agent-bytecode-tampered)仍 critical、injectedStrings 仍提取
- 已知局限:纯中继型 Filter 马(自身字节码无 4 个恶意锚点,通过反序列化链/JNDI/外置 payload 触发执行)若类名平实、codeSource 在 WEB-INF/classes、字节码可读且「干净」, 在无其它正分信号叠加时可能被压到 low。这是「降误报优先」的设计权衡;后续版本评估对 「动态注册 + 无任何可执行特征」保留更高基准分
- 参考:
docs/superpowers/specs/2026-06-03-v0.12-false-positive-reduction-design.md
v0.12 合并后实地复扫中了冰蝎 Agent 马的 Tomcat 9.0.94,暴露三个 v0.12 没覆盖到的 误报源,逐一根因修复:
- 未实例化组件的 codeSource 解析:Tomcat examples 是 lazy-load(load-on-startup 未设),
扫描时 servlet 未实例化 → 旧 scanner 报
codeSource=null。这一处缺口连锁触发CodeSourceNullRule +3加分 + 阻断 BenignComponentRule 抑制。新增WebappCodeSourceResolver:用类名 + webapp 类加载器(context.getLoader().getClassLoader()) 按名解析 codeSource(只读、不触发 servlet 生命周期)。Servlet + Filter scanner 同病同修。 - 移除不可靠的香农熵类名门:实测 BenignComponentRule 的高熵门既误伤正常驼峰类
(
RequestInfoExample熵 3.9)又漏掉短随机马名(Xdozy熵 2.3)——香农熵对短标识符 两头分不开。删除该门,判恶完全交给 BytecodeMaliceCheck(随机名马几乎总带恶意字节码)。 - ScanContext 注入 webapp 类加载器:根因核心。
ScanContext.resolveClassLoader在 Tomcat 模式下applicationContext=null(那是 Spring 专用),回落到 agent/system loader,而它 看不到 webapp 类 →bytecodeOf返回 null →hasReadableBytecode=false→ 所有 webapp 组件都因"字节码读不到"无法被抑制。ScanContext 现接收从已定位 Tomcat context 收集的 webapp 类加载器,默认 loader 读不到时回落到它们;MemHunterAgent 与 FindingLocator 均收集注入。 - 实地复测信噪比(冰蝎靶机,Tomcat 9.0.94 + examples webapp):
- critical:20(基线)→ 8(v0.12)→ 1(v0.12.1,纯真阳 = 冰蝎 agent-bytecode-tampered)
- high:21 → 20 → 1(纯真阳 = JSP shell
shell_jsp,work/Catalina codeSource 正确未抑制) - suspicious:170 → 19 → 0
- 132 条 JVM 反射生成类噪音 + Tomcat examples/JSP 引擎噪音全部压到 low(118 条)
- 真阳零丢失:冰蝎 critical、JSP shell high 均保留
- 实战意义:critical+high 两层从 1:125 信噪比降到 零误报,应急人员打开报告即见真马
实地复扫一台注入哥斯拉(Godzilla)内存马的 Tomcat 9.0.94,发现工具检出了全部哥斯拉马
(3 Filter + 2 Servlet),但伪装包名的几个只到 high/suspicious——哥斯拉把 Jackson 类
伪装成 org.apache.coyote.* 包名,滥用受信包白名单(whitelist-hit -5 压低告警)。
核心洞察:真框架类一定从 jar 加载、有 codeSource;伪装类是动态 defineClass 生成的、codeSource=null。 「白名单前缀命中」与「codeSource=null」同时出现即自相矛盾——伪装铁证。
WhitelistHitRule加 codeSource 可信度门:受信前缀 + codeSource 非空才减 -5; codeSource 为空(伪装)则 0- 新增
MasqueradedPackageRule(+5):受信前缀 + codeSource 空 → 伪装铁证加分 - 两规则均用
ctx.whitelist.isFrameworkPackage,逻辑互斥(可信减分 / 伪装加分) - 零误报:真框架类必有 jar 来源,受信前缀类 + codeSource=null 在正常环境不存在; agent-* 型 finding 仍由 RuleEngine 早退保护,不受影响
- 已知局限:白名单含 com.fasterxml.jackson.* / org.springframework.* 等,这些框架运行时 动态生成类(Jackson 多态反序列化辅助类、Spring CGLIB 代理)codeSource 可能为 null, 理论上触发 +5;缓解——本规则只作用于已被 scanner 识别为注册组件(Filter/Servlet/Listener) 的 finding,普通动态代理不会注册成 Web 组件,实际误报概率极低
- 实地验收(哥斯拉 ground truth):3 个伪装包名 Filter 马(TypeIdResolverBase / JsonAppend / PropertyWriter)全部升 critical,数量与哥斯拉客户端 getAllFilter 一致; 真阳不回退(冰蝎 critical、JSP shell high)、v0.12.1 examples 抑制不回退
- 参考:
docs/superpowers/specs/2026-06-04-v0.13-masquerade-detection-design.md
此前 scan 跑完,用户的 attach 终端只打印 agent loaded successfully,结果全在 JSON 里得手动翻;
不带 --output 时报告落到目标 JVM 的 tmpdir,路径难找。v0.14 改善可用性:
- 新增
ScanSummaryPrinter(attach 端):读 scan 报告 JSON,在用户终端打印 summary 计数行 + 逐条 critical/high/suspicious([level] type className score=N),low 只计数不逐条; 末行打印报告完整路径(方便取证、拿 finding id 去 clean) AttachMainscan 命令:用户没传--output时默认写到 attach 进程当前目录的memhunter-scan-<时间戳>.json(绝对路径);传了则转绝对路径透传——保证 agent(目标 JVM) 与 attach 端读写同一文件(跨进程必须用绝对路径)- 读/解析报告失败降级打印一行、不让命令崩
- 已知限制:
--output路径不能含空格——agent 端AgentArgs.parse按空白切分参数, 含空格路径整条 attach→agent 管道本就不支持;v0.14 遇到含空格路径明确报错拒绝(而非静默 截断产生坏路径)。彻底支持含空格路径需改造两端参数传递(按 token 而非扁平字符串),留作后续 - 纯 attach 端改动,复用现有 Jackson;不改 agent 扫描逻辑、不改 JSON 报告内容(仍全量含 low)
- 参考:
docs/superpowers/specs/2026-06-04-v0.14-scan-summary-design.md
v0.14 的终端摘要每条只有 [level] type className score,缺内存马访问路径——而路径正是应急
封堵/溯源最需要的。v0.15 给每条 critical/high/suspicious 末尾追加路径或定位信息:
- 有 URL 访问路径的类型显示
path=[...]:tomcat-filter(urlPatterns)、tomcat-servlet(mappings)、 spring-mapping(pattern)、spring-interceptor(includePatterns + exclude)、 agent-bytecode-tampered(injectedStrings,冰蝎注入字串含访问路径/解密类名) - 无 URL、靠事件/管道/类加载触发的类型显示定位字段:listener 显
trigger=request/session/context、 valve 显pipeline=N、transformer 显class=——明确告知「无可封路径、靠 XX 触发」,避免误判漏提取 (listener 绑事件不绑 URL,任意请求都触发,本就无可封单一路径) - injectedStrings 可能很多,截断前 5 条 +
...(+N more),完整内容仍在 JSON - 纯 attach 端扩展 ScanSummaryPrinter;字段缺失/类型不符不追加、不抛;不改 agent/JSON/scanner
- 参考:
docs/superpowers/specs/2026-06-04-v0.15-summary-paths-design.md
v0.15 给摘要加了路径,但 JSP webshell(org.apache.jsp.*_jsp,经 ClassScanner 发现的
class-servlet)没显示——它经「扫所有已加载类」发现,attributes 无 mappings/urlPatterns。
JSP webshell 是文件型 webshell(磁盘有 .jsp、编译成 org.apache.jsp.*_jsp 类),访问路径
编码在类名里,可纯字符串反推。
- 新增
JspPathResolver.toJspUrl(纯函数):org.apache.jsp.wwwwxxx_jsp→/wwwwxxx.jsp、org.apache.jsp.admin.x_jsp→/admin/x.jsp;非 JSP 返回 null - ClassScanner 发现 JSP 类时把反推 URL 存进 finding.attributes.jspPath
- ScanSummaryPrinter 对 class-* 型读 jspPath 显示
path=[/wwwwxxx.jsp] - 不拼磁盘绝对路径(.jsp URL 已足够定位+删文件)、不处理 Tomcat 全套转义规则(YAGNI)
- JSP webshell vs 内存马:JSP 是文件型(磁盘有文件、删文件即清、重启仍在);内存马无磁盘文件、 codeSource=null、重启即清——摘要的 path 对前者指向待删文件、对后者指向待封 URL
- 参考:
docs/superpowers/specs/2026-06-04-v0.16-jsp-path-resolution-design.md
同一个恶意类被多个 scanner 从不同视角各报一次——哥斯拉的 org.apache.coyote.ser.PropertyWriter
既被 TomcatFilterScanner 报为 tomcat-filter(score 16、path=[/*]),又被 ClassScanner 报为
class-filter(score 13、无路径),critical 列表里同一类出现两次,使用人员迷惑。
- 新增
FindingDeduplicator.dedupe(纯函数):按 className 分组,每组留信息最全的一条—— score 最高;同分取 attributes 有路径字段(urlPatterns/mappings/jspPath/pattern/ includePatterns/injectedStrings)的;仍并列取输入靠前的(LinkedHashMap 稳定顺序) - className=null 的不合并(如哥斯拉 Servlet 马,无法判同一性,原样保留)
- MemHunterAgent 在 RuleEngine 评分后、写报告前调用 → JSON、终端摘要、summary 计数三者一致
- 效果:容器视角(tomcat-/spring-,分高有路径)自然胜出,class-* 冗余条消失;class-* 唯一 发现(JSP shell、哥斯拉工具类)保留
- 不改 scanner/评分/clean;clean 按 id 复扫定位,去重保留主条 id 仍有效
- 参考:
docs/superpowers/specs/2026-06-04-v0.17-finding-dedup-design.md
v0.17 去重暴露 critical 层混着「非内存马」:哥斯拉为让 Filter 马运行而一起注入的 Jackson 库类 (伪装成 org.apache.coyote.、class- 型、未注册成组件、无访问路径)被 masqueraded-package +5 顶到 critical,与真马同级,使 critical 数虚高(实地 critical 8 中 4 个是依赖类)。
- 改
MasqueradedPackageRule:伪装包名 +5 仅对注册组件(tomcat-/spring-)生效; class-* 型(ClassScanner 全类视角、仅被加载的类)返回 0 - 依赖类少 +5(14→9)落到 high——仍报、不漏,但不与激活的真马同级 critical
- 真注册马(tomcat-filter,有 isDynamic + urlPatterns)仍 +5、仍 critical(v0.13 行为不回退)
- 语义:只有被容器注册、能拦请求的伪装类才是激活的内存马;仅被加载的伪装类是马的依赖库
- 哥斯拉原理:Filter 马靠 Jackson 反序列化收发,整个 Jackson 改包名打包进 payload 一起注入; 注册的是 Filter(客户端 getAllFilter 可见),Jackson 工具类只是依赖库
- 实地(哥斯拉 ground truth):critical 8→4(只剩 4 个真注册 Filter 马,与客户端 getAllFilter 一致),4 个依赖类降 high;JSP shell、哥斯拉 Servlet 马不受影响
- 参考:
docs/superpowers/specs/2026-06-04-v0.18-dependency-class-downgrade-design.md
v0.10 ~ v0.18 累积达成,工具已是一个可实战的内存马应急工具——实测在中了冰蝎 Agent 马 + 哥斯拉 Filter 马 + JSP webshell 的 Tomcat 9.0.94 上,critical/high 两层信噪比降到零误报、零漏报, 且每条 finding 带可操作的访问路径。
检测能力(实测对照哥斯拉客户端 getAllFilter/getAllServlet ground truth):
- Agent 型检测(冰蝎):BytecodeTamperScanner ASM 方法体指纹比对内存 vs 磁盘字节码,检出 redefineClasses 篡改 HttpServlet.service;v0.11 从被改常量池提取注入访问路径/解密类名
- 伪装包名检测(哥斯拉):MasqueradedPackageRule——受信框架包名 + codeSource 为空 = 动态伪装; WhitelistHitRule 仅在有真 jar 来源时才给白名单减分
- 依赖类降级(v0.18):伪装加分只对注册组件生效;class-* 仅加载的伪装类(哥斯拉随 Filter 马 注入的 Jackson 库)降到 high,critical 只留激活的真马
- JSP webshell 路径反推(v0.16):org.apache.jsp.* 类名 → .jsp 访问 URL
- Tomcat Filter/Servlet/Listener/Valve + Spring Interceptor/Mapping(v0.7/v0.8)
取证体验:
- 终端摘要(v0.14):attach 端读报告打印 critical/high/suspicious 逐条 + low 计数,不刷屏; 默认输出路径
- 全类型访问路径标注(v0.15/v0.16):path=(filter urlPatterns / servlet mappings / spring pattern / agent injectedStrings / JSP URL)、trigger=/pipeline=(无 URL 的事件/管道型)
- 同类去重(v0.17):多 scanner 对同一 className 的重复 finding 合并
误报治理(v0.12/v0.12.1,不依赖基线对比):
- BenignComponentRule 压低正常业务组件;JvmGeneratedClasses 白名单;DynamicClassScanner 字节码恶意门控;webapp loader 注入 ScanContext
清理: scan → dry-run → confirm → verify 5 阶段原子清理 + JSON 证据 + 回滚(v0.6~v0.8)
多环境: Tomcat 9/10、Spring Boot 2.7/3.2、JDK 8/17、Linux/Windows(v0.9)
Deferred(后续):
- HTML / Markdown 报告
- 容器 / Kubernetes 适配
- premain 模式对抗 antiAgent 封 attach 通道
- attach→agent 参数管道改造以支持含空格路径
测试内容:
- 风险评分规则
- 白名单规则
- JSON 报告生成
- ReflectUtil 字段读取
- HashUtil
- CleanPlan 生成
测试环境:
- Tomcat 8
- Tomcat 9
- Tomcat 10
- Spring Boot 2
- Spring Boot 3
- JDK 8
- JDK 11
- JDK 17
- JDK 21
测试场景:
- 正常业务应用,无内存马。
- 存在动态 Filter。
- 存在动态 Servlet。
- 存在动态 Listener。
- 存在动态 Valve。
- 存在动态 Spring Mapping。
- 存在动态 Interceptor。
- 存在正常 APM Agent。
- 存在 Arthas。
- 存在 CGLIB / ByteBuddy 正常代理类。
测试内容:
- 清理前快照是否生成。
- dry-run 是否不修改对象。
- 清理指定 Filter 后是否不可访问。
- 清理指定 Mapping 后是否不可访问。
- 清理失败是否停止。
- 回滚是否恢复对象。
- 清理后复扫是否确认消失。
mvn clean package输出:
target/memshell-attach.jar
target/memshell-agent.jar
jps -lv或:
java -jar memshell-attach.jar listjava -jar memshell-attach.jar 12345 ./memshell-agent.jar scan --format json --output /tmp/report.jsoncat /tmp/report.jsonjava -jar memshell-attach.jar 12345 ./memshell-agent.jar clean --id finding-filter-a3f92c1d --dry-runjava -jar memshell-attach.jar 12345 ./memshell-agent.jar clean --id finding-filter-a3f92c1d --confirmjava -jar memshell-attach.jar 12345 ./memshell-agent.jar scan --format json --output /tmp/report-after.json可能误报对象:
- Spring CGLIB 代理
- Hibernate Proxy
- MyBatis Mapper Proxy
- ByteBuddy 动态类
- APM Agent
- Arthas
- JRebel
- SkyWalking
- OpenTelemetry
解决方式:
- 业务包名白名单
- CodeSource 白名单
- Agent 白名单
- 基线对比
- 多特征评分,不单点判断
可能漏报场景:
- 恶意逻辑嵌入正常业务类
- 字节码被动态修改但类名不变
- 恶意逻辑只在特定条件触发
- Agent 型内存马隐藏 transformer
- 容器对象结构不兼容导致未扫描到
缓解方式:
- 增加 transformer 扫描
- 增加类 hash 基线
- 增加线程栈扫描
- 增加 JMX/MBean 扫描
- 增加请求链对比
- 增加业务源码路由对比
可能影响:
- 删除正常 Filter 导致鉴权失效
- 删除正常 Interceptor 导致业务异常
- 修改容器内部结构导致请求异常
- 清理时并发请求触发异常
- 无法完全回滚
处理策略:
- 默认只读
- dry-run
- 指定 ID 清理
- 清理前快照
- 非兼容对象不清理
- 生产环境优先建议重启服务
- 支持 Jetty Handler / Filter / Servlet 扫描。
- 支持 Undertow Handler 链扫描。
- 支持 WebLogic Filter / Servlet / WorkContext 相关排查。
- 支持 WebSphere 特定容器对象扫描。
- 支持线程栈可疑行为分析。
- 支持 ClassFileTransformer 枚举。
- 支持已加载类 dump。
- 支持与 EDR / SIEM 对接。
- 支持远程 Agent 管理。
- 支持 Web 控制台。
- 支持扫描报告差异对比。
- 支持容器 Kubernetes 场景自动进入 namespace。
- 支持 Docker / containerd 目标进程发现。
- 支持源码路由表与运行时路由表对比。
- 支持异常请求日志与运行时组件关联。
推荐不要一开始就做清理,先实现扫描闭环:
第一阶段:
PID 发现 → Attach → Agent 加载 → JVM 类枚举 → JSON 报告
第二阶段:
Tomcat Context 定位 → Filter/Servlet/Listener/Valve 枚举 → 评分
第三阶段:
Spring Context 定位 → Mapping/Interceptor/Bean 枚举 → 评分
第四阶段:
基线生成 → 基线对比 → 白名单 → HTML 报告
第五阶段:
dry-run 清理 → 按 ID 清理 → 快照 → 复扫 → 回滚
扫描命令:
java -jar memshell-attach.jar 12345 ./memshell-agent.jar scan --format console输出:
[+] Target PID: 12345
[+] Java Version: 17.0.10
[+] Container: Tomcat 9.x
[+] Spring MVC: detected
[+] Scan finished
[CRITICAL] finding-filter-a3f92c1d
Type : tomcat-filter
Name : DebugFilter
Class : com.example.Abc123
Pattern : /*
CodeSource : null
Score : 12
Reasons:
- runtime only filter
- url-pattern is /*
- codeSource is null
- bytecode contains ProcessBuilder
Recommended:
1. Export evidence.
2. Run clean dry-run.
3. Confirm with business owner.
4. Clean by finding ID.
5. Restart service if necessary.
清理命令:
java -jar memshell-attach.jar 12345 ./memshell-agent.jar clean --id finding-filter-a3f92c1d --dry-run确认清理:
java -jar memshell-attach.jar 12345 ./memshell-agent.jar clean --id finding-filter-a3f92c1d --confirm复扫:
java -jar memshell-attach.jar 12345 ./memshell-agent.jar scan --format console该工具本质上是一个防守型 Java 运行时安全检测工具。核心不是查文件,而是进入 JVM 内部检查运行时注册表、已加载类、容器组件和 Spring 映射关系。
设计关键点有三个:
- 扫描必须全面:JVM 类、Tomcat 组件、Spring Mapping、ClassLoader、字节码特征、基线对比都要覆盖。
- 判断必须谨慎:不能单靠关键字,需要多维度评分和白名单。
- 清理必须保守:默认只读,清理必须按 ID 执行,清理前保存证据,清理后复扫,无法安全处理时建议重启服务。
最终推荐落地形态:
memshell-attach.jar + memshell-agent.jar
其中:
memshell-attach.jar负责连接目标 JVM。memshell-agent.jar负责在目标 JVM 内部扫描、报告和清理。- 默认只读扫描。
- 清理必须人工确认并指定 Finding ID。
Goals:
- Archive real
test-targetE2E artifacts indocs/superpowers/specs/v0.7.1-clean-flow-evidence/ - Verify clean disappearance for filter, servlet, listener, and valve findings
- Support Tomcat 9 listener storage names in scanner and cleaner paths
- Keep the v0.7 cleaner CLI contract unchanged