小程序sign逆向和渗透两种思路,总有一款适合你
文章首发奇安信攻防社区:https://forum.butian.net/share/3815
本公众号技术文章仅供参考! 文章仅用于学习交流,请勿利用文章中的技术对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失,均由使用者本人负责。 |
1. sign发现和排查思路
起因是在做小程序渗透的过程中,改了数据重新发包显示系统异常,数据包如下图所示,猜测可能是signature
这行是sign校验,所以才会出现异常
然后因为正常请求显示的是code无效
,且请求头中存在大量参数,于是为了排除干扰就开始依次修改请求头中的每一组值再发包,如果修改后发包显示系统异常,那就证明这个sign值校验跟这个请求头也有关,最后如下图所示,初步判断请求头中有四处busi-timestamp
、busi-noncestr
、busi-appid
、json请求体
、这四处变一下响应就变成了系统异常,所以盲猜busi-signature
就是根据他们四个的值进行计算得来的
2. 解开微信devtools进行debug获取加密逻辑
接下来使用第一种方式进行破译sign校验,虽然这个方法非常简单,但是其实这个方式我并不推荐,因为它是需要解开微信小程序devtools,存在封号风险,所以如果要尝试一定要使用自己的微信小号、同时注意一定不要使用虚拟机!!!
我这里远控我另一台电脑登录我的微信小号,工具地址:https://github.com/JaveleyQAQ/WeChatOpenDevTools-Python
(这个工具使用过程中可能存在一些bug,不过好在聪明的我都解决了)
打开微信后执行
WechatOpenDevTools-Python.exe -x
成功打开devtools
然后我这里直接快捷键,全局搜索busi-signature:
,但是发现其存在bug,进度条一直卡住,不过好在js文件也不多,我就挨个点进去单个js文件的去搜索,然后这里有个点比较坑,就是你一定要触发对应js功能,Sources
才加载对应js,你才能搜到,一定要注意,最后在appContext--usr--appservice.app.js里面发现了熟悉的代码
上图代码分析,首先要知道busi-appID
是写死的后续不会改变,然后可以看到busi-signature
的值是a,而a又是一个函数计算得到的,我们直接跟进这个函数,如下,可以看到它先创建个HMAC对象秘钥是t,然后对e进行了计算,这里直接debug
debug后,我们将秘钥拿出来,这里秘钥我们直接获取到了,是GZR3qIxxx
(其实我们可以看之前调用该函数那张图片,上方有个变量r,它其实就是秘钥,它的值是一堆值的相加之和,那个我之前也简单看了一眼都是字符串加在一块)
然后开始查看变量e,看着乱七八糟但实际上也很简单,分成四部分,第一部分就是请求体的json参数,第二部分就是请求头中的时间戳,第三部分就是请求头中的noncestr,第四部分就是请求地址了,然后字符串中间用n
拼接起来,就是最终的值e了
3. 还原js后硬逆
接下来的方式就比较头铁了,它不能debug,而是利用wxapkg
还原出js然后硬逆,这种方式有一点好处就是不用担心封号风险,还原wxpakg的方式有很多,之前我都是手动找然后手动还原的,现在也是有师傅写了工具,非常简单:https://github.com/wux1an/wxapkg
这个程序有一点就是它寻找微信的路径是默认路径,由于我的电脑微信的路径非默认路径,所以我改了下代码重新编译了下
wxapkg.exe scan
直接扫描,选择后回车直接解密
同理我这里也是从0开始,用vs code(当然你也可以用小程序开发者工具,看个人习惯)打开后全局搜busi-signature:
,它在一个函数里面
把这个函数拿出来整理一下,如下,接下来我们挨个去分析,首先busi-appId
这个是一个固定的值所以没什么好说的,然后busi-timeStamp
就是时间戳这个也没什么好说的
其次就是busi-nonceStr
,其实有经验的就知道它是个随机字符串,busi-signature
的计算依赖于noncestr,但是noncestr是什么其实没多大关系,接下来我们可以简单看一眼,busi-nonceStr
怎么来的,代码在前面,我这里直接粘过来
最重要的还是busi-signature,而它是根据函数encryptWithHmacSHA1计算出来的,这个函数有两个参数,我们先来看简单的r,也就是秘钥,r的赋值就在上方的语句中
归根结底也是一堆字符串的拼接,但是有个问题就是我们分别得找这些对象c
、l
、f.k3
我们不知道这些对象是谁
r = "".concat(c.k1).concat(l.k2).concat(f.k3.slice(2)).concat(c.k4),
其实找他们也很简单,有一个便捷方法, 但是讲解便捷方法之前,我先硬扣一下k1和k4,而他们属于对象c,那么对象c在哪里呢,我们根据这个代码往前回溯,如下图之前分析的所有代码其实都隶属于38
,而看到如下图所示的代码后,其实一眼看出这就是构建工具生成的代码,38
代表38这个模块,而C
的值其实也能看到是n(39)
,所以可以知道n大概就是模块加载器,而我们想知道的c对象其实就是39
这个模块
所以我们直接搜索39:
,至此k1和k4拿到
然后回想我们刚才的代码如下图,现在还剩下标红的两部分,那么这块我们就用我们上文中提到的简便思路,其实非常简单,直接搜索k2 =
和k3 =
k2如下
k3如下
所以最后拼接得到秘钥(要注意k3用了slice(2)函数,取得是从索引为2位置开始的字符串)
GZR3qIBe^lC@I!KpkyBtEg$&@0UncJpr
秘钥得到了,接下来就该研究,另一个参数n了,其实也很简单,把之前的图拿来,我们重点看红框部分
为了更加直观,我把代码粘贴出来并修改了下格式,下方代码判断t.method
是否是get,如果是get就按照第一行计算,如果不是就按照第二行计算,其实这里就能看出来这个网站对于GET请求和POST请求计算是不一样的
//这里判断如果t.metchod是get,这里t不能猜出就是当前http对象,n就等于后续的字符串相加,s通过上图可以看出是get请求中url的参数部分,d通过上上张图片可以得出是busi-timeStamp的值,y则是busi-nonceStr,后续又加上了请求中的去除参数的url部分
if (n = "get" == t.method ? (s || "") + "n" + d + "n" + y + "na671602347467n" + (t.url.includes("?") ? t.url.split("?")[0] : t.url) + "n"
//这里类似,非get请求走的是下面的代码, 跟上面类似,唯一一个区别就是最开始的字符串是t.data,也就是post请求中的请求参数
: (JSON.stringify(t.data) || "") + "n" + d + "n" + y + "na671602347467n" + (t.url.includes("?") ? t.url.split("?")[0] : t.url) + "n",
其实从上面的讲解不难看出,虽然sign加密对get请求和post请求加密逻辑不一样,不过后面的值是相同的都是timeStamp的值、nonceStr、url,区别就在于最前面那个psot请求是post的参数,get请求是get参数,不过get的参数值拿来其实不能直接用,往后看就明白了
这里我们将参数n和r都拿到了,就剩encryptWithHmacSHA1
函数还不知道,直接一搜就明了了,如下图
至此,我们把整体的逻辑通过js代码还原了出来
==注意:==
有两点需要注意,第一点就是不管是POST请求还是GET请求,计算sign都要用到url,而请求包中有些请求url后面要拼接一些参数,所以当计算的时候要去掉,不过非常贴心的一点是,这个网站发的请求包中存在一个请求头url
,可以看到值是已经是去掉参数后的路径,非常贴心,我后面写脚本的时候取得也是这个值
第二点就是,GET请求稍微复杂一点,在后来的测试中发现有两个点需要注意,一个就是排序它会将参数排序也就是下图那个部分的代码处,这个代码我就不具体说了,跟大家讲下实际效果
实际效果如下
//正常请求的参数如下时
dd=333&ab=123&bb=222&aa=111
//计算前需要重新排序,根据键名的首字母顺序来,如下
aa=111&ab=123&bb=222&dd=333
//还有一点很重要有些请求会带一个v=172912323,要把这个v去掉,v的值也就是时间戳,但是这个时间戳跟请求头中的时间戳不通,不过好在这个v没有参与运算
//例子如下
v=172912323&=333&ab=123&bb=222&aa=111
//实际计算
aa=111&ab=123&bb=222&dd=333
4. 利用
这里提到利用的话,通常按照之前的习惯我肯定是用mitmproxy
,代码如下,已经写好注释了
python代码(mitm_sign.py)
import json
import time
import execjs
def str_sort(word):
pairs = word.split('&')
param_dict = {}
# 将参数对解析为字典
for pair in pairs:
key, value = pair.split('=')
param_dict[key] = value
# 如果有参数 'v',则将其移除
if 'v' in param_dict:
del param_dict['v']
# 对字典中的项进行排序
sorted_pairs = sorted(param_dict.items())
# 将排序后的参数对重新组合为字符串
sorted_word = '&'.join([f"{key}={value}" for key, value in sorted_pairs])
return sorted_word
#截获请求加密data,以及更新sign
def request(flow):
json_data = ''
sign = ''
# url无参数
url = flow.request.headers.get('url', '')
# noncestr
noncestr = flow.request.headers.get('busi-nonceStr', '')
url_header = flow.request.headers.get('url', '')
# 时间戳
now_time = str(time.time()).replace('.', '')[:13]
# 加载js文件
with open('huishang.js', 'r', encoding='utf-8') as f:
js_file = f.read()
# 加载执行环境
js_exec = execjs.compile(js_file)
#这里如果是post请求那么json_data就是请求体中的json数据
if flow.request.method == "POST":
#获取json数据,不为空直接赋值,为空的话会抛异常那就手动赋值为空字符串
try:
json_data = flow.request.json()
except Exception as e:
json_data = ''
sign = js_exec.call('get_sign', url_header, json.dumps(json_data).replace(' ', ''), noncestr, now_time)
#如果是get则变成url参数并去v后排序得到的值
if flow.request.method == "GET":
url = flow.request.pretty_url
if "?" in url:
# 获取参数部分
params = url.split("?", 1)[1].strip()
json_data = str_sort(params)
sign = js_exec.call('get_sign', url_header, json_data, noncestr, now_time)
#更改时间戳和sign
flow.request.headers['busi-signature'] = sign
flow.request.headers['busi-timestamp'] = str(now_time)
js代码(exec.js)
const crypto = require('crypto');
secretKey = 'GZR3qIBe^lC@I!KpkyBtEg$&@0UncJpr'
function get_sign(url,jsondata,noncestr,now_time){
e = jsondata
+'n'+now_time
+ 'n'+noncestr
+ 'na671602347467'
+ 'n'+url
+ 'n'
const hmac = crypto.createHmac('sha1', secretKey);
hmac.update(e)
sign = hmac.digest("hex")
return sign
}
然后执行
mitmproxy.exe -p 8081 -s .mitm_sign.py
之后就可以随意发包了不用担心sign了
5. 结语
像微信小程序目前有很多项目或思路能强制打开devtools,这样我们debug去调试非常简单就能还原加密过程,但是一些其它APP为载体的小程序,目前还没有思路,如果有思路的师傅可以指点一下
原文始发于微信公众号(小惜渗透):小程序渗透 小程序sign逆向的两种思路
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论