“
JS加解密是目前黑盒渗透测试中非常大的痛点, 为了满足等保需求, 越来越多行业的内部系统使用了加密技术对HTTP的请求体加密, 增加签名, 时间戳, RequestId等防止进行数据包的抓包改包. 这篇文章就把网络上一些流行的JS逆向后进行数据包的抓包改包方式来个大汇总, 希望能够帮助到大家入门, 提高黑盒渗透的水平。
值得注意的一点是, 本篇不太适合完全不懂代码的同学看, 因为介绍的工具和方法均需要对JS和python等代码有一定的了解, 关于JS加密这点没法一蹴而就, 只能慢慢的学习。
环境
环境靶场:
http://39.98.108.20:8085/
各位高抬贵手, 写了几个逻辑漏洞,可以自己试试,但别把服务器搞崩了
项目地址:
0ctDay/encrypt-decrypt-vuls: 加解密逻辑漏洞靶场 (https://github.com/0ctDay/encrypt-decrypt-vuls/)
可以根据需求自行修改
说到环境本人也是下了很大的心血,毕竟项目上遇到的存在前端加解密的系统都是保密性质的, 不太好当作公开文章复现学习所使用。在网上找了很久也没现成的,索性自己搞了一套加解密网关和一个比较基础的图书管理系统,这里的前端使用VUE进行开发, 没有网上许多简单通过Jquery实现的一套加解密逻辑漏洞的靶场那么简单。但也没那么难, 仅仅使用了固定密钥进行AES加密的形式对请求体进行加密。
关于加解密也引出许多思考,第一是实现HTTP加解密是一对一实现的吗?第二是如果把数据包加密了,那安全设备是不是也失效了,这里带着几个疑问简单了解一下加解密的实现流程。
这里以请求为例
用户输入明文消息在被JS提取后, 通过JS中定义的加密方法进行加密并制作请求包进行请求。发送到后端后,一般来说由加解密网关进行解密后, 发送到真正提供服务的服务器上。
加解密网关本质其实就是一种反向代理,除了本身转发数据包的功能以外,还对数据包进行解密。
按照这种部署模式,有两个优势:
-
如果新增分布式系统, 无需重复实现加解密功能, 直接使用作为该网关的下游服务即可。 -
在springcloud gateway和 分布式系统之间部署安全设备, 可以检测明文流量识别攻击行为。
改包的防范
先汇总一下目前流行的防止改包方式
主要是这么几个方面
1. 请求参数和路径的加密(靶场未实现)
-
如果原始请求是GET请求,或防止访问者获取请求路径,通常会将用户实际的请求路径和GET请求参数封装都封装为POST请求的请求体,通过加解密网关再还原为原始GET请求传入后端分布式服务上。在APP中比较常见。
表现的形式通常为:抓包后发现访问任何功能都是同一路径,并且请求全为密文
2. 请求体的加密
-
这类在纯web中最常见, 通常仅仅加密接口请求的请求体内容,但有以下几类加密问题。 -
使用固定密钥 — 顾名思义, 这种情况一般JS中会存储密钥, 属于最简单的一种 -
使用动态密钥 — JS中不存储,一般用户第一次请求后将密钥加密写入COOKIE或本地存储中, 这类加密追踪难度较大。 -
对称加密 — 加解密数据包内容同一套密钥 -
非对称加密 — 加密一套解密一套 -
算法 — 算法就不是特别固定了, 常见的诸如AES RSA等, 也遇到过使用国密算法或一些冷门算法。
3. 签名
-
签名的应用也十分广泛,app,小程序和现在许多web中均存在,签名的构成主要是以下几点 -
RequestId — 为了防止重放攻击, 客户端生成随机RequestId 服务端接收后保存至Redis中, 如果再次接收到此RequestID, 则视为非法请求 -
时间戳 — 添加时间戳的超时时间, 一旦超时, 原始数据包失效 -
签名本身 — 通过 requestId + 原始请求体或请求参数 + 时间戳 + 盐值合并生成哈希值 从而保证以上参数的有效性和唯一性
JS逆向
JS逆向老生常谈的问题了, 需要懂得基础的JS知识,和调试方法, 这里主要就不讲解如何详细的逆向了,我们直接切入正题:如何解决数据包的修改问题。
关于如何找到JS中的加解密的方法,很多新手会认为一定得找到加解密函数。其实不然, 老手都会告诉你无论是APP还是JS中可以先找到明文点。
什么是明文点?
在前端进行复杂的请求操作时,肯定会经过一系列从A函数–>B函数–>C函数–>D函数–>E函数之类的流程, 那么在这个流程中,假设D函数是加密函数,那么ABC函数中原始请求参数均是明文的,这就是明文点,找到明文点后再一步步调试, 其实就能顺腾摸瓜找到加密函数了。
v_jstools
cilame/v_jstools: 模仿着写一个 chrome 插件,用来快速调试前端 js 代码。(https://github.com/cilame/v_jstools)
一款比较好用的工具,能够一键监测JS中指定函数的调用
这里我们就以靶场举例看看实际的操作
1. 首先安装好v_jstools
进行配置, 选择需要挂钩的函数即可,然后点击挂钩总开关。
具体挂钩什么函数,需要根据具体需求, 千万不要全部都勾上,那样信息流就太多了不利于我们的筛选
2. 打开挂钩功能, 打开浏览器控制台, 刷新页面
3. 发送数据包,查看提示
这里就可以看到, 针对请求包, 我们找到了请求的明文点
4. 跟进对应的JS文件
在该代码处打上断点, 跟踪后续的处理
重新发送数据包, 并点击步过, 我们就可以看到 名字为 n 的变量就是我们的提交内容
此时n还是明文, 那么我们继续步过, 经过 t.data = l(n)
后,data内容为密文,
经过实际请求包的比较, 发现t.data 即为 加密后的内容, 那么l() 函数即为加密函数
所以我们跟进l() 函数中看看,发现l函数即为加密函数,也顺便发现了解密函数就在下面,其中t参数为原始的内容,f参数为密钥,h为密码。
再回到刚刚的起始点, 也可以发现requestID算法,时间戳算法 和 签名算法
5. 问题
当然这算是个比较简单的例子,算法和对象的跟踪的都比较直接
1. 为什么不用搜索的方式
如果能搜索到当然好,比如这个环境可以直接通过搜索AES的方式找到上下文。
但现在大部分webpack项目都自带混淆,在生产环境中许多变量和字符无法直接搜索,即使搜索到了阅读上下文也看不懂代码到底是个什么意思。
2. 明文点
上文提到明文点,在这个过程中我们跟踪的两个函数都是明文点,一个是v_jstools 提示的函数位置 u.interceptors.request.use((function(t){} ,还有一个是l()函数。生产情况下,我们可能无法直接分析出该函数的作用,即便看不懂,也是大有用处,这点我们后面娓娓道来。
修改数据包
找到明文点后,我们开始着手数据包的修改了,那么我们来了解一下主流的修改方式。
分类
1、 修改当前的数据包
浏览器发包后,代理到burp上或通过其他的形式修改这个数据包的内容,主要针对当前数据包的修改
场景:分析请求参数、添加额外参数、绕过前端校验等等
2、主动发包的加密与解密
脱离浏览器, 主动发包并加密, 对响应的数据包解密
场景:自动化工具插入漏洞payload、暴力破解、重放测试等
第一大类: 修改当前数据包
青铜 — 直接在明文点处修改
这里有个账号: test 密码:123
首先我们在表单处输入 test 密码 1234,很明显我们是无法登录的。
这时我们进入到调试中,走到加密前的一步,直接在作用域中修改
然后继续运行,即可登录成功了
这个方式需要在加密方式和一些签名的步骤之前,否则可能修改无效
白银 — JS-forward
个人认为仅仅改包的话,是一个比较万金油的办法
首先我们了解一下JS-forward运行原理,简单来说就是在明文点处插入一段JS代码,这段代码先通过AJAX请求将现有的请求内容发送给burpsuite,burpsuite拦截并修改内容后,返回到原始变量中,优点是操作比较统一,如果明文点正确,后续所有的改包操作都可以在burpsuite中进行
大概就是这样一个流程
所以我们来实现, 步骤有一丢丢麻烦,请耐心看完
a. 找到明文点
确认明文变量名,然后启动JS-forward
这里我们需要将变量名向前看一点,确认明文变量就是t.data
启动JS-forward
输入变量名: t.data
输入数据类型:json (JS原始对象也可设置为JSON)
输入请求标识:REQUEST
这里的请求标识仅仅作为标识使用, 没有任何意义, 主要是为了在burpsuite中区分请求包和响应包
输入$end 结束后, 会监听2个端口 分别是38080, 28080, 还会生成一段JS代码我们留作后续使用。
b. 插入JS代码
JS-forward 的使用尽量在明文点的函数第一行插入相关代码,因为不知道后续代码会做什么样的操作。
具体的插入方式
b1:找到F12–源代码–替换(覆盖)–点击选择文件夹–选择我们硬盘中一个空文件夹
如果浏览器有提示点击允许
b2: 在 网页–明文点JS文件处–右键–替换内容
(因为只有这样才能修改JS中的代码)
b3: 将JS-forward中生成的代码,复制到函数第一行,Ctrl+S保存
c. 打开burpsuite
关闭调试功能或关闭F12,刷新页面,再次发包时即可接收到明文信息
d. 注意
-
能够理解原理的话, 尽量自己思考思考。此功能与浏览器–burpsuite代理无关,浏览器的代理可不设置为burpsuite。
-
另外在实际测试过程中谷歌浏览器会报CORS错误,edge正常,具体原因不明,以后有机会再分析
第二大类: 主动发包的加密与解密
以上方法只适合修改浏览器的提交操作后的数据包修改
-
优点:是简单易上手,就算是复杂的加密环境,只要找到明文点,后续工作不太复杂。
-
缺点:是无法应对主动发包的情况,比如要使用被动扫描工具,暴力破解,重放测试等需求的时候,无法自动化完成。
所以我们介绍第二类的解决方案,为什么不直接介绍这个方式呢?主要还是因为主动发包的加密和解密更加复杂,需要读懂目标JS代码环境中防范改包的一些业务逻辑,如果目标的JS代码混淆和加密并不是特别厉害,还是可以一试的。
在这之前再来了解一款工具
JS-RPC
jxhczhl/JsRpc: 远程调用(rpc)浏览器方法,免去抠代码补环境 (https://github.com/jxhczhl/JsRpc)
所谓RPC,翻译过来是远程调用的意思,简单来说就是搭建一个桥梁让两个不同的应用系统之间一方能主动调用另外一方的api或函数。
我们知道浏览器中的加解密都是通过JS实现的,但如果想脱离浏览器在本地运行JS代码最大一个问题就是如何调用浏览器的api。举个例子: 比如我们想在python中执行JS中的解密函数,我们通常是两个方法:
-
读懂JS加密函数的内容,在python中通过python代码使用同样的逻辑来实现。 -
通过execJs,selenium等框架执行指定的JS代码,理想状态是好的。但是,如果目标环境的加密很复杂,又伴随着一些复杂的对象操作,需要解析各种变量以及补环境来满足函数调用,意味着可能我们还没开始渗透就已经脱了几层皮了。
所以我们可以使用这款工具提高渗透测试前期的效率。
JS-RPC这款工具的工作原理就是在控制台中执行一段代码,通过websocket与本地的python服务端相连。这样一来如果python中想要执行代码,只需要通过RPC即可调用控制台中的函数了,不需要再本地还原。
我们来看看实现
步骤
1. 先打开客户端,然后打开控制台,将JSrpc的注入代码输入
先输入函数内容
function Hlclient(wsURL) {
this.wsURL = wsURL;
this.handlers = {
_execjs: function (resolve, param) {
var res = eval(param)
if (!res) {
resolve("没有返回值")
} else {
resolve(res)
}
}
};
this.socket = undefined;
if (!wsURL) {
throw new Error('wsURL can not be empty!!')
}
this.connect()
}
Hlclient.prototype.connect = function () {
console.log('begin of connect to wsURL: ' + this.wsURL);
var _this = this;
try {
this.socket = new WebSocket(this.wsURL);
this.socket.onmessage = function (e) {
_this.handlerRequest(e.data)
}
} catch (e) {
console.log("connection failed,reconnect after 10s");
setTimeout(function () {
_this.connect()
}, 10000)
}
this.socket.onclose = function () {
console.log('rpc已关闭');
setTimeout(function () {
_this.connect()
}, 10000)
}
this.socket.addEventListener('open', (event) => {
console.log("rpc连接成功");
});
this.socket.addEventListener('error', (event) => {
console.error('rpc连接出错,请检查是否打开服务端:', event.error);
});
};
Hlclient.prototype.send = function (msg) {
this.socket.send(msg)
}
Hlclient.prototype.regAction = function (func_name, func) {
if (typeof func_name !== 'string') {
throw new Error("an func_name must be string");
}
if (typeof func !== 'function') {
throw new Error("must be function");
}
console.log("register func_name: " + func_name);
this.handlers[func_name] = func;
return true
}
//收到消息后这里处理,
Hlclient.prototype.handlerRequest = function (requestJson) {
var _this = this;
try {
var result = JSON.parse(requestJson)
} catch (error) {
console.log("catch error", requestJson);
result = transjson(requestJson)
}
//console.log(result)
if (!result['action']) {
this.sendResult('', 'need request param {action}');
return
}
var action = result["action"]
var theHandler = this.handlers[action];
if (!theHandler) {
this.sendResult(action, 'action not found');
return
}
try {
if (!result["param"]) {
theHandler(function (response) {
_this.sendResult(action, response);
})
return
}
var param = result["param"]
try {
param = JSON.parse(param)
} catch (e) {}
theHandler(function (response) {
_this.sendResult(action, response);
}, param)
} catch (e) {
console.log("error: " + e);
_this.sendResult(action, e);
}
}
Hlclient.prototype.sendResult = function (action, e) {
if (typeof e === 'object' && e !== null) {
try {
e = JSON.stringify(e)
} catch (v) {
console.log(v)//不是json无需操作
}
}
this.send(action + atob("aGxeX14") + e);
}
function transjson(formdata) {
var regex = /"action":(?<actionName>.*?),/g
var actionName = regex.exec(formdata).groups.actionName
stringfystring = formdata.match(/{..data..:.*..w+..:s...*?..}/g).pop()
stringfystring = stringfystring.replace(/\"/g, '"')
paramstring = JSON.parse(stringfystring)
tens = `{"action":` + actionName + `,"param":{}}`
tjson = JSON.parse(tens)
tjson.param = paramstring
return tjson
}
然后再输入
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=zzz");
其中变量名demo, 和group的值可以自己定
2. 记录加密函数
首先还是调试到加密那一步
这里我们就知道了, 加密函数为l()
在控制台中输入window.enc = l
, 控制台会显示当前函数信息, 并保存非形参的参数, 注册成功后我们可以主动调用enc()
函数, 查看是否有效
window.enc() = l
#测试
enc("123")
3. 向JsRPC中注册这些函数
#有参
demo.regAction("enc", function (resolve, param) {
var res = enc(String(param));
resolve(res);
})
4. 测试调用
发送get请求, 或post请求到本机的12080端口, 即可获取调用结果
http://127.0.0.1:12080/go?group=zzz&action=enc¶m=123
懂得原理后, 我们可以继续进行操作了
黄金 — JS-RPC + MITM
目前比较流行的一个解决方案, 通过 mitm 将原始请求发送到JS-RPC中进行加密后修改原始数据包内容, 再进行发包
mitmproxy 为一款代理工具, 你可以把他理解为python版的burpsuite, 可以进行拦截,改包等操作, 所以我们的思路是这样:
接下来就到实际应用的阶段了:
针对目前的靶场, 我们需要分析一下JS的代码。
几个关键的变量和函数:
-
r:很明显就是时间戳。 -
n:原始的表单数据请求经过v() 函数处理后, 再进行JSON编码。 -
i:使用p函数生成的requestId。 -
s:使用MD5()函数生成的哈希值, 生成的方式为n+i+r的字符串拼接。 -
加密:对变量n使用l()函数进行加密。
针对实际请求包的修改:
我们需要在请求头中添加 timestamp,requestId, sign 等字段。
然后修改明文请求体进行加密。
接下来就是实现:
1. 启动JS-rpc, 并注入代码
2. 打上断点并调试, 记录函数, 并注册
记录
//时间戳
window.time = Date.parse
//requestId
window.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. 构建MITM
之前介绍过Mitmproxy , 就是python版的burpsuite, 所以我们只需要知道核心的代码逻辑: 即提取原始请求体后, 向请求头中添加requestId, timestamp, sign字段 并且 替换原始请求体为加密后的内容就OK了, 直接Chatgpt生成
代码:
import json
import time
import hashlib
import uuid
from mitmproxy import http
import requests
import requests
def request(flow: http.HTTPFlow) -> None:
if flow.request.pretty_url.startswith("http://39.98.108.20:8085/api/"):
# 提取原始请求体
original_body = flow.request.content.decode('utf-8')
data = {"group": "zzz", "action": "req", "param": original_body}
res = requests.post("http://127.0.0.1:12080/go",data=data)
res_json = json.loads(res.text)["data"]
data_json = json.loads(res_json)
print(data_json)
# 对请求体进行加密处理(这里假设加密方法是简单的哈希)
encrypted_body = data_json["encstr"]
# 替换请求体
flow.request.text = encrypted_body
# 生成 requestId,sign 和 timestamp
request_id = data_json["requestid"]
timestamp = data_json["timestamp"]
sign = data_json["sign"]
# 添加或替换请求头
flow.request.headers["requestId"] = request_id
flow.request.headers["timestamp"] = str(timestamp)
flow.request.headers["sign"] = sign
# 运行 mitmproxy 时加载这个脚本:mitmproxy -s your_script.py
例:
mitmproxy -p 8083 -s mitm.py
将代码运行起来后, burpsuite 的upstream 设为 mitm的监听端口
4. 测试
在burpsuite中发送明文数据包, 在经过mitm处理后, 自动加密, 此时服务端再不会报错了
钻石 — JS-RPC + YAKIT 热加载
https://www.yaklang.com/products/Web%20Fuzzer/fuzz-hotpatch/
在刚刚的例子里面, 我们虽然可以实现加解密, 但是毕竟数据包拐了山路十八弯, 难免优点麻烦。有没有少拐点弯的方法呢?当然有啦, yakit作为国内优秀的渗透一体化工具,现在的在渗透中的使用率越来越高,相信随着国产化的普及,以后会更加流行。还不会使用yakit的同学真的可以好好学习一下, 有的功能挺好用的。在yakit中有一个模块叫做“web fuzzer“,有点像burpsuite中 repeater 和 intruder的结合体, 提供了数据包的重放和fuzz功能。
热加载
通过web fuzzer自带热加载功能, 通过官方对热加载的描述, 我们可以构建一段代码,在发送后自动加密, 这样就省去mitm的使用了。
yak官网对这个热加载功能解释并不是特别详细,仅仅提到了热加载中自带了两个魔术方法, 分别对请求和响应自动做处理
八、热加载 | Yak Program Language (yaklang.com)
1. 原理
通过研究, 可以详细解释解释:
请求包处理:实现beforeRequest..方法即可, 其中行参”req“为一个字节数组, 保存了完整的请求内容字节。
那么我们通过yak官方的poc库(实际就是HTTP库),提供的方法,可以做如下操作:
//获取请求体
requestBody = poc.GetHTTPPacketBody(req)
//修改请求包中指定的请求头
req = poc.ReplaceHTTPPacketHeader(req, "请求头名", "请求头值")
//修改请求体
req = poc.ReplaceHTTPPacketBody(req, "修改后的值")
2. 实现
首先我们需要准备好, 解密后的请求体, 可以直接把之前提到的变量n的值拿过来
完整热加载内容(JsRPC沿用上面的内容)
// 定义加密函数
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"]
}
// beforeRequest 允许发送数据包前再做一次处理,定义为 func(origin []byte) []byte
beforeRequest = 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) []byte
afterRequest = func(rsp) {
return []byte(rsp)
}
// mirrorHTTPFlow 允许对每一个请求的响应做处理,定义为 func(req []byte, rsp []byte, params map[string]any) map[string]any
// 返回值回作为下一个请求的参数,或者提取的数据,如果你需要解密响应内容,在这里操作是最合适的
mirrorHTTPFlow = func(req, rsp, params) {
return params
}
最后通过fuzz功能测试暴力破解,爆破成功
当然在这个过程中也并不是一帆风顺, 最大的一个问题还是yakit官方文档对热加载功能和poc库的解释还不够详细,导致很多功能都是靠猜(小小的吐槽一下)。但是该有的功能其实都有,工具本身还是很强大的。
yakit解密
关于解密,其实和加密一样,如果懂得前面的逻辑,完全可以实现自动化,这里就不具体介绍了。
但除此之外,我们还可以使用yak中新增插件的方式来手动解密数据。原理也非常简单,我们来看看:
1. 还是在控制台中记录和注册解密函数
//记录:这里解密函数就是d函数
window.dec = d
//注册
demo.regAction("decrypt", function (resolve,param) {
resolve(dec(param))
})
2. 插件–本地–新建插件
类型选择codec插件, 勾选右键执行
3. 编辑内容–保存
handle = func(origin /*string*/) {
//JSrpc的group
group = "zzz";
//jsrpc的action
action = "decrypt";
rsp,rep = poc.Post("http://127.0.0.1:12080/go",poc.replaceBody("group="+group+"&action="+action+"¶m="+codec.EncodeUrl(origin), false),poc.appendHeader("content-type", "application/x-www-form-urlencoded"))~
return json.loads(rsp.GetBody())["data"];
}
可以做个测试
4. 在web_fuzzer中使用
当然肯定有人问, burpsuite怎么办呢?当然也有办法,比如autodecoder插件就可以完成,但实现比这个还是要麻烦得多。
最强王者 — JS原生
相信看到这里的,一定是努力学习的你,所以这一段也是结语啦。
JS解密的最终形态是什么呢?爬虫高手会告诉你,当然是用JS解密JS啊,通过反混淆–分析代码逻辑–研究参数作用–添加补环境–最终通过本地运行JS来加解密。但是我们之前也讨论过,等这一套操作搞完,可能渗透测试项目都快结束了, 所以为了速度,我们只能用魔法打败魔法。
那么yakit近期的版本更新中也提到使用了更为强大的JS库GoJa,这是相关api的文章
上新!更强大的JS引擎:goja (https://mp.weixin.qq.com/s/MuUdMud4fyuocIWnULVyXw)
也就是说,如果条件允许,完全可以脱离JSrpc直接去运行加解密,签名等逻辑。
至于在这个靶场怎么实现呢?当作部置作业了,毕竟这个加解密逻辑已经相当简单了,相信你一定可以独立完成!
另外靶场还存在几个逻辑漏洞,可以在完成加解密的基础上,把他们都找出来!
原文链接:https://xz.aliyun.com/t/14629
免责声明:本公众号的内容仅供合法、正当、健康的用途,切勿将其用于违反法律法规的行为。如因此导致任何法律责任或纠纷,本公众号概不负责。谢谢您的理解与配合!
原文始发于微信公众号(三十的安全屋):保姆级前端JS加密对抗(附带靶场)