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

📋 目录
一、背景介绍
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 标签,执行一次登录操作:
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 页面源码分析
右键查看页面源代码,搜索关键字 password、encrypt、salt:
<!-- 发现密钥存储位置 -->
<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 随机字符串生成器
// 定义随机字符集(排除易混淆字符 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加密核心函数
// 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 完整加密流程
// 密码加密入口函数
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;
}
加密流程可视化:
原始密码: "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 密钥获取与加密调用
// 表单验证与加密函数
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;
}
流程说明:
用户点击登录按钮
↓
触发 checkForm() 函数
↓
验证用户名和密码格式
↓
从页面获取密钥: $("#pwdEncryptSalt").val()
↓
获取用户输入: $(LOGIN_PASSWORD_ID).val()
↓
调用加密函数: encryptPassword(password, aesKey)
↓
将密文存入隐藏字段: $("#saltPassword").val(encrypted)
↓
禁用原始密码输入框
↓
提交表单(只提交密文)
3.3 HTML页面分析:login_page.html
3.3.1 关键表单字段
<!-- 完整的登录表单结构 -->
<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 密钥生成机制
服务器端(伪代码):
@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";
}
密钥生命周期:
用户访问登录页面
↓
服务器生成随机密钥(16位)
↓
存入Session: session.put("aesKey", key)
↓
嵌入HTML: <input id="pwdEncryptSalt" value="xxx"/>
↓
返回页面给浏览器
↓
JavaScript读取密钥进行加密
↓
用户提交登录
↓
服务器从Session取出密钥
↓
使用相同密钥解密密码
↓
验证密码是否正确
四、加密流程深度解析
4.1 完整加密流程图
┌─────────────────────────────────────────────────────┐
│ 步骤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位随机前缀
作用:防止密码字典攻击
// 实现代码
randomString(64) + password
原理:每次加密都在密码前添加64位随机字符,使得相同密码产生完全不同的密文
4.2.2 机制二:16位随机IV
作用:确保相同明文产生不同密文
// 实现代码
var iv = randomString(16);
CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC });
原理:CBC模式下,IV参与第一个数据块的加密,每次使用不同的IV会产生不同的密文
4.2.3 机制三:动态密钥
作用:防止密钥重用和会话劫持
<!-- 密钥存储在HTML页面中 -->
<input type="hidden" id="pwdEncryptSalt" value="V4Kh77JL6yk52pNG"/>
原理:服务器为每个会话生成独立的加密密钥,存储在Session中,客户端从HTML页面获取并使用
4.3 数据结构
4.3.1 加密前的明文结构
┌──────────────────────────────────────┬─────────────────┐
│ 64位随机前缀 │ 原始密码 │
├──────────────────────────────────────┼─────────────────┤
│ TxmRpDkNwYsFqJzHcBnMaXvKgWeRtPsJf │ MyPassword123 │
│ DhLmQwZxCvBnMkYuHgTrEwQpLkJh │ │
└──────────────────────────────────────┴─────────────────┘
64 字符 13 字符
↓
总长度: 77 字符
↓
UTF-8编码: 77 字节
↓
PKCS7填充: 80 字节
4.3.2 加密后的密文结构
┌─────────────────┬──────────────────────────────────┐
│ IV (16字节) │ 实际密文 (80字节) │
├─────────────────┼──────────────────────────────────┤
│ AbCdEfGhJkMn... │ [加密后的二进制数据] │
└─────────────────┴──────────────────────────────────┘
16字节 80字节
↓
总计: 96字节
↓
Base64编码: 128字符
4.4 服务器端解密流程
接收密文: "U2FsdGVkX1+abc..."
↓
Base64解码 → 96字节
↓
分离: IV(16字节) + 密文(80字节)
↓
从Session获取密钥: "V4Kh77JL6yk52pNG"
↓
AES-CBC解密
↓
去除PKCS7填充 → 77字节
↓
去掉前64位随机前缀
↓
得到原始密码: "MyPassword123"
↓
验证密码是否正确
五、总结
5.1 核心技术要点
加密算法:
算法: AES-128
模式: CBC (Cipher Block Chaining)
填充: PKCS7
编码: Base64
三大核心机制:
| 序号 | 机制 | 参数 | 作用 |
|---|---|---|---|
| 1 | 随机前缀 | 64位字符 | 防止字典攻击 |
| 2 | 随机IV | 16字节 | 增加密文随机性 |
| 3 | 动态密钥 | 16字节 | 防止密钥重用 |
完整流程:
前端流程:
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()
核心函数:
randomString(length) // 生成随机字符串
getAesString(data, key, iv) // AES加密核心
encryptPassword(password, key) // 密码加密入口
5.3 常见问题
Q: 密钥从哪里来?
A: 从HTML页面的隐藏字段 <input id="pwdEncryptSalt"> 中获取
Q: 每次登录密钥都不同吗?
A: 是的,每次访问登录页面服务器会生成新的密钥
Q: 为什么要加64位随机前缀?
A: 防止密码字典攻击,增加破解难度到 10^108
Q: IV的作用是什么?
A: 确保相同密码产生不同密文,可以公开传输(编码在密文前16字节)
5.4 免责声明
本文档仅供学习和研究使用,请勿用于非法用途。
使用本文档内容造成的任何后果由使用者自行承担。
建议在合法合规的前提下学习相关技术。
文档信息
-
版本: v1.0
-
类型: 逆向分析文档
-
目的: 学习研究
-
许可: 仅供学习使用