
1. 项目概述从“黑盒”到“白盒”的邮箱登录逆向最近在整理一些自动化工具时遇到了一个经典需求如何让程序自动登录某个邮箱服务比如获取收件箱列表或者发送邮件。直接使用用户名密码的明文请求这在现代Web安全体系下几乎寸步难行。页面上的登录按钮背后早已不是简单的表单提交而是一套由JavaScript精心构建的加密、混淆和验证流程。这就是“JS逆向”的典型战场。今天我们就以“13x邮箱”这个虚构但极具代表性的目标为例手把手拆解一个邮箱登录接口的逆向过程。整个过程就像在解一个设计精巧的密码锁我们需要找到生成密钥的算法而不是试图去撬开锁芯。“13x邮箱”代表了国内主流邮箱服务商如163、126、QQ邮箱等普遍采用的前端安全策略。其登录过程通常会涉及动态生成的Token、对密码进行非对称或对称加密、以及包含时间戳和随机数的签名机制。我们的目标不是破解密码而是理解前端JavaScript如何生成这些认证参数并用Python等后端语言复现这一逻辑从而实现程序的自动化登录。这对于需要批量管理邮箱、进行邮件监控或集成邮件功能的开发者来说是一项非常实用的技能。无论你是爬虫工程师、测试开发还是对Web安全感兴趣的开发者通过这个实战案例你都能深入理解现代Web应用的前后端交互安全模型。2. 核心思路与逆向环境准备逆向工程的第一步不是直接扎进代码里而是先清晰地理解整个数据流转过程。我们的核心思路可以概括为“抓包观察 - 定位关键JS - 静态分析与动态调试 - 算法还原与复现”。2.1 网络请求流程分析首先我们需要使用浏览器开发者工具F12切换到Network网络标签页并勾选“Preserve log”保留日志。然后在邮箱登录页面输入错误的账号密码避免真实账号被锁定点击登录。这时网络请求列表中会出现一个关键的登录请求通常是POST类型URL可能包含login、auth、submit等关键词。点击这个请求查看其Headers和Payload。你会发现提交的数据绝非简单的username和password。一个典型的请求负载可能如下所示{ username: your_emailexample.com, password: 一大串看似无规律的密文, token: a1b2c3d4..., clientId: web, signature: e5f6g7h8..., timestamp: 1640995200000, nonce: random_string }这里的password字段就是加密后的结果token和signature是防止重放攻击和确保请求完整性的关键。我们的核心任务就是找出生成password密文、token和signature的JavaScript代码。2.2 逆向工具链搭建工欲善其事必先利其器。以下是进行JS逆向的必备工具组合浏览器开发者工具Chrome DevTools这是我们的主战场。除了Network面板Sources源代码面板用于查看和调试JS文件Console控制台用于执行代码片段。代码美化工具线上JS代码通常被压缩成一行难以阅读。DevTools自带“Pretty Print”功能点击源代码左下角的{}图标可以格式化代码。全局搜索在Sources面板按CtrlShiftF可以全局搜索所有加载的JS文件中的字符串。这是定位关键函数如encrypt、encode、RSA、AES等最快捷的方式。断点调试在怀疑的关键函数或代码行左侧单击设置断点。当登录动作触发时代码执行会在此暂停允许我们查看此时的变量值、调用栈这是理解算法逻辑的终极手段。Node.js环境用于在本地执行、测试我们还原出来的JavaScript加密函数。有时也需要安装一些npm包来模拟浏览器环境如jsdom、crypto-js。Python环境最终我们需要用Python复现算法。requests库用于发送HTTP请求execjs库可以调用本地Node.js环境执行JS代码或者直接用Python的加密库如pycryptodome、rsa实现算法。注意在逆向任何网站前请务必阅读其robots.txt文件和服务条款确保你的行为符合法律法规和网站规定。本教程仅用于安全研究和学习目的请勿用于非法爬取、攻击或侵犯他人隐私。3. 关键JS定位与算法解析实战假设我们通过抓包发现提交的密码密文是一串固定的64位十六进制字符串长度疑似AES或DES加密结果。同时请求头里有一个自定义的X-Client-Encrypt-Key字段。这给了我们明确的搜索方向。3.1 搜索与定位加密入口在DevTools的Sources面板进行全局搜索CtrlShiftF。我们可以尝试搜索以下关键词密文的前几位如a7f8c9字段名password、encrypt、encode加密算法名AES、RSA、CryptoJS一个常用的前端加密库可能的关键函数调用setPublicKeyRSA、encrypt、getKey很快我们可能会在一个名为login.xxxxxx.js的文件中找到类似下面的代码片段经过美化后function encryptPassword(password, key) { var encrypted CryptoJS.AES.encrypt( CryptoJS.enc.Utf8.parse(password), CryptoJS.enc.Utf8.parse(key), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 } ); return encrypted.ciphertext.toString(CryptoJS.enc.Hex).toUpperCase(); } function getDynamicKey() { // 可能从服务器接口获取也可能本地生成 var timestamp new Date().getTime(); return md5(timestamp.toString().substr(0, 10) some_salt).substr(8, 16); } // 在提交表单时调用 var pwd document.getElementById(pwdInput).value; var dynamicKey getDynamicKey(); var encryptedPwd encryptPassword(pwd, dynamicKey); // 将encryptedPwd和dynamicKey可能以其他形式放入表单提交这段代码清晰地展示了流程先通过getDynamicKey函数生成一个16字节的动态密钥然后用AES-ECB模式PKCS7填充方式对密码明文进行加密最后输出十六进制大写字符串。而dynamicKey很可能通过其他字段如token或一个独立的key字段发送给服务器服务器用同样的逻辑解密。3.2 动态调试验证猜想找到代码不等于理解流程。我们需要验证getDynamicKey的逻辑。在getDynamicKey函数入口和encryptPassword调用处打上断点。刷新登录页面清空Network日志。在密码框输入test123点击登录。代码执行会在断点处暂停。在Console面板我们可以手动执行getDynamicKey()查看其返回值。也可以查看pwd和dynamicKey的值。单步执行F10进入encryptPassword观察每一步的中间变量特别是CryptoJS.AES.encrypt的输入和输出是否与我们抓包看到的密文一致。通过调试我们确认了算法AES-128-ECB加密密钥是本地生成的一个16位MD5子串。但这里有个关键点密钥是动态的每次登录可能不同但算法是静态的。服务器如何知道这个动态密钥它很可能被放在了另一个字段比如一个名为sessionKey或encryptKey的隐藏输入框里或者通过之前的某个初始化接口返回。3.3 追踪密钥的生成与传递我们回头分析登录前的网络请求。在点击登录按钮前页面可能已经发起了一个GET请求比如/api/init或/api/getPublicKey。这个请求的响应里很可能包含了加密所需的密钥或密钥的种子。假设我们找到了一个/api/getEncryptInfo的请求其响应为{ code: 200, data: { keySeed: 1640995200, aesMode: ECB } }那么前端的getDynamicKey函数很可能就是利用这个keySeed拼接一个固定的盐值salt再经过MD5运算后截取部分字符串生成最终密钥。我们需要在JS代码中搜索keySeed这个变量名找到它被使用的地方。最终我们还原出完整的密钥生成逻辑// 伪代码基于假设 var serverSeed response.data.keySeed; // 从初始化接口获得 var salt a_fixed_salt_value_from_js; // 在JS代码里写死的盐 var rawKey md5(serverSeed salt); // 32位MD5 var finalAesKey rawKey.substr(8, 16); // 取第9-24位共16字符作为AES-128密钥至此加密流程完全清晰服务器提供一个种子前端用固定算法MD5截取生成密钥再用该密钥以AES-ECB模式加密密码。4. Python复现加密算法与自动化登录理解了算法我们就可以用Python来复现这一过程实现自动化登录。4.1 环境准备与依赖安装首先确保你的Python环境已安装必要的库。我们将使用requests处理网络请求execjs来执行我们还原出的JS代码对于复杂的、涉及大量前端对象的逻辑用execjs直接调用JS文件是最稳妥的。当然如果算法是标准的MD5和AES我们也可以直接用Python的hashlib和pycryptodome库实现。pip install requests pyexecjs # 或者为了更好的性能安装 node.js 环境然后 pip install PyExecJS # 同时需要安装 pycryptodome 用于可能的纯Python加密实现 pip install pycryptodome4.2 分步复现登录流程我们的Python脚本需要模拟浏览器的完整行为步骤一获取初始化信息密钥种子import requests import execjs import json import hashlib from Crypto.Cipher import AES from Crypto.Util.Padding import pad import binascii session requests.Session() # 模拟浏览器头 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } # 1. 访问登录页获取必要的Cookie如session id login_page_url https://mail.13x.com/ resp session.get(login_page_url, headersheaders) # 2. 请求获取加密信息的接口 init_api_url https://mail.13x.com/api/getEncryptInfo init_resp session.get(init_api_url, headersheaders) init_data init_resp.json() key_seed init_data[data][keySeed] print(f获取到的密钥种子: {key_seed})步骤二复现前端密钥生成算法这里有两种方式用execjs执行我们提取的JS函数或用Python重写。方式A使用execjs更通用适合复杂JS逻辑# 将我们找到的JS关键函数保存到一个文件比如 encrypt.js js_code function md5(str) { // 这里需要实现或引入一个MD5函数或者使用CryptoJS // 为了示例假设我们有一个简单的md5函数 // 实际中可能需要引入完整的CryptoJS库 const crypto require(crypto); return crypto.createHash(md5).update(str).digest(hex); } function getDynamicKey(seed) { var salt a_fixed_salt_value_from_js; var rawKey md5(seed salt); return rawKey.substr(8, 16).toUpperCase(); // 注意大小写需与前端一致 } ctx execjs.compile(js_code) aes_key ctx.call(getDynamicKey, key_seed) print(f生成的AES密钥: {aes_key})方式B使用Python重写更高效依赖清晰def get_dynamic_key_py(seed: str) - str: salt a_fixed_salt_value_from_js raw_key hashlib.md5((seed salt).encode(utf-8)).hexdigest() aes_key raw_key[8:24] # 取第9-24位共16字节 return aes_key.upper() aes_key get_dynamic_key_py(key_seed) print(f生成的AES密钥(Python): {aes_key})步骤三复现AES加密算法def encrypt_password_aes_ecb(password: str, key: str) - str: 模拟前端的 CryptoJS.AES.encrypt(mode:ECB, padding:Pkcs7) key: 16字节十六进制字符串 password: 明文密码 返回: 十六进制大写密文 # 将16进制字符串密钥转换为字节 key_bytes bytes.fromhex(key) # 将明文密码转换为字节并使用PKCS7填充到16字节的倍数 data_bytes pad(password.encode(utf-8), AES.block_size) # 创建AES-ECB加密器 cipher AES.new(key_bytes, AES.MODE_ECB) # 加密 encrypted_bytes cipher.encrypt(data_bytes) # 转换为十六进制大写字符串 encrypted_hex binascii.hexlify(encrypted_bytes).decode(utf-8).upper() return encrypted_hex password your_real_password encrypted_pwd encrypt_password_aes_ecb(password, aes_key) print(f加密后的密码: {encrypted_pwd})步骤四组装登录请求并发送我们需要从登录页的HTML中或者从之前的初始化接口响应中找到其他必要的参数如token、clientId等。这些可能藏在隐藏的input标签里或者由其他JS函数生成。# 假设我们从页面解析出了一个token这里用模拟值 login_token some_token_from_page # 假设客户端ID固定 client_id web login_api_url https://mail.13x.com/api/login login_payload { username: your_email13x.com, password: encrypted_pwd, # 使用我们加密后的密码 token: login_token, clientId: client_id, # 可能还需要其他字段如rsaKey? 需根据实际抓包补充 encryptKey: aes_key, # 将密钥也传给服务器服务器需要用它解密 } login_resp session.post(login_api_url, headersheaders, jsonlogin_payload) result login_resp.json() print(f登录结果: {result}) if result.get(code) 200: print(登录成功) # 登录后session会保持登录状态可以继续访问需要认证的页面如收件箱 inbox_url https://mail.13x.com/api/inbox inbox_resp session.get(inbox_url) print(inbox_resp.json()) else: print(f登录失败: {result.get(message)})4.3 关键细节与避坑指南字符编码与大小写JS和Python的字符串处理、十六进制表示有时存在大小写差异。务必确保密钥、密文的字母大小写与前端生成的结果完全一致。在调试时将Python生成的中间结果与浏览器调试器中看到的结果进行逐位对比。填充模式AES加密有多种填充模式PKCS7、ZeroPadding等。CryptoJS默认使用PKCS7填充。我们的Python实现也必须使用对应的pad函数来自Crypto.Util.Padding。如果填充不对服务器解密会失败。加密模式除了ECB还可能遇到CBC模式这就需要初始化向量IV。如果抓包发现加密参数中有iv字段那么在Python中创建AES.new时需要指定iv参数并使用AES.MODE_CBC。密钥的传递动态密钥是如何传给服务器的可能是像我们例子中一样放在encryptKey字段也可能是放在请求头里或者被RSA加密后传输。必须通过抓包准确确定。其他加密方式除了AES还可能遇到RSA加密。RSA通常用于加密对称密钥如AES密钥本身。流程可能是前端生成一个随机AES密钥用RSA公钥加密它然后将加密后的AES密钥和用该AES密钥加密的密码一起发送。这就需要定位RSA公钥和加密函数。环境依赖使用execjs时确保本地Node.js环境可用并且JS代码中所需的库如crypto-js已通过npm全局安装或在代码中正确引入。对于复杂的库有时需要将整个相关的JS文件合并加载。5. 常见问题排查与进阶技巧即使按照步骤操作也可能会遇到各种问题。下面是一些常见坑点及解决方案。5.1 问题排查清单问题现象可能原因排查步骤登录返回“密码错误”加密结果与前端不一致1. 对比Python和浏览器Console中对相同明文密码和密钥的加密输出。2. 检查密钥生成逻辑盐值是否正确MD5后的截取位置和长度是否正确3. 检查加密参数AES模式ECB/CBC、填充PKCS7/ZeroPadding、密钥和数据的编码UTF-8/Hex。登录返回“无效的token”或“请求过期”Token或签名验证失败1. Token可能有时效性需要从最新的页面或接口获取。2. 可能存在签名signature算法需要将请求参数如timestamp, nonce, body按特定规则排序后与密钥进行HMAC运算。需在JS中找到签名函数。execjs执行JS报错JS代码依赖浏览器环境1. JS代码中使用了window、document等浏览器特有对象。需要模拟这些环境或修改JS代码用Node.js的crypto模块替代CryptoJS。2. 使用execjs的eval方法时注意字符串转义。请求被拒绝返回403/412缺少必要的请求头或Cookie1. 检查登录请求的Headers是否缺少Referer、Origin、X-Requested-With等关键头。2. 检查Cookie初始化请求获得的Cookie是否被正确带入后续请求。使用requests.Session()可以自动管理Cookie。初始化接口返回错误反爬机制如滑块验证1. 首次访问可能需要完成滑块或点选验证码。这超出了纯JS逆向范围可能需要图像识别或打码平台介入。2. 检查请求头中的User-Agent是否像真实浏览器。5.2 进阶逆向技巧Hook技术对于混淆严重的代码直接搜索关键词可能无效。可以使用浏览器插件如Tampermonkey或Fiddler/Charles等代理工具注入自定义的JS代码对关键函数如JSON.stringify,XMLHttpRequest.send,CryptoJS.AES.encrypt进行“钩子”Hook在它们被调用时打印出输入参数和结果。这是定位加密入口的“大杀器”。AST抽象语法树反混淆遇到高度混淆的JS变量名被替换成a,b,c逻辑被分割打乱可以借助AST解析工具如babel对代码进行解析、分析和还原将代码变得可读。这需要较高的JS功底。补环境当提取的JS代码严重依赖浏览器环境时需要在Node.js中构造一个模拟环境。核心是创建一个包含window、navigator、document等基本属性的全局对象并实现关键的方法。网上有成熟的补环境框架可以参考。关注WebAssembly一些对性能和安全要求极高的加密操作可能会使用WebAssemblyWasm来实现。在Network面板中查找.wasm文件并在Sources面板的WebAssembly调试器中进行分析。逆向Wasm更为复杂通常需要将其转换为C/C代码进行分析。5.3 个人实操心得在逆向了多个不同站点的登录后我最大的体会是耐心和细致大于一切。不要指望一眼就能找到核心函数。很多时候加密逻辑分散在多个文件通过事件监听、Promise链式调用层层传递。我的工作流通常是完整录制从打开登录页到点击登录全程录制网络请求保存所有请求和响应的HAR文件。先定入口从最终的登录POST请求出发在Initiator发起者栏查看是哪个JS文件发起了这个请求点击跳转到源代码。逆向回溯在发起登录请求的JS代码处打断点向上回溯调用栈Call Stack一步步找到最初触发加密的函数。最小化验证将疑似加密函数的代码片段提取出来放在一个干净的HTML文件或Node.js脚本中用最简化的输入进行测试验证其输出是否与浏览器环境一致。持续比对在Python复现的每一步都尽量与浏览器调试器中的中间变量值进行比对确保分毫不差。最后请始终牢记法律与道德的边界。JS逆向是一项强大的技术它帮助我们理解系统工作原理、开发合法的自动化工具、进行安全审计。但绝不能用于破解他人账号、窃取数据、绕过付费墙等非法用途。保持技术的好奇心同时坚守技术的善意。