首页
社区
课程
招聘
[原创]【银行逆向百例】11小程序逆向之修复页面未注册 位置权限 Yakit 魔术方法afterRequest修改明文数据
发表于: 2025-7-4 13:01 513

[原创]【银行逆向百例】11小程序逆向之修复页面未注册 位置权限 Yakit 魔术方法afterRequest修改明文数据

2025-7-4 13:01
513

那一天我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云。——《黄金时代》

01环境版本

环境:
电脑,Windows 11 专业版 23H2
JiaoSuInfoSec/JiaoSuInfoSec_T00ls_Win11: 角宿武器库官方发布页面

软件:
Yakit,v1.4.2-0613
Yak Language Yak Program Language | Yak Program Language
微信,Windows 3.9.10.19
wechat-windows-versions
KillWxapkg,2.4.1
Ackites/KillWxapkg: 自动化反编译微信小程序
微信开发者工具,1.06.2503300
微信开发者工具下载地址与更新日志
JsRpc,1.095
jxhczhl/JsRpc: 远程调用(rpc)浏览器方法

02操作步骤

1、点击个人信息进入登录页面
图片描述

2、请求体由URL编码的head参数和十六进制的body组成,响应体为十六进制的字符串
head参数包含SignData,可能存在签名
图片描述

3、开启小程序控制台
KillWxapkg_2.4.1_windows_amd64.exe -hook
图片描述

03解密分析

4、日志输出请求req,响应res明文数据,跟进res
图片描述

5、点击继续执行脚本,直到u的值和日志输出一致
RetMsg: '用户注册登录失败:用户账户信息不正确!'
图片描述

6、跟调用堆栈,e.data是明文
图片描述

7、搜索e.data =发现响应解密函数t.decrypt
图片描述

8、进入t.decrypt,发现this.secretKey,this.iv动态变化
图片描述

9、搜索this.secretKey =发现this.iv,this.secretKey与n有关,n = i(r),r生成18位随机字符串
图片描述

04固定密钥

10、unveilr.exe反编译小程序,导入微信开发者工具,componentFramework,plugins参考文章修复后,出现trees.wxss文件unexpected token $,修复方案删掉"即可
【银行逆向百例】05小程序逆向之微信开发者工具反编译修复插件未授权+WXSS+WXML格式错误
d0bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9J5c8X3u0n7P5i4W2F1g2K6q4a6L8h3N6s2K9V1c8C8e0q4S2p5f1p5q4W2y4q4p5`.
图片描述

11、点击个人信息登录,提示Page "pages/index/login" has not been registered yet.
图片描述

12、发现app.json中"pages/index/login"一开始在111行,移动到86行后从主页点击个人信息可以正常跳转登录页面,如果移动的太前会导致登录页面变成启动页,而主页还有其他功能就无法点击,如果太后会导致提示login未注册空白页
图片描述

13、回到第九步,将r函数的返回值e写死,e=000000000000000000,清除缓存编译
这样一来this.iv恒等于464,this.secretKey恒等于edd
图片描述

05位置权限

14、验证短信,提示设置位置权限,点击去设置空白页面
图片描述

15、修改app.json中的__usePrivacyCheck__,清除缓存重新编译,出现权限申请,允许之后就可以正常请求
"usePrivacyCheck": false,
图片描述

06请求分析

16、搜索SignData =发现s.doSignature,i是请求body参数,5AF是签名密钥,hash非0就是1,e.AppCode是请求head参数中的AppCode,r是空
图片描述

17、日志输出请求req,响应res明文数据,跟进req
图片描述

18、e是明文,d.encrypt是请求加密函数,进入d.decrypt解密函数
图片描述

19、发现来到了第八步响应解密函数处,说明请求响应用的同一套解密函数
图片描述

07JsRpc配置

20、导出第七步响应解密函数t.decrypt
globalThis.t=t;
图片描述

21、取消断点,注入JsRpc环境
b9eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6B7P5r3S2U0P5X3S2D9i4K6u0r3d9Y4y4d9M7r3y4Q4x3V1k6T1L8r3!0T1i4K6u0r3L8h3q4A6L8W2)9J5c8Y4u0W2M7$3!0#2j5$3g2K6i4K6u0r3g2$3g2o6K9r3q4@1i4K6g2X3c8r3g2$3i4K6u0W2K9Y4x3`.
图片描述

22、连接通信
var demo = new Hlclient("ws://192.168.85.128:12080/ws?group=zzz"); 图片描述

23、注册解密方法

1
2
3
4
5
demo.regAction("decrypt", function (resolve, param) {
    console.log("param值是" + param)
    var data = t.decrypt(param)
    resolve(data);
})

图片描述

24、测试解密方法成功获取请求和响应明文
a8cK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0R3#2i4K6u0W2x3e0t1^5i4K6y4m8x3e0t1H3z5o6m8Q4x3V1k6Y4L8#2)9K6c8X3N6J5L8%4g2H3i4K6y4p5P5Y4A6*7i4K6t1$3j5h3#2H3i4K6y4n7j5h3y4@1K9h3!0F1i4K6y4p5k6r3g2U0M7Y4W2H3N6q4)9J5y4X3q4E0M7q4)9K6b7Y4m8S2M7X3q4E0i4K6y4p5P5s2S2^5
图片描述

08MITM热加载

25、编写热加载代码,URL解码请求head参数,t.decrypt解密请求body参数,t.decrypt解密整个响应体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
hijackSaveHTTPFlow = func(flow /* *yakit.HTTPFlow */, modify /* func(modified *yakit.HTTPFlow) */, drop/* func() */) {
    req = str.Unquote(flow.Request)~
    rsp = str.Unquote(flow.Response)~
 
    // ================================
    // 解码 head 参数
    // ================================
    if str.Contains(req, "head=") {
        parts := str.Split(req, "head=")
        if len(parts) >= 2 {
            encPart := parts[1]
            encValue := str.Split(encPart, "&")[0]
            decoded = codec.DecodeUrl(encValue)~
            req = str.ReplaceAll(req, encValue, decoded)
        }
    }
 
    // ================================
    // 解密请求 body 参数
    // ================================
    if str.Contains(req, "body=") {
        parts := str.Split(req, "body=")
        if len(parts) >= 2 {
            encPart := parts[1]
            encValue := str.Split(encPart, "&")[0]
 
            rsp2, req2 = poc.HTTP(`GET /go?group=zzz&action=decrypt&param={{params(paramStr)}} HTTP/1.1
Host: 192.168.85.128:12080
User-Agent: Mozilla/5.0
Accept: */*
Connection: close
 
`, poc.params({
                "paramStr": encValue,
            }))~
 
            rspIns2 = poc.ParseBytesToHTTPResponse(rsp2)~
            body2 = io.ReadAll(rspIns2.Body)~
 
            parts2 := str.Split(body2, "\"data\":\"")
            if len(parts2) >= 2 {
                dataPart2 := parts2[1]
                dataEnd := str.Split(dataPart2, "\",\"group\"")[0]
                plainData := str.ReplaceAll(dataEnd, "\\", "")
                req = str.ReplaceAll(req, encValue, plainData)
            }
        }
    }
 
    // ================================
    // 解密响应正文
    // ================================
    if str.Contains(rsp, "\r\n\r\n") {
        headerBody := str.SplitN(rsp, "\r\n\r\n", 2)
        if len(headerBody) == 2 {
            header = headerBody[0]
            encBody = headerBody[1]
 
            rsp2, req2 = poc.HTTP(`GET /go?group=zzz&action=decrypt&param={{params(paramStr)}} HTTP/1.1
Host: 192.168.85.128:12080
User-Agent: Mozilla/5.0
Accept: */*
Connection: close
 
`, poc.params({
                "paramStr": encBody,
            }))~
 
            rspIns2 = poc.ParseBytesToHTTPResponse(rsp2)~
            body2 = io.ReadAll(rspIns2.Body)~
 
            parts2 := str.Split(body2, "\"data\":\"")
            if len(parts2) >= 2 {
                dataPart2 := parts2[1]
                dataEnd := str.Split(dataPart2, "\",\"group\"")[0]
                plainData := str.ReplaceAll(dataEnd, "\\", "")
                rsp = header + "\r\n\r\n" + plainData
            }
        }
    }
 
    // 写回并保存入库
    flow.Request = str.Quote(req)
    flow.Response = str.Quote(rsp)
    flow.AddTag("decrypted")
    modify(flow)
}

图片描述

09WebFuzzer热加载
26、搜索d.encrypt定位到加密函数,i在请求head参数中,u是明文body参数,a是空可以不用,d.encrypt(i, u)输出的body参数就是密文
globalThis.d=d;
图片描述

27、注册加密方法,这里需要传入head中的i和u也就是明文body

1
2
3
4
5
6
7
demo.regAction("encrypt", function (resolve, param) {
    console.log("[encrypt] 接收到参数:", JSON.stringify(param));
    var key = param["key"];
    var body = param["body"];
    var data = d.encrypt(key, body);
    resolve(data);
});

图片描述

28、构造param,使用python测试成功加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import json
 
key = {
}
body_str = json.dumps({}
}, separators=(',', ':'))
param = {
    "key": key,
    "body": body_str
}
url = "180K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5&6x3W2)9J5k6e0p5$3z5q4)9J5k6e0R3#2i4K6u0W2x3e0t1^5i4K6y4m8x3e0t1H3z5o6m8Q4x3V1k6Y4L8#2)9K6c8X3N6J5L8%4g2H3i4K6y4p5P5Y4A6*7i4K6t1$3j5h3#2H3i4K6y4n7j5h3y4@1K9h3!0F1i4K6y4p5k6h3&6U0M7Y4W2H3N6l9`.`."
response = requests.post(url, data={
    "param": json.dumps(param, separators=(',', ':'))
})
print(response.text)

图片描述

29、将MITM中的明文请求发送到WebFuzzer重放攻击,发现请求参数解析失败
图片描述

30、编写WebFuzzer热加载魔术方法beforeRequest将body明文加密还原后发送请求,
afterRequest将响应密文替换为明文显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
beforeRequest = func(https, originReq, req) {
    params := poc.GetAllHTTPPacketPostParamsFull(req)
    if params["head"] == nil || len(params["head"]) == 0 || params["body"] == nil || len(params["body"]) == 0 {
        return req
    }
    headStr, bodyStr := params["head"][0], params["body"][0]
 
    headJson := json.loads(headStr)
    if headJson == nil {
        log.error("解析 head JSON 失败")
        return req
    }
 
    keyForEncrypt := {
        "AppCode":    headJson["AppCode"],
        "WaterMark":  headJson["WaterMark"],
        "deviceInfo": headJson["deviceInfo"],
    }
 
    encryptPayload := {
        "key":  keyForEncrypt,
        "body": bodyStr,
    }
    payloadJsonStr := json.dumps(encryptPayload)
 
    postValue := codec.EncodeUrl(payloadJsonStr)
    postBodyForEncrypt := "param=" + postValue
 
    encryptReqRaw := str.f(`POST /go?group=zzz&action=encrypt HTTP/1.1
Host: 192.168.85.128:12080
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: %d
 
%s`, len(postBodyForEncrypt), postBodyForEncrypt)
 
    encryptRsp, _, err := poc.HTTP(encryptReqRaw)
    if err != nil {
        log.error("请求加密接口失败: %v", err)
        return req
    }
 
    encryptRspBody := poc.GetHTTPPacketBody(encryptRsp)
    resultMap := json.loads(encryptRspBody)
    if resultMap == nil {
        log.error("解析加密接口响应失败")
        return req
    }
 
    encryptedDataStr := resultMap["data"]
    if encryptedDataStr == nil || !str.Contains(encryptedDataStr, "head") {
        log.error("加密接口返回数据异常: %s", encryptedDataStr)
        return req
    }
 
    newDataMap := json.loads(encryptedDataStr)
    if newDataMap == nil {
        log.error("解析嵌套 data 失败")
        return req
    }
 
    newEncryptedHead := newDataMap["head"]
    newEncryptedBody := newDataMap["body"]
 
    finalPostBody := "head=" + headStr + "&body=" + newEncryptedBody
    finalReq := poc.ReplaceHTTPPacketBody(req, []byte(finalPostBody))
 
    log.info("最终发送给目标服务器的请求体:\n---\n%s\n---", string(finalReq))
    return finalReq
}
 
afterRequest = func(https, originReq, req, originRsp, rsp) {
    encryptedBody := poc.GetHTTPPacketBody(rsp)
    if len(encryptedBody) == 0 {
        return rsp
    }
 
    if str.HasPrefix(string(encryptedBody), "{") {
        log.warn("目标服务器返回非密文,不解密。")
        return rsp
    }
 
    log.info("收到加密响应体: %s", string(encryptedBody))
 
    pathAndQuery := "/go?group=zzz&action=decrypt&param=" + string(encryptedBody)
    decryptReqRaw := str.f(`GET %s HTTP/1.1
Host: 192.168.85.128:12080
Connection: close
User-Agent: Yak Fuzzer
 
`, pathAndQuery)
 
    log.info("构建的解密请求:\n---\n%s\n---", decryptReqRaw)
 
    decryptRsp, _, err := poc.HTTP(decryptReqRaw)
    if err != nil {
        log.error("请求解密接口失败: %v", err)
        return rsp
    }
 
    decryptRspBody := poc.GetHTTPPacketBody(decryptRsp)
    log.info("解密接口响应: %s", string(decryptRspBody))
 
    resultMap := json.loads(decryptRspBody)
    if resultMap == nil || resultMap["data"] == nil {
        log.error("解析解密响应失败或 data 字段不存在")
        return rsp
    }
 
    decryptedDataStr := resultMap["data"]
 
    return poc.ReplaceHTTPPacketBody(rsp, []byte(decryptedDataStr))
}

图片描述

31、解密交给MITM热加载,加密交给WebFuzzer热加载,实现修改明文请求,显示明文响应
图片描述


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

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回