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

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 标签,执行一次登录操作:

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

<!-- 发现密钥存储位置 -->
<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

  • 类型: 逆向分析文档

  • 目的: 学习研究

  • 许可: 仅供学习使用