@dsh-fly/dsh-plugin-fly-mode 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -2
  3. package/cordis.patch.yml +399 -0
  4. package/package.json +32 -4
  5. package/tools/dsh-plugin-fly-binary/index.js +520 -0
  6. package/tools/dsh-plugin-fly-binary/package.json +23 -0
  7. package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/SKILL.md +163 -0
  8. package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/code/mitm_addon_template.py +302 -0
  9. package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/code/rules.example.json +52 -0
  10. package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/reference/case-ttw.md +157 -0
  11. package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/reference/checklist.md +133 -0
  12. package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/reference/playbook.md +228 -0
  13. package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/reference/rules.schema.md +76 -0
  14. package/tools/dsh-plugin-fly-blackbox/index.js +1453 -0
  15. package/tools/dsh-plugin-fly-blackbox/package.json +31 -0
  16. package/tools/dsh-plugin-fly-diag/index.js +772 -0
  17. package/tools/dsh-plugin-fly-diag/package.json +24 -0
  18. package/tools/dsh-plugin-fly-diag/wifi.js +402 -0
  19. package/tools/dsh-plugin-fly-guard/index.js +101 -0
  20. package/tools/dsh-plugin-fly-guard/package.json +26 -0
  21. package/tools/dsh-plugin-fly-pwn/index.js +583 -0
  22. package/tools/dsh-plugin-fly-pwn/package.json +23 -0
  23. package/tools/dsh-plugin-fly-recon/index.js +184 -0
  24. package/tools/dsh-plugin-fly-recon/package.json +23 -0
  25. package/tools/dsh-plugin-fly-reverse/index.js +178 -0
  26. package/tools/dsh-plugin-fly-reverse/package.json +23 -0
@@ -0,0 +1,157 @@
1
+ # 案例复盘:某网球小程序「性别字段死锁」导致账号无法报名
2
+
3
+ > 对应框架:`SKILL.md` §1–§3 · 五阶段全文:`playbook.md` · 清单:`checklist.md`
4
+ >
5
+ > 一句话:**一个从建号起就为空、且现网没有任何写入路径的必填字段,挡在报名流程前面。**
6
+ > 用户折腾了一周,背后是**三处独立缺陷**,任何一处单独存在都不至于卡死。
7
+ >
8
+ > 脱敏说明:本文只保留**方法与结构**。账号 / 订单 / 赛事 / 交易流水等一律写成占位符
9
+ > (`<会员ID>` / `<订单号>` / `<赛事ID>`),域名写成 `<业务域名>` / `<静态资源域名>`,
10
+ > 时间戳只保留日期形状。原始抓包与原始工单留在案例工作区,不进仓库。
11
+
12
+ ---
13
+
14
+ ## 1. 最终结论
15
+
16
+ | 层 | 缺陷 | 证据 |
17
+ | --- | --- | --- |
18
+ | **S1 渲染** | 性别为空时,选择器按选项第 0 项渲染成"男" | 注入 `1` → 界面变"女";注入 `"0"` → 界面变"男"。证明显示源就是这个字段 |
19
+ | **S2 提交** | 该页面**没有性别编辑控件**;保存校验在本地 `return`,**请求根本不发出** | 全流程抓包:对业务域名的非 GET 请求**只有**一个赛事查询接口 |
20
+ | **S3 存储** | 资料更新接口的字段白名单里**没有该字段** | 改前 / 改后 diff:省 / 市 / 地址等字段全部落库、"最后修改时间"更新,**目标字段纹丝不动** |
21
+ | **G 闸门** | 报名预检接口返回 `state:false` 拦住客户端 | `{"state":false,"confirm":true,"Y":"去补充","info":"您的性别未填写,是否去补充?"}` |
22
+
23
+ **编码约定(实测):`0 = 男`,`1 = 女`。**
24
+
25
+ ---
26
+
27
+ ## 2. 时间线(含每个决策点的判据)
28
+
29
+ | 阶段 | 做了什么 | 结果 / 判据 |
30
+ | --- | --- | --- |
31
+ | **Phase 0** | 用 S1/S2/S3/G 建模;注意到**两句提示文案不一致**(报名页"请先补充性别" vs 资料页"请选择性别") | 两段独立校验 → 至少两处断裂点(T2 的前置线索) |
32
+ | **Phase 0** | 用户补充关键事实:性别无编辑入口、来自实名认证、退出重登 / 注销找回 / 换号全部无效 | 锁定"存在一个**无法完成的必填项**" |
33
+ | **Phase 1** | 探测环境:macOS 上的小程序跑在 Chromium 内核宿主里 → **跟随系统代理** | 客户端矩阵第一行:PC 端就能抓,不必碰手机 |
34
+ | **Phase 1** | 发现本机另有一个代理工具常驻在一个高位端口,但**系统代理是关的** | 排除抢流量风险;确认可以安全接管系统代理 |
35
+ | **Phase 1** | 装 mitmproxy 失败(默认镜像源解析不到包,连基础依赖都拉不到) | **自检三连**先证明管道:换官方源 `-i https://pypi.org/simple` 立刻成功 |
36
+ | **Phase 1** | 自检三连全绿(HTTP 200 / HTTPS 200 / 目标客户端出现 `[NEW HOST]`) | **Gate 1 通过** —— 之后才敢对"抓到什么"下结论 |
37
+ | **Phase 2** | 全量抓包 → 域名分布 → 业务 API 锁定 `<业务域名>` | 一个静态 CDN 域名被排除 |
38
+ | **Phase 2** | **判决性动作**:把两句报错文案丢进全部响应体搜索 | "请选择性别"**一处都搜不到** → 纯前端文案(T2);"您的性别未填写"出现在预检接口响应里 → 服务端判定 |
39
+ | **Phase 2** | 数非 GET 请求 | **只有一个赛事查询接口** → 保存请求从未发出(T3 落地) |
40
+ | **Phase 2** | 读完整请求头 | 鉴权只有一个自增数字的会员头 + 一个开发遗留的占位头 → **无有效鉴权,可直连** |
41
+ | **Phase 3** | 对账 S1/S3 | 资料读接口返回 `"gender": null`、`"genderName": null`;界面却显示"男" → **渲染兜底**(T1) |
42
+ | **Phase 3** | **诱出法**:往资料接口响应注入 `gender:1` | 用户反馈"现在显示女了" → 一举确认:① 显示源就是该字段 ② `1 = 女` |
43
+ | **Phase 3** | 改用字符串 `"0"` 注入(T5 的通用钥匙) | 界面显示"男"(数组下标取 `"0"`)且**前端校验放行** → 成功逼出请求 |
44
+ | **Phase 3** | 抓到资料更新接口的完整请求体 | 拿到接口名 + 全部字段 + 编码 |
45
+ | **Phase 3** | 独立读接口复核(不信回显,T4) | 库里仍是空 → **回显陷阱确认** |
46
+ | **Phase 3** | **变更审计**:改前 / 改后整条记录 diff | 省 / 市 / 地址等字段落库、"最后修改时间"更新、**目标字段不变** → 判定:字段白名单缺失 |
47
+ | **Phase 3** | 存活性探测:对候选写接口发 `GET`(先打对照组指纹) | 两个候选写接口 → `405 not supported`(存在,只收 POST);其余十几个候选全 404 |
48
+ | **Phase 4** | **主动放弃**另一个写接口 | 请求体里手机号 / 身份证是打码值,整行覆盖风险不可接受 → 风险清单拦住 |
49
+ | **Phase 4** | T6 判定闸门性质 | 服务端把 `confirm` / `Y` / `N` / `state` 交给客户端 → **客户端门禁,可解除** |
50
+ | **Phase 4** | 篡改预检接口响应 → `state:true, confirm:false` | 用户"不再提示补充性别" → 进入报名页 |
51
+ | **Phase 4** | 报名提交 | 报名接口返回成功 → **服务端完全不校验性别**(反证:性别只是资料完善度门槛,不是报名资格) |
52
+ | **Phase 4** | 支付 | 支付成功、交易流水号正常生成(**计费照常,未绕过**) |
53
+ | **Phase 5** | 独立核验 | 报名状态查询 → `state:true`;按订单查名单 → 含本人 |
54
+
55
+ ---
56
+
57
+ ## 3. 关键转折点(最值得复用的三招)
58
+
59
+ ### ① 文案搜索定层(30 秒,省一天)
60
+
61
+ 把报错原文丢进全部响应体搜。搜到 = 服务端判定;搜不到 = 纯前端文案。
62
+ 本案例两句文案分属两层,一下就拆成了两个独立子问题。
63
+
64
+ ### ② 诱出法(Inducement)
65
+
66
+ 前端校验拦住不发包时,你拿不到接口格式。**篡改上游响应让校验放行**,
67
+ 前端就会主动把那条本不会发出的请求交给你 —— 接口名、完整字段、编码,一次全拿到。
68
+
69
+ 即使最终发现服务端不落这个字段,这次诱出也**没有白做**:它把"哪一层丢的"从猜测变成了 diff 事实。
70
+
71
+ ### ③ 变更审计(改前 / 改后 diff)
72
+
73
+ 一次就区分了三种可能:
74
+
75
+ | "最后修改时间" | 其他字段 | 目标字段 | 结论 |
76
+ | --- | --- | --- | --- |
77
+ | 未变 | 未变 | 未变 | 接口没被执行 / 打到了别的记录 |
78
+ | **变了** | **变了** | **未变** | **字段白名单缺失**(本案例) |
79
+ | 变了 | 部分变 | 变了 | 成功 |
80
+
81
+ 配上"接口返回体是**回显请求对象**"这个反例(T4),这套组合能拆穿绝大多数"看起来成功了"。
82
+
83
+ ### ④ 假值陷阱的通用钥匙(T5)
84
+
85
+ 平台编码 `0 = 男`。任何 `if (!gender)` 的写法都会让"男"永久不可达 —— 这极可能就是线上 bug 的真身。
86
+ 而 **JS 里字符串 `"0"` 是真值**,能同时过 `!v` 判假和 `==null` 判空;
87
+ 服务端 JSON 反序列化又会把它正常转成数字 `0`。**一个字符的差别,从"永远存不进去"变成"一次写入成功"。**
88
+
89
+ ---
90
+
91
+ ## 4. 提交给厂商的工单(结构照抄,值一律占位)
92
+
93
+ > **会员 ID `<会员ID>`**(实名认证已完成:身份证、生日齐全)。
94
+ > 报名时提示"您的性别未填写,是否去补充?",跳转资料页后性别栏显示"男",
95
+ > 点击"保存修改"却提示"请选择性别"。
96
+ >
97
+ > **前端问题**
98
+ > ① 性别为空时按选项第 0 项渲染成"男",误导用户以为已填写;
99
+ > ② 该页面**没有任何性别编辑入口**;
100
+ > ③ 点保存时校验在本地 `return`,**请求根本没有发出**
101
+ > (抓包确认:整个流程对 `<业务域名>` 的非 GET 请求只有一个赛事查询接口)。
102
+ > 因此退出重登、注销后找回、更换客户端、清除缓存**均不可能生效**。
103
+ >
104
+ > **后端问题(关键)**
105
+ > 通过代理注入使前端发出请求后,资料更新接口返回 `code:0`,
106
+ > 省 / 市 / 地址等字段全部成功落库、"最后修改时间"已更新,
107
+ > **但性别列纹丝不动** —— 该更新接口的字段白名单里没有性别。
108
+ > 返回体中的 `"gender":0` 只是**回显请求对象**,不是数据库中的值
109
+ > (用独立的资料读接口复查,仍为空)。
110
+ >
111
+ > **结论**
112
+ > 数据库里的性别字段自历史迁移数据起就为空;
113
+ > 现网**没有任何一条写入路径**能让用户把它填上;
114
+ > 而报名预检接口又以该字段作为报名硬门槛 → 该账号**永久无法报名**。
115
+ >
116
+ > **请执行**
117
+ > ① 修数据:`UPDATE <会员表> SET gender = 0 WHERE id = <会员ID>;`
118
+ > ② 修复项:
119
+ > - 资料更新接口的字段白名单补上该字段;
120
+ > - 资料页补回编辑控件,并修掉"空值被渲染成男"的误导显示;
121
+ > - 既然报名提交并不校验性别,预检接口的性别门槛应改为非阻断或可跳过。
122
+ >
123
+ > **附带安全发现(负责任披露,未利用)**
124
+ > `<业务域名>` 的接口**鉴权不足**,仅凭一个自增数字的会员请求头
125
+ > 即可读取 / 修改任意会员资料(姓名、手机号、身份证号)。
126
+ > 另有开发遗留的占位请求头。建议尽快加固。
127
+
128
+ ---
129
+
130
+ ## 5. 本案例对框架的贡献(框架里哪几条是被它逼出来的)
131
+
132
+ | 框架条目 | 本案例的来源 |
133
+ | --- | --- |
134
+ | **T1 用户看到的文字不是数据** | 界面显示"男",服务端是空 |
135
+ | **T2 文案位置决定责任层** | 两句提示一句前端一句服务端,拆开才破局 |
136
+ | **T3 "没发出" vs "被拒"** | 数非 GET 请求才确认保存从未发出 |
137
+ | **T4 回显陷阱** | 返回体里明明白白 `"gender":0`,库里是空 |
138
+ | **T5 假值陷阱 + 字符串 `"0"` 钥匙** | 平台 `0 = 男`,判假校验必然拦死;`"0"` 一击破局 |
139
+ | **T6 客户端门禁 vs 服务端门禁** | `confirm` / `Y` / `N` 暴露了决策权在客户端 → 故可安全解除 |
140
+ | **变更审计(diff)** | 唯一能证明"字段白名单缺失"的手段 |
141
+ | **存活性探测 + 对照组指纹** | `405 not supported` 精准定位写接口 |
142
+ | **写入前风险清单** | 主动放弃一个接口,避免打码字段被写成星号 |
143
+ | **自检三连** | 镜像拉不到包时,先证明管道再解释现象 |
144
+ | **环境还原顺序** | 先关代理再杀进程,否则瞬断网 |
145
+ | **合规红线(不碰计费)** | 报名照常支付,只绕过坏掉的自检 |
146
+
147
+ ---
148
+
149
+ ## 6. 如果重来一次,可以更快
150
+
151
+ 1. **第一步就搜响应体文案**(T2)—— 本案例绕了一圈才做,它其实能立刻把问题劈成两半
152
+ 2. **诱出法应该更早用** —— 不必先纠结"怎么把字段写进库",先让前端把请求交出来
153
+ 3. **变更审计应该在第一次写入后就做** —— 能省掉中间一轮误判("服务端返回成功码了,是不是成了")
154
+ 4. **不要先花时间找编辑入口** —— 用户的描述"没有编辑入口"已经足够定性,直接进协议层
155
+
156
+ 总体路径是对的,但**第一小时的顺序可以更优**:
157
+ `搜文案定层 → 抓包定"发没发" → 诱出拿格式 → diff 定"哪层丢" → 判门禁性质 → 最小侵入达成目标`。
@@ -0,0 +1,133 @@
1
+ # 客户端黑盒排查 · 全流程检查清单
2
+
3
+ > 配套:`playbook.md`(五阶段全文)· `case-ttw.md`(脱敏案例)· `rules.schema.md`(规则 schema)
4
+ >
5
+ > 用法:复制一份,边做边勾。**每个 Gate 没过,不要进入下一阶段。**
6
+
7
+ ---
8
+
9
+ ## Phase 0 · 症状分流
10
+
11
+ - [ ] 用 S1/S2/S3/G 模型写出问题陈述
12
+ `S1 显示___ | S2 提交___ | S3 存储___ | 闸门在〈前端 / 服务端〉`
13
+ - [ ] 把用户已尝试的无效操作逐条列出,并解释**为什么无效**
14
+ - [ ] 找出提示语原文,去响应体里搜(T2)
15
+ - [ ] 搜到 → 服务端判定
16
+ - [ ] 搜不到 → 纯前端文案
17
+ - [ ] 明确"用户物理上做不到的那个动作"是什么(例:页面没有编辑控件)
18
+
19
+ **Gate 0**:能自洽解释"为什么重登 / 重装 / 换设备全部无效"。
20
+
21
+ ---
22
+
23
+ ## Phase 1 · 建立可观测性
24
+
25
+ ### 环境探测
26
+ - [ ] 用 `bx_doctor` 的第④段扫一遍:本机有没有别的代理工具在监听常见高位端口
27
+ (Clash / Surge / mihomo 之类;它们可能抢流量或与我们的代理冲突)
28
+ - [ ] 确认当前**系统代理真实状态**(`bx_sysproxy status`;看 `Enabled` 而不是 `Server` 字段)
29
+ - [ ] 代理证书目录设到工作区内(`.fly-blackbox/mitmproxy`),别用默认的 `~/.mitmproxy`
30
+ - [ ] 装 mitmproxy:**注意解释器**。系统自带 / 常见 `pip3` 可能装到老版本,
31
+ dump 格式与新版不同(读取器会按版本闸拒绝)。用显式版本的解释器建 venv,
32
+ 并对齐版本:
33
+ - [ ] `bx_proxy status` 会告诉你本机缺不缺 mitmdump,以及点名解释器的两行安装命令
34
+ - [ ] 若镜像拉不到包,直接换官方源:`-i https://pypi.org/simple`
35
+ - [ ] 信任 CA(macOS 要写系统钥匙串,需要密码 → **人的动作**)
36
+ - [ ] 命令用 `bx_ca command` 取;本仓**永不提权**
37
+ - [ ] 事后用 `bx_ca verify` 取证据(退出码 + 指纹)
38
+ - [ ] 系统代理 HTTP 与 HTTPS **两个都要设**(`bx_sysproxy on`)
39
+ - [ ] **写好还原命令**:`bx_sysproxy status` 每次都会回显一条
40
+
41
+ ### 自检三连(Gate 1 硬指标)
42
+ - [ ] ① HTTP 通过代理返回 200
43
+ - [ ] ② HTTPS 通过代理且 `--cacert` 用 confdir 里那张 CA,返回 200
44
+ - [ ] ③ **HTTPS 证书链的签发者就是 confdir 里的 mitm CA**(只看 200 不算过)
45
+ - [ ] ④ 打开目标客户端,日志出现 `[NEW HOST]`
46
+ - [ ] 若 ③ / ④ 失败:区分"自带网络栈忽略代理" vs **证书绑定**(pinning)→ 后者跳 Phase 4 人工分支
47
+
48
+ **Gate 1**:①②③④ 全绿。(`bx_doctor` 把这几条一次判完;它说"无抓包能力"时是**一等状态**,不是失败。)
49
+
50
+ ---
51
+
52
+ ## Phase 2 · 协议测绘
53
+
54
+ - [ ] 把目标功能**正常路径 + 出错路径各点一遍**(保持代理开着)
55
+ - [ ] `bx_flows hosts`:输出域名分布,标出业务 API 域名 vs 静态资源 CDN
56
+ - [ ] `bx_flows list`:逐条记录接口:`method / path / 关键参数 / 鉴权方式 / 响应模型`
57
+ - [ ] 标注:只读接口 / 写入接口 / **预检接口**(`check` / `verify` / `pre`)
58
+ - [ ] 提示语原文全局搜索(`bx_flows grep --pattern '…'`),定位到具体接口与字段(T2)
59
+ - [ ] 拉一条**对照组指纹**:必然存在的路径 + 必然不存在的路径,各返回什么
60
+
61
+ **Gate 2**:能画出"操作 → 请求 → 谁判断"的流程图。
62
+
63
+ ---
64
+
65
+ ## Phase 3 · 三源对账 + 诱出
66
+
67
+ ### 对账
68
+ - [ ] S1 vs S3:界面显示值 vs 读接口返回值 → 差值 = 渲染兜底(T1)
69
+ - [ ] S2 vs S3:抓到的请求体 vs 读接口返回值 → 差值 = 服务端丢弃
70
+ - [ ] **变更审计**(`bx_diff`):对整条记录做**改前 / 改后 diff**,列出所有真正变化的字段
71
+ - [ ] 看记录里的"最后修改时间"字段有没有变(判断接口到底有没有被执行)
72
+ - [ ] 注意 diff 的三种标注:**缺失** / **假值** / **类型变化** —— 它们指向完全不同的根因
73
+
74
+ ### 诱出法(仅当请求根本没发出)
75
+ - [ ] 选定**单一变量**:注入哪个字段、注入成什么值
76
+ - [ ] 注入值考虑 T5:枚举字段优先试**字符串 `"0"`**(JS 真值,能同时过判假与判空)
77
+ - [ ] 明确定义"观察点"(界面显示 / 是否发包 / 响应码)
78
+ - [ ] 规则写进 `rules.json`(schema 见 `rules.schema.md`),改完**保存即生效,不重启**
79
+ - [ ] 注入后复现操作,确认请求**确实发出**
80
+ - [ ] 抓下完整请求体(接口名、字段、编码一次全拿到)
81
+ - [ ] **立即撤掉注入**(显式 `{"enabled": false}` —— 空文件不算关),避免污染后续判断
82
+
83
+ ### 找写入路径(非破坏性)
84
+ - [ ] 对候选路径发 `GET`
85
+ - [ ] `404` → 不存在
86
+ - [ ] `405 / not supported` → **存在,只接受 POST** ← 目标
87
+ - [ ] `200` → 可读
88
+ - [ ] 记录候选接口清单(后续交给厂商)
89
+
90
+ **Gate 3**:能用一句话说清"哪个接口 / 哪个字段 / 在哪一层丢的"。
91
+
92
+ ---
93
+
94
+ ## Phase 4 · 修复决策
95
+
96
+ - [ ] 目标字段能否写入存储?
97
+ - [ ] 有专用写接口 → 直接调用(诱出法拿格式)
98
+ - [ ] 有通用接口但不落该字段 → 白名单缺失 → **厂商修**
99
+ - [ ] 无写接口 → 判断闸门性质
100
+ - [ ] 闸门性质(T6)
101
+ - [ ] 客户端门禁(服务端交出了 `state` / `confirm` / `Y` / `N` 决策权)→ 可篡改响应解除
102
+ - [ ] 服务端门禁(直接拒绝)→ **停手**,转人工 / 工单 / 换入口
103
+
104
+ ### 写入前风险清单(逐条勾选,任一不明确就不要写)
105
+ - [ ] 该接口是**选择性更新**还是**整行覆盖**?
106
+ - [ ] 请求体里有没有**打码字段**(手机号 / 身份证是星号)?整行覆盖会把真实值写成星号
107
+ - [ ] 接口名是否含 `save` / `insert` / `create`(可能是插入,产生重复记录)?
108
+ - [ ] 改动可回滚吗?有软删除吗?
109
+ - [ ] 绕的是"展示 / 自检"还是"计费 / 资格 / 身份"?**后者一律不碰**
110
+
111
+ **Gate 4**:用户目标达成,且**无脏数据**(或已记录并规划回滚)。
112
+
113
+ ---
114
+
115
+ ## Phase 5 · 收口
116
+
117
+ - [ ] **先关系统代理**(`bx_sysproxy off`),**再停代理进程**(`bx_proxy stop`)(顺序!)
118
+ - [ ] 验证网络恢复正常
119
+ - [ ] 存档全量流量(`.mitm`)、分析结论、变更 diff、精确时间线
120
+ - [ ] 存档前**脱敏**(token / 手机号 / 身份证)
121
+ - [ ] 写工单,七段齐全:上下文 / 现象 / **已排除项** / 证据(diff) / 定位 / 可执行诉求 / 安全披露
122
+ - [ ] 安全发现:**只披露,不利用**
123
+ - [ ] 留一份"下次怎么再做一遍"的操作手册(若是需要重复执行的绕过)
124
+
125
+ ---
126
+
127
+ ## 通用纪律(全程)
128
+
129
+ - [ ] 一次只改一个变量,并写明观察点
130
+ - [ ] 每个结论都要有**独立的验证手段**(不靠接口回显)
131
+ - [ ] 在证明管道通畅之前,不对"没抓到数据"做任何解释
132
+ - [ ] 每次操作前问一句:**这一步可逆吗?**
133
+ - [ ] 只碰自己的账号,只碰自己的数据
@@ -0,0 +1,228 @@
1
+ # 客户端黑盒排查 · 五阶段全文
2
+
3
+ > 这是 SKILL.md 第 3 节那张表的展开版。每阶段都有 **Gate**:没过不要往下走 —— 这是本框架最重要的纪律。
4
+ >
5
+ > 配套:`checklist.md`(可勾选清单)· `case-ttw.md`(脱敏案例)· `rules.schema.md`(规则 schema)
6
+
7
+ ---
8
+
9
+ ## Phase 0 · 症状分流【目标:把症状翻译成假设】
10
+
11
+ **动作**
12
+
13
+ 1. 用 S1/S2/S3/G 把症状写成一句话:
14
+ > "S1 显示 X,S2 实际提交 Y,S3 存储 Z,闸门 G 在〈前端 / 服务端〉"
15
+ 2. 列出用户已尝试的无效操作(重登 / 重装 / 换设备 / 清缓存 / 换网络),**逐条解释为什么无效**。
16
+ 解释不通 ⇒ 说明模型错了,回第 1 步重写。
17
+ 3. 初步判 T2:提示语出自前端还是服务端(看它像不像模板文案,还是像服务端返回的 `info` 字段)。
18
+
19
+ **产出**:一页纸的问题陈述 + 初始假设 + 待验证项清单。
20
+
21
+ **Gate 0**:能用 S1/S2/S3/G 把症状说清楚,且"为什么重登 / 重装无效"有自洽解释。
22
+
23
+ > 这一阶段最常见的错误是**跳过它**:直接去装代理。装完代理你仍然不知道该看什么,
24
+ > 于是抓一堆流量回来还是无从下手。
25
+
26
+ ---
27
+
28
+ ## Phase 1 · 建立可观测性【目标:能看见每一帧流量】
29
+
30
+ ### 客户端选择矩阵(优先从上往下)
31
+
32
+ | 客户端形态 | 可抓性 | 代理行为 | 备注 |
33
+ | --- | --- | --- | --- |
34
+ | PC 客户端(Electron / Chromium 内核宿主) | ★★★ | 跟随**系统代理** | **首选**:流量与本机同源,抓包文件直接落地 |
35
+ | 浏览器 / 网页版 | ★★★ | 跟随系统代理 | 最理想,往往还能直接看 DevTools |
36
+ | iOS 真机 | ★★★ | 跟随 Wi-Fi 代理 | 需装描述文件,并在"证书信任设置"里手动打开 |
37
+ | 桌面原生应用 | ★★ | 多数跟随系统代理 | 少数自带网络栈,直接忽略代理 |
38
+ | Android 真机 | ★★ | 跟随 Wi-Fi 代理 | 用户证书可能不被信任,部分应用直接拒绝 |
39
+ | 官方开发者工具 | ★★★ | 内置 Network 面板 | 需开发权限,通常拿不到 |
40
+
41
+ > **优先选 PC 客户端**:小程序 / App 在 PC 上常跑在 Chromium 内核里,天然跟随系统代理,
42
+ > 不需要碰手机、不需要装描述文件,抓包文件就在手边。
43
+
44
+ ### 搭建要点(每一步都有坑)
45
+
46
+ 1. **证书目录放在工作区内**(`--set confdir=<workspace>/.fly-blackbox/mitmproxy`),别用默认的
47
+ `~/.mitmproxy`:路径好找、好清理、好跟项目一起归档。
48
+ 2. **信任 CA** 需要密码,所以是**人的动作**:用 `bx_ca command` 取那条命令自己跑,跑完 `bx_ca verify` 取证据。
49
+ 3. **系统代理** HTTP 与 HTTPS **都要指向** `127.0.0.1:<port>`;`bx_sysproxy on` 会一起设。
50
+ 4. **先查有没有别的代理工具在跑**(Clash / Surge / mihomo 之类常驻在本地几个高位端口)——
51
+ 它们可能抢流量。`bx_doctor` 第④段专门扫这件事。
52
+ 5. **第一分钟就写还原脚本**,或者至少记下 `bx_sysproxy` 给出的那条还原命令。
53
+ 6. **靶机 / 内网自签证书的上游会被默认拒绝**:mitmproxy 默认**验证上游**(`ssl_insecure: false`),
54
+ 连你自己那个自签靶也一样 —— 客户端侧看到的是我们签的证书(`issuer: CN=mitmproxy`),
55
+ 但响应是 `502`,**原因只在 mitm 日志里**(`Certificate verify failed: self-signed certificate`)。
56
+ 起代理时加 `--set ssl_verify_upstream_trusted_ca=<你的测试 CA>`(只认这一张,比 `ssl_insecure=true`
57
+ 精确);自建靶场图省事也可以 `ssl_insecure=true`,**别对业务流量这么干**。
58
+ 判据别只写"看到 502 就以为拦截失败":链上的签发者才是判据。
59
+
60
+ ### 自检三连(Gate 1 的硬指标)
61
+
62
+ ```bash
63
+ # ① HTTP 能过
64
+ curl -s -o /dev/null -w '%{http_code}\n' -x http://127.0.0.1:8080 http://example.com/
65
+ # ② HTTPS 中间人能过(这一步才是真正的验证)
66
+ curl -s -o /dev/null -w '%{http_code}\n' -x http://127.0.0.1:8080 \
67
+ --cacert <workspace>/.fly-blackbox/mitmproxy/mitmproxy-ca-cert.pem https://example.com/
68
+ # ③ 目标客户端确实走了代理:看日志里有没有 [NEW HOST]
69
+ tail -f <workspace>/.fly-blackbox/capture/all.log
70
+ ```
71
+
72
+ **只看到 ② 返回 200 还不够**:那可能是直连、也可能是另一个代理在中间。真正的判据是
73
+ **HTTPS 证书链里的签发者就是 confdir 里那张 mitm CA** —— `bx_doctor` 第②段正是这么判的。
74
+
75
+ **Gate 1**:三条全绿。特别是 ③:浏览器有流量但目标客户端没有 ⇒ 要么客户端自带网络栈忽略代理,
76
+ 要么它做了证书绑定(pinning)。后者意味着此路不通,直接转 Phase 4 的人工分支。
77
+
78
+ > 教训:**在证明管道通畅之前,不要对"没抓到东西"做任何解释。** 本仓的案例里,
79
+ > 第一版脚本把 host 白名单写窄了,差点把"没流量"误判成证书绑定。
80
+
81
+ ---
82
+
83
+ ## Phase 2 · 协议测绘【目标:把黑盒变成一份接口清单】
84
+
85
+ **动作**
86
+
87
+ 1. 开着代理,把目标功能的**全流程点一遍**(正常路径 + 出错路径各一遍)。
88
+ 2. 输出域名分布(`bx_flows hosts`),找出**业务 API 域名**,与静态资源 CDN 分开。
89
+ 3. 逐条记录:`method / path / 关键参数 / 鉴权方式 / 响应模型`。
90
+ 4. **重点标注**:鉴权机制(token?签名?还是裸的会员 ID 头)、只读接口 vs 写入接口、
91
+ 预检接口(名字常含 `check` / `verify` / `pre`)。
92
+
93
+ **产出**:接口清单表。
94
+
95
+ **关键判据(T2 落地)**:把出错的提示语原文,在**全部响应体**里搜一遍
96
+ (`bx_flows grep --pattern '<提示语>'`)。搜到 ⇒ 服务端判定;搜不到 ⇒ 纯前端文案。
97
+ **这一步 30 秒,能省一天。**
98
+
99
+ **Gate 2**:能画出"用户操作 → 发出的请求 → 谁做判断"的流程图;知道提示语出自哪一层。
100
+
101
+ ---
102
+
103
+ ## Phase 3 · 三源对账 + 诱出法【目标:定位到具体那一行】
104
+
105
+ ### 3.1 对账
106
+
107
+ | 对账 | 方法 | 差值说明什么 |
108
+ | --- | --- | --- |
109
+ | S1 vs S3 | 界面上看到的 vs 读接口返回的 | 差值 = **渲染兜底**(T1) |
110
+ | S2 vs S3 | 抓到的请求体 vs 读接口返回的 | 差值 = **服务端丢弃** |
111
+ | 变更审计 | **改前 / 改后 diff 整条记录**(`bx_diff`) | 看**哪些字段真的落库了** |
112
+
113
+ > **变更审计是本框架性价比最高的一招。** 它一次区分了三种可能:
114
+
115
+ | `updateTime` | 其他字段 | 目标字段 | 结论 |
116
+ | --- | --- | --- | --- |
117
+ | 未变 | 未变 | 未变 | 接口没被执行 / 打到了别的记录 |
118
+ | **变了** | **变了** | **未变** | **字段白名单缺失** |
119
+ | 变了 | 部分变 | 变了 | 成功 |
120
+
121
+ ### 3.2 诱出法(Inducement):当前端把请求拦住时
122
+
123
+ 如果 T3 判定"请求根本没发出",你是拿不到接口格式的。这时**主动篡改上游响应**,让前端校验通过,
124
+ 从而**逼出那条本不会发出的请求**:
125
+
126
+ ```
127
+ 上游真实响应: {"字段": null}
128
+ ↓ 代理注入
129
+ 前端收到: {"字段": "0"} ← 字符串 "0"(T5 的通用钥匙)
130
+ ↓
131
+ 前端校验放行 → 真正发出那条 POST → 抓到完整请求体 + 接口名
132
+ ```
133
+
134
+ **这是本框架最有价值的一招**:把"前端不配合"变成"前端主动交出接口格式"。
135
+ 即使最终发现服务端不落该字段,这次诱出也没白做 —— 它把"哪一层丢的"从猜测变成了 diff 事实。
136
+
137
+ **纪律:诱出必须可撤、可观测、单一变量。**
138
+
139
+ - 一次只改一个字段,并**明确写出观察点**(界面显示?发包?响应码?)
140
+ - 改完先看观察点,再继续
141
+ - 注入前先记下原始响应的存档(证据不能被自己改没了)
142
+
143
+ ### 3.3 找写入路径(存在性探测)
144
+
145
+ 用**非破坏性**方法枚举候选接口:对候选路径发 `GET`。
146
+
147
+ - `404` → 不存在
148
+ - `405 not supported` → **存在,但只接受 POST** ← 这就是你要找的
149
+ - `200` → 已存在且可读
150
+
151
+ 先打一组**对照组**(一个必然存在的路径 + 一个必然不存在的路径),拿到本地的"存在 / 不存在"
152
+ 响应指纹,否则分不清 `404` 是"不存在"还是"路由没匹配"。
153
+
154
+ **Gate 3**:能写出"哪个接口、哪个字段、哪一层丢的"这样一句话。
155
+
156
+ ---
157
+
158
+ ## Phase 4 · 修复决策树【目标:用最小侵入达成用户目标】
159
+
160
+ ```
161
+ 目标字段能否写入存储(S3)?
162
+ │
163
+ ┌─────────────────────┼─────────────────────┐
164
+ │ │ │
165
+ 有专用写接口 有通用更新接口 无任何写接口
166
+ │ 但不落该字段 │
167
+ ↓ ↓ ↓
168
+ 直接调用写入 → 白名单缺失 → 厂商修 判断闸门 G 性质
169
+ (诱出法拿格式) (同时用绕过达成用户目标) │
170
+ ┌────────────┴────────────┐
171
+ │ │
172
+ 客户端门禁 G 服务端门禁 G
173
+ │ │
174
+ 响应篡改解除拦截 ❌ 停手
175
+ (其余流程照常) 转人工 / 工单 / 换入口
176
+ ```
177
+
178
+ **风险控制清单(写入操作前逐条过)**
179
+
180
+ | 风险 | 检查项 |
181
+ | --- | --- |
182
+ | 全字段覆盖 | 该接口是"选择性更新"还是"整行覆盖"?先用**单字段**试探 |
183
+ | 打码字段污染 | 请求体里手机号 / 身份证是打码值吗?整行覆盖会把真实值**写成星号** |
184
+ | 名字带 `save` / `insert` / `create` | 可能是插入!可能产生重复记录,**不要盲试** |
185
+ | 不可逆 | 有没有软删除?改动可回滚吗? |
186
+ | 计费 / 权限 | 绕过的是**自检 / 展示**,还是**计费 / 资格**?后者绝对不碰 |
187
+
188
+ **Gate 4**:用户目标达成,且**没有产生任何脏数据**(或已明确记录并规划了回滚)。
189
+
190
+ ---
191
+
192
+ ## Phase 5 · 收口与交接【目标:可还原、可复现、可追责】
193
+
194
+ 1. **还原环境(顺序不能错)**:先关系统代理(`bx_sysproxy off`),**再**停代理进程(`bx_proxy stop`)。
195
+ 顺序反了会在杀进程的瞬间断网 —— 还原动作本身不依赖网络,所以还能救回来,但会吓人一跳。
196
+ 2. **固化证据**:全量流量存档(`.mitm`)+ 变更 diff + 精确到秒的时间线 + 复现步骤。
197
+ 3. **写一份"无法推诿"的工单**(结构见下)。
198
+ 4. **顺手做安全披露**:排查过程中常会发现鉴权缺陷。**只披露,不利用。**
199
+
200
+ **工单结构(让对方无法用"你重新填一下"打发你)**
201
+
202
+ | 段 | 内容 | 要求 |
203
+ | --- | --- | --- |
204
+ | 1 上下文 | 账号 ID、已完成的绑定 / 认证、客户端版本、时间戳 | 精确到可复现 |
205
+ | 2 现象 | 客观描述用户操作与反馈 | 不带情绪 |
206
+ | 3 **已排除项** | 重登 / 重装 / 换端 / 清缓存 / 换网络,逐条说明**为什么必然无效** | 堵住"你试试重装" |
207
+ | 4 证据 | 抓包结论 + **字段级前后 diff** | 数据说话 |
208
+ | 5 定位 | 指向具体接口 / 字段 / 行为 | "字段白名单不含 X" |
209
+ | 6 诉求 | 可执行的 SQL / 明确的修复项 | 别写"请尽快解决" |
210
+ | 7 附带 | 发现的安全问题 | 负责任披露 |
211
+
212
+ ---
213
+
214
+ ## 一页速记
215
+
216
+ ```
217
+ 症状:显示有值,但提交失败 / 提示无法完成
218
+ └─ ① 搜响应体:提示语在不在里面? → 定层(前端 or 服务端)
219
+ └─ ② 抓包:请求发出了吗?
220
+ 未发出 → 前端拦截 → 诱出法(注入上游响应,逼它发包)→ 拿到接口格式
221
+ 已发出 → 读响应 + 变更审计(改前改后 diff)→ 看哪个字段被丢
222
+ └─ ③ 目标字段能写吗?
223
+ 能写 → 直接调接口写进去(最小侵入)
224
+ 不能写 → 那道闸门是客户端门禁吗?
225
+ 是 → 篡改响应解除拦截,业务照常走
226
+ 否 → 停手,走人工 / 工单
227
+ └─ ④ 收口:先关代理再杀进程 → 存档 → 写一份带 diff 和 SQL 的工单
228
+ ```
@@ -0,0 +1,76 @@
1
+ # `rules.json` schema 与纪律
2
+
3
+ 规则文件固定 `<workspace>/.fly-blackbox/rules.json`;`bx_proxy start` **只读并校验**它,
4
+ 校验不过就**拒绝启动**。写入者是 agent / 用户,用现成的文件工具写。
5
+
6
+ ---
7
+
8
+ ## 1. 顶层
9
+
10
+ ```json
11
+ {
12
+ "enabled": true,
13
+ "hosts": ["<业务域名>"],
14
+ "watch": { "paths": ["…"], "log_bodies": true, "log_all_requests": true, "noise_hosts": ["…"] },
15
+ "patch_request": [ { "path": "…", "methods": ["POST"], "mode": "set_if_empty", "set": { "…": "…" } } ],
16
+ "patch_response": [ { "path": "…", "mode": "force", "set": { "…": true } } ]
17
+ }
18
+ ```
19
+
20
+ | 键 | 类型 | 语义 |
21
+ | --- | --- | --- |
22
+ | `enabled` | boolean | **`false` = 显式关掉注入**(只记录不改包)。缺省(不写这个键)= 注入口打开但只按 `patch_*` 里的规则动 |
23
+ | `hosts` | string[] | 生效的 host 子串白名单;**空数组 = 不过滤**(所有 host 都处理) |
24
+ | `watch` | object | 只读记录:`paths`(正则数组,命中就记完整请求 / 响应)、`log_bodies`、`log_all_requests`、`noise_hosts`(日志降噪) |
25
+ | `patch_request` | object[] | 改**请求**(补字段 / 改值 / 绕前端校验) |
26
+ | `patch_response` | object[] | 改**响应**(解除客户端门禁 / 伪造状态 / 诱出前端请求) |
27
+
28
+ ---
29
+
30
+ ## 2. `patch_request` / `patch_response` 条目
31
+
32
+ | 字段 | 必填 | 语义 |
33
+ | --- | --- | --- |
34
+ | `path` | ✅ | **正则字符串**(大小写不敏感),对请求路径做 `search` |
35
+ | `methods` | — | 仅 `patch_request`:只对这些方法生效,缺省 `POST` / `PUT` / `PATCH` |
36
+ | `mode` | — | `set_if_empty`(缺省)/ `force` / `only_missing` |
37
+ | `set` | ✅ | 要写入的 `{字段: 值}` |
38
+ | `create_missing` | — | 配合 `mode: "only_missing"`:键不存在时新增 |
39
+
40
+ **三种 mode 的差别(选错就白干)**
41
+
42
+ | mode | 什么时候写 | 典型用途 |
43
+ | --- | --- | --- |
44
+ | `set_if_empty` | 原值是"空"(`null` / `""` / `0` / `"0"` / `[]` / `{}`)时 | 补一个前端认为"没填"的字段 |
45
+ | `force` | 无条件覆盖 | 解除客户端门禁(`state` / `confirm`) |
46
+ | `only_missing` | 键**不存在**时(需 `create_missing: true`) | 补一个上游根本没有的键 |
47
+
48
+ > **`0` 与 `"0"` 都算"空"** —— 这正是 T5 假值陷阱的来源,也是 `set_if_empty` 的判定口径。
49
+ > 要让 `0` 被写进去,用 `mode: "force"`,或注入**字符串** `"0"`(JS 真值,能同时过判假与判空)。
50
+
51
+ ---
52
+
53
+ ## 3. 纪律(每条都有代价)
54
+
55
+ 1. **永远先只开 `watch` 跑一遍**,看清真实流量,再加 `patch`。
56
+ 2. **`patch` 一次只动一个字段**,并事先写明"观察点"(界面显示?发包?响应码?)。
57
+ 3. **显式 off、无静默默认值**:关掉注入必须写 `{"enabled": false}`。
58
+ **空文件是非法**(`bx_proxy start` 会拒绝启动)—— 反面教材:一次性脚本 `set_inject.sh off`
59
+ 写了个空文件,addon 于是**静默回退到默认注入**继续改包,排查结论跟着一起坏掉。
60
+ `bx_proxy start` / `bx_doctor` 都按这条口径校验并回显 `rules: none / invalid / valid`。
61
+ 4. **热加载**:改完保存即生效,不用重启代理(addon 按 mtime 重读;加载失败沿用旧规则并大声报错)。
62
+ 5. **存档优先于篡改**:响应先落盘再改,证据不能被自己改没了。
63
+ 6. **规则文件不进仓库、不带真实标识**:`hosts` 写企业域名前先想清楚这份文件会不会被提交。
64
+
65
+ ---
66
+
67
+ ## 4. 校验失败会看到什么
68
+
69
+ `bx_proxy start` 拒绝启动并给出路径 + 原因,例如:
70
+
71
+ - `规则文件是空的:<path> —— 空文件曾经静默回退成"默认注入",所以这里一律当非法。要关掉注入请显式写 {"enabled": false}`
72
+ - `规则文件不是合法 JSON:<path> —— <解析器原话>`
73
+ - `patch_response[0] 缺少 path(正则字符串):<path>`
74
+ - `patch_response[0].mode 只能是 set_if_empty / force / only_missing:<path>`
75
+
76
+ 示例文件见 `code/rules.example.json`(含 `_说明` / `_场景` 这类注释键,addon 会忽略它们)。