2021-09-13
前端密码 AES-CBC 加密方式的分析
本成果已经成功在 XMU-Check 和 NJU-Check 打卡系统中成功应用。
背景
如果访问 ids.xmu.edu.cn 这个页面,登录抓包,可以发现密码是加密后的 Base64 编码。
所以如果要模拟登录,直接 POST 密码是不行的(事实上这个页面服务端可能都考虑了,所以不加密密码直接 POST 目前也是可以的),因此自然想到研究一下这个密码的加密方式。
JS 逆向分析
首先,可以定位到表单提交事件位于login—wisedu_v1.0.js
的doLogin
函数中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | var casLoginForm = $("#casLoginForm");
casLoginForm.submit(doLogin);
function doLogin() {
var username = casLoginForm.find("#username");
var password = casLoginForm.find("#password");
if (!checkRequired(username, "usernameError")) {
username.focus();
return false;
}
if (!checkRequired(password, "passwordError")) {
password.focus();
return false;
}
var captchaResponse = casLoginForm.find("#captchaResponse");
if (!checkRequired(captchaResponse, "cpatchaError")) {
captchaResponse.focus();
return false;
}
_etd2(password.val(), casLoginForm.find("#pwdDefaultEncryptSalt").val());
}
|
函数最后通过_etd2
对密码进行加密,可以看到加密时用到了pwdDefaultEncryptSalt
字段的值。这个字段是表单中的一个隐藏属性:
接下来我们寻找_etd2
函数,可以发现在该文件中有这样一段混淆后的代码,格式化之后如下:
| function _etd2(_p0, _p1) {
try {
var _p2 = encryptAES(_p0, _p1);
$("#casLoginForm").find("#passwordEncrypt").val(_p2);
} catch (e) {
$("#casLoginForm").find("#passwordEncrypt").val(_p0);
}
}
|
可以看到加密后的密码_p2
是通过encryptAES
这一函数得到。在encrypt.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 | function _gas(data, key0,
iv0) {
key0 = key0.replace(/(^\s+)|(\s+$)/g, "");
var key = CryptoJS.enc.Utf8.parse(key0);
var iv = CryptoJS.enc.Utf8.parse(iv0);
var encrypted = CryptoJS.AES.encrypt(data, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
function encryptAES(data, _p1) {
if (!_p1) {
return data;
}
var encrypted = _gas(_rds(64) + data, _p1, _rds(16));
return encrypted;
}
var $_chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
var _chars_len = $_chars.length;
function _rds(len) {
var retStr = '';
for (i = 0; i < len; i++) {
retStr += $_chars.charAt(Math.floor(Math.random() * _chars_len));
}
return retStr;
}
|
其中,真正的加密操作在_gas
函数中进行,encryptAES
调用了_gas
函数。_rds
函数用于生成随机的字符串,因此最后加密的内容为:
- 数据:_rds(64) + data,即 64 个随机字符+原来的密码
- 密钥:_p1,即之前的 pwdDefaultEncryptSalt
- IV(初始向量):_rds(16),即 16 个随机字符
加密方式为 AES-CBC,填充方式为 Pkcs7。这样我们就把密码的加密流程分析清楚了。
密码学分析
第一反应是,这样加密之后,服务端是没有办法解密的!因为 AES-CBC 解密时,除了用到密钥,也要知道 IV 才可以。但是这里的 IV 是随机生成的,也没有在第一节的抓包中看到有把 IV 发送给服务端。如果我们在前端调试这个encryptAES
函数,可以发现每次生成的加密密码都不一样:
但是如果了解 AES-CBC 的加解密过程,可以发现,AES-CBC 以 128bit,即 16 字节为一个 Block 进行加解密。前一组的加密结果被用作后一组的初始向量:
所以在解密的时候,如果不知道初始 IV,也可以根据前一个 Block 的密文作为这一个 Block 的 IV 进行解密,因此可以得到除了第一个 Block 以外其他所有 Block 的解密结果:
而 AES-CBC 恰好以 16 字节为一个 Block,所以加密时,原先的密码前方填充了 64 个随机字符,也即多了 4 个没用的 Block,通过第四个 Block 的密文作为第五个 Block 的 IV,即可解密之后的密码。或者随机选取 IV 进行解密,然后丢弃前四个 Block 的结果,也能获得解密后的密码。
实现
按照上述分析的思路,使用pycryptodome
,加密和解密的算法如下:
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 | import base64
import random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
def random_bytes(length: int) -> bytes:
result = ''
chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
for i in range(length):
result += random.choice(chars)
return result.encode('utf-8')
def encrypt(password: str, salt: bytes):
iv = random_bytes(16)
plain_text = pad(random_bytes(64) + password.encode('utf-8'), 16, 'pkcs7')
aes_cipher = AES.new(salt, AES.MODE_CBC, iv)
cipher_text = aes_cipher.encrypt(plain_text)
return base64.b64encode(cipher_text).decode('utf-8')
def decrpyt(encrypted_password: str, salt: bytes):
iv = random_bytes(16)
cipher_text = base64.b64decode(encrypted_password.encode('utf-8'))
aes_cipher = AES.new(salt, AES.MODE_CBC, iv)
plain_text = unpad(aes_cipher.decrypt(cipher_text), 16, 'pkcs7')
return plain_text[64:].decode('utf-8')
|
用 123456 作为测试,结果如下:
| password = '123456'
salt = b'tls6AJdDsockG4az'
for i in range(10):
encrypted_password = encrypt(password, salt)
print('Encrpyed:', encrypted_password)
print('Original:', decrpyt(encrypted_password, salt))
|
可以看到,每次加密的结果都不一样,但是仅通过密钥 (salt) 都能解密得到相同的数据。
这种做法可以在一定程度上提高安全性,不过因为是对称加密,仍然无法防止中间人攻击,因为中间人可以同时获得服务端事先传来的 salt 和加密后的密码,从而通过上面的算法得到解密后的密码。
更安全的加密方式可以考虑非对称加密,前端通过公钥将密码加密,服务器通过私钥解密。这样中间人在仅获得公钥和密文的情况下也难以得到明文。