重庆大学登录密码加密逆向实战

概述

本文是针对重庆大学统一身份认证系统(authserver.cqu.edu.cn)的前端密码加密逆向实战笔记。我们将通过浏览器开发者工具定位加密入口,拆解加密逻辑,最终用Python复现前端加密流程并模拟登录请求。

系统采用的是较为常见的前端加密方案:AES-128-CBC模式,搭配自定义随机字符集生成的盐值前缀、随机IV,增强了单次加密的不可预测性。

免责声明:本文仅用于学习交流前端逆向技术,请勿用于非法用途,遵守学校网络使用规范。


浏览器逆向网页分析

我们先通过Chrome DevTools(或其他现代浏览器开发者工具)定位密码加密的核心位置:

步骤1:打开登录页和开发者工具

访问 http://authserver.cqu.edu.cn/authserver/login,按 F12 打开DevTools,切换到 Network 面板,勾选「Preserve log」。

步骤2:模拟提交抓包

输入任意用户名、密码,点击「登录」按钮。注意!不要输入真实密码,我们只是抓分析包。

在Network面板中过滤 login 请求,会看到一个POST请求:

  • 请求URL:http://authserver.cqu.edu.cn/authserver/login
  • 核心参数:username(明文)、password(一串Base64编码的密文)、ltdlltexecution

步骤3:定位加密入口

接下来重点找 password 的生成位置,有两个常用方法:

  1. Network面板搜索变量名:直接搜索 pwdDefaultEncryptSalt(抓包时注意到请求前可能有类似的加密盐变量)或 encrypt 相关关键词
  2. Sources面板XHR断点+单步调试:在Sources面板右侧添加 XHR/fetch Breakpoint,断点URL设为 *login*,再次点击登录,会在请求发送前暂停,再一步步往上找调用栈

最终我们可以在登录页嵌入的某个 <script> 标签中找到完整的加密代码。


加密流程核心拆解

找到的加密代码主要分为三个核心部分,我们逐一解析:

1. AES-128-CBC底层实现

系统直接使用了开源加密库 CryptoJS 实现底层的加密逻辑:

const CryptoJS = require("crypto-js")

// AES-128-CBC加密模式,key必须为16位(对应128位密钥长度),mode和padding固定
function getAesString(data, key0, iv0) {
  // 先清除密钥两端可能存在的空格
  key0 = key0.replace(/(^\s+)|(\s+$)/g, "");
  // CryptoJS需要将字符串密钥、IV转换为内部的WordArray对象
  var key = CryptoJS.enc.Utf8.parse(key0);
  var iv = CryptoJS.enc.Utf8.parse(iv0);
  // 执行加密:指定CBC模式、Pkcs7填充
  var encrypted = CryptoJS.AES.encrypt(data, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });
  // 返回Base64格式的最终密文(与抓包看到的password参数格式一致)
  return encrypted.toString();
}

2. 自定义随机字符串生成

为了让同一密码每次加密的结果都不同(防止彩虹表攻击),系统使用了去掉易混淆字符的自定义字符集来生成随机盐值前缀和随机IV:

// 自定义字符集:去掉了oOLl、9gq、Vv、Uu、I1这些容易看错的字符
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;
}

3. 密码加密主函数入口

主函数是前端调用加密的最后一步,主要逻辑是:获取密钥 -> 生成随机盐值前缀和IV -> 组合明文 -> 调用底层AES加密

function encryptAES(data, aesKey) {
  // 如果没有密钥就直接返回明文(防御性编程)
  if (!aesKey) {
    return data;
  }
  // 关键组合:在原始密码前加64位随机前缀,再用16位随机IV加密
  var encrypted = getAesString(randomString(64) + data, aesKey, randomString(16));
  return encrypted;
}

登录流程完整复现

要模拟完整的登录,除了加密密码,还需要获取服务器动态生成的加密密钥登录相关的一次性参数(如lt、execution)。

前置准备

  1. 安装必要的Python库:
    pip install requests beautifulsoup4 execjs
  2. 下载或复制前端找到的加密代码,保存为 cqu_encrypt.js(注意:需要去掉浏览器特有的全局变量引用,只保留三个加密函数+引入CryptoJS的代码)

Python完整代码

import requests
from bs4 import BeautifulSoup
import re
import execjs

# ====================== 配置部分 ======================
# 前端加密代码文件路径
ENCRYPT_JS_PATH = "./cqu_encrypt.js"
# 登录页URL
LOGIN_URL = "http://authserver.cqu.edu.cn/authserver/login"
# 测试用的账号密码(不要填真实的!)
TEST_USERNAME = "test_user"
TEST_PASSWORD = "test_pwd_123"
# ====================== 配置部分 ======================

# 通用请求头(模拟浏览器行为,避免被识别为爬虫)
COMMON_HEADERS = {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Content-Type": "application/x-www-form-urlencoded",
    "Origin": "http://authserver.cqu.edu.cn",
    "Pragma": "no-cache",
    "Referer": LOGIN_URL,
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
}

def get_login_params_and_cookies(session: requests.Session) -> tuple:
    """
    从登录页HTML中获取:
    1. 加密密钥 pwdDefaultEncryptSalt
    2. 登录一次性参数 lt、dllt、execution、_eventId、rmShown
    3. 登录页返回的cookies(需要保留会话)
    """
    # 发起GET请求获取登录页
    response = session.get(LOGIN_URL, headers=COMMON_HEADERS, verify=False)
    response.raise_for_status()  # 检查请求是否成功

    # 解析HTML
    soup = BeautifulSoup(response.text, "html.parser")

    # 1. 提取加密密钥(在<script>标签中)
    pwd_salt = ""
    scripts = soup.find_all("script")
    for script in scripts:
        if script.string:
            match = re.search(r'pwdDefaultEncryptSalt\s*=\s*"([^"]+)"', script.string)
            if match:
                pwd_salt = match.group(1)
                break
    if not pwd_salt:
        raise ValueError("未找到加密密钥 pwdDefaultEncryptSalt!")

    # 2. 提取登录表单中的一次性参数(在隐藏的input标签中)
    login_params = {}
    hidden_inputs = soup.find_all("input", type="hidden")
    for inp in hidden_inputs:
        name = inp.get("name")
        value = inp.get("value", "")
        if name:
            login_params[name] = value

    # 补充固定的登录参数
    login_params["username"] = TEST_USERNAME
    login_params["dllt"] = login_params.get("dllt", "userNamePasswordLogin")
    login_params["_eventId"] = login_params.get("_eventId", "submit")
    login_params["rmShown"] = login_params.get("rmShown", "1")

    return pwd_salt, login_params, session.cookies

def encrypt_password(password: str, salt: str) -> str:
    """
    调用前端加密代码,复现密码加密流程
    """
    with open(ENCRYPT_JS_PATH, "r", encoding="utf-8") as f:
        js_code = f.read()
    # 编译JS代码并调用encryptAES函数
    ctx = execjs.compile(js_code)
    return ctx.call("encryptAES", password, salt)

if __name__ == "__main__":
    # 禁用requests的SSL警告(因为测试用的是HTTP,HTTPS可能不需要,但为了兼容保留)
    requests.packages.urllib3.disable_warnings()

    # 1. 创建Session对象(自动管理cookies,保持会话一致性)
    session = requests.Session()

    try:
        # 2. 获取登录所需的所有参数和cookies
        print("正在获取登录参数和加密密钥...")
        pwd_salt, login_params, cookies = get_login_params_and_cookies(session)
        print(f"获取到的加密密钥:{pwd_salt}")

        # 3. 加密测试密码
        print("正在加密密码...")
        encrypted_pwd = encrypt_password(TEST_PASSWORD, pwd_salt)
        login_params["password"] = encrypted_pwd
        print(f"加密后的密码:{encrypted_pwd}")

        # 4. 发送模拟登录请求
        print("正在发送模拟登录请求...")
        login_response = session.post(
            LOGIN_URL,
            headers=COMMON_HEADERS,
            data=login_params,
            verify=False,
            allow_redirects=True  # 允许自动重定向(登录成功后会跳转到主页或回调地址)
        )
        login_response.raise_for_status()

        # 5. 简单验证登录是否成功(这里可以根据实际跳转后的页面内容判断)
        if "统一身份认证" not in login_response.text:
            print("✅ 模拟登录请求成功!(可能已跳转)")
        else:
            print("❌ 模拟登录请求失败!(仍在登录页)")

    except Exception as e:
        print(f"❌ 出错了:{str(e)}")

修正后的前端加密JS代码(cqu_encrypt.js)

注意:如果直接从浏览器复制代码,可能不需要引入require(浏览器是全局加载CryptoJS的),但在Node.js/execjs环境中需要:

// execjs环境需要先引入crypto-js(如果用的是PyExecJS的默认Node.js,需要先全局或在同级目录安装:npm install crypto-js)
const CryptoJS = require("crypto-js");

// AES-128-CBC底层加密
function getAesString(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();
}

// 自定义随机字符串生成
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;
}

// 密码加密主函数
function encryptAES(data, aesKey) {
  if (!aesKey) {
    return data;
  }
  var encrypted = getAesString(randomString(64) + data, aesKey, randomString(16));
  return encrypted;
}

安全分析与注意事项

  1. 密钥暴露问题:加密密钥 pwdDefaultEncryptSalt 直接以明文形式嵌入在登录页HTML中,攻击者可以轻松获取,这降低了前端加密的防窃听之外的安全性(但能防止明文密码被直接记录在日志中)
  2. 随机性设计合理:通过在密码前加64位随机盐值、每次生成不同的随机IV,确保同一账号同一密码每次加密的结果都不同,有效抵御了彩虹表攻击
  3. 传输安全建议:学校目前使用的是HTTP协议传输,虽然密码被加密,但仍建议升级为HTTPS协议,防止中间人攻击
  4. 不要用于非法用途:本文仅用于学习前端逆向技术,违反学校网络规范或法律法规后果自负

总结

本文通过浏览器开发者工具定位了重庆大学统一身份认证系统的前端加密入口,拆解了AES-128-CBC加密、自定义随机字符串生成、密码组合三个核心流程,并最终用Python + PyExecJS复现了加密逻辑,模拟了登录请求。

这是一个非常典型的高校/企业统一身份认证前端加密案例,掌握这种方法可以快速分析类似的系统。