文章目录
-
- 声明
- 一、先看请求
- [二、加密函数真身:XOR + hex](#二、加密函数真身:XOR + hex)
- [三、不要忽略 JS 字符模型](#三、不要忽略 JS 字符模型)
- 四、样本对照:静态结论必须动态验证
- 五、真实请求体是什么样?
- [六、普通 requests 为什么不行?](#六、普通 requests 为什么不行?)
- [七、最终 Python 实现](#七、最终 Python 实现)
- 八、总结
声明
本文章中所有内容仅供学习交流,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请私信我立即删除!
目标站点:
https://file.sztbhz.com/

目标接口:
https://file.sztbhz.com/Account/LoginResult逆向目标:请求体中的
userName、passWord加密逻辑
这次目标看起来像一个很典型的登录参数逆向:账号密码不是明文提交,接口名也很直接,叫 /Account/LoginResult。
这类任务的重点不是"把页面点开看看",而是搞清楚三件事:
- 参数在哪里被加工?
- 加工逻辑是不是依赖浏览器环境?
- 协议请求能不能脱离浏览器稳定跑?
最后的结论有点朴素,甚至朴素得让人想给前端算法倒杯茶:它不是 AES,不是 RSA,也不是一套 JSVMP 迷宫,而是一个固定 key 的 XOR 编码。
不过,简单不代表可以拍脑袋,下面来复盘一下。
一、先看请求
首页加载后,主要资源包括:
jquery-3.6.3.min.jsbootstrapvue3element-plusvantkmui.jslucas-common-bundle.js- 微信扫码登录脚本
wxLogin.js
页面上同时存在微信扫码登录和账号密码登录。
我们关心账号密码登录区域:
html
<input type="text" id="UserName" placeholder="用户名" />
<input type="Password" id="Password" placeholder="密码" />
<input type="button" onclick="login()" value="登录" />
看到 onclick="login()" 基本就能确定入口函数在页面内联脚本里。这个时候不要急着全局搜索 AES、RSA,先找业务入口更高效。
搜索关键词:
text
LoginResult
userName
passWord
login()
很快定位到核心提交代码:
javascript
function login() {
var secKey = 147;
var userName = $("#UserName").val();
var passWord = $("#Password").val();
$.post("/Account/LoginResult", {
userName: Encry(userName, secKey),
passWord: Encry(passWord, secKey)
}, function (data) {
loginDone(data);
})
}
这段代码给了两个关键信息:
secKey是固定值147userName和passWord都调用同一个Encry
这时已经能闻到"轻量编码"的味道了。
二、加密函数真身:XOR + hex
继续往上看,找 Encry 定义:
javascript
function StringToByte(str) {
var re = [], idx;
for (var i = 0; i < str.length; i++) {
idx = str.charCodeAt(i);
if (idx & 0xff00) {
re.push(idx >> 8);
re.push(idx & 0xff);
} else {
re.push(idx);
}
}
return re;
}
function Encry(str, key) {
var data = StringToByte(str)
var res = []
for (var i = 0; i < data.length; i++) {
var n = data[i] ^ key
res.push(n.toString(16))
}
return res.join('')
}
算法流程非常直白:
- 遍历字符串。
- 用
charCodeAt(i)取 UTF-16 code unit。 - 如果 code unit 大于
0xff,拆成高字节和低字节。 - 每个字节和固定 key
147做 XOR。 - 每个结果转十六进制字符串。
- 拼接成最终密文。
用一句话说:
text
JS 字符 -> 字节数组 -> byte ^ 147 -> hex 拼接
这不是加密界的钢铁侠,顶多算穿了件雨衣。但对接口来说,只要服务端这么验,我们就必须完全复现。
三、不要忽略 JS 字符模型
这个算法最容易踩坑的地方不在 XOR,而在 charCodeAt。
JavaScript 字符串按 UTF-16 code unit 处理。中文字符会被拆成两个字节,例如:
javascript
StringToByte("测试")
// [109, 75, 139, 213]
所以 Python 不能简单用:
python
"测试".encode("utf-8")
因为 UTF-8 下 "测试" 是:
text
e6 b5 8b e8 af 95
而前端逻辑要的是 UTF-16 code unit 拆出来的:
text
6d 4b 8b d5
因此 Python 里要按 utf-16-be 处理,模拟 JS 的 charCodeAt:
python
def string_to_byte(value: str) -> list[int]:
raw = value.encode("utf-16-be", "surrogatepass")
result = []
for index in range(0, len(raw), 2):
code_unit = (raw[index] << 8) | raw[index + 1]
if code_unit & 0xFF00:
result.append(code_unit >> 8)
result.append(code_unit & 0xFF)
else:
result.append(code_unit)
return result
这一步是复现一致性的关键。否则 ASCII 账号能对上,一遇到中文用户名、特殊字符就翻车,排查时还容易误以为服务端在"玄学风控"。
四、样本对照:静态结论必须动态验证
静态代码看懂只是第一步。真正交付前,必须做样本对照。
浏览器侧执行结果:
text
admin -> f2f7fefafd
123456 -> a2a1a0a7a6a5
测试 -> fed81846
a测 -> f2fed8
Python 侧复现:
python
SEC_KEY = 147
def encry(value: str, key: int = SEC_KEY) -> str:
return "".join(format(item ^ key, "x") for item in string_to_byte(value))
对照结果一致:
text
'admin' [97, 100, 109, 105, 110] f2f7fefafd
'123456' [49, 50, 51, 52, 53, 54] a2a1a0a7a6a5
'测试' [109, 75, 139, 213] fed81846
'a测' [97, 109, 75] f2fed8
到这里,参数算法已经闭环。
五、真实请求体是什么样?
使用页面上下文直接触发 login(),并 hook jQuery.post,可以拿到真实提交数据。
测试账号:
text
username = codex_probe_user
password = codex_probe_pass
浏览器真实请求体:
text
userName=f0fcf7f6ebcce3e1fcf1f6cce6e0f6e1
passWord=f0fcf7f6ebcce3e1fcf1f6cce3f2e0e0
响应:
text
账号或密码不正确
这个响应很重要。它说明请求已经到达业务层,而不是被网关、WAF、CSRF 校验或参数格式拦截。
爬虫工程里有个朴素判断:
text
能拿到"账号或密码不正确",通常比拿到 200 HTML 更值得高兴。
因为前者是业务服务在说话,后者可能只是防护系统给你塞了一份"请先证明你像浏览器"的试卷。
六、普通 requests 为什么不行?
直接用 Python requests 访问时,GET 首页可以拿到 acw_tc:
text
acw_tc=...
但 POST 登录接口时,返回的不是业务文本,而是一段阿里云 WAF challenge HTML:
html
<meta name="aliyun_waf_aa" ...>
<meta name="aliyun_waf_bb" ...>
这说明参数加密没问题,卡点在协议层指纹。常见差异包括:
- TLS ClientHello 指纹
- HTTP/2 行为
- header 顺序和细节
- 浏览器请求上下文
- WAF cookie 校验链路
这里不要把问题误判成"加密还没还原"。如果同一 payload 在浏览器里返回 账号或密码不正确,而 requests 返回 WAF HTML,优先怀疑协议指纹。
最终使用 curl_cffi:
python
from curl_cffi import requests
session = requests.Session(impersonate="chrome")
它仍然是纯 Python 调用,不依赖浏览器,不执行页面 JS,只是在 TLS/HTTP 指纹上更接近真实 Chrome。
七、最终 Python 实现
核心代码如下:
python
from typing import Dict, List
from curl_cffi import requests
BASE_URL = "https://file.sztbhz.com"
LOGIN_URL = f"{BASE_URL}/Account/LoginResult"
SEC_KEY = 147
def string_to_byte(value: str) -> List[int]:
raw = value.encode("utf-16-be", "surrogatepass")
result: List[int] = []
for index in range(0, len(raw), 2):
code_unit = (raw[index] << 8) | raw[index + 1]
if code_unit & 0xFF00:
result.append(code_unit >> 8)
result.append(code_unit & 0xFF)
else:
result.append(code_unit)
return result
def encry(value: str, key: int = SEC_KEY) -> str:
return "".join(format(item ^ key, "x") for item in string_to_byte(value))
def build_login_payload(username: str, password: str) -> Dict[str, str]:
return {
"userName": encry(username),
"passWord": encry(password),
}
def login(username: str, password: str, timeout: int = 20) -> str:
session = requests.Session(impersonate="chrome")
session.headers.update(
{
"Accept-Language": "zh-CN,zh;q=0.9",
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/148.0.0.0 Safari/537.36"
),
}
)
session.get(BASE_URL + "/", timeout=timeout)
headers = {
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Origin": BASE_URL,
"Referer": BASE_URL + "/",
"X-Requested-With": "XMLHttpRequest",
}
response = session.post(
LOGIN_URL,
data=build_login_payload(username, password),
headers=headers,
timeout=timeout,
)
response.raise_for_status()
return response.text
调用方式:
python
import main
payload = main.build_login_payload("admin", "123456")
print(payload)
result = main.login("admin", "123456")
print(result)
输出 payload:
python
{
"userName": "f2f7fefafd",
"passWord": "a2a1a0a7a6a5"
}

对比浏览器一致,ojbk。
八、总结
这次逆向不复杂,但非常适合作为爬虫工程里的标准小案例:
- 入口清晰:
login() - 参数清晰:
userName、passWord - 算法清晰:
StringToByte + XOR 147 + hex - 坑点明确:JS 字符模型和 WAF 协议指纹
- 结果可验证:伪账号返回业务层
账号或密码不正确
如果把逆向过程比作查案,这次不是密室杀人,而是门口贴了张纸条:
text
钥匙在花盆底下,记得用 UTF-16 拿。
真正的工程价值不在于算法多炫,而在于把每一层都拆清楚:前端怎么生成参数、接口怎么接收、服务端怎么响应、防护层在哪里插手。这样写出来的爬虫代码才不是"一次性脚本",而是能维护、能解释、能排障的工程组件。