Web登录加密逆向实战:从CryptoJS到Python复现的完整流程
1. 项目概述:从登录框到加密黑盒
最近在分析一些高校系统的自动化流程时,遇到了重庆大学统一身份认证的登录接口。乍一看,就是一个常见的用户名密码登录页面,但当你尝试用常规的requests库模拟登录时,会发现提交的表单数据里,密码字段是一串完全看不懂的、每次请求都不同的长字符串。这显然不是简单的Base64或MD5,而是前端JavaScript进行了实时加密处理。对于需要实现自动登录、数据采集或系统集成的开发者来说,理解并逆向这个加密过程就成了一个绕不开的“门槛”。今天,我就以一个实际逆向者的视角,带大家完整走一遍重庆大学登录加密的逆向分析流程,不仅还原加密逻辑,更重要的是分享在类似场景下的通用分析思路和避坑技巧。
这个实战的目标很明确:找到网页上点击“登录”按钮时,密码从明文到密文的完整转换链条,并用Python代码复现这一过程,最终实现稳定的自动化登录。整个过程会涉及浏览器开发者工具的使用、JavaScript关键代码定位、加密算法识别、以及Python的模拟实现。无论你是对Web逆向感兴趣的新手,还是遇到过类似加密困扰的同行,相信这篇详尽的记录都能给你带来直接的帮助。
2. 逆向环境准备与初步抓包
工欲善其事,必先利其器。在开始逆向之前,准备好趁手的工具和环境是成功的第一步。对于Web端的加密逆向,我们主要依靠浏览器和配套的调试工具。
2.1 核心工具链选择
我的主力浏览器是Chrome,其内置的开发者工具(DevTools)功能强大且更新及时。对于加密逆向,以下几个面板是关键:
- Network(网络):记录所有网络请求,这是我们分析的起点,用于定位登录的API接口和查看提交的数据。
- Sources(源代码):查看和调试页面加载的所有JavaScript文件。
- Console(控制台):执行JavaScript代码片段,用于测试我们的解密函数或调用特定的加密方法。
除了浏览器,一个代码编辑器(如VSCode)用于编写Python复现代码,以及一个可以方便发送HTTP请求的工具(如Postman或Hoppscotch)用于测试,就构成了基础环境。
注意:有些网站会检测开发者工具,打开后页面行为异常或加密逻辑改变。如果遇到这种情况,可以尝试使用无头浏览器模式(如Puppeteer)直接执行页面JS来获取加密结果,或者寻找检测逻辑并绕过。幸运的是,在此次分析中,重庆大学的登录页面没有这类反调试机制。
2.2 关键请求的定位与捕获
打开重庆大学的统一身份认证登录页面,按F12打开开发者工具,并切换到Network面板。记得勾选上“Preserve log”(保留日志),防止页面跳转后请求记录被清空。
在页面的用户名和密码框输入测试信息(例如,用户:test, 密码:123456),然后点击登录按钮。此时,Network面板会刷出一系列新的请求。我们需要从中找到那个真正提交登录凭证的请求。
通常,这个请求会有以下特征:
- 请求方法为POST(因为提交表单数据)。
- URL路径包含
login、auth、signin等关键字。 - 请求的Payload(负载)中包含用户名和加密后的密码。
快速浏览后,我找到了目标请求:一个指向/auth/login路径的POST请求。点击该请求,查看其“Headers”和“Payload”。
在“Payload”标签页下,我们看到表单数据类似这样:
username: test password: U2FsdGVkX19vBwQ8p4sVZ5J7K9mN...(一长串字符)这里,username是明文的,而password正是我们想要破解的密文。同时,在“Headers”中,注意查看Content-Type,通常是application/x-www-form-urlencoded,这决定了我们后续模拟提交的数据格式。
初步观察,这个密文长度较长,且含有/、+等字符,很可能是Base64编码后的结果。但Base64只是一种编码方式,并非加密算法,其原始内容才是加密后的二进制数据。我们的核心任务,就是找到生成这串密文的JavaScript代码。
3. 加密逻辑的定位与静态分析
找到加密发生的位置,是整个逆向过程中最具技巧性的环节。我们需要从数以万计的JavaScript代码行中,定位到那几行关键的加密函数。
3.1 搜索与断点追踪策略
在Sources面板中,我们可以使用全局搜索(Ctrl+Shift+F)来查找可能的关键字。搜索“password”、“encrypt”、“encode”、“Crypto”、“AES”、“RSA”等词汇。在这次分析中,搜索“password”在某个压缩过的JS文件里找到了多处匹配,但代码可读性极差。
更高效的方法是使用“Event Listener Breakpoints”(事件监听器断点)。在Sources面板的右侧,找到这个选项,展开“Mouse”事件,勾选“click”。然后回到页面,再次点击登录按钮。此时,代码执行会立即在第一个鼠标点击事件处理函数处暂停。
然后,我们使用“Step Over”(F10)和“Step Into”(F11)逐行执行。关注执行过程中,何时我们的明文密码被读取,何时被传入某个函数进行处理。当程序执行到类似var encryptedPwd = someFunction(document.getElementById('pwd').value);这样的语句时,我们就找到了加密的入口。
通过逐步跟踪,我发现密码在被提交前,被传入了一个名为encryptPassword的函数。双击这个函数名,或者顺着调用栈,我们就能跳转到该函数的定义位置。
3.2 核心加密函数剖析
最终定位到的encryptPassword函数,其代码经过格式化后,核心逻辑如下所示(为保护原系统安全,以下为模拟逻辑,但算法和结构一致):
function encryptPassword(password) { // 1. 生成一个随机的16字节字符串作为盐(Salt) var salt = CryptoJS.lib.WordArray.random(16); // 2. 将密码和盐拼接 var saltedPassword = password + salt.toString(); // 3. 进行多次SHA256哈希迭代 var key = CryptoJS.SHA256(saltedPassword); for (var i = 0; i < 1000; i++) { key = CryptoJS.SHA256(key.concat(salt)); } // 4. 使用AES算法,以key的前32字节作为密钥,盐作为IV,对原始密码进行加密 var iv = salt; var encrypted = CryptoJS.AES.encrypt( CryptoJS.enc.Utf8.parse(password), CryptoJS.enc.Hex.parse(key.toString().substring(0, 64)), // 取前64个16进制字符,即32字节 { iv: CryptoJS.enc.Hex.parse(iv.toString()), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); // 5. 将盐和加密后的密文拼接,再进行Base64编码 var result = salt.toString() + encrypted.ciphertext.toString(); return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(result)); }逻辑拆解:
- 加盐(Salt):每次加密都生成一个随机的盐值,目的是即使相同密码,每次加密结果也不同,防止“彩虹表”攻击。
- 密钥派生:使用密码和盐,经过SHA256的多次迭代(1000次),生成一个复杂的密钥。这个过程称为PBKDF2(Password-Based Key Derivation Function 2)的简化模拟,目的是增加暴力破解的难度。
- AES加密:使用上一步派生的密钥(取前32字节)和盐(作为初始化向量IV),采用CBC模式和PKCS7填充,对原始明文密码进行AES加密。
- 组合与编码:将盐和AES加密后的密文(ciphertext)拼接起来,最后将整个拼接字符串进行Base64编码,得到最终提交的
password参数。
实操心得:遇到
CryptoJS这个库,逆向就成功了一大半。它是一个前端知名的加密库,算法实现标准。我们的任务就从“猜算法”变成了“读参数”——弄清楚它调用的是CryptoJS的哪个算法、什么模式、用了哪些参数(密钥、IV、盐)。
4. Python复现加密算法
理解了JavaScript的加密逻辑后,接下来就是用Python实现完全相同的流程。这里我们需要用到pycryptodome库,它是Python下功能强大的加密工具库。
4.1 环境搭建与库安装
首先,确保安装了必要的库:
pip install pycryptodome4.2 分步复现加密过程
下面是根据逆向分析结果编写的Python加密函数:
import base64 import os from hashlib import sha256 from Crypto.Cipher import AES from Crypto.Util.Padding import pad def cqu_encrypt_password(password: str) -> str: """ 模拟重庆大学登录密码加密过程 :param password: 明文密码 :return: 加密后的Base64字符串 """ # 1. 生成16字节随机盐 salt = os.urandom(16) # 对应 CryptoJS.lib.WordArray.random(16) salt_hex = salt.hex() # 转换为16进制字符串,便于后续拼接 # 2. 密钥派生:密码 + 盐,迭代SHA256 # 注意:JS中是 password + salt.toString(), salt.toString()默认是16进制字符串 salted_password = (password + salt_hex).encode('utf-8') key = sha256(salted_password).digest() # 第一次SHA256 for i in range(1000): # 迭代1000次 # JS中是 key.concat(salt), 这里将当前key的字节与salt字节拼接 key = sha256(key + salt).digest() # 派生出的key是32字节(SHA256输出长度)。JS代码取前64个16进制字符(即32字节)作为AES密钥 aes_key = key[:32] # 前32字节作为AES-256密钥 # 3. AES-CBC加密 # 使用盐作为IV iv = salt cipher = AES.new(aes_key, AES.MODE_CBC, iv=iv) # 对明文密码进行PKCS7填充并加密 padded_password = pad(password.encode('utf-8'), AES.block_size) ciphertext = cipher.encrypt(padded_password) # 4. 组合与编码 # JS: salt.toString() + encrypted.ciphertext.toString() # salt.toString() 是16进制字符串,ciphertext.toString() 在CryptoJS里默认也是16进制字符串 combined = salt_hex + ciphertext.hex() # 最后对整个组合字符串进行UTF-8编码,再Base64 # 对应 CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(result)) final_base64 = base64.b64encode(combined.encode('utf-8')).decode('utf-8') return final_base64 # 测试 if __name__ == '__main__': encrypted_pwd = cqu_encrypt_password('YourPassword123') print(f"加密后的密码: {encrypted_pwd}")关键点解析:
- 盐的格式:
CryptoJS.lib.WordArray.random(16)生成的是一个16字节的随机WordArray,其.toString()方法默认输出16进制字符串。因此我们用os.urandom(16)生成字节,再用.hex()转成16进制字符串来模拟。 - 密钥派生细节:JavaScript中的
key.concat(salt),key是上一次哈希的结果(一个WordArray),salt是最初的盐(WordArray)。在Python中,我们需要将字节类型的key和字节类型的salt直接拼接。 - AES参数:密钥是派生密钥的前32字节(AES-256),IV就是盐(16字节),模式为CBC,填充为PKCS7。
pycryptodome的pad函数帮我们处理了填充。 - 最终编码:最后一步的
CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(result))很容易出错。它先将组合后的16进制字符串(result)用UTF-8编码成字节,再对这个字节进行Base64编码。而不是对16进制字符串本身进行Base64编码。
5. 完整登录流程模拟与测试
有了加密函数,我们就可以组装完整的登录请求了。这涉及到处理Cookie、Session以及可能的其他验证参数。
5.1 使用Session维持状态
在Python中,使用requests.Session()对象可以自动处理Cookies,模拟浏览器行为。
import requests def cqu_login(username, password): login_url = "https://你的重庆大学认证中心地址/auth/login" # 请替换为实际地址 # 使用Session session = requests.Session() # 通常先访问一次登录页,获取必要的初始Cookie或Token session.get(login_url) # 加密密码 encrypted_password = cqu_encrypt_password(password) # 构造登录数据 login_data = { 'username': username, 'password': encrypted_password, # 可能还有其他隐藏字段,如csrf token,需要从登录页HTML中提取 # '_csrf': csrf_token } # 发送登录请求 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': login_url } resp = session.post(login_url, data=login_data, headers=headers, allow_redirects=False) # 分析响应 print(f"状态码: {resp.status_code}") print(f"响应头: {resp.headers}") # 登录成功往往返回302跳转,Location头指向登录后的页面 if resp.status_code == 302: print("登录成功(或跳转中)") # 可以继续用这个session访问需要登录的页面 # home_resp = session.get(resp.headers['Location']) return session else: print("登录可能失败") print(resp.text) # 查看错误信息 return None5.2 处理动态Token与验证码
很多现代登录系统不止有密码加密,还有CSRF Token或动态验证码。
- CSRF Token:通常隐藏在登录表单的一个
<input type="hidden">标签里,名字可能是_csrf、csrf_token等。我们需要先用Session访问登录页,用BeautifulSoup或正则表达式解析出这个token值,然后将其填入login_data。 - 验证码:如果遇到验证码,处理会复杂很多。可能需要:
- 用
Session下载验证码图片。 - 人工识别或接入打码平台进行识别。
- 将识别结果填入表单。
- 对于复杂的点选、滑动验证码,可能需要更复杂的逆向或使用自动化工具(如Selenium、Playwright)来模拟用户操作。
- 用
在本次重庆大学的案例中,核心挑战在于密码加密。如果系统还有Token,只需在上述流程中增加提取和提交的步骤即可。
6. 逆向过程中的常见问题与调试技巧
即使按照步骤操作,也难免会遇到加密结果对不上、登录失败的情况。这里分享几个排查思路和技巧。
6.1 加密结果对比验证
这是最有效的调试方法。在浏览器开发者工具的Console面板中,直接调用我们找到的JavaScript加密函数,对同一个密码进行加密。
// 在Console中执行 var testPwd = '123456'; console.log('JS加密结果:', encryptPassword(testPwd));同时,在Python中运行我们的cqu_encrypt_password('123456'),对比两个输出是否完全一致。如果不一致,问题一定出在复现过程的某个环节。
对比排查清单:
- 盐的生成与格式:确保Python生成的16字节随机盐,在转换成16进制字符串后,与JS中
salt.toString()的格式一致。可以打印出来对比。 - 密钥派生过程:
- 拼接顺序:是
password + salt还是salt + password?字符串编码是UTF-8吗? - 迭代次数:确认是1000次吗?第一次哈希的输入是什么?
- 中间状态:可以在JS和Python中,打印出第一次哈希后、第十次哈希后的结果(转为Hex)进行比对,定位从哪一次开始出现分歧。
- 拼接顺序:是
- AES加密参数:
- 密钥长度:确认是取派生结果的前32字节吗?AES-256密钥就是32字节。
- IV:确认IV就是盐的原始字节,而不是盐的16进制字符串。
- 模式与填充:一定是CBC模式和PKCS7填充。
- 最终编码:这是最容易出错的一步。仔细对照JS代码的最后一行,理解
Base64.stringify(Utf8.parse(result))的含义。result是盐和密文的16进制字符串拼接。在Python中,是先把这个拼接字符串用.encode('utf-8')变成字节,再用base64.b64encode编码。
6.2 网络请求的细节差异
即使加密结果一致,登录请求也可能失败,需要检查网络请求的细节:
- 请求头(Headers):仔细比对浏览器成功登录时的请求头和你用Python发送的请求头。特别注意
Content-Type、User-Agent、Origin、Referer等字段。User-Agent最好模拟一个真实的浏览器。 - 请求数据(Payload):除了
username和password,是否还有其他必填字段?比如一个固定的clientId、grant_type或者从页面获取的lt、execution等参数。这些都需要从登录页的HTML源码或首次GET请求的响应中提取。 - Cookie处理:确保你的
Session正确接收和发送了Cookie。有些系统需要先访问页面获取一个会话ID。 - HTTPS与重定向:确保
requests正确处理了HTTPS(通常没问题)。allow_redirects=False可以阻止自动重定向,方便我们查看登录接口的直接响应。登录成功后再手动处理重定向。
6.3 应对代码混淆与反调试
如果遇到的网站代码被严重混淆(变量名变成a,b,c,逻辑被打乱),或者有反调试手段,可以尝试以下方法:
- 搜索特征常量:加密算法中常有一些固定值,如AES的初始化向量(IV)如果是固定的,可能会在代码中直接出现一长串数字或字符串。搜索这些特征串可能直接定位到加密函数附近。
- Hook关键函数:在Console中重写标准的加密函数或API,例如重写
CryptoJS.AES.encrypt,在里面打印出调用参数和结果,这是动态分析的利器。var _encrypt = CryptoJS.AES.encrypt; CryptoJS.AES.encrypt = function(message, key, cfg){ console.log('AES Encrypt Called:'); console.log('Message:', message); console.log('Key:', key); console.log('Cfg:', cfg); var result = _encrypt(message, key, cfg); console.log('Result:', result); return result; }; - 使用AST解混淆工具:对于复杂的混淆,可以尝试使用像
de4js这样的在线工具或本地库进行反混淆,但成功率取决于混淆强度。 - “油猴”脚本辅助:编写Tampermonkey脚本,在页面加载时注入我们的调试代码,可以更早地介入执行流程。
逆向工程就像侦探破案,需要耐心、细致的观察和逻辑推理。每一次成功的逆向,不仅解决了一个具体问题,更积累了一套应对加密黑盒的方法论。重庆大学登录加密的案例,涵盖了前端加密逆向的典型流程:抓包定位、断点追踪、算法分析、代码复现和调试验证。掌握这个流程,再遇到类似的登录加密,你就能从容地拿出这套工具和方法,一层层剥开它的外壳,看到核心的逻辑。
