来Track安全社区投稿~
千元稿费!还有保底奖励~( https://bbs.zkaq.cn)
前言
最近社区推广的有些太好了,有很多想看的文章,几年前的文章金币早就挥洒完了,所以这篇文章目的很明确,主要是换赏点金币。用最近遇到的比较多的一些加解密来水水案例。
工具说明
涉及的大部分都是小程序的逆向加密分析,反调试以及强开 debug 有多项工具,此处不多叙述,参考
- https://github.com/wux1an/wxapkg
- https://github.com/JaveleyQAQ/WeChatOpenDevTools-Python
其他文章
https://xz.aliyun.com/news/14066
yakit
其次是安利一下 yakit,这玩意在 webfuzz 方面真的很方便,用好热加载功能,重新生成签名和解密数据包变的和拼积木一样简单。我也是花了几天研究了一下,在后面案例中会体现她的好用和优雅。类似 burp 也有 autodecoder 或其他插件支持,但总体协调适配性上 yakit 是遥遥领先的。
https://yaklang.com/docs/yak-basic/string-basic
https://yaklang.com/products/Web Fuzzer/fuzz-hotpatch/
案例 1 简单 AES 加密+签名
简单的 aes 加密,直接 iv,key,模式写在前端的是最简单的,使用上述工具导出小程序源码善用 vscode 打开搜索即可。用 yakit 的热加载函数或者 webuffz 函数都能实现比较优雅的加解密。我想讲讲最近遇到的几个自带签名验证的情况。
如下是一个经典的越权,越权的点在 ID,但是存在时间戳和签名的校验。
先来看看她签名是怎么生成的。我对着代码看了半天也问了 chatgpt 半天,只知道是取参数再进行 md5,但是细则多少带点偏差,所以强制开启调试很有必要。
签名的关键函数如下
const m = (e, t) => {
const n = [];
if (e.url.indexOf('?') > 0) {
const t = e.url.split('?');
(e.url = t[0]), (e.params = { ...e.data, ...r.a.parse(t[1]) });
}
for (const c in e.params) {
let t = e.params[c];
('[object Null]' !== Object.prototype.toString.call(t) &&
'[object Undefined]' !== Object.prototype.toString.call(t)) ||
(t = ''),
t && 'object' === typeof t && (t = JSON.stringify(t)),
n.push(c + t);
}
n.sort();
const a =
e.data && Object.keys(e.data).length > 0
? `${JSON.stringify(e.data)}key${t}`
: `${n.join('')}key${t}`;
return u()(a);
},
加密函数如下
var f = n('8237'),
l = n.n(f),
p = n('4328'),
h = n.n(p),
b = n('3452'),
m = b.enc.Utf8.parse('e1c3xxxxxxd41666'),
g = b.enc.Utf8.parse('0000000000000000'),
v = function (e) {
var t = b.enc.Hex.parse(e),
n = b.enc.Base64.stringify(t),
r = b.AES.decrypt(n, m, {
iv: g,
mode: b.mode.CBC,
padding: b.pad.Pkcs7,
});
return r.toString(b.enc.Utf8);
},
k = function (e) {
var t = b.enc.Utf8.parse(e),
n = b.AES.encrypt(t, m, {
iv: g,
mode: b.mode.CBC,
padding: b.pad.Pkcs7,
});
return n.ciphertext.toString().toUpperCase();
},
当然如果你前端基础知识扎实,能一眼认出,那自然好说。
其实签名的格式就为格式其实就是 参数+值+key+时间戳
,解密的格式就是上述的 key:e1c3xxxxxxd41666,iv:0000000000000000,AES 的 CBC 模式。
格式已知,那么如果是你,你会怎么进行做下一步测试呢?python 脚步?burp 插件?或是什么,我也想学习一下大家的思路。
这里就用 yakit 演示会有多方便。
yakit 的热加载详情见文档,它是能满足在你发包之前对数据包进行处理并对返回的包再二次处理的,其次强大的 tag 功能能让你构造参数时事半功倍。
看看效果,关键的越权参数 id 的传入直接调用 默认的 tag ({{int}}会自动填充整数型数字去爆破),时间戳也可以直接利用默认的fuzztag 功能一键生成。关键就在签名了,我们来看看热加载函数的构造。
这里只列举了两个关键函数 handle_sign,afterRequest。
handle_sign 是返回标签函数的摘要值用来生成签名,是我们自己写的新函数。一开始估计大多数人思维是想着把参数全部传入这个函数然后生成好签名再返回,但其实这个热加载函数只能有一个传入。
但好在它 fuzz 能力够强,能再传参数的时候就构造好。热加载函数格式为 yak(handle|param)。他就像一个函数一样在你发包前提前处理一遍数据,正好适用于我们发包前处理好签名。
afterRequest 是官方自带的函数,他允许对每一个请求的响应做处理,其实更官方更建议用 mirrorHTTPFlow,时间紧,能用就不想动了。这里的 rsp 就是响应的 http 数据包,需要提取正文再解密,再拼接,其中的 poc、codec 库都可见官方文档。这里主要是用它来对返回包的加密数据进行解密,这样就能一目了然解密的结果,不用多此一举去解密。
// mirrorHTTPFlow 允许对每一个请求的响应做处理,定义为 func(req []byte, rsp []byte, params map[string]any) map[string]any
// 返回值回作为下一个请求的参数,或者提取的数据,如果你需要解密响应内容,在这里操作是最合适的
mirrorHTTPFlow = func(req, rsp, params) {
return params
}
这样就可以构建好热加载函数,如下。编写的按钮就在数据包的右上侧。
// 使用标签 {{yak(handle|param)}} 可触发热加载调用
handle_sign = func(param) {
// 在这里可以直接返回一个字符串
return codec.Md5(param)
}
// afterRequest 允许对每一个请求的响应做处理,定义为 func(origin []byte) []byte
afterRequest = func(rsp) {
key =b"e1c3xxxxx67d4666"
iv = b"0000000000000000"
rspIns = poc.ParseBytesToHTTPResponse(rsp)~
println(rspIns.StatusCode) // 响应状态码
body = io.ReadAll(rspIns.Body)~
// println() // 响应体
obj = codec.AESCBCDecryptWithPKCS7Padding(key, codec.DecodeHex(string(body))~, iv)~
dec_rsp = poc.ReplaceBody(rsp,obj,false)
// rsp = re.ReplaceAll(rsp, r"(?<=n)[A-F0-9]*$", obj)
return []byte(dec_rsp)
}
调试好热加载函数,效果如下,还是很优雅的吧,webfuzz 页面一键就能搞定签名和数据解密,顺利拿下越权信息泄漏。
案例 2 复杂签名
数据包大概长这样,签名生成比较复杂。也是一个越权,需要注意的参数都红框出来了。
签名的生成代码如下,大概是取了固定参数 appid 随机 nonce 再拼接请求的参数和 header 头,大概流程知道,但细则偏差很多,再调试下也搞了很久。
代码一:
var t = u.default.randomString(6),
a = {
timeStamp: Date.parse(new Date).toString(),
nonce: t,
appId: "wxf34xxxxbbe6e"
};
Object.assign(e, a);
var l = (0, s.default)(f(e)),
r = i.default.HmacSHA256(l.toString(), "h3xxxxxxxe8o"),
n = i.default.enc.Base64.stringify(r);
return v(v({}, a), {}, {
sign: n
})
代码二:
y.interceptors.request.use((function(e) {
var t = c.default.getAccessToken(),
a = Boolean(t),
l = e.params || {},
r = e.data || {},
n = g(g(g({}, Object.keys(l).length ? l : r), (0, h.querystringformart)(e.url)), {}, {
url: (0, h.sliceUrl)(e.url)
});
return a && (l = g({}, l), e.params = l, e.needToken && (e.header.accessToken = t)), e.header = g(g({}, e.header), {}, {
uuid: c.default.getUuid() || d.default.v1()
}, (0, h.AES_EN)(n)), console.log(e, "===================config================="), e
}),
看代码真的会头晕,坑点在几个点,一是 json 拼接好的顺序排序,二是部分参数类型为字符型需要加双引号,有的为 int 不需要加 "",三是还需要对部分数据为空去除该参数,而且由于是对字符串再 HmacHASH,单引号也不能写成双引号。这种签名没有调试条件构造起来比较困难。
对着调试页面调试细则费了点时间。
因为签名是跟着数据包生成的,规则为固定字符串拼接请求参数再进行排序在 HMAC
,再 HMAC 前大概是这么一段的签名数据。
{"appId":"wxf34xxxxxe6e","nonce":"qNADIr","timeStamp":"1717727308000","type":"DRAW_ACTIVITY","url":"xxxx/userPrizeList"}
{"appId":"wxf34xxxxxe6e","bizAccount":"1xxxxx7","channelId":11xxx,"client":"WECHAT_MP","extra":1,"nonce":"2uE5a6","remark":[{"remark":"","storeId":"1441211111158658"}],"selectBeansNum":0,"selectIntegralDictId":"","selectIntegralNum":0,"timeStamp":"1717817654000","url":"xxxxx/create/trade","way":"BUY_NOW"}
马马虎虎拼了个能用的代码,代码基础不是很好,见谅.........
// beforeRequest 允许发送数据包前再做一次处理,定义为 func(origin []byte) []byte
beforeRequest = func(req) {
// 取http请求的数据
httpuripath = poc.GetHTTPRequestPath(req)
httpuripath = re.ReplaceAll(httpuripath,"/huiyuan","")
httpbody = poc.GetHTTPPacketBody(req)
httpnonce = poc.GetHTTPPacketHeader(req,"nonce")
httptimestamp = poc.GetHTTPPacketHeader(req,"timestamp")
// print(httptimestamp)
// 构造签名数据
wx_header = {"appId":"wxxxxxxe6e","nonce":"2ErUlm","timeStamp":"1717727764000","url":""}
wx_header["url"] = httpuripath
wx_header["timeStamp"] = httptimestamp
wx_header["nonce"] = httpnonce
// 转换成string
wx_header,err = json.Marshal(wx_header)
if err != nil {
println("Error generating compact JSON: %vn", err)
return
}
mergedJSON = ""
if poc.GetHTTPRequestMethod(req) == "POST"{
jsonString1 = str.TrimRight(httpbody, "}")
jsonString2 = str.TrimLeft(wx_header, "{")
mergedJSONString := jsonString1 + "," + jsonString2
// println(mergedJSONString)
mergedJSON := json.loads(mergedJSONString)
// print(mergedJSON)
} else {
mergedJSON = json.loads(wx_header)
// println(string(mergedJSON))
}
// 计算签名
sign_data_json, err = json.Marshal(mergedJSON)
if err != nil {
println("Error generating compact JSON: %vn", err)
return
}
println(string(sign_data_json))
sign = codec.HmacSha256("h3xxxxxxhe8o", sign_data_json)
sign = codec.EncodeBase64(sign)
// 打印签名
println(sign)
req = poc.ReplaceHTTPPacketHeader(req,"sign",sign)
print(string(req))
return req
}
这个案例主要是签名的构造拼接了 POST 请求包,并且会在请求数据类型 int/str 之间切换单双引号,并且会去除空数据,相对逆向难度较大。
当然有兴趣的各位可以去看看官网的官方的极度严苛的环境,标题为《渗透测试高级技巧(二):对抗前端动态密钥与非对称加密防护》。
用上了 RSA+AES + 序列功能。非常精彩。
案例 3 补充
大概应用场景就在这种,需要签名处理或者返回数据是加密的情况,如下这种情况就可以用热加载函数在数据返回之后解密好再成功返回,以上都只是一些简单用法的抛砖引玉,欢迎大家补充。
案例 4 一个影视 APP 逆向
生活中遇到的一个场景,过了太久,图可能不全,见谅。一个盗版影视 app,后面搜了一下用的是一套框架,网上有在卖好几百,就没管了。其实一开始也是想着能不能挖到点越权、sql 等漏洞来检点便宜,虽说他是盗版 app 但也是收费的,类似几十块一年这种,也是因为这个 app 跑路了气的我直接氪了一年的腾讯会员。
但一抓包数据全加密,burp 基本用不上了,用 frida 调试一下。
请求包如下:
POST /xxx.php?app=10000&act=user_logon HTTP/1.1
Authorization: Basic cxxxtYQ==
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
User-Agent: Dalvik/2.1.0 (Linux; U; Android 11;)
Host:
Connection: close
Accept-Encoding: gzip, deflate
Content-Length: 198
data=b2525f1xxxxxxa6075753548e952c928c10400f0ed8&sign=dd654d906e7xxxxb57c29a7&
返回包如下:
{"code":200,"msg":"a813481a46bbbxxxx2a865bd28exxxxx88c689e865ac9b8a1148627b61d2fcc1e73fd2e34ce8d07514","time":1222221}
开始逆向解包,逆向加解密过程对我来说还是非常费劲,但得亏是没加壳之类,慢慢更还是理清楚了。
先就看登陆接口中的两个参数 data、sign,毕竟这两加密了,顺着网络请求看跟到 l.c,k.a 两个函数分别对应 data 和 sign。
先看简单的 k.a 这个函数,一眼 md5
再来看 l.c,比较复杂了,但是根据之前给的文字提示,看出是 RC4,分析是分析不出了,想想怎么 hook 出密钥就行,得亏是对称加密,省时一点。看这个传的参,应该就是一个明文,一个密钥,还算简单明了。
this.T = l.c(var1, var5);
开始 hook,不是很懂,照着网上资料依葫芦画瓢,主要是找对那个类,效果如下
Java.perform(function () {
console.log("Frida Hook");
// showStacks();
//摘要
var zhaiyao = Java.use("com.shenma.tvlauncher.utils.k");
zhaiyao.a.implementation = function(var1){
var res = this.a(var1)
// showStacks();
console.log("nmd5 parms:" + var1)
console.log('sign:' + res)
return res
}
// 登陆RC加密
var cl = Java.use("com.shenma.tvlauncher.utils.l");
cl.c.overload('java.lang.String','java.lang.String').implementation = function(var1,var2){
// showStacks();
var res = this.c(var1,var2)
console.log("n加密参数var1:" + var1)
console.log("加密密钥:" + var2)
console.log('data=' + res)
return res
}
// 返回包解密
var rep = Java.use("com.shenma.tvlauncher.utils.l");
rep.a.overload('java.lang.String','java.lang.String').implementation = function(var1,var2){
// showStacks();
var res = this.a(var1,var2)
console.log("n秘文:" + var1)
console.log("key:" + var2)
console.log('response=' + res)
return res
}
var rep1 = Java.use("java.net.URLDecoder");
rep1.decode.overload('java.lang.String','java.lang.String').implementation = function(var1,var2){
// showStacks();
var res = this.decode(var1,var2)
console.log("nURL编码解码var1:" + var1)
console.log("var2:" + var2)
console.log('response=' + res)
return res
}
});
但其实内部功能挺缺失的,属于是全部阉割掉了,除了注册 / 登陆,就只剩一个激活了。唯一感兴趣的就是这个激活码看能不能遍历了,因为这样就能逃课,不用花钱激活了,但都试了一遍,不好使。
所以等于说解密了数据也是白忙活,但也就是演示个解密过程。
但是后续还是渗透进了这个的后台,干进去了,一些流水看上去是真的在用的,不过他后面跑路了,也不是这篇文章的重点,看个乐子。
最后
欢迎看到这里的各位有啥问题可以多沟通,👋。
所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.
原文始发于微信公众号(掌控安全EDU):实战解密数据包案例
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论