Day 164 完成清单 · 安全审计工具箱(三):代理中间人 + 流量分析
用法:先自己跑,再对照打勾。打不上的勾就是今天没掌握的部分。 所有命令都在
days/day-164-安全审计工具箱-part3/code/下执行。
✅ 一、完成清单(照着打勾)
概念与原理(读 README.md)
- [ ] 能说清"资产面 / 漏洞面 / 流量面"三者的区别, 并各举一个"只有它才能回答"的问题
- [ ] 能说清正向代理与反向代理的立场差异, 以及为什么审计要用正向代理
- [ ] 能说清"绝对 URI 形式"与"源站形式"两种请求行,
以及为什么门禁要校验 request-target 的主机而不是
Host头 - [ ] 能说清
CONNECT之后代理"能看见什么 / 看不见什么"(各三项) - [ ] 能解释为什么不去解密(两条路各是什么性质)
- [ ] 能说清逐跳头与端到端头的区别,并说出不剔除的三个后果
- [ ] 能解释代理为什么主动去掉
Accept-Encoding,以及它的两项代价 - [ ] 能说清"先脱敏,再检测"的原理:检测靠哪两个结构信号工作
- [ ] 能解释值指纹为什么保留(
sha256 前 8 位的两个用途) - [ ] 能说清 PII 擦除与 PII 检测为什么必须分开
- [ ] 能说清两层覆盖率(流量层 / 业务层)各抓什么盲区
- [ ] 能解释为什么退出码
4的优先级高于3 - [ ] 能说清"优先级 = 严重度 × 置信度"为什么比"只看严重度"更诚实
代码与实测(在 code/ 下跑)
- [ ]
python3 mitm_core.py --self-test输出SELF-TEST OK - [ ]
python3 01-flow-capture.py跑通,档案里出现 14 条 flow - [ ] 亲眼确认档案里的
request_body不含明文口令,但note记着 "命中过 password"(这是今天的核心设计) - [ ]
python3 02-flow-analyze.py看到 34 条发现,且退出码打印为4 - [ ]
python3 02-flow-analyze.py的四个踩坑演示全部看懂, 能不看注释复述每个坑的现象 → 根因 → 教训 - [ ]
python3 03-traffic-audit-cli.py --tag demo产出 JSON + Markdown 两个报告 - [ ]
python3 03-traffic-audit-cli.py --capture flows.example.jsonl --quiet退出码为4,并能解释"流量层 100% 为什么还是 4" - [ ] 手工构造一份含越界主机的档案,确认 CLI 给出退出码
2并拒绝出结论 - [ ] 跑一遍 README §7 的"防泄密验证"脚本,全部输出
✅ 无明文
工程习惯
- [ ] 能说清"改完脱敏逻辑要跑什么来防回归"(答案:自检第 ② 组断言 + 全文扫描)
- [ ] 能说清"为什么
AuditProxy对0.0.0.0是硬拒绝而不是警告" - [ ] 主动没有对任何非本人拥有/非授权的目标发起过请求
📝 二、基础练习题(3 道)
练习 1:读档案,找出"看得见"与"看不见"的边界(观察题)
cd days/day-164-安全审计工具箱-part3/code
python3 01-flow-capture.py > /dev/null
python3 - <<'EOF'
import json
rows = [json.loads(l) for l in open("out/day164-basic-flows.jsonl", encoding="utf-8")]
for r in rows:
print(f"{r['fid']} {r['method']:<7} inspected={str(r['inspected']):<5} "
f"status={r['response_status']:<4} url={r['url']}")
EOF
问题:
- 哪一条 flow 的
inspected是False?它的method是什么? - 它的
response_body和request_body分别是什么?为什么? coverage_percent算出多少?如果这条 flow 不存在,覆盖率会变成多少?
验收标准:能用一句话说清"inspected=False 意味着没有结论,
而不是没有问题",并说出它会把退出码推到哪个值。
练习 2:只加一条规则,并解释它的代价
给 FlowAnalyzer 加一条 R16 http-method-over-plaintext-header:
当 scheme == "http" 且请求头里出现 X-API-Key 时报告一条 high 发现。
# 在 FlowAnalyzer._analyze_one 里找位置插入(参考 R3 的写法)
问题:
- 为什么这条规则要单独加,而不是直接放进
SENSITIVE_KEYS? 提示:SENSITIVE_KEYS管的是"值是否脱敏",规则管的是"这件事是不是问题"。 - 加完之后
--self-test会不会失败?为什么? 提示:靶场里有没有端点在明文连接上发X-API-Key? - 如果要让自检通过,你会改靶场还是改断言?两个选择各自的含义是什么?
验收标准:能说清"新增检测规则"与"新增脱敏字段"是两件不同的事, 且能说出为什么改动规则时必须同步考虑靶场/断言。
练习 3:手算一次优先级(算术题)
现有三条发现:
| 规则 | 严重度 | 置信度 |
|---|---|---|
A cleartext-credential |
high | 0.95 |
B tls-tunnel-uninspected |
medium | 1.00 |
C suspected-time-based-sqli(流量层推测) |
high | 0.30 |
问题:
- 分别算出三条的
priority与level - 按优先级排序,并说明 C 为什么排在 A 之后——尽管它俩严重度相同
- 如果 C 的置信度被提到 0.80,排序会怎么变?这次变化说明了什么?
验收标准:能说清"严重但很可能假阳性的发现必须排在 不严重但确定的发现之后",并说出理由(否则运维会不再读报告)。
🔍 三、自检命令(一键跑完)
cd days/day-164-安全审计工具箱-part3/code
# 1) 引擎自检(7 组断言,含"档案不得含明文"这条硬线)
python3 mitm_core.py --self-test # 期望 SELF-TEST OK
# 2) 基础闭环
python3 01-flow-capture.py | tail -15
# 3) 规则引擎 + 四个踩坑复现
python3 02-flow-analyze.py --no-pitfalls | tail -15
# 4) CLI 三种模式的退出码
python3 03-traffic-audit-cli.py --tag verify > /dev/null; echo "auto exit=$?" # 4
python3 03-traffic-audit-cli.py --capture flows.example.jsonl --quiet >/dev/null; echo "fixture exit=$?" # 4
# 5) 防泄密全文扫描(应全部 ✅)
python3 - <<'EOF'
import json, pathlib
BAD = ["Sup3rSecret!", "tk_live_164_fake", "sk_live_164_FAKE_NOT_REAL", "13800138000"]
for p in sorted(pathlib.Path("out").glob("*.jsonl")):
blob = p.read_text(encoding="utf-8")
hits = [b for b in BAD if b in blob]
print(f"{p.name}: {'❌ ' + str(hits) if hits else '✅ 无明文'}")
EOF
📌 四、今日关键结论(背下来)
-
流量面给的是事实,前两面给的是推断。 事实不需要争论,所以它在报告里最站得住脚。
-
CONNECT之后代理是哑管子。 能看见 host:port、字节数、时间;看不见路径、头、体。 不伪造证书、不降级 TLS——"看不见"必须如实记账。 -
"没看" ≠ "没问题"。 这是整份报告诚实性的基准线,也是退出码
4存在的唯一理由。 -
逐跳头必须剔除。 不剔除会污染两段连接的状态, 严重时构成请求走私(request smuggling)的土壤。
-
先脱敏,再检测。 检测靠"字段名 + 指纹标记"两个结构信号工作, 全程不需要看到明文。脱敏是唯一入口,不是事后补救。
-
PII 擦除与 PII 检测必须分开。 擦掉就再也扫不到——所以必须在擦除的同一时刻记下类别 (
flow.pii_labels)。 -
覆盖率是两层的。 流量层(内容看得见多少)+ 业务层(清单端点出现在流量里的比例)。 只看第一层会得到"100% 覆盖"的假象。
-
优先级 = 严重度 × 置信度。 报告的价值在"被读",不在"条目多"。降噪是为了让人继续读。
-
退出码的优先级顺序 = 谁该被最先通知。
2找流程、4找运维、3找开发、0才能自动通过。 -
五个真实 bug 的共性:假设当成了事实。 ① 框架默认行为 ② 编码后字面量 ③ 同源数据漏脱敏 ④ 破坏性操作与检测的顺序 ⑤ 端口漂移造成的假盲区。 对策:把假设变成断言。
🚀 五、进阶挑战题(3 道)
挑战 1:把"业务层覆盖"做成真正的门禁
背景:现在 reconcile_targets() 只是算出"哪些清单端点从未出现",
并在退出码为 0 时把它降级成 4。但它有两个明显的漏洞:
- 如果退出码已经是 3(有发现),业务层缺口就被吞掉了;
- 输出的"未见端点"只是一行行打印,没有结构化产物,没法交给下一环节。
任务:
- 让
facts["targets"]的结构里新增gate_passed: bool, 并让业务层覆盖率低于阈值(新增--min-target-coverage 80)时 独立地把退出码推到 4(而不是"只在原本是 0 的时候"); - 产出一份
out/day164-uncovered-endpoints.md, 含表头端点 | 建议复核方式 | 优先级,并把清单里的端点分成两类: - 静态端点(/、/health)→ "低优先,属预期未被访问" - 带参端点(/api/user?id=、/search?)→ "高优先:带参端点最可能藏漏洞, 却从未被真实流量覆盖,必须用其它手段(Day 163 的检测器)补上"
验收标准:
- 输出一份"未覆盖清单" Markdown;
- 说清"为什么带参端点的未覆盖比静态端点的未覆盖严重";
- 说清"把两种情况分开"为什么不能靠简单字符串匹配判断
(提示:
/api/user?id=1与/api/user?name=x是同一个端点吗?看你的归一化函数怎么定义)。
挑战 2:给代理加"转发但不落档"的旁路模式,并论证它什么时候是错的
背景:有时你只想让代理转发,不想留下档案(比如处理的是明文隐私数据, 连脱敏后的结构都不想留)。
任务:
- 给
AuditProxy加参数record: bool = True;record=False时 正常转发、不写 JSONL,但仍在内存计数(这样报告能说"本轮转了 N 条,未落档"); - 让报告在这种情况下显式写一行:
⚠️ 本轮为旁路模式:流量未被记录,本报告无法复核任何结论; - 让
exit_code()在旁路模式下永远返回 4(因为没有可复核的证据链)。
问题:
- 为什么"不落档"仍然必须让退出码变成 4,而不是"干净通过"?
- 如果业务方说"我就是想要一个不留痕的代理",你该怎么回应? 提示:把"需求"翻译成"他要解决什么问题",再判断留痕是不是必要条件。
- 这个参数会不会让本工具变得危险?为什么它必须默认
True?
验收标准:能说清"证据链是审计工具的产品本身"—— 没有证据链的报告只是"某个程序的输出",不具备任何证明力。
挑战 3:用 mitmproxy 重写同一个代理,并映射流模型
背景:本课的代理是纯 socket 实现的,好处是"60 行核心逻辑能读懂",
代价是不支持 keep-alive、不支持 HTTP/2、不支持 WebSocket。
生产环境的标准答案是 mitmproxy(Day 155 已接触过它的 addon 机制)。
任务:用 mitmproxy addon 实现同一套流模型与同一套规则:
# 参考骨架(本机未安装 mitmproxy,属于"设计题",按文档写出来即可)
class AuditAddon:
def __init__(self, store: FlowStore):
self.store = store
def request(self, flow):
# 就在这里做脱敏 + 请求侧规则
...
def response(self, flow):
# 响应侧脱敏 + 规则 + 落档
self.store.append(to_my_flow(flow))
问题:
- mitmproxy 的
flow对象与本课的Flow数据结构如何一一映射? 哪些字段是它能免费给你的(本课要手写)?哪些需要你补? CONNECT在 mitmproxy 里对应哪个钩子?它默认会尝试解密—— 这正是本课刻意不做的事。请说明"mitmproxy 默认解密"这件事 在自己设备与别人设备上的性质差异,以及你应当怎么配置它。- 最重要的一题:本课的五条规则里,哪几条在 mitmproxy 下
"本来是免费的"(框架已经算好),哪几条仍然必须自己写?
提示:
Set-Cookie解析、URL 拆分、chunked 解码都有现成的; 而"明文传输了凭据"这个判断,任何框架都不会替你做。
验收标准:
- 产出 addon 骨架代码(跑不起来也能提交设计);
- 能说清"框架替你省的是机械劳动,不是判断"—— 规则引擎的判定逻辑才是你真正的产品。
挑战 4(附加):用"重复指纹"发现凭据复用
背景:value_fingerprint() 保留下来的指纹支持一类只能在流量层发现的结论:
同一个指纹(如 a1b2c3d4)出现在 3 个不同端点 / 不同主机的请求里
↓
结论:这个口令/令牌被重复使用 → 横向移动风险
任务:
- 在
FlowAnalyzer里新增规则credential-reuse: 统计所有 flow 中出现过的"敏感字段值指纹", 当同一个指纹跨 ≥ 2 个不同端点出现时,报一条medium发现, 证据里写"指纹xxx出现在 N 个端点"(不写值); - 让靶场在
/login之外再加一个端点(比如/api/token), 让它也用同一个口令,这样演示能真的触发这条规则; - 想清楚一个陷阱:如果指纹统计把"每个请求都不一样的会话令牌" 算了进去(每次登录指纹都不同),这条规则就会失效。 你要怎么区分"长期凭据(指纹稳定)"与"一次性令牌(指纹易变)"?
验收标准:
- 规则能触发,且证据里没有明文;
- 能说清"指纹统计"这类元数据关联分析为什么是脱敏设计 带来的额外收益(而不是为了脱敏而不得不做的妥协)。