问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
JsRpc+Yakit热加载解决请求响应体加解密问题
渗透测试
引言 继上一期(传送门:https://forum.butian.net/share/4452 )我们探讨了如何利用 JsRpc 与 Yakit 热加载技术,实现对加密请求的明文编辑与自动发包后,本期我们将焦点转向一个更为复杂的场...
引言 == 继上一期(传送门:<https://forum.butian.net/share/4452> )我们探讨了如何利用 **JsRpc 与 Yakit 热加载**技术,实现对加密加签请求的明文编辑与自动发包后,本期我们将焦点转向一个更为复杂的场景:**请求与响应双向加密**。 当服务器返回的数据也是密文时,传统的测试流程将完全失效。本文将**不再赘述如何定位具体的加解密函数**,而是假定您已找到目标函数。在此基础上,我们将深入演示 **JsRpc 的核心用法**——如何将其作为桥梁,去调用前端页面中的解密函数;并展示 **Yakit 热加载的强大之处**——如何无缝集成 JsRpc 的调用能力,实现对返回流量的自动化实时解密。 本文旨在展示一种高效的工具联动思路,帮助测试人员彻底摆脱加解密的束缚,实现端到端的明文测试。 本文测试环境基于 Yakit 自带靶场,所用工具及项目地址如下: JsRpc 项目地址:<https://github.com/jxhczhl/JsRpc> Yakit 项目地址:[https://yaklang.com/products/download\_and\_install/](https://yaklang.com/products/download_and_install/) 解密前:  解密后:  **操作步骤** ======== **安装并启动yakit靶场** ----------------  我们首先需要**定位前端的加解密函数**,然后在浏览器控制台中进行验证,确认这些函数是否能够**正确处理我们抓包得到的** **message** **数据**,实现预期的加密或解密效果。    **JsRpc食用** ----------- 关于JsRpc的详细解释可移步至<https://mp.weixin.qq.com/s/vHoVPINf4GKhR36LSQlDXw>,小天师傅讲的很详细。 ### **注入JS,构建通信环境(**[**/resouces/JsEnv\_De.js**](https://github.com/jxhczhl/JsRpc/blob/main/resouces/JsEnv_Dev.js)**)** 将[https://github.com/jxhczhl/JsRpc/blob/main/resouces/JsEnv\_Dev.js](https://github.com/jxhczhl/JsRpc/blob/main/resouces/JsEnv_Dev.js)代码复制到控制台  ### **远程调用(加密): 浏览器预先注册js方法 传递函数名调用** 通过对加密函数的分析可以确认,该加密方式为 **AES-CBC 模式,使用 PKCS7 填充**,其中 **key 和 iv 均为随机生成**。既然是随机生成的,我们完全可以在脚本中**将其固定下来**,以便复现加密过程并实现稳定可控的加解密操作。 由于加密函数只依赖一个 message 参数,**key 和 iv 可以在辅助函数中固定**,因此我们只需在控制台**编写一个辅助函数**,用于接收待加密的 message 并调用原始加密逻辑即可。 ```js //控制台执行 window.myEncrypt = function(data) { // 1.将 key 和 iv 都从十六进制字符串解析成 WordArray 格式,key与iv是随机生成的自然也能固定。 const key = CryptoJS.enc.Hex.parse("6190987ec48f0394752f09a81ee869a2"); const iv = CryptoJS.enc.Hex.parse("557784509b449b0d67287c1b53571e72"); // 如果传入的是对象,最好先转为 JSON 字符串 const message = typeof data === 'object' ? JSON.stringify(data) : data; const encryptedResultObject = CryptoJS.AES.encrypt( message, key, // 使用解析后的 key { iv: iv, // 使用解析后的 iv mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); // 2.对返回的原始对象调用 .toString() 来获取密文字符串 return encryptedResultObject.toString(); }; ``` 接下来,我们对刚刚创建的辅助函数进行测试,**验证其加密结果是否与原始加密逻辑一致,确保功能正确**。  ```js //控制台执行 var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=encrypt"); //写一个传入字符串,返回base64值的接口(调用内置函数atob) demo.regAction("encrypt", function (resolve,param) { //这样添加了一个param参数,http接口带上它,这里就能获得 var params = atob(param) //通过js代码分析formData其实是一个对象,而这里的参数是字符串,我们需要处理下,不然加密结果会不一样。这不是一个符合json的字符串数据所以不能使用JSON.parse,通过eval可解决。 esultObject = eval('(' + params + ')'); resolve(myEncrypt(esultObject)); }) ``` 在控制台注入辅助 JS 代码后,我们可以进行简单测试:**使用抓包获取的 key 和 iv 对数据进行加密**,然后将加密结果与原始请求中的密文进行对比。**如果加密结果一致,说明函数逻辑无误,可以正常复用**。 如下图所示,加密结果与原始数据一致,验证通过:  随后,我们通过 **JsRpc 工具提供的接口**对请求数据进行编码并发包测试。**加密结果与原始请求保持一致,说明流程无误,可正常使用。** ```php e3VzZXJuYW1lOiAnYWRtaW4nLCBwYXNzd29yZDonYWRhc2Rmc2RmJ30= ```  [http://127.0.0.1:12080/go?group=encrypt&action=encrypt¶m=e3VzZXJuYW1lOiAnYWRtaW4nLCBwYXNzd29yZDonYWRhc2Rmc2RmJ30=](http://127.0.0.1:12080/go?group=encrypt&action=encrypt&param=e3VzZXJuYW1lOiAnYWRtaW4nLCBwYXNzd29yZDonYWRhc2Rmc2RmJ30=) 结果一致,没有问题。  ### **远程调用(解密): 浏览器预先注册js方法 传递函数名调用** 接下来,我们按照相同的思路编写一个辅助函数,用于对响应体进行解密。由于 AES-CBC 模式下的 **key、iv 和密文(message)每次都会变化**,因此该函数需要同时接收这三个参数,才能正确还原原始数据。 ```js //控制台执行 window.get_decrypt = function(key,iv,message) { const decrypted = CryptoJS.AES.decrypt( message, CryptoJS.enc.Hex.parse(key), { iv: CryptoJS.enc.Hex.parse(iv), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); decryptedData = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8)); return decryptedData; }; ``` 为了验证加密结果的正确性,我们在控制台中调用解密函数对返回的数据进行还原。需要注意的是,由于 AES-CBC 模式下的 **key 和 iv 每次都会随机生成**,因此 **每次的 key、iv 和 message 都不相同**,这是正常现象。 ```js //解密 const decrypted = CryptoJS.AES.decrypt( "9BmCEL2x8hRojOyzoqeKY7z5PSuRDtB5jl4e+z0dTEC2+80pmm1VIiOGEcNh349J+nN/1xzYZBchRV3+21ikjY+NrXVdIwVfYbfkbNO8ieHR0KyUkUqtNn7NBsHCU6gqpZifd0IY3LUj0P0pjcWD8Jxs+1Pn2ncj1WW2yhvkQwf3GBoujkv5ylxv6ZCrXCml", //key CryptoJS.enc.Hex.parse("43dcc720aaabdb65c39b90cb71e2b8d6"), { iv: CryptoJS.enc.Hex.parse("825608c3018f0b8468923d23426caa77"), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); decryptedData = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8)); ``` 测试结果显示,解密过程正常,**密文能够成功还原为原始数据**,验证通过。 认证失败:   认证成功:  ```js //注入js方法,多参 var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=get_decrypt"); demo.regAction("get_decrypt", function (resolve,param) { //这里还是param参数 res=get_decrypt(param["key"],param["iv"],param["message"]) resolve(res); }) ```  接下来,我们使用 JsRpc 的解密接口对响应数据进行处理。由于需要传递多个参数,发包操作由 Python 脚本完成,以提高灵活性和可控性。 ```py import requests import json url = "http://127.0.0.1:12080/go" data = { "group": "get_decrypt", "action": "get_decrypt", "param": json.dumps({"key":"9ca83c435044b0750777c05dec8df6bd" ,"iv": "fc973c1d2895d89b445989939381b18c","message":"gcCZ3CRVQa7C1ZSTH06+Rfjz7E8x63WKTexphNg9YKcBpcUhW7yRnpMw+115wDO8XEqM12ECpCiP+7vfQDjkj08osdjmZhBw0EDZOv2FEBo=" }) } print(data["param"]) res=requests.post(url, data=data) print(res.text) ```  ```php POST /go HTTP/1.1 Host: 127.0.0.1:12080 User-Agent: python-requests/2.31.0 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive Content-Length: 297 Content-Type: application/x-www-form-urlencoded group=get_decrypt&action=get_decrypt&param=%7B%22key%22%3A+%229ca83c435044b0750777c05dec8df6bd%22%2C+%22iv%22%3A+%22fc973c1d2895d89b445989939381b18c%22%2C+%22message%22%3A+%22gcCZ3CRVQa7C1ZSTH06%2BRfjz7E8x63WKTexphNg9YKcBpcUhW7yRnpMw%2B115wDO8XEqM12ECpCiP%2B7vfQDjkj08osdjmZhBw0EDZOv2FEBo%3D%22%7D ```  通过以上方法的实现,对热加载脚本进行修改。 **yak runner脚本** ---------------- ### **利用 Yakit 热加载脚本调用 JsRpc** **encrypt** **接口实现加密** 接下来,我们编写一个 **Yakit Runner 脚本**,通过调用 JsRpc 提供的加密接口,**自动生成加密后的请求数据**。 ```js //yak runner脚本 # port scan plugin yakit.AutoInitYakit() handleCheck = func(target,port,word){ addr = str.HostPort(target, port) isTls = str.IsTLSServer(addr) packet = ` GET /go?group=encrypt&action=encrypt&param={{params(word)}} HTTP/1.1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br, zstd Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cache-Control: no-cache Connection: keep-alive Host: {{params(target)}} ` rsp,req,_ = poc.HTTP(packet, poc.params({"target":addr,"word":word}), poc.https(isTls), poc.redirectTimes(0), ) println(req) if len(rsp) > 0 { body = poc.GetHTTPPacketBody(rsp) printf("获取加密结果:\n"+string(body)) jsonObj = json.loads(string(body)) // 获取字段值 sign = jsonObj["data"] return sign }else{ println("失败") } return } func base_word(name,passwd){ word = "{ username: '"+name+"', password: '"+passwd+"' }" println(word) return codec.EncodeBase64(word) } word = base_word("admin","admin") sign = handleCheck("127.0.0.1",12080,word) println("\nencrypt: "+sign) // signRequest = result => { // pairs := result.SplitN("|", 2) // dump(pairs) // //接收两个参数,按照yakit语法写即可 {{yak(signRequest|admin|admin)}} // word = base_word(pairs[0], pairs[1]) // sign = handleCheck("127.0.0.1",12080,word) // return sign // } ``` 经过调试运行,脚本可以**正常与浏览器通信并成功获取加密结果**,说明整体流程已配置无误。  在 Web Fuzz 测试过程中,我们通常需要频繁修改请求包和测试数据,因此通过 **热加载机制** 来实现快速迭代尤为重要。这样可以在无需重启脚本的情况下,**对加密逻辑或请求内容进行灵活调整**。 ```js //热加载脚本 # port scan plugin yakit.AutoInitYakit() handleCheck = func(target,port,word){ addr = str.HostPort(target, port) isTls = str.IsTLSServer(addr) packet = ` GET /go?group=encrypt&action=encrypt&param={{params(word)}} HTTP/1.1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br, zstd Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cache-Control: no-cache Connection: keep-alive Host: {{params(target)}} ` rsp,req,_ = poc.HTTP(packet, poc.params({"target":addr,"word":word}), poc.https(isTls), poc.redirectTimes(0), ) println(req) if len(rsp) > 0 { body = poc.GetHTTPPacketBody(rsp) printf("获取加密结果:\n"+string(body)) jsonObj = json.loads(string(body)) // 获取字段值 sign = jsonObj["data"] return sign }else{ println("失败") } return } func base_word(name,passwd){ word = "{ username: '"+name+"', password: '"+passwd+"' }" println(word) return codec.EncodeBase64(word) } // word = base_word("admin","admin") // sign = handleCheck("127.0.0.1",12080,word) // println("\nencrypt: "+sign) signRequest = result => { pairs := result.SplitN("|", 2) dump(pairs) //接收两个参数,按照yakit语法写即可 {{yak(signRequest|admin|admin)}} word = base_word(pairs[0], pairs[1]) sign = handleCheck("127.0.0.1",12080,word) return sign } ``` 调试热加载脚本,检查是否能通过 JsRpc 正常获取加密结果,确保功能可用。   ### **利用 Yakit 热加载脚本调用 JsRpc** **get\_decrypt** **接口实现解密** 这里会用到 Yakit 的 afterRequest 魔术方法。根据官网的解释,afterRequest 允许我们在响应数据返回给客户端之前,对响应内容进行自定义处理和修改,从而实现对响应体的动态解密或其他操作。  在第一个热加载脚本基础上,我们新增了从响应体中提取 key、iv 和 message 的功能,并调用 JsRpc 的解密接口,将解密后的明文数据返回至响应窗口。这样一来,后续即可直接查看明文响应体,极大方便分析与调试。 ```js //完整的热加载脚本 # port scan plugin yakit.AutoInitYakit() handleCheck = func(target,port,word){ addr = str.HostPort(target, port) isTls = str.IsTLSServer(addr) packet = ` GET /go?group=encrypt&action=encrypt&param={{params(word)}} HTTP/1.1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br, zstd Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cache-Control: no-cache Connection: keep-alive Host: {{params(target)}} ` rsp,req,_ = poc.HTTP(packet, poc.params({"target":addr,"word":word}), poc.https(isTls), poc.redirectTimes(0), ) // println(req) if len(rsp) > 0 { body = poc.GetHTTPPacketBody(rsp) printf("获取加密结果:\n"+string(body)) jsonObj = json.loads(string(body)) // 获取字段值 sign = jsonObj["data"] return sign }else{ println("失败") } return } func base_word(name,passwd){ word = "{ username: '"+name+"', password: '"+passwd+"' }" println(word) return codec.EncodeBase64(word) } // word = base_word("admin","admin") // sign = handleCheck("127.0.0.1",12080,word) // println("\nencrypt: "+sign) signRequest = result => { pairs := result.SplitN("|", 2) dump(pairs) word = base_word(pairs[0], pairs[1]) sign = handleCheck("127.0.0.1",12080,word) return sign } func get_decrypt(key,iv,message){ rsp, req = poc.HTTP("POST /go HTTP/1.1\r\nHost: 127.0.0.1:12080", poc.replaceAllPostParams({ "group": "get_decrypt", "action": "get_decrypt", "param": json.dumps({"key": key,"iv": iv,"message": message}) }))~ println(req) // poc.ReplaceBody println(rsp) return rsp } func decrypt(rsp){ if len(rsp) > 0 { body = poc.GetHTTPPacketBody(rsp) printf("获取加密结果:\n"+string(body)) jsonObj = json.loads(string(body)) // 获取字段值 key = jsonObj["key"] iv = jsonObj["iv"] message = jsonObj["message"] println("\nkey: "+string(key)+"\niv: "+string(iv)+"\nmessage: "+string(message)) if key == nil && iv == nil && message == nil { return rsp }else{return key,iv,message} }else{ return rsp } return } afterRequest = func (rsp){ // println(string(rsp)) key,iv,message=decrypt(rsp) println("\nkey: "+string(key)+"\niv: "+string(iv)+"\nmessage: "+string(message)) rsp = get_decrypt(key,iv,message) return rsp } ``` 至此,我们实现了请求的明文编辑与响应的明文展示,极大提升了调试和测试的效率。   在编写热加载脚本的过程中,调试是必不可少的环节。所有调试信息都会输出到控制台,方便我们实时监控和快速定位问题,从而高效完成脚本开发。   **参考资料:** ========= <https://github.com/jxhczhl/JsRpc> [https://yaklang.com/articles/vulnerability\_testing\_after\_being\_encrypted\_by\_the\_front-end](https://yaklang.com/articles/vulnerability_testing_after_being_encrypted_by_the_front-end) <https://mp.weixin.qq.com/s/vHoVPINf4GKhR36LSQlDXw>
发表于 2025-07-14 14:03:52
阅读 ( 173 )
分类:
渗透测试
4 推荐
收藏
0 条评论
请先
登录
后评论
all
2 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!