前端JS加密抗衡

前言

现如今越来越多的站点使用前端数据加密,从之前传统的base64、md5变向AES、RSA、或SM2/SM4(国产替代RSA)加密,有时候或干脆进行签名校验,这就需要我们逆向提取加密算法以及密钥,过程相对繁琐复杂,并且越来越多行业的内部系统使用了加密技术对HTTP的请求体加密, 时间戳, RequestId等防止进行数据包的抓包改包,这样一来增大了安全测试人员的测试难度,也使得很多不懂加密测试的人员望而却步

正文

从测试人员的角度出发,当我们拿到一个站点,在抓包时我们都希望碰到的是明文传输,因为简单且清晰明了,方便我们做测试,如下图

但是现如今这样的站点是少之又少,大多数都进行了加密处理,这就要求我们对JS逆向和前端调试有一定的了解,说起JS逆向,那都是老生常谈了,想找到JS中的加解密的方法,需要我们首先快速定位到加解密函数和明文点的位置,以下面这个站点为例,一个很常规很普通很常见的登录

在开始之前给大家推荐一款Google浏览器插件:v_jstools,能够一键监测JS中指定函数的调用,下载好之后,本地导入插件即可,在配置v_jstools的时候一定要注意不要全部勾选,根据实际情况选择需要挂钩的函数即可,否则信息流太大干扰我们测试

一切都配置好后,打开挂钩功能,并F12打开浏览器控制台,此时刷新页面,如果出现“inject start!”则表示插件生效了

当我们输入账号密码发送数据时,针对请求包,v_jstools帮我们找到了请求的明文点

既然定位到了请求的明文点,那就跟进对应的JS文件,并在该代码处(JSON.stringify)打上断点跟踪

当我们重新发送数据包,点击步过一次,就可以很容易的发现n变量就是我们提交的账号密码明文内容

继续点击步过,经过t.data=l(n)后,data内容为密文,并且通过对作用域中的请求包比较,发现t.data即为加密后的内容,那么l()函数即为加密函数

说干就干!我们跟进l()函数中看看,发现l函数即为加密函数,并且在附近也发现了解密函数,其中t参数为原始的内容,f参数为密钥,h为密码

再返回看,我们也可以发现requestID算法,时间戳算法和签名算法

假设现在我们知道了正确的账号密码:test/123,我们如何修改数据包,这里有三种解决方案,第一种方案就是直接在明文点修改,进入到调试中,走到加密前的一步,直接在作用域中修改

不过这种方式可能会引文加密方式和一些签名的限制,可能修改无效,有很大的局限性,赌一赌还是可以的。第二种方式需要用到JS-forward,大致的原理就是在明文点处插入一段JS代码,这段代码会通过AJAX请求发送给bp,bp做拦截处理并修改内容后,会将数值返回到原始变量中,首先确定明文变量名,通过调试,可以判定明文变量就是t.data

随后启动JS-forward,输入变量名、数据类型、请求标识(REQUEST),输入$end 结束后,会开启2个端口进行监听,分别是38080和28080,并且会生成一段js代码,这段代码就是我们即将插入到明文点的代码

在插入修改JS代码之前,要注意的是,我们需要新创一个文件夹,然后F12,找到对应的源代码进行替换,否则是无法直接进行修改JS中的代码,成功的话,右下角会有一个紫色圆的标识

然后将刚才生成的JS代码复制到明文点函数的第一行,最后保存

关闭F12,刷新页面再次发包时bp即可接收到明文信息

第三种方式是通过JSrpc注入,主动主动发包加解密,需要借助工具JS-RPC,这款工具的工作原理就是在浏览器控制台中执行一段代码,通过websocket与本地的python服务端进行连接,当我们想要执行代码,只需要通过RPC调用控制台中的函数即可,打开客户端,然后在控制台中输入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
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
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 liuty = new Hlclient("ws://127.0.0.1:12080/ws?group=liuty111"); //名字随意

首先还是调试到加密那里,前面我们也已经知道了加密函数为l(),然后在控制台中输入window.enc = l, 控制台会显示当前函数信息,并注册保存非形参的参数,那我们可以主动调用enc()函数, 随便做个测试,就123吧,查看是否有效

可以看到是没问题的,那我们就向JSrpc中注册这些函数

1
2
3
4
demo.regAction("enc", function (resolve, param) {
var res = enc(String(param));
resolve(res);
})

显示“true”则代表成功,那我们就可以通过mitmproxy将原始请求发送到JS-RPC中进行加密后修改原始数据包内容, 再进行发包(mitmproxy是一款代理工具, 想bp一样可以进行拦截,改包等操作)
再分析一遍JS,加上前面的分析和理解,不难做出判断:r很明显就是时间戳,n是将数据请求经过v()函数处理后再进行JSON编码,i是使用p函数生成requestId,s是使用MD5()函数并通过n+i+r字符串拼接的方式生成HASH,最后对变量n使用l()函数进行加密,知道这些之后,我们开始打上断点进行调试,首先记录这些函数

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);
})

这样一来,我们就可以一次性获取所有请求的需求了,对于mitmproxy我就不多说了,现在我们构建mitmproxy的脚本,通过前面的分析我们的代码逻辑为: 提取原始请求体后, 向请求头中添加requestId, timestamp, sign字段并且替换原始请求体为加密后的内容

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
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://测试IP"):
# 提取原始请求体
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时加载这个脚本并开启一个监听端口即可,但别忘了bp的upstream也要设为mitmproxy一样的的监听端口

下面做个测试,浏览器做好代理后,当bp发送明文数据包时, 在经过mitm处理后,会自动加密,okkkkk,success! 解密同理

好了,就分享到这了
大考就要开始了,该收拾行李去了


前端JS加密抗衡
http://example.com/2024/06/29/前端js加密抗衡/
作者
liuty
发布于
2024年6月29日
许可协议