“ 我们在日常生活中度过的每一天,实际上也许是连续发生的奇迹也说不定呢。——《日常》E04 ”
01环境版本 环境:
电脑, Windows 11 专业版 23H2
软件:
Yakit,v1.4.2-0613
微信,Windows 3.9.10.19
KillWxapkg, 2.4.1
02操作步骤 1、响应加密,无法分析
2、请求签名,无法重放
3、开启DevTools
c5eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6m8j5$3E0A6N6r3g2K6i4K6u0r3d9$3W2D9L8q4N6^5j5i4m8C8k6H3`.`.
4、进入DevTools
03签名分析 5、网络跟调用堆栈最顶层
6、依次点击调用堆栈,在Re这一步发现signStr有结果显示
7、发现请求签名参数signStr = b.header.signStr = p
8、分析签名p生成逻辑
w == 1,p = (0, r.default)(M + b.header.reqFlowNo)
w == 2,p = (0, r.default)(M + b.header.busiCode)
else,(0,r.default)(M + "12345678900987654321!@#$%^&*()qazwsxedcrfvtgbyhnujmikolp<>?:{}")
9、此时w=0,将M拼接字符串然后SHA1加密就得到了签名p也就是signStr
p = (0,r.default)(M + "12345678900987654321!@#$%^&*()qazwsxedcrfvtgbyhnujmikolp<>?:{}")
(0,r.default)等价SHA1
04解密分析
10、搜索decrypt关键字定位到解密函数,t.body是响应的body参数
11、l = p(c, t)
12、进入p函数内部
13、e.data.header.reqClient=evcc_v2,拼接o+i返回n,也就是密钥l
如果e.data.header.reqClient包含"evcc"和"v2",返回o+i,也就是r.nonce+r.reqFlowNo
如果只有"evcc",返回去掉"-"的r.reqFlowNo
如果不包含"evcc",返回空字符串""
14、抓包分析流量,当请求中的reqClient=evcc_v2,解密函数密钥l=nonce+reqFlowNo
15、响应参数body赋值给t.body,nonce+reqFlowNo赋值给l,调用解密函数
04JsRpc配置 16、导出r函数
globalThis.r = r;
17、注入小程序的JsRpc环境
491K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6B7P5r3S2U0P5X3S2D9i4K6u0r3d9Y4y4d9M7r3y4Q4x3V1k6T1L8r3!0T1i4K6u0r3L8h3q4A6L8W2)9J5c8Y4u0W2M7$3!0#2j5$3g2K6i4K6u0r3g2$3g2o6K9r3q4@1i4K6g2X3c8r3g2$3i4K6u0W2K9Y4x3`.
18、连接通信
var demo = new Hlclient("ws://192.168.14.159:12080/ws?group=zzz");
19、注册解密函数,这里需要传入密钥和密文两个值
demo.regAction("decrypt", function (resolve, param) {
console.log("[decrypt] 接收到参数:", JSON.stringify(param));
var key = param["key"];
var body = param["body"];
var data = r.decryptAES(body, key);
resolve(data);
});
20、构造param,这里需要注意密文过长浏览器无法处理,使用python测试
import requests
import json
payload = {
"key": "xxx",
"body": "xxx"
}
url = "229K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0p5@1i4K6u0W2x3e0f1&6i4K6y4m8x3e0t1H3z5o6m8Q4x3V1k6Y4L8#2)9K6c8X3N6J5L8%4g2H3i4K6y4p5P5Y4A6*7i4K6t1$3j5h3#2H3i4K6y4n7j5h3y4@1K9h3!0F1i4K6y4p5k6r3g2U0M7Y4W2H3N6q4)9J5y4Y4q4#2L8%4c8Q4x3@1t1`.
response = requests.post(url, data={"param": json.dumps(payload)})
print(response.text)
05Yakit热加载 21、提取响应参数nonce+reqFlowNo当作密钥和密文body一起传入param,热加载实现实时明文
hijackSaveHTTPFlow = func(flow /* *yakit.HTTPFlow */, modify /* func(modified *yakit.HTTPFlow) */, drop/* func() */) {
req = str.Unquote(flow.Request)~
rsp = str.Unquote(flow.Response)~
// ================================
// 解密响应中的 body 字段
// ================================
if str.Contains(rsp, "\"body\":\"") {
// 提取响应中的 JSON 数据
rspLines := str.Split(rsp, "\r\n")
var jsonBody
for i, line := range rspLines {
if line == "" && i+1 < len(rspLines) {
jsonBody = rspLines[i+1]
break
}
}
if jsonBody != "" {
// 提取 nonce
nonceParts := str.Split(jsonBody, "\"nonce\":\"")
var nonce
if len(nonceParts) >= 2 {
nonceEnd := str.Split(nonceParts[1], "\"")[0]
nonce = nonceEnd
}
// 提取 reqFlowNo
reqFlowNoParts := str.Split(jsonBody, "\"reqFlowNo\":\"")
var reqFlowNo
if len(reqFlowNoParts) >= 2 {
reqFlowNoEnd := str.Split(reqFlowNoParts[1], "\"")[0]
reqFlowNo = reqFlowNoEnd
}
// 提取加密的 body
bodyParts := str.Split(jsonBody, "\"body\":\"")
var encryptedBody
if len(bodyParts) >= 2 {
bodyEnd := str.Split(bodyParts[1], "\"")[0]
encryptedBody = bodyEnd
}
// 如果成功提取到所有必要字段,进行解密
if nonce != "" && reqFlowNo != "" && encryptedBody != "" {
// 构造密钥(nonce + reqFlowNo)
key := nonce + reqFlowNo
// 构造解密请求的参数
decryptParam := sprintf(`{"key":"%s","body":"%s"}`, key, encryptedBody)
// 发送解密请求
rsp2, req2 = poc.HTTP(`POST /go?group=zzz&action=decrypt HTTP/1.1
Host: 192.168.14.159:12080
User-Agent: Mozilla/5.0
Accept: */*
Content-Type: application/x-www-form-urlencoded
Connection: close
param=` + codec.EncodeUrl(decryptParam))~
rspIns2 = poc.ParseBytesToHTTPResponse(rsp2)~
body2 = io.ReadAll(rspIns2.Body)~
// 从解密服务返回的JSON中提取data字段
decryptedResponse := string(body2)
dataParts := str.Split(decryptedResponse, "\"data\":\"")
if len(dataParts) >= 2 {
dataEnd := str.Split(dataParts[1], "\",\"group\"")[0]
plainData := str.ReplaceAll(dataEnd, "\\", "")
// 替换原响应中的加密body为解密后的data
newJsonBody := str.ReplaceAll(jsonBody, `"body":"`+encryptedBody+`"`, `"body":"`+plainData+`"`)
// 重新构造响应
newRsp := ""
for i, line := range rspLines {
if line == "" && i+1 < len(rspLines) {
newRsp += line + "\r\n" + newJsonBody
break
} else {
newRsp += line + "\r\n"
}
}
rsp = newRsp
}
}
}
}
// 写回并保存入库
flow.Request = str.Quote(req)
flow.Response = str.Quote(rsp)
flow.AddTag("decrypted")
modify(flow)
}
06beforeRequest 22、第八步得知signStr与请求body有关,reqFlowNo每次重放需要生成不一样的值
23、来到WebFuzzer重放功能,使用热加载魔术方法beforeRequest,随机生成reqFlowNo,根据业务场景计算signStr
beforeRequest = func(req) {
// 先替换请求中的 __reqFlowNo__
req = str.ReplaceAll(req, "__reqFlowNo__", uuid())
// 提取请求POST参数
params, err = poc.ExtractPostParams(req)
if err != nil {
return req
}
// 定义签名用的字符串
qazwsxedcrfvtgbyhnujmikolp = "12345678900987654321!@#$%^&*()qazwsxedcrfvtgbyhnujmikolp<>?:{}"
bodyMap = orderedmap.New()
// 将 type 转换为 int 类型
typeValStr = params["body[type]"]
typeValNum = int(typeValStr)
// 设置值
bodyMap.Set("type", typeValNum)
bodyMap.Set("orderType", params["body[orderType]"])
bodyMap.Set("applyChannlVCC", params["body[applyChannlVCC]"])
// 转 JSON
bodyJson = json.dumps(bodyMap)
// 清理 JSON 格式
bodyJson = str.ReplaceAll(bodyJson, "\n", "")
bodyJson = str.ReplaceAll(bodyJson, " ", "")
bodyJson = str.ReplaceAll(bodyJson, " ", "")
// 计算签名逻辑
reqTime = params["header[reqTime]"]
busiCode = params["header[busiCode]"]
// 计算w
reqTimeStr = reqTime + ""
chars = str.Split(reqTimeStr, "")
lastChar = chars[len(chars) - 1]
lastDigit = int(lastChar)
w = lastDigit % 3
printf("w= "+ string(w) + "\n")
if w == 1 {
println("bodyJson + reqFlowNo" + "\n" + bodyJson + string(params["header[reqFlowNo]"]) + "\n")
params["header[signStr]"] = codec.Sha1(bodyJson + params["header[reqFlowNo]"])
printf("signStr" + "\n" + params["header[signStr]"] + "\n")
} else if w == 2 {
println("bodyJson + busiCode" + "\n" + bodyJson + busiCode + "\n")
params["header[signStr]"] = codec.Sha1(bodyJson + busiCode)
printf("signStr" + "\n" + params["header[signStr]"] + "\n")
} else {
println("bodyJson + qazwsxedcrfvtgbyhnujmikolp" + "\n" + bodyJson + qazwsxedcrfvtgbyhnujmikolp + "\n")
params["header[signStr]"] = codec.Sha1(bodyJson + qazwsxedcrfvtgbyhnujmikolp)
printf("signStr" + "\n" + params["header[signStr]"] + "\n")
}
// 替换请求中的 __signStr__
req = str.ReplaceAll(req, "__signStr__", params["header[signStr]"])
dump(params)
return []byte(req)
}
23、signStr和reqFlowNo填入占位符,成功实现动态修改签名
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!