九江学院统一身份认证系统登录加密逆向分析

2026-01-14T17:50:59.png

📋 目录


一、背景介绍

1.1 目标系统

  • 系统名称:九江学院统一身份认证平台
  • 登录地址https://authserver.jju.edu.cn/authserver/login
  • 目标:分析并理解前端密码加密逻辑

1.2 技术栈

  • 前端加密:CryptoJS (AES-CBC加密)
  • 后端框架:基于CAS (Central Authentication Service)
  • 传输协议:HTTPS

1.3 分析工具

  • Chrome DevTools (网络抓包、调试)
  • HTML源码查看
  • JavaScript代码分析

二、初步侦察

2.1 网络请求抓包

打开登录页面,按 F12 进入开发者工具,切换到 Network 标签,执行一次登录操作:

1
2
3
4
5
6
7
8
9
POST /authserver/login? service=https://ehall.jju.edu.cn/login HTTP/1.1
Host: authserver.jju.edu.cn
Content-Type: application/x-www-form-urlencoded

username=202012345&
password=U2FsdGVkX1%2BabcdefghijklmnopqrstuvwxyzABCDEF... &
execution=e7637a06-8937-4197-b207-5986a5a159e6_... &
_eventId=submit&
cllt=userNameLogin

关键发现

  • ✅ 用户名明文传输
  • ✅ 密码已加密(Base64格式)
  • ✅ 存在 execution 参数(类似CSRF Token)

2.2 页面源码分析

右键查看页面源代码,搜索关键字 passwordencryptsalt

1
2
3
4
5
6
7
8
<!-- 发现密钥存储位置 -->
<input type="hidden" id="pwdEncryptSalt" value="V4Kh77JL6yk52pNG"/>

<!-- 加密后的密码存储字段 -->
<input type="hidden" id="saltPassword" name="password"/>

<!-- 原始密码输入框 -->
<input id="password" name="passwordText" type="password"/>

发现AES加密密钥直接嵌入在HTML页面的隐藏字段中


三、关键文件分析

3.1 加密核心文件:encrypt.js

定位到 /authserver/jjuThemea/static/common/encrypt.js

3.1.1 随机字符串生成器

1
2
3
4
5
6
7
8
9
10
11
12
// 定义随机字符集(排除易混淆字符 I, L, O, 0, 1, 9)
var $aes_chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
var aes_chars_len = $aes_chars.length;

// 生成指定长度的随机字符串
function randomString(len) {
var retStr = '';
for (i = 0; i < len; i++) {
retStr += $aes_chars.charAt(Math.floor(Math.random() * aes_chars_len));
}
return retStr;
}

原理解析

  • 字符集包含 48 个字符(26个大写字母 + 22个小写字母和数字)
  • 排除了 I, L, O, 0, 1, 9 等易混淆字符
  • 使用 Math.random() 生成伪随机数
  • 每次调用返回指定长度的随机字符串

3.1.2 AES加密核心函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// AES加密核心函数
function getAesString(data, key0, iv0) {
// 去除密钥首尾空字符
key0 = key0.replace(/(^\s+)|(\s+$)/g, "");

// 将密钥和IV转换为CryptoJS可识别的格式
var key = CryptoJS.enc. Utf8.parse(key0);
var iv = CryptoJS.enc.Utf8.parse(iv0);

// 执行AES加密
var encrypted = CryptoJS.AES.encrypt(data, key, {
iv: iv, // 初始化向量
mode: CryptoJS.mode.CBC, // CBC模式
padding: CryptoJS.pad.Pkcs7 // PKCS7填充
});

// 返回Base64编码的密文
return encrypted.toString();
}

原理解析

参数 作用 说明
data 明文数据 要加密的内容
key0 加密密钥 16字节AES密钥
iv0 初始化向量 16字节随机IV
mode: CBC 加密模式 密码块链接模式
padding: Pkcs7 填充方式 PKCS#7填充标准

3.1.3 完整加密流程

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
// 密码加密入口函数
function encryptPassword(pwd0, key) {
try {
return encryptAES(pwd0, key);
} catch (e) {
return pwd0;
}
}

// 完整加密实现
function encryptAES(data, aesKey) {
if (!aesKey) {
return data;
}

// 关键步骤:
// 1. 生成64位随机前缀
// 2. 拼接:随机前缀 + 原始密码
// 3. 生成16位随机IV
// 4. 调用AES加密
var encrypted = getAesString(
randomString(64) + data, // 64位随机前缀 + 密码
aesKey, // 加密密钥
randomString(16) // 16位随机IV
);

return encrypted;
}

加密流程可视化

1
2
3
4
5
6
7
8
9
10
11
12
13
原始密码:  "MyPassword123"

生成64位随机前缀: "TxmRpDkNwYs... 64个字符..."

拼接明文: "TxmRpDkNwYs...64个字符... MyPassword123"

生成16位随机IV: "AbCdEfGhJkMnPqRs"

AES-CBC加密 (使用密钥和IV)

Base64编码

最终密文: "U2FsdGVkX1+abc..."

3.2 登录逻辑文件:login.js

定位到 /authserver/jjuThemea/static/web/js/login.js

3.2.1 密钥获取与加密调用

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 checkForm() {
var cllt = $(". login-main #cllt").val();

if (cllt == 'userNameLogin') {
// 验证用户名和密码...
if (utils.requireInput($(LOGIN_USERNAME_ID), 0, 100, ... )) {
return;
}
if (utils.requireInput($(LOGIN_PASSWORD_ID), 0, 32, ...)) {
return;
}

// 从HTML页面获取AES密钥
var aesKey = $("#pwdEncryptSalt").val();

// 获取用户输入的密码
var password = $(LOGIN_PASSWORD_ID).val();

// 使用密钥加密密码
var encryptedPassword = encryptPassword(password, aesKey);

// 将加密后的密码存入隐藏字段
$("#saltPassword").val(encryptedPassword);

// 禁用原始密码输入框(防止明文提交)
$(LOGIN_PASSWORD_ID).attr("disabled", "disabled");
}

return true;
}

流程说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户点击登录按钮

触发 checkForm() 函数

验证用户名和密码格式

从页面获取密钥: $("#pwdEncryptSalt").val()

获取用户输入: $(LOGIN_PASSWORD_ID).val()

调用加密函数: encryptPassword(password, aesKey)

将密文存入隐藏字段: $("#saltPassword").val(encrypted)

禁用原始密码输入框

提交表单(只提交密文)

3.3 HTML页面分析:login_page.html

3.3.1 关键表单字段

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
<!-- 完整的登录表单结构 -->
<form method="post" id="pwdFromId" action="/authserver/login">

<!-- 用户名输入框 -->
<input id="username" name="username" type="text"
placeholder="请输入学号/工号"/>

<!-- 原始密码输入框(提交时会被禁用) -->
<input id="password" name="passwordText" type="password"
placeholder="请输入密码" maxlength="32"/>

<!-- AES加密密钥(从服务器动态生成) -->
<input type="hidden" id="pwdEncryptSalt" value="V4Kh77JL6yk52pNG"/>

<!-- 加密后的密码(真正提交给服务器的字段) -->
<input type="hidden" id="saltPassword" name="password"/>

<!-- 验证码输入框(如果需要) -->
<input id="captcha" name="captcha" type="text"/>

<!-- 登录类型标识 -->
<input type="hidden" id="cllt" name="cllt" value="userNameLogin"/>

<!-- 登录方式标识 -->
<input type="hidden" id="dllt" name="dllt" value="generalLogin"/>

<!-- 执行流程标识(类似CSRF Token) -->
<input type="hidden" id="execution" name="execution"
value="e7637a06-8937-4197-b207-5986a5a159e6_..."/>

<!-- 事件标识 -->
<input type="hidden" name="_eventId" value="submit"/>

<!-- 登录按钮 -->
<button type="button" onclick="startLogin(this)">登录</button>
</form>

字段说明表

字段名 ID/Name 类型 作用 是否提交
用户名 username text 用户输入 ✅ 明文提交
原始密码 passwordText password 用户输入 ❌ 被禁用
加密密码 password (saltPassword) hidden 存储密文 ✅ 密文提交
加密密钥 pwdEncryptSalt hidden 存储密钥 ❌ 不提交
验证码 captcha text 用户输入 ✅ 明文提交
执行标识 execution hidden CSRF保护 ✅ 提交

3.3.2 密钥生成机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
服务器端(伪代码):

@GetMapping("/login")
public String loginPage(HttpSession session, Model model) {
// 1. 生成16位随机密钥
String aesKey = generateRandomKey(16);
// 例如: "V4Kh77JL6yk52pNG"

// 2. 存入服务器Session(用于后续解密)
session.setAttribute("aesKey", aesKey);

// 3. 将密钥传递给前端页面
model.addAttribute("pwdEncryptSalt", aesKey);

return "login";
}

密钥生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
用户访问登录页面

服务器生成随机密钥(16位)

存入Session: session.put("aesKey", key)

嵌入HTML: <input id="pwdEncryptSalt" value="xxx"/>

返回页面给浏览器

JavaScript读取密钥进行加密

用户提交登录

服务器从Session取出密钥

使用相同密钥解密密码

验证密码是否正确

四、加密流程深度解析

4.1 完整加密流程图

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
┌─────────────────────────────────────────────────────┐
│ 步骤1: 用户输入密码 │
│ 输入: "MyPassword123" │
└────────────────┬────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 步骤2: JavaScript从HTML获取密钥 │
│ $("#pwdEncryptSalt").val() → "V4Kh77JL6yk52pNG" │
└────────────────┬────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 步骤3: 生成64位随机前缀 │
│ randomString(64) │
│ → "TxmRpDkNwYsFqJzH... 64个字符..." │
└────────────────┬────────────────────────────────────┘

┌─────────────────���───────────────────────────────────┐
│ 步骤4: 拼接明文 │
│ "TxmRpDkNwYsF...64位... MyPassword123" │
│ 总长度: 64 + 13 = 77 字符 │
└────────────────┬────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 步骤5: 生成16位随机IV │
│ randomString(16) │
│ → "AbCdEfGhJkMnPqRs" │
└────────────────┬────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 步骤6: UTF-8编码 │
│ 明文 → 77字节 │
│ 密钥 → 16字节 │
│ IV → 16字节 │
└────────────────┬────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 步骤7: PKCS7填充 │
│ 77字节 → 填充到16的倍数 → 80字节 │
│ 填充内容: 添加3个字节 0x03 │
└────────────────┬────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 步骤8: AES-CBC加密 │
│ 明文块1(16B) ⊕ IV → AES → 密文块1 │
│ 明文块2(16B) ⊕ 密文块1 → AES → 密文块2 │
│ 明文块3(16B) ⊕ 密文块2 → AES → 密文块3 │
│ 明文块4(16B) ⊕ 密文块3 → AES → 密文块4 │
│ 明文块5(16B) ⊕ 密文块4 → AES → 密文块5 │
│ 结果: 80字节密文 │
└────────────────┬────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 步骤9: 打包IV和密文 │
│ [IV(16字节)][密文(80字节)] = 96字节 │
└────────────────┬────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 步骤10: Base64编码 │
│ 96字节 → Base64 → 128字符 │
│ "U2FsdGVkX1+abcdefghijk..." │
└────────────────┬────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 步骤11: 提交到服务器 │
│ POST /authserver/login │
│ password=U2FsdGVkX1+abc... │
└─────────────────────────────────────────────────────┘

4.2 三大核心机制

4.2.1 机制一:64位随机前缀

作用:防止密码字典攻击

1
2
// 实现代码
randomString(64) + password

原理:每次加密都在密码前添加64位随机字符,使得相同密码产生完全不同的密文

4.2.2 机制二:16位随机IV

作用:确保相同明文产生不同密文

1
2
3
// 实现代码
var iv = randomString(16);
CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC });

原理:CBC模式下,IV参与第一个数据块的加密,每次使用不同的IV会产生不同的密文

4.2.3 机制三:动态密钥

作用:防止密钥重用和会话劫持

1
2
<!-- 密钥存储在HTML页面中 -->
<input type="hidden" id="pwdEncryptSalt" value="V4Kh77JL6yk52pNG"/>

原理:服务器为每个会话生成独立的加密密钥,存储在Session中,客户端从HTML页面获取并使用

4.3 数据结构

4.3.1 加密前的明文结构

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────────────────────────────┬─────────────────┐
│ 64位随机前缀 │ 原始密码 │
├──────────────────────────────────────┼─────────────────┤
│ TxmRpDkNwYsFqJzHcBnMaXvKgWeRtPsJf │ MyPassword123 │
│ DhLmQwZxCvBnMkYuHgTrEwQpLkJh │ │
└──────────────────────────────────────┴─────────────────┘
64 字符 13 字符

总长度: 77 字符

UTF-8编码: 77 字节

PKCS7填充: 80 字节

4.3.2 加密后的密文结构

1
2
3
4
5
6
7
8
9
10
┌─────────────────┬──────────────────────────────────┐
│ IV (16字节) │ 实际密文 (80字节) │
├─────────────────┼──────────────────────────────────┤
│ AbCdEfGhJkMn... │ [加密后的二进制数据] │
└─────────────────┴──────────────────────────────────┘
16字节 80字节

总计: 96字节

Base64编码: 128字符

4.4 服务器端解密流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
接收密文:  "U2FsdGVkX1+abc..."

Base64解码 → 96字节

分离: IV(16字节) + 密文(80字节)

从Session获取密钥: "V4Kh77JL6yk52pNG"

AES-CBC解密

去除PKCS7填充 → 77字节

去掉前64位随机前缀

得到原始密码: "MyPassword123"

验证密码是否正确

五、总结

5.1 核心技术要点

加密算法

1
2
3
4
算法:  AES-128
模式: CBC (Cipher Block Chaining)
填充: PKCS7
编码: Base64

三大核心机制

序号 机制 参数 作用
1 随机前缀 64位字符 防止字典攻击
2 随机IV 16字节 增加密文随机性
3 动态密钥 16字节 防止密钥重用

完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
前端流程: 
1. 访问登录页面 → 获取动态密钥
2. 生成64位随机前缀 → 拼接密码
3. 生成16位随机IV → 执行AES加密
4. 打包IV和密文 → Base64编码
5. 提交密文到服务器

后端流程:
1. 接收Base64密文 → 解码
2. 分离IV和密文 → 从Session获取密钥
3. AES解密 → 去除填充
4. 去掉64位前缀 → 得到原始密码
5. 验证密码 → 返回结果

5.2 关键知识点

密钥管理

  • 来源: HTML页面隐藏字段 <input id="pwdEncryptSalt">
  • 长度: 16字节 (AES-128)
  • 生命周期: Session级别(约30分钟)
  • 获取方式: $("#pwdEncryptSalt").val()

核心函数

1
2
3
randomString(length)           // 生成随机字符串
getAesString(data, key, iv) // AES加密核心
encryptPassword(password, key) // 密码加密入口

5.3 常见问题

Q: 密钥从哪里来?

1
A: 从HTML页面的隐藏字段 <input id="pwdEncryptSalt"> 中获取

Q: 每次登录密钥都不同吗?

1
A: 是的,每次访问登录页面服务器会生成新的密钥

Q: 为什么要加64位随机前缀?

1
A: 防止密码字典攻击,增加破解难度到 10^108

Q: IV的作用是什么?

1
A: 确保相同密码产生不同密文,可以公开传输(编码在密文前16字节)

5.4 免责声明

1
2
3
本文档仅供学习和研究使用,请勿用于非法用途。
使用本文档内容造成的任何后果由使用者自行承担。
建议在合法合规的前提下学习相关技术。

文档信息

  • 版本: v1.0

  • 类型: 逆向分析文档

  • 目的: 学习研究

  • 许可: 仅供学习使用