1.前言
近几年,越来越多网站流行起来sign值防篡改、添加http头字段防重放、数据包加密等机制,这类型机制本质上是为了与爬虫做对抗,随着安全渗透场景和要求不断变高,这类型机制往往对安全人员测试造成极大的干扰。笔者由于有安全测试背景,项目中也遇到过爬虫需求,也是经历过当前的对抗,有一些较为成熟的解决方案,分享出来供大家一起学习。
2.从最开始说起
笔者两年前遇到过一个项目,大概长这样:
笔者经过测试发现该网站求包还添加相关参数用来防止重放数据包的情况。
POST /api/sms/sendsms HTTP/2Host: xxxContent-Length: 73Sec-Ch-Ua: "Chromium";v="113", "Not-A.Brand";v="24"Sec-Ch-Ua-Mobile: ?0Authorization: Bearer nullRt: /5asJSe+gKXuuIdOsOg6kw==Content-Type: application/jsonAccept: application/json, text/javascript, */*; q=0.01User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.93 Safari/537.36Api-Version: 1.0Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9{"phone":"1888888881","smsTypes":2,"uniqueId":"1684756825417","code":""}
通过对Header头进行分析,发现其中RT参数正是其中关键字段,主要用来防止数据包重放,现在就需要看看这个RT参数从何而来。经过对JS代码的分析,发现了如下代码:
var aeskey2 = '1122334455667788';xxxxxfunction encryptRt(data) { var key = CryptoJS.enc.Utf8.parse(aeskey2); // 加密 var encryptedData = CryptoJS.AES.encrypt(data, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return encryptedData + '';}xxx
发现Rt参数正是通过aes方式进行加密,并且获得其中的key和加密逻辑。
曾经的笔者选择手搓加解密逻辑,选择封装Burp插件的方式,绕过RT检测。
整体步骤下来无非两步:找到加解密逻辑+手搓插件联动Burp当时也是比较丝滑的完成了项目交付。
这种场景在项目交付周期长还好说,可是遇到赶时间的时候,哪里来的时间去一步步分析加密逻辑、去手搓插件?也算是绕了一个远路。
3.手搓插件?我选择mitmproxy
在JS加密逻辑看懂的背后,后面一直纠结的无非是如何快速完成爆破等场景的实现。如果不去逆向JS来实现的话,之前有师傅提到过用得最多的就是自动化工具,比如 Selenium、Puppeteer 等,很显然这些自动化工具配置繁琐、运行效率极低。我也曾使用过Selenium模拟的活,不得不说也是没有办法中的办法,但是比我手搓插件会更优雅。
先来看看平常我们渗透场景下Burpsuite代理转发数据包的原理不挂Burpsuite代理,客户端浏览器直接访问服务器的流程:
挂上Burpsuite代理之后
现在想要在Burpsuite代理转发前后添加脚本进行加解密操作,那分别在客户端浏览器与Burpsuite之间、Burpsuite与服务端服务器之间添加一个代理即可实现。
这类型流程有很多方法可以使用,考虑到泛用性,一般会选择mitmproxy来实现相关操作:
这里一般需要编写加密逻辑和解密逻辑,然后使用使用mitmProxy作为代理来实现在burp中查看明文参数,方便渗透选手大显身手。效果图如下:
这样也可以,无非是代理链长一点,能够完成我们明文操作数据包的权限。也是目前较为通用的一个思路。
这个方法的使用局限:首先你要读懂加密逻辑,根据逻辑编写对应的mitimproxy的加解密脚本。遇到简单易懂的还好,遇到特别复杂的加解密场景估计能够蜕层皮。
4.yakit热加载+JSRPC技术的丝滑渗透
从理论上来说,这种手法和mitmproxy其实大差不差,,但是数据包拐了山路十八弯, 难免优点麻烦。 有没有少拐点弯的方法呢? 这里就要介绍yakit热加载。当然再说热加载之前呢,我们还是完整的分析举一个加密逻辑的例子,
Step1 从数据包分析开始:
POST /api/user/login HTTP/1.1Host: xxxtimestamp: 1743910198000requestId: 6869e6784760f441ce4b51e499e7d9cfUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36Accept: application/json, text/plain, */*Accept-Encoding: gzip, deflateX-Requested-With: XMLHttpRequestCache-Control: no-cacheAccept-Language: zh-CN,zh;q=0.9sign: 784c4a21ace7acd8fbc59da385729f95Content-Type: application/json;charset=UTF-8Pragma: no-cacheContent-Length: 88jf+P6PvnZB2wo8MIi5NybkwZgmNLSJD03a3C0AT5mvcoAAaKex1u88ehfMhSvrWkiPsqFH4If/usaGmAQGy+bA==
这是一个测试网站登录的逻辑,可以看到其中有一些防重放参数和完全加密的数据包以及响应包
timestamp
requestId
sign
body
接下来我们一步一步分析,完成相关参数的来去。
保姆级插件:https://github.com/cilame/v_jstools
能够一键检测JS中指定函数的调用
下载下来配置如下:
此时刷新页面,打开F12控制台,此时出现inject start表示注入成功
这个时候输入数据包、打断点找加密逻辑等等后续就不用我赘述。都属于基本操作也不是本文重点。
此时我们已经找到加密的逻辑:
这里很明显就能发现我们的防重放参数和加密逻辑分别是:r、i、s、和l(n)
u.interceptors.request.use((function(t) {t.headers["X-Requested-With"] = "XMLHttpRequest",t.headers["Content-Type"] = "application/json;charset=utf-8";var e = sessionStorage.getItem("user"), r = Date.parse(new Date), n = JSON.stringify(v(t.data)), i = p(), s = a.a.MD5(n + i + r);t.headers["timestamp"] = r,t.headers["requestId"] = i,t.headers["sign"] = s,t.data = l(n);
这里给出靶场地址,有需要的同学可以自行搭建分析。
https://github.com/0ctDay/encrypt-decrypt-vuls/
几个关键的变量和函数:
- r: 很明显就是时间戳。
- n: 原始的表单数据请求经过v() 函数处理后, 再进行JSON编码。
- i: 使用p函数生成的requestId。
- s: 使用MD5()函数生成的哈希值, 生成的方式为n+i+r的字符串拼接。
- 加密: 对变量n使用l()函数进行加密。
针对实际请求包的修改:
我们需要在请求头中添加 timestamp,requestId, sign 等字段。
然后修改明文请求体进行加密。
Step2 使用jsrpc生成参数
jsrpc下载地址:
https://github.com/jxhczhl/JsRpc
JS-RPC这款工具的工作原理就是在控制台中执行一段代码,通过websocket与本地的python服务端相连。这样一来如果python中想要执行代码,只需要通过RPC即可调用控制台中的函数了,不需要再本地还原。(这就省去了我们的困扰第一步,JS扣代码)
具体搭建不再赘述,使用步骤分三步:
1)启动jsrpc
直接在控制台注入代码,这里的注入代码来源
https://github.com/jxhczhl/JsRpc/blob/main/resouces/JsEnv_Dev.js直接复制放到f12中,回车即可。
然后连接通信
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=自定义");
此时就已经代表注册完毕。
2)寻找加密变量,浏览器注册js方法,传递函数名调用。这里都是跟项目里说的一样。
打上断点,记录函数,将函数升级为全局变量(大坑,必须要打断点,注册完了记得放开断点)
//时间戳window.time = Date.parse//requestIdwindow.id = p//v函数window.v1 = v//签名window.m = a.a.MD5//加密window.enc = l
//md5函数demo.regAction("req", function (resolve,param) { //请求头 let timestamp = time(new Date()); let requestid = id(); let v_data = JSON.stringify(v1(param)); let sign = m(v_data + requestid + timestamp).toString(); //加密请求体 let encstr = enc(v_data); let res = { "timestamp":timestamp, "requestid":requestid, "encstr":encstr, "sign":sign }; resolve(res);})
3)调用相关函数方法
POST /go HTTP/1.1Host: 192.168.0.183:12080Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36Accept: 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.7Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Connection: keep-aliveContent-Type: application/x-www-form-urlencodedContent-Length: 86group=fu11y&action=req¶m=HOy2E7rFAyTFuv1vr1QHq7cHdct8QmlIxHspZJD61rpEqNCa92LNL7ViLIGcmg6pulMs2nYkE0lATS0jgP5rPQ==
此时我们已经能够通过jsrpc技术成功调用函数获取到我们想要的字段。
Step3 yakit热加载联动jsrpc
https://www.yaklang.com/products/Web%20Fuzzer/fuzz-hotpatch/
其中提到了两个特殊的魔术方法:beforeRequest
和afterRequest
这两个魔术方法分别在每次请求之前和每次请求拿到响应之后调用,它们可以用于修改我们 Web Fuzzer 的请求与响应。通过这两个魔术方法配合 Yak代码,我们实际上可以实现许多有用的功能。
当然文档写的很简单,稍微理一下就知道,这个功能就是把我们要发送的东西请求前处理一边,请求后处理一边,是不是非常符合我们说的mitmproxy的功能。
再来温习一下这个图,相当于是我们从客户端浏览器到服务端之间这所有流程都可以用yakit替代,非常的丝滑。
我们了解过核心代码:
//获取请求体requestBody = poc.GetHTTPPacketBody(req)//修改请求包中指定的请求头req = poc.ReplaceHTTPPacketHeader(req, "请求头名", "请求头值")//修改请求体req = poc.ReplaceHTTPPacketBody(req, "修改后的值")
这里不懂代码没关系,直接使用deepseek一键生成即可。
// 定义加密函数func getEnc(data){ rsp,rep,err = poc.Post("http://127.0.0.1:12080/go",poc.replaceBody("group=zzz&action=req¶m="+data, false),poc.appendHeader("content-type", "application/x-www-form-urlencoded")) if(err){ return(err) } return json.loads(rsp.GetBody())["data"]}//定义解密函数func getDec(data){ action = "decrypt";x`` rsp,rep,err = poc.Post("http://127.0.0.1:12080/go",poc.replaceBody("group=zzz&action=decrypt¶m="+codec.EncodeUrl(data), false),poc.appendHeader("content-type", "application/x-www-form-urlencoded")) if(err){ return(err) } group = "zzz"; //jsrpc的action return json.loads(rsp.GetBody())["data"]}// beforeRequest 允许发送数据包前再做一次处理,定义为 func(origin []byte) []bytebeforeRequest = func(req) { //获取请求体 req_body = poc.GetHTTPPacketBody(req) //加密 res = getEnc(string(req_body)) //获取其他的参数 res = json.loads(res) //修改其他的请求头 req = poc.ReplaceHTTPPacketHeader(req, "requestId", res["requestid"]) req = poc.ReplaceHTTPPacketHeader(req, "timestamp", res["timestamp"]) req = poc.ReplaceHTTPPacketHeader(req, "sign", res["sign"]) //修改请求体 req = poc.ReplaceHTTPPacketBody(req, res["encstr"]) return []byte(req)}// afterRequest 允许对每一个请求的响应做处理,定义为 func(origin []byte) []byteafterRequest = func(rsp) { body = poc.GetHTTPPacketBody(rsp) rep = getDec((string(body))) rep = json.loads(rep) output = sprintf("code: %v, msg: %v,data: %v", rep["code"], rep["msg"],rep["data"]) rsp = poc.ReplaceHTTPPacketBody(rsp, output) return []byte(rsp)}// mirrorHTTPFlow 允许对每一个请求的响应做处理,定义为 func(req []byte, rsp []byte, params map[string]any) map[string]any// 返回值回作为下一个请求的参数,或者提取的数据,如果你需要解密响应内容,在这里操作是最合适的mirrorHTTPFlow = func(req, rsp, params) { return params}
此时我们已经是明文数据包,想干嘛干嘛了。
5.总结
本文结合当前渗透测试实战场景,描述了笔者近几年在JS对抗方面所遇到的一些场景,并给出个人日常使用中较为通用的解决方案。
行文匆忙,还请是师傅们斧正。
点击下方小卡片或扫描下方二维码观看更多技术文章
师傅们点赞、转发、在看就是最大的支持
原文始发于微信公众号(猪猪谈安全):渗透测试|RPC技术+yakit热加载的JS加密解决方案分享
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论