首页
社区
课程
招聘
[原创]【银行逆向百例】10小程序逆向之Yakit 魔术方法beforeRequest实时修改签名
发表于: 2025-6-24 11:38 833

[原创]【银行逆向百例】10小程序逆向之Yakit 魔术方法beforeRequest实时修改签名

2025-6-24 11:38
833

 我们在日常生活中度过的每一天,实际上也许是连续发生的奇迹也说不定呢。——《日常》E04 

01环境版本

环境:

电脑,Windows 11 专业版 23H2

软件:

Yakit,v1.4.2-0613

微信,Windows 3.9.10.19

KillWxapkg2.4.1


02操作步骤

1、响应加密,无法分析


2、请求签名,无法重放


3、开启DevTools

e80K9s2c8@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环境

098K9s2c8@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 = "6e6K9s2c8@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填入占位符,成功实现动态修改签名


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 4217
活跃值: (6894)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
写的很详细呀!
2025-7-6 17:19
0
雪    币: 254
活跃值: (624)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
2025-8-31 05:21
0
游客
登录 | 注册 方可回帖
返回