Files
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

20 KiB
Raw Permalink Blame History

api-recon — 参考手册

Grep 配方、config.json 模板与排障。所有 grep 针对 js/ 目录执行。bundle 单行时可先 js-beautify 或 sed 's/}/}\n/g',通常带上下文窗口的 raw grep 即可。

脚本说明

scripts/ 内所有文件均为参考模板,执行前必须按目标站点调整。典型改动点:

脚本 常见需调整项
harvest_static.py endpoint 正则、webpack/Vite manifest 解析、微前端 publicPath、重试/并发
runtime_harvest.js neutralize 字段名与成功值、stub 匹配规则与 body 结构、routes 来源、WS 录制、waitUntil/routeTimeout/proxy
preload.js loginPathRe、L1 stubs、neutralize.fields、apiPattern、是否启用 L3、recordDetail、observe.*、neutralizeVueRouter
spider_mpa.py --exclude 破坏性链接、cookie、depth/max、同域过滤
extract_route_map.py routeMap / routeLink 正则、KEY 命名模式
build_perm_tree.py userRouteAuth 解析、ROOTS/PREFIX_PARENT 层级启发式、stub 外层字段名
config.json 以上全部站点专属参数的统一入口

调整后的文件建议放在任务工作目录(如 recon/),报告中注明相对参考脚本的具体改动。


A. 逆向三道门

A1. 渲染门 — 「如何判断已登录?」

grep -rhoaE '.{0,40}(isLogin|isAuthenticated|loggedIn|hasLogin|requireAuth)\b.{0,80}' js | head
grep -rhoaE 'function (getUser|getToken|getAuth)[0-9]?\([^)]*\)\{.{0,200}' js | head
grep -rhoaE '(localStorage|sessionStorage)\.getItem\("[^"]+"\)' js | sort -u
grep -rhoaE '(Cookies?|cookie)\.(get|load)\("[^"]+"\)' js | sort -u
grep -rhoaE '\batob\(|JSON\.parse\(|jwt|decode' js | head

找链路 isLogin = f(getUser()) → getUser = decode(storage.read(KEY)),确定 存储键、容器(Cookie vs localStorage)、编码:

编码 config 伪造方式
明文字符串 / "1" / token "value": "anything-truthy"
JSON.parse(x) "value": "json:{\"id\":1,\"username\":\"admin\"}"
JSON.parse(atob(x)) "value": "b64json:{\"id\":1,\"username\":\"admin\"}"
JWT 无签名/alg:none JWT,或 bundle 内密钥签名
加密(SM2/AES/RSA) 找硬编码密钥;渲染门仅需可解码 blob 时可 forge;否则静态兜底

→ 写入 cookies / localStorage。

A2. 拦截器门 — 「什么触发跳 /login?」

grep -rhoaE '.{0,60}(interceptors\.response|axios|request\.use).{0,120}' js | head
grep -rhoaE '.{0,40}(response_code|errcode|errno|\bcode\b|\bret\b|\bstatus\b)\s*[=!]==?\s*[\-0-9]{1,4}.{0,60}' js | head -20
grep -rhoaE '.{0,40}(未登录|请重新登录|登录已过期|unauthorized|登录失效|授权|token.{0,10}invalid).{0,40}' js | head
grep -rhoaE '.{0,30}(location\.href|router\.(push|replace)|navigate)\([^)]*login[^)]*\)' js | head

确定:字段名、成功值(通常 0 或 200)、触发跳转的失败值。用 junk session 验证:

curl -sk -X POST -H 'Cookie: <fakekey>=junk' https://target/api/<protected> -d '{}' -H 'Content-Type: application/json'

→ 写入 neutralize.fields + neutralize.success。

A3. 内容门 — 「菜单/权限从哪来?」

grep -rhoaE '"/api[^"]*(permission|perm|role|menu|acl|resource|nav)[^"]*"' js | sort -u
grep -rhoaE '.{0,30}(menus|permissions|menuList|routeList|authList|role_permissions)\b.{0,120}' js | head
grep -rhoaE 'userRouteAuth|getResultTree|routeMap|routeLink|hasPermission|checkAuth' js | head
grep -rhoaE '([A-Z_][A-Z0-9_]*):\{name:"[^"]*",link:"/[^"]+"\}' js | head

两层数据(常见企业后台):

API 典型 payload 消费方
.../role_permissions { permissions: string[], role_type } 路由守卫、按钮级 ACL
.../permissions/all tree[{ code, position, children }] 侧栏菜单渲染
bundle 内 userRouteAuth { CODE: { url, name? } } code → 前端 path
bundle 内 routeMap { KEY: { name, link } } 别名解析(webpack o.DASHBOARD)

读消费方代码确认:getResultTree(tree, permissions) 如何过滤、v-if / hasAuth(code) 检查哪个字段。

手工 forge(小站点):构建 permissive payload → stubs。

完整权限树还原(大站点,侧栏/子模块仍空白):见 I 节。


B. config.json 模板

{
  "baseUrl": "https://target/",
  "runtimeMode": "both",
  "chromium": "/usr/bin/chromium",

  "cookies": [
    { "name": "auth", "value": "b64json:{\"id\":1,\"username\":\"admin\",\"role\":\"admin\",\"func\":{},\"permissions\":[\"*\"]}" }
  ],
  "localStorage": { "token": "faketoken", "isLogin": "1" },

  "neutralize": {
    "fields": ["response_code", "code", "errno", "ret", "status"],
    "success": 0,
    "flags": { "success": true, "message": "ok" }
  },
  "forward": true,
  "loginUrlPattern": "/login",
  "apiPattern": "/api/|/rest/|/graphql",

  "mockTier": "L1+L2",
  "recordDetail": true,
  "observe": {
    "storageReads": false,
    "cookieReads": false,
    "xhrHeaders": true
  },
  "neutralizeVueRouter": true,
  "stubs": [
    {
      "match": "permissions/all|/menu|role_permissions",
      "body": {
        "response_code": 0, "code": 0,
        "data": {
          "permissions": ["*"],
          "menus": [
            { "name": "dashboard", "path": "/dashboard", "show": true, "children": [] },
            { "name": "alert", "path": "/alert", "show": true, "children": [] }
          ]
        }
      }
    }
  ],

  "explore": {
    "clickTabs": true,
    "clickTables": true,
    "pushStateFallback": true,
    "maxMenuItems": 50
  },

  "routes": ["/dashboard", "/alert", "/asset", "/device", "/report", "/config", "/system"],
  "waitMs": 1500, "perRouteMs": 900, "headless": true,
  "waitUntil": "domcontentloaded",
  "routeTimeout": 12000,
  "proxy": "",

  "captureResponses": true, "recordWs": true, "respMax": 600
}

字段说明:

  • runtimeMode:depth(Puppeteer)、coverage(browser MCP)、both
  • cookies[].value 前缀:b64json: → base64(JSON);json: → 原始 JSON;无前缀 → 字面量
  • forward: true 转发真实请求并改写码字段;false 完全离线 stub
  • mockTier:coverage 模式 preload 启用层级,如 L1+L2、L1+L2+L3
  • routes 来自 routes.txt;forge 菜单后 harness 自动追加 <a href>
  • captureResponses / recordWs 仅 depth 模式有效
  • waitUntil:大型 SPA 用 domcontentloaded,避免 networkidle2 挂起
  • routeTimeout:单路由 page.goto 超时(毫秒)
  • proxy:Puppeteer --proxy-server;也可设 HTTP_PROXY / HTTPS_PROXY

B1. 双 stub 模板(role_permissions + permissions/all)

"stubs": [
  {
    "match": "role_permissions",
    "body": {
      "response_code": 0,
      "data": {
        "permissions": ["MONITOR", "MONITOR_ALERT", "THREAT", "ASSETS_RISK"],
        "role_type": "SUPER_ADMIN"
      }
    }
  },
  {
    "match": "permissions/all",
    "body": {
      "response_code": 0,
      "data": [
        {
          "code": "MONITOR",
          "position": 1,
          "children": [
            { "code": "MONITOR_ALERT", "position": 1, "children": [] }
          ]
        }
      ]
    }
  }
]

外层字段名(response_code / code / data)须与 A2 拦截器门一致;permissions 须覆盖 tree 中所有 leaf code。


C. coverage 模式:preload 配置

编辑 scripts/preload.js 顶部 CONFIG 对象,或通过 CDP 注入前替换:

const CONFIG = {
  loginPathRe: /\/(login|signin)(\/|$|\?)/i,
  mockTier: 'L1+L2',
  forward: true,
  recordDetail: true,
  extractUrlsFromResponse: true,
  neutralizeVueRouter: true,
  observe: { storageReads: false, cookieReads: false, xhrHeaders: true },
  neutralize: { fields: ['response_code', 'code'], success: 0 },
  stubs: [ /* 同 config.json stubs */ ],
  apiPattern: /\/(api|apis|v\d+|dev|internal|graphql)\//i,
};

验证:window.__API_RECON_PRELOAD__ === true 且 pathname 稳定。

导出录制结果:

JSON.stringify({
  apis: [...window.__API_RECON_LOG__],
  detail: window.__API_RECON_DETAIL__,
  routes: [...(window.__API_RECON_ROUTES__ || [])],
  observe: window.__API_RECON_OBSERVE__,
}, null, 2)

D. preload / runtime Hook 能力

preload(coverage)与 runtime_harvest(depth)内置的浏览器 Hook 能力及覆盖范围:

Hook 能力 对 API 发现的价值 覆盖
Hook fetch / XHR.open 录请求 URL/方法 ✅ recordDetail + __API_RECON_LOG__
Hook XHR.setRequestHeader 发现 Authorization 等头 ✅ observe.xhrHeaders
Hook localStorage/cookie 读 确认会话键名 ⚠️ 可选 observe.storageReads/cookieReads
Vue 获取路由 补全 frontendRoutes ✅ __API_RECON_ROUTES__(已加载路由)
Vue 路由守卫中和 / 登录跳转阻断 撑开模块触发 API ✅ neutralizeVueRouter + 原生跳转中和
React 获取路由 补路由 ⚠️ 静态 + 点击;无专用 Hook
页面跳转阻断(登录 path) 留页分析 ⚠️ 仅阻断登录 path,避免挡业务导航
Hook 加密库(CryptoJS/SM 等) 加密参数 → 明文 API body ❌ 须手工 Hook 加密函数入参;结论写 config
反调试 bypass 否则 runtime 录不到 API ❌ 须手工处理;静态仍可用

E. Endpoint 提取正则(静态过少时)

在 harvest_static.py 的 extract_endpoints 放宽,或手动:

grep -rhoaE '"/[a-z][A-Za-z0-9_/\-]{3,}"' js | sort -u
grep -rhoaE '/api/[a-zA-Z0-9_./-]+' js | sort -u

F. 排障

现象 原因 → 处理
静态 API 很少 endpoint 方言不匹配 → 放宽正则(D 节)
chunk 数 ≪ manifest CSS-only 或未部署 chunk;404 已重试
runtime 仍显示登录页 渲染门错误 → 复查 A1:键名、容器、编码、domain
进壳但模块空白 内容门 → forge 菜单(A3);routes path 可能不对
每路由只有 bootstrap/locale 权限码不全 → I 节权限树还原;检查 role_permissions + permissions/all 双 stub
侧栏有项但子页空白 tree 缺 intermediate 节点或 code 与 userRouteAuth 不一致
每个 API 都跳登录 拦截器门 → 确认 neutralize;嵌套字段需扩展 walk 逻辑
WS 帧为 0 需用户交互后才 subscribe;加长 perRouteMs
响应体空 仅 forward: true 时有真实响应
Chromium 缺失 安装 chromium 或设置 config.chromium / CHROMIUM
Mock 很多仍回登录 Hook 太晚或缺 location.href setter → document-start + preload
列表全空 L3 空数组正常;继续点 Tab/设置/详情
误把 Redux action 当路由 过滤含 get/set/change/clear/toggle/upload 的内部 path
Vue 仍跳登录 preload 非 document-start → 改注入时机;或 neutralizeVueRouter: false 时手动清守卫
响应里有 URL 但未进 log 开 extractUrlsFromResponse;或从 __API_RECON_DETAIL__ 人工提取
不知 Authorization 头名 开 observe.xhrHeaders 或 DevTools 查看请求头
runtime 极慢 / 超时 改 waitUntil: domcontentloaded;降 routeTimeout;勿用 networkidle2
代理连接失败 检查 proxy / 环境变量;Puppeteer 与 curl 代理端口一致

G. hardened 目标

服务端逐步校验会话(不可 forge 的签名 cookie、服务端渲染且不可 stub 的菜单)时,runtime 会在 shell 处卡住。预期行为:

  • 静态足够做 endpoint 枚举 — 模块 path 在代码里
  • 若授权允许,用真实会话跑同一 harness:forward: true、无需 neutralize,捕获真实 methods/params/responses

H. 单次任务清单

  1. 确认授权范围
  2. 阅读 scripts/harvest_static.py → 按目标调整 → 运行 → 审 api_static.txt、routes.txt
  3. Phase 1b:path 锚点扩窗 + 绑定层 → param_candidates.json(J 节)
  4. 逆向 A1/A2/A3 → 写站点专属 config.json
  5. 阅读并调整 runtime_harvest.js / preload.js 后再执行
  6. runtimeMode=depth:npm install → 运行调整后的 harvest 脚本
  7. runtimeMode=coverage/both:document-start 注入调整后的 preload → browser MCP 动态枚举 + 参数触发矩阵
  8. 模块不渲染 → I 节权限树还原 → patch stubs → 重跑
  9. 参数多样本 diff + 错误反推 → params_merged.json
  10. 合并 → site_map.json + api_merged.txt,诚实标注覆盖、缺口及脚本改动点

I. 权限树还原(Phase 4 深化)

当 forge 简单 menus: [{ path, show: true }] 无效、子模块仍不 mount 时使用。

I1. 定位 auth 模块

grep -l 'userRouteAuth' js/*.js
grep -l 'routeMap\|routeLink' js/*.js
grep -rhoaE 'getResultTree|role_permissions|permissions/all' js | head

记录:权限 API path、响应字段名、消费 chunk 文件名。

I2. 提取 routeMap

python3 scripts/extract_route_map.py recon/js recon/
# 产出 recon/route_map.json

若 [!] no routeMap pattern found:放宽 extract_route_map.py 中正则,或手工 grep:

grep -rhoaE '([A-Z_][A-Z0-9_]*):\{name:"[^"]*",link:"/[^"]+"\}' js | head -20

I3. 构建权限树 + stub

python3 scripts/build_perm_tree.py recon/js recon/ --config recon/config.json

脚本逻辑:

  1. 解析 userRouteAuth={MONITOR:{url:...},...}(含 webpack 别名 He=o.DASHBOARD)
  2. 用 route_map.json 解析 alias → 真实 path
  3. 按 code 前缀推断 parent(MONITOR_ALERT → MONITOR)
  4. 输出 permissions_tree.json、permissions_all_stub.json、role_permissions_stub.json
  5. --config 时自动写入 config.json 的 stubs 与扩展 routes

按目标调整(在脚本顶部):

  • DEFAULT_ROOTS:顶级模块 code 列表
  • DEFAULT_PREFIX_PARENT:PREFIX_ → parent 映射
  • DEFAULT_EXTRA_PARENT:非前缀关系的 orphan 节点

I4. 校验 stub 一致性

# permissions 数量应 ≈ userRouteAuth 条目数
wc -l recon/perm_codes_all.txt
# routes 应覆盖 route_map 全部 link
python3 -c "import json; m=json.load(open('recon/route_map.json')); r=set(json.load(open('recon/config.json'))['routes']); print('missing', [v['link'] for v in m.values() if v['link'] not in r])"

I5. 重跑 runtime 并对比

node recon/runtime_harvest.js recon/config.json
# 对比 forge 前后 runtime_api.json 条数;检查 /attack、/asset 等是否出现模块 API
forge 前 forge 后(成功)
每路由相同 3–5 条 bootstrap 不同路由触发不同 module API
仅 /api/locale/language 出现 /api/web/... 模块 endpoint
routes.txt 个位路由 routes 80–110+ 来自 route_map

I6. 仍失败时

  • coverage 模式:点击侧栏 + Tab,权限 gating 可能在交互后才请求
  • stub 字段:对比真实 API(curl + 真实 session)与 stub 的 nesting
  • 额外守卫:grep hasPermission|checkRole|func. 等按钮级检查,扩展 role_permissions.permissions
  • 静态兜底:模块 API path 仍在 api_static.txt,runtime 仅补 METHOD/body;参数保留 param_candidates.json + 已录样本

J. 参数逆向(Phase 1b / 5b / 5c)

方法论,非通用脚本。 找 path 用正则;找参数用锚点扩窗 + UI 绑定链 + 多样本 diff + 错误反推。

J1. 锚点扩窗 — 从 path 找组包对象

# 以 Phase 1 已知 path 为锚
grep -n '"/api/user/list"' js/*.js
grep -rhoaE '.{0,120}("/api[^"]+").{0,200}' js | head
grep -rhoaE '(params|data|body|payload)\s*:\s*\{' js | head
grep -rhoaE '(get|post|put|delete|patch)\([^,]+,\s*\{' js | head

J2. 包装层与传输形态

# axios / 统一 request
grep -rhoaE '(axios|request)\.(get|post|put|delete|patch)\(' js | head
grep -rhoaE 'interceptors\.(request|response)' js | head

# GraphQL
grep -rhoaE '(query|mutation)\s+\w+|gql`|graphql\(' js | head
grep -rhoaE '\$[a-zA-Z_]+\s*:\s*(Int|String|Boolean|\[)' js | head

# FormData / multipart
grep -rhoaE 'FormData|\.append\(' js | head

# 路径参数
grep -rhoaE 'path:\s*"/[^"]*:[^"]+"' js | head
grep -rhoaE 'useParams|route\.params|\$route\.params' js | head

J3. 校验门 — 必填 / 格式 / 枚举

grep -rhoaE '(required|message|pattern|enum|validator)\s*:' js | head
grep -rhoaE 'yup\.|zod\.|async-validator|Form\.Item|a-form-item|el-form-item' js | head
grep -rhoaE 'rules\s*:\s*\[|name:\s*["\'][a-zA-Z_]+["\']' js | head
grep -rhoaE 'label.*value|options\s*:\s*\[' js | head

J4. 绑定层 — 表单 → API

grep -rhoaE 'onFinish|handleSubmit|getFieldsValue|validateFields' js | head
grep -rhoaE '(pick|omit|transform|dayjs|moment)\(' js | head

runtime 补位:DevTools → Network → 请求 → 发起程序(call stack)从 fetch/send 往上追组包函数。

J5. 加密参数

grep -rhoaE 'encrypt|decrypt|sign|CryptoJS|sm2|sm3|sm4|RSA|AES' js | head

勿在密文上猜字段 — Hook 加密函数入参,在加密前录 plaintext payload;结论写 config.json / param_candidates.json。

J6. 参数触发矩阵(Phase 3 必做)

对每模块按操作各录一次,diff 请求 body/query:

操作 关注
列表首屏 分页默认值
搜索 keyword、filters
高级筛选 optional 字段
新建/编辑 完整 entity
批量/导出 ids[]、exportType
排序/翻页 sortField、order

产出 param_samples.json:[{ "path", "method", "action": "search", "body", "query", "headers" }]

J7. 置信度规则

置信度 条件
高 静态 callsite + runtime ≥2 样本一致
中 仅静态,或仅 1 次 runtime
低 响应/错误反推,未二次验证
待触发 静态已知字段,UI/权限未跑到

J8. 场景快配

场景 顺序
REST 列表页 J1 组包对象 → J6 四次 diff → J3 rules
新建/编辑表单 J3 Form name → J4 submit 链 → runtime 提交 + 故意留空看 400
GraphQL J2 variables 声明 → runtime 各 operation 录 variables
加密 body J5 Hook 入参 → 加密前字段即真实 params

J9. 与 api-recon 阶段映射

api-recon 参数 recon
Phase 1 静态 J1 锚点扩窗
Phase 2 A2 拦截器 全局注入字段(tenantId、sign)
Phase 3 runtime J6 触发矩阵 + param_samples.json
Phase 4 权限树 不同模块表单不同 → 权限够才触发全字段
Phase 5 合并 params_merged.json + 置信度;勿单样本定必填

J10. 排障

现象 处理
静态有字段名 runtime 从未出现 标注「待触发」;补权限树 / 点高级筛选 / 联动 select 各 option
同 path 不同 body 形状 正常 — 按 action 分条记录,勿强行合并 schema
stub 响应假但想看 params 看 outbound 请求 body/headers,勿从 stub 响应反推
400 报 nested field 注意外层包装 data/bizData/variables
GraphQL 只见 operation 名 展开 variables JSON;静态找 $var: Type