逆向分析SHA1加密登录接口:从抓包到Python复现的完整指南
1. 项目概述:当登录遇到SHA1
最近在排查一个第三方系统的集成问题时,遇到了一个典型的场景:对方提供了一个Web登录接口,但密码字段不是明文传输,而是经过某种哈希处理后的字符串。通过抓包和初步观察,确认了其使用的是SHA1算法。对于开发者而言,这既是一个安全措施,也是一个技术黑盒。我们无法直接得知其前端的原始密码处理逻辑,但为了完成自动化登录、单点登录对接或者安全审计,我们需要“逆向”这个接口,理解其加密流程,并能在自己的代码中复现。这不仅仅是调用一个SHA1()函数那么简单,它涉及到对网络协议、前端JavaScript代码(可能被混淆或压缩)、以及哈希算法特性的综合理解。今天,我就结合这个实际案例,拆解一下逆向分析一个SHA1加密登录接口的完整思路、工具方法和实操细节,无论你是做安全测试、爬虫开发还是系统集成,这套方法都能给你提供清晰的路径。
2. 逆向分析的核心思路与准备工作
逆向分析一个加密接口,目标不是破解SHA1算法本身(这在计算上是不可行的),而是搞清楚在调用SHA1函数之前,原始密码(或其它输入)经历了哪些处理。常见的处理包括:拼接时间戳、拼接固定盐值(Salt)、进行多次哈希、或者先进行MD5再SHA1等。我们的分析将从前端到后端请求的整个数据流入手。
2.1 分析环境与工具链搭建
工欲善其事,必先利其器。逆向分析主要依赖浏览器和抓包工具。
浏览器开发者工具:这是主战场。以Chrome为例,F12打开后,重点关注以下几个面板:
- Network(网络):记录所有HTTP/HTTPS请求,筛选XHR/Fetch请求找到登录接口。查看请求头(Headers)、请求体(Payload),特别是
Form Data或Payload里加密后的密码字段。 - Sources(源代码):查看和调试前端JavaScript。关键是要找到负责构造登录请求、特别是处理密码字段的JS文件。
- Console(控制台):可以执行JavaScript代码,用于测试我们推断的加密函数。
- Network(网络):记录所有HTTP/HTTPS请求,筛选XHR/Fetch请求找到登录接口。查看请求头(Headers)、请求体(Payload),特别是
抓包工具:如Fiddler、Charles或Wireshark。当浏览器工具不够用(例如分析非浏览器客户端、或需要更强大的断点/修改功能时)它们非常有用。Fiddler的AutoResponder功能可以替换本地JS文件,方便调试。
代码搜索与格式化工具:前端JS经常被压缩(Minify)成一行,难以阅读。可以使用浏览器的“Pretty Print”功能(在Sources面板,点击
{}图标),或者使用在线工具、IDE进行格式化。哈希计算与验证工具:用于快速验证我们的猜想。可以是一个在线的SHA1计算网站,也可以是Python/Node.js的命令行环境。我习惯用Python的
hashlib库进行快速测试。
2.2 初步侦察:网络请求抓取与观察
首先,我们进行最直观的操作:在登录页面输入账号密码(可以使用测试账号),点击登录,同时在Network面板中观察。
- 找到目标请求:在Network面板中,通常登录请求是
POST类型,URL可能包含login、signin、auth等关键词。点击这个请求,查看详情。 - 分析请求载荷:在
Request Payload或Form Data部分,你会看到类似这样的结构:
这里{ "username": "testUser", "password": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "timestamp": "1712345678901", // ... 可能还有其他字段如 nonce, sign 等 }password的值a94a8...就是一个40位的十六进制字符串,这正是SHA1哈希的典型特征(160位,40个十六进制字符)。关键点来了:这个password的值,是仅仅对用户输入的明文密码进行SHA1的结果吗?很大概率不是。我们需要观察同一请求中是否还有其他字段,如timestamp、nonce(随机数)、salt等。这些字段很可能被一起用于计算最终的password值。
注意:不要直接在真实的生产环境用户账号上进行逆向分析,务必使用测试账号或自己搭建的测试环境,避免法律和安全风险。
3. 深入前端:定位与解析加密逻辑
确定了加密后的数据格式和可能的关联字段后,下一步就是找到执行加密的JavaScript代码。
3.1 搜索加密函数入口
在Network面板中,找到登录请求,在其上右键选择Initiator(或Initiator选项卡),它会显示是哪个JS文件发起了这个请求。点击该文件名,会跳转到Sources面板对应的代码行。
更通用的方法是使用搜索:
- 在Sources面板,按
Ctrl+Shift+F(Windows)或Cmd+Opt+F(Mac)打开全局搜索。 - 搜索关键词。关键词的选择至关重要:
- 直接搜索密码字段名:如
"password"、'password'。 - 搜索加密相关函数名:如
SHA1、sha1、encrypt、hash、Hex、toString(16)。 - 搜索可能的关键变量:如
timestamp、nonce、sign、key。 - 搜索常见的加密库标识:如
CryptoJS(一个常用前端加密库),搜索CryptoJS.SHA1。
- 直接搜索密码字段名:如
3.2 解读混淆的JavaScript代码
找到的代码很可能被压缩和混淆。变量名可能是单个字母(如a, b, c, d),函数结构难以阅读。这时需要:
- 格式化代码:点击代码窗口左下角的
{}(Pretty Print)按钮,让代码恢复一定的结构。 - 设置断点:在疑似处理密码的行(例如,找到
password: xxx赋值的地方,或者调用SHA1函数的地方)点击行号设置断点。 - 重新触发登录:回到登录页,再次点击登录,浏览器会在断点处暂停。
- 调试与观察:
- 在右侧的
Scope面板,可以查看当前作用域内的所有变量值。 - 将鼠标悬停在变量上,可以查看其当前值。
- 使用
Console面板,可以手动执行表达式,测试我们的猜想。例如,如果怀疑密码是SHA1(明文密码 + timestamp),可以在Console里输入CryptoJS.SHA1('yourPassword' + '1712345678901').toString()来验证输出是否与抓包到的password值一致。
- 在右侧的
3.3 一个典型的加密流程还原案例
假设我们通过断点调试,发现加密逻辑如下:
// 伪代码,经过还原后 function encryptPassword(plainPwd, timestamp) { var salt = "fixedStaticSalt"; // 可能是一个写死在代码里的固定盐值 var step1 = md5(plainPwd + salt); // 先MD5加盐 var step2 = sha1(step1 + timestamp); // 再将MD5结果拼接时间戳做SHA1 return step2.toHexString(); // 转换为十六进制字符串 }那么,我们逆向分析的结果就是:最终传输的password = SHA1( MD5(用户密码 + “fixedStaticSalt”) + 时间戳 )。
实操心得:混淆代码中,
MD5和SHA1的函数名可能被重命名。但它们的输出特征明显(MD5是32位十六进制,SHA1是40位)。在调试时,可以观察中间变量的值和长度,来推断它经过了哪种哈希处理。
4. 使用Python复现加密算法
一旦在前端明确了加密逻辑,下一步就是在我们的后端(以Python为例)中复现这个流程,以确保能够生成与服务端验证完全一致的加密字符串。
4.1 环境准备与基础哈希
确保Python环境已安装。我们主要使用内置的hashlib库。
import hashlib import time def sha1_hex(data): """计算字符串的SHA1并返回十六进制形式""" # 注意:data需要是字节串(bytes) if isinstance(data, str): data = data.encode('utf-8') sha1_obj = hashlib.sha1() sha1_obj.update(data) return sha1_obj.hexdigest() def md5_hex(data): """计算字符串的MD5并返回十六进制形式""" if isinstance(data, str): data = data.encode('utf-8') md5_obj = hashlib.md5() md5_obj.update(data) return md5_obj.hexdigest()4.2 复现复杂加密逻辑
根据前面分析出的逻辑SHA1( MD5(密码 + 固定盐) + 时间戳 ),我们编写复现函数:
def encrypt_password_for_login(plain_password, timestamp_str, static_salt="fixedStaticSalt"): """ 复现前端加密逻辑 :param plain_password: 用户明文密码 :param timestamp_str: 时间戳字符串(需与前端的完全一致,通常是13位毫秒级) :param static_salt: 前端代码中写死的固定盐值 :return: 加密后的40位SHA1十六进制字符串 """ # 步骤1: MD5(密码 + 固定盐) step1_input = plain_password + static_salt step1_md5 = md5_hex(step1_input) # 得到32位十六进制字符串 # 步骤2: SHA1(MD5结果 + 时间戳) step2_input = step1_md5 + timestamp_str final_sha1 = sha1_hex(step2_input) # 得到40位十六进制字符串 return final_sha1 # 使用示例 if __name__ == "__main__": test_pwd = "mySecret123" # 这个时间戳需要从登录请求中实时获取,或者模拟生成 test_timestamp = "1712345678901" encrypted = encrypt_password_for_login(test_pwd, test_timestamp) print(f"加密后的密码: {encrypted}") # 输出应与抓包到的password字段值完全一致4.3 处理动态参数与时间戳
在实际逆向中,时间戳timestamp往往是动态的,每次登录请求都不同。我们的自动化脚本必须模拟这一行为。
- 同步时间:确保你的服务器时间与目标服务器时间基本同步。有时服务器会检查时间戳的合理性(如是否在前后几分钟内)。
- 获取服务器时间:有些接口会在登录前提供一个获取服务器时间的接口。如果没有,可以从登录请求的响应头
Date字段,或者首页HTML中嵌入的JS变量里获取。 - 模拟生成:最常用的方法是使用当前时间的毫秒级时间戳。在Python中:
关键细节:务必确认前端生成时间戳的精度(是10位秒级,还是13位毫秒级)和类型(是字符串还是数字)。在Network面板中查看请求体,它是import time timestamp = str(int(time.time() * 1000)) # 13位毫秒时间戳"timestamp": "1712345678901"(字符串)还是"timestamp": 1712345678901(数字),我们的复现代码必须完全一致。
5. 常见问题、排查技巧与安全思考
即使逻辑清晰,在复现过程中也难免会遇到结果不一致的情况。以下是常见的排查方向和技巧。
5.1 加密结果不一致的排查清单
当你计算的哈希值与抓包到的值不同时,请按以下顺序检查:
| 排查步骤 | 可能原因 | 验证方法 |
|---|---|---|
| 1. 输入字符串是否完全一致? | 多一个空格、换行符,或者字符串编码不同。 | 在JS调试器Console和Python中,分别打印每一步的原始字符串和其长度、字符编码。对于中文字符,编码问题尤其常见。 |
| 2. 拼接顺序是否正确? | 盐值、时间戳、密码的拼接顺序错误。例如是盐+密码还是密码+盐。 | 仔细阅读JS代码,确认拼接操作(+)的顺序。有时拼接的不是字符串,而是变量直接相连。 |
| 3. 哈希的输入是十六进制还是二进制? | 第一步MD5产生的是32位十六进制字符串,但第二步SHA1可能需要的是这个字符串的二进制字节,还是将其作为普通字符串处理?99%的情况是作为普通字符串。 | 在JS中观察中间变量step1_md5的类型和值。在Python中,确保step1_md5是字符串,然后encode('utf-8')。 |
| 4. 是否存在额外的转换? | JS中SHA1(...)返回的可能是一个对象,需要调用.toString()或.toString(CryptoJS.enc.Hex)才能得到十六进制字符串。 | 在JS断点处,查看最终赋值给password的变量是什么,以及它经过了哪些方法调用。 |
| 5. 时间戳来源和格式? | 时间戳不是当前时间,而是服务器下发的;或者是秒级而非毫秒级。 | 对比抓包请求中的timestamp值和你代码生成的值。检查前端是否从某个API或页面变量获取了时间戳。 |
| 6. 是否有未发现的隐藏参数? | 除了timestamp,可能还有nonce、deviceId等也参与了签名计算。 | 仔细对比多次登录请求的Payload,看哪些字段是变化的。尝试在JS中搜索这些字段名,看它们是否被用于某个sign或hash的计算函数。 |
5.2 更复杂的场景:涉及RSA或AES
有些登录接口,密码并非直接用哈希,而是先经过前端RSA公钥加密,再传输。这时逆向分析的重点就变成了找到并解析RSA公钥,以及使用的填充模式(如PKCS1_v1_5)。
- 搜索关键词:
RSA、encrypt、publicKey、setPublicKey、JSEncrypt(一个常用库)。 - 提取公钥:公钥通常以
-----BEGIN PUBLIC KEY-----开头,直接写在JS变量或从接口获取。 - 使用库复现:在Python中,可以使用
cryptography或rsa库,按照相同的填充模式进行加密。
from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import padding import base64 # 加载从JS中提取的公钥字符串 public_key_pem = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" public_key = serialization.load_pem_public_key(public_key_pem.encode()) # 使用PKCS1 v1.5填充进行加密 encrypted_password = public_key.encrypt( plain_password.encode(), padding.PKCS1v15() ) # 通常结果会被Base64编码后传输 encrypted_password_b64 = base64.b64encode(encrypted_password).decode()5.3 安全启示与伦理边界
完成逆向分析后,我们获得了与前端一致的加密能力。但这里必须强调几点:
- 目的正当性:此类技术应仅用于授权测试、系统集成(在获得接口规范不清晰时)、个人学习或安全审计(获得明确授权)。切勿用于非法破解、入侵他人系统。
- 安全风险暴露:通过分析,我们可能发现其安全设计的弱点,例如使用固定盐、哈希方式过于简单、前端加密逻辑暴露等。真正的安全应该依赖于HTTPS、后端强哈希(如bcrypt、Argon2)、动态盐值、以及二次验证等。
- 法律合规:未经授权对他人系统进行逆向工程可能违反《计算机软件保护条例》或服务条款。在行动前,请务必确认行为的合法性。
逆向分析SHA1加密登录接口,是一个典型的“黑盒”探秘过程。它考验的是开发者的耐心、观察力和逻辑推理能力。从抓包定位到JS调试,再到算法复现,每一步都需要细致入微。掌握这套方法,不仅能解决眼前的集成难题,更能深刻理解Web前端与后端交互中数据安全的实现方式与潜在漏洞。最后记住,技术是把双刃剑,运用之妙,存乎一心。
