← 返回 Day 154 正文

Day 154 · 练习与清单

Day 154 — 目录扫描与指纹识别 · 完成清单与练习

阶段:Phase 7 — 进阶与性能优化 目标:能独立写出一个误报率可控、限速合规、带指纹识别的 Web 资产发现工具。


✅ 今日完成清单

理解层面(能自己讲出来才算过)

操作层面(动手做过才算过)

产出物


📝 基础练习(必做)

点击展开答案要点 ### Q1 状态码语义练习 不写代码,回答:你的扫描器请求了以下 6 个路径,分别返回下列状态码。 哪些应该记录为"存在"?分别说明理由。 | 路径 | 状态码 | Location | 是否记录 | 理由 | |---|---|---|---|---| | `/admin` | 302 | `/login` | ? | | | `/backup.zip` | 200 | — | ? | | | `/zzz-random` | 200 | — | ? | | | `/.git/HEAD` | 403 | — | ? | | | `/phpinfo.php` | 404 | — | ? | | | `/api/v2` | 429 | — | ? | | **答案要点:** - `/admin → 302 /login`:**记录**。说明受保护资源存在,只是需要登录。 - `/backup.zip → 200`:**记录**(但需与基线比对,确认不是软 404)。 - `/zzz-random → 200`:**不记录**。这是随机路径,属于基线;如果它和字典路径 返回内容一致 → 说明站点有通配路由,必须切换判定策略。 - `/.git/HEAD → 403`:**记录,且是高危**。Git 仓库被部署到 Web 根目录, 即使当前被 nginx 规则挡了,也说明源码可能泄露。 - `/phpinfo.php → 404`:**不记录**(确认不存在)。 - `/api/v2 → 429`:**不记录为命中,但要立即降速**。429 是限流信号, 与资源是否存在无关;继续扫会被封 IP。 ### Q2 写一个最小可用扫描器(20 行内)
python
import requests, sys
from concurrent.futures import ThreadPoolExecutor

BASE = "http://127.0.0.1/"
assert BASE.startswith("http://127.0.0.1") or BASE.startswith("http://localhost")

def probe(w):
    try:
        r = requests.get(BASE + w, timeout=3, allow_redirects=False)
        return w, r.status_code, len(r.content)
    except requests.RequestException:
        return w, None, None

words = ["admin", "login", "robots.txt", ".git/HEAD", "backup.zip", "api/v1"]
with ThreadPoolExecutor(20) as ex:
    for w, st, ln in ex.map(probe, words):
        if st and st != 404:
            print(f"{st} {ln:>6} /{w}")
**要点:** `allow_redirects=False` 必须有;`timeout` 必须有; `except RequestException` 必须兜底;目标必须是本机或已授权。 ### Q3 基线比对函数 实现 `is_soft404(status, body_hash, body_len, baseline)`。
python
def is_soft404(status, body_hash, body_len, baseline, tol=0.05):
    # baseline: [(status, hash, norm_len), ...]
    for b_status, b_hash, b_len in baseline:
        if status != b_status:
            continue
        if body_hash == b_hash:
            return True
        if b_len and abs(body_len - b_len) / max(b_len, 1) <= tol:
            return True
    return False
**要点:** 先比哈希(最可靠),再比归一化长度(容差可配),状态码不同直接不算。 ### Q4 为示例 03 增加"后缀变体" 给字典里每个词自动追加 `.bak` / `.old` / `~` / `.swp` / `.zip`:
python
SUFFIXES = [".bak", ".old", "~", ".swp", ".zip", ".tar.gz", ".1"]

def expand(words):
    out = []
    for w in words:
        out.append(w)
        for s in SUFFIXES:
            out.append(w + s)
    return out
**注意:** 字典会膨胀 8 倍,必须同步提高延迟或降低并发, 否则你会从"扫描"变成"DDoS 练习"。 ### Q5 安全头体检为什么比目录列表更有价值? **答案要点:** 目录列表告诉你"哪里有东西",安全头体检告诉你 "**哪里该有的防护没有**"。前者是信息,后者是可直接排期的加固项: 缺 HSTS → 可被 SSL 剥离;缺 CSP → XSS 无第二道防线; 缺 `X-Content-Type-Options` → MIME 嗅探可执行上传文件。 一份"缺失清单"能直接变成工单,而"发现 `/admin`"需要进一步验证才能定性。

🚀 进阶挑战(选做,做完写进当天笔记)

点击展开 ### C1 让扫描器支持"重试 + 断点续扫" 需求: 1. 每扫完 100 条,把已扫路径与结果写入 `progress.jsonl`; 2. 进程被 Ctrl-C 打断后,重新运行能从断点继续(跳过已扫路径); 3. 遇到 `429` 时把当前并发数减半(而不是只 sleep)。 **提示:** 并发数减半可以用"动态 `Semaphore`"实现—— 每次限流时 `acquire()` 一个许可不放,等效于缩小并发窗口。 ### C2 实现一个"通配路由检测 + 自适应判定" 需求:当检测到通配路由时,不再依赖状态码,改为: 1. 取基线页面的**内容长度分布**(多次采样求均值与标准差); 2. 判定命中条件改为 `|len - μ| > 3σ`; 3. 输出时明确标注"本结果在通配路由下产生,置信度低"。 **思考:** 如果目标对每个路径返回**长度随机**的页面,这个方法还成立吗? 你有什么替代思路? ### C3 把工具改成"被动优先"模式 需求:默认只做被动指纹(响应头/Cookie/HTML), 只有加 `--active` 才发主动探针(`/actuator/env`、`/.git/HEAD` 等)。 **为什么这样设计更好?** 主动探针会产生明显异常流量,容易触发告警; 被动指纹零风险。真实项目里应该"默认安静,按需喧闹"。 ### C4 用 `httpx.AsyncClient` 重写扫描核心 对比 `asyncio` 版本与线程池版本的: - 1 万路径总耗时; - 内存占用(`tracemalloc` 或 `psutil`); - 代码复杂度。 **提示:** 用 `asyncio.Semaphore(n)` 控制并发,`asyncio.gather` 收集结果, `httpx.AsyncClient(http2=True, limits=httpx.Limits(max_connections=100))`。 注意:**并发上限依然要压住**,协程只是更省资源,不代表可以更快地打别人。 ### C5 加一个"自我审计"子命令 `--audit-self` 参数:只检查本机是否暴露了危险文件:
bash
python3 03-asset-discovery.py --url http://127.0.0.1:8080 --audit-self
# 只检查: .git/HEAD, .env, wp-config.php.bak, phpinfo.php,
#         server-status, actuator/env, .DS_Store
**这才是本日最推荐的用法**:拿它扫自己的站点,把发现的问题修掉。

📌 今日自检的三个问题

  1. 如果目标站点对你每一个路径都返回 200,你还能不能扫?怎么扫?
  2. 401 / 403 为什么比 200 更值得记录?
  3. 你的扫描器里,如果只能保留一条限速措施,你保留哪条?为什么?

🚫 今日红线