本质

JS的对抗 本质无非就是开发者通过一系列加密算法防止用户恶意爆破,攻击者通过逆向,推测与实践实现自加密逻辑爆破的过程。

image-20250822222901893

JS逆向

懂得基础的JS知识,和调试方法,可以移步到基础篇 这里主要就不讲解如何详细的逆向了,我们直接切入正题: 如何解决数据包的修改问题。

关于如何找到JS中的加解密的方法,不一定非得找到加解密函数。无论是APP还是JS中可以先找到明文点

什么是明文点?

在前端进行复杂的请求操作时,肯定会经过一系列从A函数–>B函数–>C函数–>D函数–>E函数之类的流程, 那么在这个流程中,假设D函数是加密函数,那么ABC函数中原始请求参数均是明文的,这就是明文点,找到明文点后再一步步调试, 其实就能顺腾摸瓜找到加密函数了。

img

这里推荐一个环境靶场:http://39.98.108.20:8085

项目地址

0ctDay/encrypt-decrypt-vuls: 加解密逻辑漏洞靶场 (github.com)

v_jstools

cilame/v_jstools: 模仿着写一个 chrome 插件,用来快速调试前端 js 代码。 (github.com)

一款比较好用的工具,能够一键监测JS中指定函数的调用

这里对工具的使用不做过多讲解,感兴趣的直接去看保姆级教程—前端加密的对抗(附带靶场)-先知社区的相关内容

以简单的例子使用与摸索

就我而言 摸索了一下 主要还是以标准hook配置为主

image-20250824150142443

使用正确账密123/123并hook调试,取得更多有价值的信息

当登录成功时,我们可以以功能点为导向反向推理加密逻辑.

可以很清楚的看到数据的完整流动

  • 明文点,用户传入账密和验证码

    image-20250824153903848

    image-20250824160538049

    这里可以看见t.data就是用户传入的信息

  • 用户登录认证逻辑 生成加密载荷并获取服务端响应

    image-20250824153750016

    结合网络查看请求包内容

    image-20250824162223389

  • 回显登录成功 并重定向到dashboard

    image-20250824154847949

这里就完整的实现了一整套认证逻辑,本质上也是配合断点一步一步调试 得到完整的数据传输日志,同时也能利用函数步过大致定位到对应的加密逻辑

image-20250824162036799

image-20250824155941125

在这样的定位过程中 就能确定经过 t.data = l(n)后,data内容为密文,结合前面对登录功能点的推测与跟进,可确定t.data 即为 加密后的内容, 那么l() 函数即为加密函数

image-20250824162317893

这个时候就可以进行跟进,典型的AES加密,甚至下面就是解密函数。其中t参数为原始的内容,f参数为密钥,h为密码。

image-20250824162401524

同时所谓的签名 时间戳防绕也可以在最开始定位的加密逻辑中找到,那么这个时候就可以愉快的写脚本实现爆破了

img

1. 为什么不用搜索的方式

如果能搜索到当然好,比如这个环境可以直接通过搜索AES的方式找到上下文。

但现在大部分webpack项目都自带混淆,在生产环境中许多变量和字符无法直接搜索,即使搜索到了阅读上下文也看不懂代码到底是个什么意思。

2. 明文点

上文提到明文点,在这个过程中我们跟踪的两个函数都是明文点,一个是v_jstools 提示的函数位置 u.interceptors.request.use((function(t){} ,还有一个是l()函数。生产情况下,我们可能无法直接分析出该函数的作用,即便看不懂,也是大有用处,

改包手法

主流的修改方式。

分类

1、 修改当前的数据包

浏览器发包后,代理到burp上或通过其他的形式修改这个数据包的内容,主要针对当前数据包的修改

场景: 分析请求参数、添加额外参数、绕过前端校验等等

2、主动发包的加密与解密

脱离浏览器, 主动发包并加密, 对响应的数据包解密

场景: 自动化工具插入漏洞payload、暴力破解、重放测试等

修改当前数据包

作用域修改法

这里有个账号: test 密码:123

首先我们在表单处输入 test 密码 1234,很明显我们是无法登录的。

这时我们进入到调试中,走到加密前的一步,直接在作用域中修改即可登录成功

img

这个方式需要在加密启动和签名加签的同时启动

JS-forward

首先我们了解一下JS-forward运行原理,简单来说就是在明文点处插入一段JS代码,这段代码先通过AJAX请求将现有的请求内容发送给burpsuite,burpsuite拦截并修改内容后,返回到原始变量中,优点是操作比较统一,如果明文点正确,后续所有的改包操作都可以在burpsuite中进行

img

a. 找到明文点

工具链接:JS-Forward

确认明文变量名,然后启动JS-forward

这里我们需要将变量名向前看一点,确认明文变量就是t.data

img

启动JS-forward

输入变量名: t.data

输入数据类型: json (JS原始对象也可设置为JSON)

输入请求标识: REQUEST

这里的请求标识仅仅作为标识使用, 没有任何意义, 主要是为了在burpsuite中区分请求包和响应包

输入$end 结束后, 会监听2个端口 分别是38080, 28080, 还会生成一段JS代码我们留作后续使用。

img

b. 插入JS代码

JS-forward 的使用尽量在明文点的函数第一行插入相关代码,因为不知道后续代码会做什么样的操作。

具体的插入方式

b1: 找到F12–源代码–替换(覆盖)–点击选择文件夹–选择我们硬盘中一个空文件夹

img

如果浏览器有提示点击允许

img

具体的插入方式

b1: 找到F12–源代码–替换(覆盖)–点击选择文件夹–选择我们硬盘中一个空文件夹

img

img

b2: 在 网页–明文点JS文件处–右键–替换内容

(因为只有这样才能修改JS中的代码)

img

b3: 将JS-forward中生成的代码,复制到函数第一行,Ctrl+S保存

img

c. 打开burpsuite

关闭调试功能或关闭F12,刷新页面,再次发包时即可接收到明文信息

img

d. 注意

  1. 能够理解原理的话, 尽量自己思考思考。此功能与浏览器–burpsuite代理无关,浏览器的代理可不设置为burpsuite。
  2. 另外在实际测试过程中谷歌浏览器会报CORS错误,edge正常,具体原因不明,以后有机会再分析

本质就是 我又新建了一个代理服务器 相当于两个代理服务器负责不同的职能 一个是负责爬取明文点转发并转回 Burp负责处理改包罢了

主动发包的加密与解密

以上方法只适合修改浏览器的提交操作后的数据包修改

  • 优点:是简单易上手,就算是复杂的加密环境,只要找到明文点,后续工作不太复杂。
  • 缺点:是无法应对主动发包的情况,比如要使用被动扫描工具,暴力破解,重放测试等需求的时候,无法自动化完成。

所以我们介绍第二类的解决方案,为什么不直接介绍这个方式呢?主要还是因为主动发包的加密和解密更加复杂,需要读懂目标JS代码环境中防范改包的一些业务逻辑,如果目标的JS代码混淆和加密并不是特别厉害,还是可以一试的。

在这之前再来了解一款工具

JS-RPC

jxhczhl/JsRpc: 远程调用(rpc)浏览器方法,免去抠代码补环境 (github.com)

所谓RPC,翻译过来是远程调用的意思,简单来说就是搭建一个桥梁让两个不同的应用系统之间一方能主动调用另外一方的api或函数。

我们知道浏览器中的加解密都是通过JS实现的,但如果想脱离浏览器在本地运行JS代码最大一个问题就是如何调用浏览器的api。举个例子: 比如我们想在python中执行JS中的解密函数,我们通常是两个方法:

方法:

  1. 读懂JS加密函数的内容,在python中通过python代码使用同样的逻辑来实现。 也就是所谓的读懂逻辑本地搓
  2. 通过execJs,selenium等框架执行指定的JS代码,理想状态是好的。但是,如果目标环境的加密很复杂,又伴随着一些复杂的对象操作,需要解析各种变量以及补环境来满足函数调用,意味着可能我们还没开始渗透就已经脱了几层皮了。

所以我们可以使用这款工具提高渗透测试前期的效率。

JS-RPC这款工具的**工作原理就是在控制台中执行一段代码,通过websocket与本地的python服务端相连。这样一来如果python中想要执行代码,只需要通过RPC即可调用控制台中的函数了,**不需要再本地还原。

img

步骤
1. 先打开客户端,然后打开控制台,将JSrpc的注入代码输入

img

先输入函数内容

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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
}

然后再输入

1
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=zzz");

其中变量名demo, 和group的值可以自己定

2. 记录加密函数

首先还是调试到加密那一步

img

这里我们就知道了, 加密函数为l()

在控制台中输入window.enc = l, 控制台会显示当前函数信息, 并保存非形参的参数, 注册成功后我们可以主动调用enc()函数, 查看是否有效

1
2
3
4
window.enc() = l

#测试
enc("123")
3. 向JsRPC中注册这些函数
1
2
3
4
5
#有参
demo.regAction("enc", function (resolve, param) {
var res = enc(String(param));
resolve(res);
})

img

4. 测试调用
1
http://127.0.0.1:12080/go?group=zzz&action=enc&param=123

img

懂得原理后, 我们可以继续进行操作了

JS-RPC + MITM

目前比较流行的一个解决方案, 通过 mitm 将原始请求发送到JS-RPC中进行加密后修改原始数据包内容, 再进行发包

mitmproxy 为一款代理工具, 你可以把他理解为python版的burpsuite, 可以进行拦截,改包等操作, 所以我们的思路是这样:

img

接下来就到实际应用的阶段了:

针对目前的靶场, 我们需要分析一下JS的代码。

img

几个关键的变量和函数:

  • r: 很明显就是时间戳。
  • n: 原始的表单数据请求经过v() 函数处理后, 再进行JSON编码。
  • i: 使用p函数生成的requestId。
  • s: 使用MD5()函数生成的哈希值, 生成的方式为n+i+r的字符串拼接。
  • 加密: 对变量n使用l()函数进行加密。

针对实际请求包的修改:

我们需要在请求头中添加 timestamp,requestId, sign 等字段

然后修改明文请求体进行加密。

接下来就是实现:

1. 启动JS-rpc, 并注入代码

img

2. 打上断点并调试, 记录函数, 并注册

记录

1
2
3
4
5
6
7
8
9
10
//时间戳
window.time = Date.parse
//requestId
window.id = p
//v函数
window.v1 = v
//签名
window.m = a.a.MD5
//加密
window.enc = l

注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//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);
})

测试

img

这样我们就可以一次性获取所有请求的需求了

3. 构建MITM

之前介绍过Mitmproxy , 就是python版的burpsuite, 所以我们只需要知道核心的代码逻辑: 即提取原始请求体后, 向请求头中添加requestId, timestamp, sign字段 并且 替换原始请求体为加密后的内容就OK了, 直接Chatgpt生成

其实本质也就是本地搓+远程注册 然后应对加签也有对应的处理方案

代码:

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
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的监听端口

img

4. 测试

在burpsuite中发送明文数据包, 在经过mitm处理后, 自动加密, 此时服务端再不会报错了

img

JS-RPC + YAKIT 热加载

在刚刚的例子里面, 我们虽然可以实现加解密, 但是毕竟数据包拐了山路十八弯, 难免优点麻烦。 有没有少拐点弯的方法呢? 当然有啦, yakit作为国内优秀的渗透一体化工具,现在的在渗透中的使用率越来越高,相信随着国产化的普及,以后会更加流行。 还不会使用yakit的同学真的可以好好学习一下, 有的功能挺好用的。 在yakit中有一个模块叫做“web fuzzer“,有点像burpsuite中 repeater 和 intruder的结合体, 提供了数据包的重放和fuzz功能。

热加载

通过web fuzzer自带热加载功能, 通过官方对热加载的描述, 我们可以构建一段代码,在发送后自动加密, 这样就省去mitm的使用了。

img

热加载中自带了两个魔术方法, 分别对请求和响应自动做处理

img

1. 原理

通过研究, 可以详细解释解释:

请求包处理: 实现beforeRequest..方法即可, 其中行参”req“为一个字节数组, 保存了完整的请求内容字节。

那么我们通过yak官方的poc库(实际就是HTTP库),提供的方法,可以做如下操作:

1
2
3
4
5
6
//获取请求体
requestBody = poc.GetHTTPPacketBody(req)
//修改请求包中指定的请求头
req = poc.ReplaceHTTPPacketHeader(req, "请求头名", "请求头值")
//修改请求体
req = poc.ReplaceHTTPPacketBody(req, "修改后的值")
2. 实现

首先我们需要准备好, 解密后的请求体, 可以直接把之前提到的变量n的值拿过来

img

完整热加载内容(JsRPC沿用上面的内容

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
// 定义加密函数
func getEnc(data){
rsp,rep,err = poc.Post("http://127.0.0.1:12080/go",poc.replaceBody("group=zzz&action=req&param="+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功能测试暴力破解,爆破成功

img


本站由 Satoru 使用 Stellar 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。