-
-
[原创]【银行逆向百例】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¶m={{params(paramStr)}} HTTP/1.1Host: 192.168.85.128:12080User-Agent: Mozilla/5.0Accept: */*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¶m={{params(paramStr)}} HTTP/1.1Host: 192.168.85.128:12080User-Agent: Mozilla/5.0Accept: */*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 requestsimport jsonkey = {}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.1Host: 192.168.85.128:12080Content-Type: application/x-www-form-urlencodedConnection: closeContent-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¶m=" + string(encryptedBody) decryptReqRaw := str.f(`GET %s HTTP/1.1Host: 192.168.85.128:12080Connection: closeUser-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热加载,实现修改明文请求,显示明文响应
