Day 154 图解 — 目录扫描与指纹识别原理
本文件只使用 Mermaid 代码块 与 ASCII 字符画,不生成任何图片文件。
图 1:HTTP 请求在多层中间件中的流转(ASCII)
┌─────────────────────────────────────────┐
GET /admin/ │ 1. CDN / WAF(Cloudflare、阿里云盾) │
────────────────►│ · IP 信誉、请求速率、正则规则 │
│ · 命中 → 403 / 自定义拦截页 / JS 挑战 │
└───────────────┬─────────────────────────┘
│ 放行
┌───────────────▼─────────────────────────┐
│ 2. 反向代理 / Web 服务器 (nginx) │
│ · location 匹配 │
│ · try_files $uri $uri/ /index.html │ ◄── 软404 制造者
│ · 目录无斜杠 → 301 │
└───────────────┬─────────────────────────┘
│ proxy_pass
┌───────────────▼─────────────────────────┐
│ 3. 应用服务器 (gunicorn / uvicorn / php) │
│ · 进程池满 → 502 │
│ · 超时 → 504 │
└───────────────┬─────────────────────────┘
│ WSGI/ASGI
┌───────────────▼─────────────────────────┐
│ 4. 框架路由 (Django / Flask / FastAPI) │
│ · 鉴权中间件: 未登录 → 302 /login │
│ · catch-all 路由 → 200 自定义页 │
│ · 无匹配 → 404 │
└──────────────────────────────────────────┘
结论:同一次请求可能被 4 层任意一层"改写"响应,
这就是为什么"状态码"不能单独作为存在性判据。
图 2:软 404 检测算法(Mermaid 流程图)
flowchart LR
subgraph 基线阶段
A1["随机路径 ×3<br/>/a9f3-notexist"] --> A2["记录 (status, sha256, len, title)"]
A2 --> A3{三份基线<br/>完全一致?}
A3 -->|是| A4["⚠️ 检测到通配响应<br/>启用哈希差异模式"]
A3 -->|否| A5["✅ 正常站点<br/>启用基线比对模式"]
end
subgraph 扫描阶段
B1["字典路径 GET"] --> B2{"与基线哈希相同的条目<br/>是否存在?"}
B2 -->|"是(完全匹配)"| B3["丢弃 ❌ 软404"]
B2 -->|"否"| B4{"长度差 < 容差 5%?"}
B4 -->|是| B3
B4 -->|否| B5{"状态码 ∈ {401,403,500}"}
B5 -->|是| B6["🔑 存在但受限 → 记录"]
B5 -->|否| B7["✅ 命中 → 记录"]
end
A4 --> B1
A5 --> B1
flowchart LR
subgraph 基线阶段
A1["随机路径 ×3<br/>/a9f3-notexist"] --> A2["记录 (status, sha256, len, title)"]
A2 --> A3{三份基线<br/>完全一致?}
A3 -->|是| A4["⚠️ 检测到通配响应<br/>启用哈希差异模式"]
A3 -->|否| A5["✅ 正常站点<br/>启用基线比对模式"]
end
subgraph 扫描阶段
B1["字典路径 GET"] --> B2{"与基线哈希相同的条目<br/>是否存在?"}
B2 -->|"是(完全匹配)"| B3["丢弃 ❌ 软404"]
B2 -->|"否"| B4{"长度差 < 容差 5%?"}
B4 -->|是| B3
B4 -->|否| B5{"状态码 ∈ {401,403,500}"}
B5 -->|是| B6["🔑 存在但受限 → 记录"]
B5 -->|否| B7["✅ 命中 → 记录"]
end
A4 --> B1
A5 --> B1
容差为什么取 5%? 经验值:动态页面(时间戳、访问计数)通常让长度抖动 在 1%~3%;超过 5% 的差异往往意味着"结构不同",即不同页面。这个值应该可配置, 因为不同站点差异极大。
图 3:目录扫描的三种"命中"类型(ASCII)
字典里的 10000 个路径
│
├──────────────► 200 + 内容独特 → ✅ 真实文件
├──────────────► 200 + 与基线一致 → ❌ 软404(丢弃)
├──────────────► 301 /admin → /admin/ → 🔁 目录(存在!)
├──────────────► 302 → /login → 🔑 受保护资源(存在!)
├──────────────► 401 / 403 → 🔑 存在但受限
├──────────────► 404 → ❌ 不存在
├──────────────► 429 / 503 → ⚠️ 限流:立刻降速
└──────────────► 500 → 🟡 参数/环境问题,人工复核
最容易漏掉的:
· 302 → 登录页 (90% 的扫描器用 allow_redirects=True,跟随跳转后拿到
登录页的 200,长度全都一样,反而被当成"软404"丢弃!)
· 403 (被默认配置忽略)
· 200 + 空 body (有些 API 只要路由存在就返回 200 + {})
重要细节:
allow_redirects=True是很多扫描器误报/漏报的根源。 必须allow_redirects=False,自己判断Location。
图 4:线程池扫描的时间线(ASCII)
单线程(1 worker):
t=0 ├─ req1 ├─ req2 ├─ req3 ├─ req4 ─┤ 总耗时 = 4RTT
RTT RTT RTT RTT
4 线程:
t=0 ├─ req1 ─┤
├─ req2 ─┤ 总耗时 ≈ 1RTT(理想)
├─ req3 ─┤
└─ req4 ─┘
│◄───── RTT ─────►│
50 线程 + keep-alive:
┌── worker-1 ── req1 ─ req53 ─ req105 ... ─┐
├── worker-2 ── req2 ─ req54 ─ req106 ... ─┤
│ ... │ 瓶颈从「RTT × N」
└── worker-50 ── req50 ─ req102 ─ ... ─┘ 转移到「服务端处理能力」
和「WAF 速率阈值」
⚠️ 并发不是越大越好:超过服务端 MaxClients 后,
你自己的请求开始排队 → 超时 → 误报为 "不存在"。
图 5:指纹识别证据加权(Mermaid)
flowchart TD
R["目标响应"] --> H["解析响应头"]
R --> C["解析 Cookie 名"]
R --> B["解析 HTML body"]
R --> P["主动探针路径"]
H --> H1["Server: nginx/1.24.0"]
H --> H2["X-Powered-By: PHP/8.1"]
C --> C1["PHPSESSID → PHP"]
C --> C2["csrftoken → Django"]
B --> B1["meta generator → WordPress"]
B --> B2["/_next/static → Next.js"]
P --> P1["/wp-login.php 200 → WordPress"]
P --> P2["/actuator/health 200 → Spring Boot ⚠️"]
H1 --> S["加权评分"]
H2 --> S
C1 --> S
C2 --> S
B1 --> S
B2 --> S
P1 --> S
P2 --> S
S --> O1["技术栈: WordPress 6.4 (PHP 8.1)"]
S --> O2["Web 服务器: nginx 1.24.0"]
S --> O2B["⚠️ 暴露 /actuator → 高危"]
S --> O3["安全头缺失: CSP / HSTS / X-Frame-Options"]
flowchart TD
R["目标响应"] --> H["解析响应头"]
R --> C["解析 Cookie 名"]
R --> B["解析 HTML body"]
R --> P["主动探针路径"]
H --> H1["Server: nginx/1.24.0"]
H --> H2["X-Powered-By: PHP/8.1"]
C --> C1["PHPSESSID → PHP"]
C --> C2["csrftoken → Django"]
B --> B1["meta generator → WordPress"]
B --> B2["/_next/static → Next.js"]
P --> P1["/wp-login.php 200 → WordPress"]
P --> P2["/actuator/health 200 → Spring Boot ⚠️"]
H1 --> S["加权评分"]
H2 --> S
C1 --> S
C2 --> S
B1 --> S
B2 --> S
P1 --> S
P2 --> S
S --> O1["技术栈: WordPress 6.4 (PHP 8.1)"]
S --> O2["Web 服务器: nginx 1.24.0"]
S --> O2B["⚠️ 暴露 /actuator → 高危"]
S --> O3["安全头缺失: CSP / HSTS / X-Frame-Options"]
加权建议:
| 证据类型 | 权重 | 理由 |
|---|---|---|
| 主动探针命中已知路径 | 3 | 路径存在是硬证据 |
Set-Cookie 名 |
2 | 框架默认值,改动成本高 |
| 响应头 | 2 | 可伪造/可关闭 |
| HTML 特征字符串 | 2 | 可能来自模板复制 |
| favicon 哈希 | 1 | 有误报(同一公司多个产品复用) |
| 默认错误页样式 | 1 | 最不可靠 |
图 6:授权边界检查(ASCII,代码护栏逻辑)
┌────────────────────────────┐
用户传入 --url ──►│ 解析 host │
└──────────────┬─────────────┘
│
┌───────────────▼──────────────┐
│ host ∈ {127.0.0.1, localhost, │
│ ::1, 0.0.0.0} ? │
└───────┬──────────────┬───────┘
│ 是 │ 否
│ ▼
│ ┌──────────────────────────┐
│ │ 是否带 --i-have-authorization? │
│ └────┬────────────────┬────┘
│ │ 是 │ 否
│ │ ▼
│ │ ┌────────────────┐
│ │ │ 打印合规提示 │
│ │ │ sys.exit(2) │
│ │ └────────────────┘
▼ ▼
┌──────────────────────────────┐
│ 打印目标 + 并发 + 预计请求数 │
│ 强制 sleep(0.1) 起步限速 │
│ 开始扫描 │
└──────────────────────────────┘
把边界写进代码而不是写进 README,是因为人总有"就试一次"的冲动。 让程序在跑之前先替你踩一次刹车。