JS逆向(三种方法)

JS逆向靶场安装

https://github.com/SwagXz/encrypt-labs

访问靶场

http://127.0.0.1/encrypt-labs-main/easy.php (js无混淆,用这个)

三种方法

autoDecoder

我们以对称加密-AES加解密为例子

可以看到,发送了一个GET请求去服务器端请求了aes_key 与 aes_iv,且是不变的

全局搜索generate,并且打断点

填入autoDecoder进行解密

加解密如下,需要在响应包那里设置null,表示为空,即不使用解密。如果不为空则就是全文本加密,这里演示请求包解密,响应包不解密

注意,一定要保存配置不然是不会生效的,文件名和后缀都没有要求

同样的,选项这里,也需要设置:

  • 加密选项为自带算法加解密
  • 域名选择需要加解密的网站
  • 设置明文关键字,如果检测出有该关键字,则认为是明文,仅登录所以关键字写了password

但是填了password发现有个bug就没填了

当上面的配置保存之后,就可以在请求模块和响应模块这里看到多出来了autoDecoder选项

可以在autoDecoder这里查看到明文

发送为加密的数据包

它会帮你自动加密,从日志可以看到

jsRPC

例一

以美团为例子

首先找到登陆的数据包

image-20250126175836158

ctrl+f搜索encrypt

首先双击运行

然后在控制台注入Jsenv_dev.js

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
var rpc_client_id, Hlclient = function (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 () {
if (this.wsURL.indexOf("clientId=") === -1 && rpc_client_id) {
this.wsURL += "&clientId=" + rpc_client_id
}
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("请求信息解析错误", requestJson);
return
}
if (result["registerId"]) {
rpc_client_id = result['registerId']
return
}
if (!result['action'] || !result["message_id"]) {
console.warn('没有方法或者消息id,不处理');
return
}
var action = result["action"], message_id = result["message_id"]
var theHandler = this.handlers[action];
if (!theHandler) {
this.sendResult(action, message_id, 'action没找到');
return
}
try {
if (!result["param"]) {
theHandler(function (response) {
_this.sendResult(action, message_id, response);
})
return
}
var param = result["param"]
try {
param = JSON.parse(param)
} catch (e) {
}
theHandler(function (response) {
_this.sendResult(action, message_id, response);
}, param)
} catch (e) {
console.log("error: " + e);
_this.sendResult(action, message_id, e);
}
}
Hlclient.prototype.sendResult = function (action, message_id, e) {
if (typeof e === 'object' && e !== null) {
try {
e = JSON.stringify(e)
} catch (v) {
console.log(v)//不是json无需操作
}
}
this.send(JSON.stringify({"action": action, "message_id": message_id, "response_data": e}));
}

在这里插入hook代码

1
2
3
4
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=test");
demo.regAction("encryptest", function (resolve,param) {
resolve(encrypt.encrypt(param));
})

ctrl + s 保存一下

然后登陆

发现成功上线

然后访问

http://127.0.0.1:12080/go?group=test&action=password&param=wqewqeasd

hook代码二

1
2
3
4
5
6
7
8
window.encryptest = function(password) {
return encrypt.encrypt(password);
};

var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=meituan");
demo.regAction("encryptest", function (resolve,param) {
resolve(window.encryptest(param));
})

http://127.0.0.1:12080/go?group=meituan&action=encryptest&param=123

在远程调用,使用JSRPC技术期间,必须关闭所有断点!!!!

例二:对称加密-AES加解密

1
2
3
4
5
6
7
8
window.encryptest = function(password) {
return CryptoJS.AES.encrypt(password, aesKey, {
iv: aesIv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
.toString();
};
1
2
3
4
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=encrypt");
demo.regAction("encryptest", function (resolve,param) {
resolve(window.encryptest(param));
})

访问

http://127.0.0.1:12080/go?group=encrypt&action=encryptest&param=%22{\%22username\%22:\%22admin\%22,\%22password\%22:\%22123456\%22}%22

image-20250127170117428

yakit热加载

当我们点击Submit按钮时,执行逻辑如下:

  1. 调用getData函数获取用户在表单中填写的用户名和密码。
  2. 调用outputObj函数,将获取到的数据进行AES CBC加密,并将加密后的数据、密钥和初始化向量(iv)一起封装成一个对象。
  3. 将这个对象转换为JSON字符串,准备发送到服务器。
  4. 使用fetch API向/crypto/js/lib/aes/cbc/handler这个后端接口发送一个POST请求,请求的body就是刚才生成的JSON字符串

接下来我们需要用 Yak 代码实现这个AES CBC加密,我们知道iv和key是固定的,所以我们可以先使用codec.DecodeHex(data)来把十六进制的字符串还原为原始的byte。

最后一步,我们编写一个真正可用的热加载函数,来对用户名和密码进行爆破,为了方便,我们将用户名固定为["admin"]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
handle = func(p) {
key = codec.DecodeHex("31323334313233343132333431323334")~
iv = codec.DecodeHex("03395d68979ed8632646813f4c0bbdb3")~
usernameDict = ["admin"]
// passwordDict = x"{{x(pass_top25)}}" // 我们可以使用x前缀字符串来通过fuzztag语法获取pass_top25字典中的值
passwordDict = ["admin", "123456", "admin123", "88888888", "666666"] // 也可以直接使用手写的list
resultList = []
for username in usernameDict {
for password in passwordDict {
m = {"username": username, "password": password}
jsonInput = json.dumps(m)
result = codec.AESCBCEncryptWithPKCS7Padding(key, jsonInput, iv)~
base64Result = codec.EncodeBase64(result)
resultList.Append(base64Result)
}
}
return resultList
}

将data参数设置为{{yak(handle)}},点击发送请求按钮

image-20250127172021017

实际上,靶场每次启动时爆破成功的密码是随机的,所以师傅们在复现时遇到可能会遇到与图中不同的情况。

具体参考https://yaklang.com/products/Web%20Fuzzer/fuzz-hotpatch-example2/