逆向js
识别算法类型
| 代码特征 | 算法 | 测试记录要写 |
|---|---|---|
| CryptoJS.AES, key+iv | AES | 模式 ECB/CBC,密钥、IV |
| JSEncrypt, publicKey | RSA | 公钥,填充方式 |
| CryptoJS.HmacSHA256 | HMAC-SHA256 | 签名密钥 |
| CryptoJS.MD5 / SHA256 | Hash 摘要 | 有无加盐 salt |
关键词清单
第一层:直球关键词
1
encrypt decrypt AES DES RSA sign signatureCryptoJS crypto-js JSEncrypt sm-crypto sm4
第二层:参数名(定位 key/iv 位置)
1
secretKey aesKey secret iv privateKey publicKeynonce timestamp appSecret setpublicKey
第三层:CryptoJS 特征函数(搜到基本就锁定加密逻辑)
1
AES.encrypt AES.decrypt Utf8.parse mode.CBC pad.Pkcs7MD5( HmacSHA256 enc.Hex enc.Base64
第四层:请求包装层(找统一加解密入口)
1
interceptor interceptors request.use response.usewx.request axios.create useEncryptHook
第五层:正则搜密钥本体(Console 或全局搜)
1
[0-9a-f]{32} → AES-256 key / MD5 值[0-9a-f]{16} → AES-128 key / IV-----BEGIN → RSA 公私钥
| 流量特征 | 大概率是什么 | 搜索方向 |
|---|---|---|
| Base64 长串,长度是 16 的倍数 | AES | AES、encrypt、key、iv |
JSON 里带 sign + timestamp/nonce | 签名防重放 | sign、MD5、HmacSHA |
| 超长 Base64(几百字节) | RSA | JSEncrypt、setPublicKey |
| 前端国密项目(政企/银行常见) | SM4/SM2 | sm4、sm2、gm |
| 请求和响应都是密文 | 双向加密(hook 点要两个) | decrypt 也要搜 |
前端堆栈原理与三大定位技巧
堆栈原理:
调用堆栈(Call Stack):当JS代码执行时,每调用一个函数,浏览器会将该函数的执行上下文(如参数、局部变量、执行位置)压入“栈”中;函数执行完毕后,会从栈中弹出。最终,堆栈会记录当前所有未执行完毕的函数调用关系。 栈的特性:后进先出(LIFO):最后被调用的函数会压在栈顶,最先执行完毕并弹出;最先被调用的函数压在栈底,最后执行完毕。
堆栈与执行顺序的关系:堆栈展示顺序:从上到下 = 调用顺序(后调用的函数在栈顶);
全局搜索:最快定位已知特征的加密函数
前端加解密逻辑往往包含明显的特征关键词,如:
函数/参数名:encrypt(加密)、decrypt(解密)、sign(签名)、password(密码)、salt(盐值)、key(密钥); 算法关键词:MD5、SHA256、AES、RSA、CryptoJS(常用加密库); 操作关键词:base64、encode(编码)、decode(解码)。
事件断点:追溯用户操作触发的加密逻辑
事件断点适用于加密逻辑由用户操作触发的场景(如点击登录按钮、提交表单、输入文本),无需提前知道任何代码特征,通过“捕获用户事件”反向追溯加密函数。
核心原理 用户操作(点击、输入、提交)会触发DOM事件(click、submit、input),而加解密逻辑通常在事件的回调函数 中执行(如点击登录按钮后,回调函数中获取表单数据并加密)。通过给DOM元素或全局事件设置断点,可在事件触发时暂停代码,再通过堆栈追溯到加密函数。 两种事件断点用法:
1)元素级事件断点(精准) 适用于已知触发加密的具体DOM元素(如登录按钮、提交按钮):
切换到「Elements」面板,通过「Ctrl+Shift+C」选中目标元素(如登录按钮
切换到「Sources」面板,展开「Event Listener Breakpoints」; 勾选对应事件类型(如「Mouse → click」、「Form → submit」); 触发用户操作,代码会暂停在所有click或submit事件的回调函数中; 结合堆栈和代码逻辑,筛选出与加解密相关的回调函数。 
XHR/Fetch断点:从请求出发逆向加密源头
XHR/Fetch断点适用于已知加密参数所在接口的场景(如登录接口/user/login、数据接口/api/data),核心是“拦截请求发送过程”,通过堆栈逆向追溯参数加密的源头。(也可以全局搜索定位对应接口)
所有前端请求(XHR或Fetch)发送前,必须先完成参数组装和加密(如将明文密码转为密文、生成sign签名)。给目标接口设置断点后,当JS执行到XMLHttpRequest.send()或fetch()时会自动暂停,此时堆栈中会保留从“加密函数”到“请求发送”的完整调用链,向上追溯即可找到加密逻辑。 操作步骤: 切换到「Sources」面板,点击「XHR/Fetch Breakpoints」标签; 点击「+」号,输入目标接口的路径(支持精确匹配和模糊匹配): 精确匹配:/user/login(仅拦截该接口); 模糊匹配:/api/(拦截所有包含/api/的接口); 触发请求(如点击登录、刷新页面),当请求即将发送时,代码会暂停在send()或fetch()处; 查看「Call Stack」调用堆栈,从栈顶(send函数)向上追溯,找到参数加密的关键函数。
debugger反调试绕过
前端使⽤debugger反调试的情况:
方法一:bp设置过滤把debugger替换为空
新增⼀条把响应包的debugger替换为空的规则
然后清除浏览器缓存,挂代理给bp再重新打开系统就可以绕过debugger(大部分情况)
方法二:使用火狐浏览器
方法三:添加脚本以忽略列表
示例:访问https://antispider8.scrape.center/
我们并没有设置任何断点,也没有执行任何额外的脚本,它就直接进入了断点模式。
这里通过 setInterval 循环,每秒执行 1 次 debugger 语句
我们可以右键行好,点击永不在此处暂停
发现成功绕过
方法4:替换文件
了解一下Overrides 面板的用法,利用它我们可以将远程的 JavaScript 文件替换成本地的 JavaScript 文件
我们将当前的 JavaScript 文件复制到文本编辑器中,删除或者直接注释掉 debugger 这个关键字
打开 Sources 面板下的 Overrides 面板,将修改后的完整 JavaScript 文件复制进去
替换完成之后,我们重新刷新网页,这时候就发现不会进入无限 Debugger 模式了。
方式5:抓包丢弃
这边如果加载过一次JS之后会无限生成debugger文件,那么我们在他加载之前直接drop掉不就可以了吗?说干就干
成功绕过
防重放绕过
首先要明确一个事情,重放攻击是二次请求,黑客通过抓包获取到了请求的HTTP报文,然后黑客自己编写了一个类似的HTTP请求,发送给服务器。也就是说服务器处理了两个请求,先处理了正常的HTTP请求,然后又处理了黑客发送的篡改过的HTTP请求。
防重放示例:timestamp和nonce
timestamp参数–因为一次正常的HTTP请求,从发出到达服务器一般都不会超过60s,所以服务器收到HTTP请求之后,首先判断时间戳参数与当前时间相比较,是否超过了60s,如果超过了则认为是非法的请求。
nonce参数–nonce的意思是仅一次有效的随机字符串,要求每次请求时,该参数要保证不同,所以该参数一般与时间戳有关,实际使用时可以加上客户端的ip地址,mac地址等信息做个哈希之后,作为nonce参数。我们将每次请求的nonce参数存储到一个“集合”中,可以json格式存储到数据库或缓存中.每次处理HTTP请求时,首先判断该请求的nonce参数是否在该“集合”中,如果存在则认为是非法请求。
如果同时使用timestamp和nonce参数呢? nonce的一次性可以解决timestamp参数60s的问题,timestamp可以解决nonce参数“集合”越来越大的问题。
签名生成:
1
2
$sign=md5($uid.$token.$stime.$nonce);
// 服务器通过uid从数据库中可读出token
自动化绕过:
通过逆向js寻找sign签名生成逻辑,生成自动化生成脚本,使用mitmproxy工具进行绕过.至于mitmproxy工具后面也会详细讲解
⾃动化加解密过防重放-使⽤mitmproxy
mitmproxy介绍:
Mitmproxy是一个免费的开源交互式的HTTPS代理。MITM即中间人攻击
mitmproxy安装:
window通过pipx直接通过pip安装
1
2
pip install pipx
pipx install mitmproxy
mitmproxy安装验证和安装证书:
cmd中输入mitmweb查看是否安装成功
或者cmd查看版本号mitmproxy –version
安装证书:
此时,还没有安装成功,我们发现还不能拦截到数据,这是因为我们没有安装证书和打开对应的监听窗口
windows平台的证书其实在我们装mitmproxy的时候系统已经自动给你下载了
Window安装路径在我们的usr\15128( 用户名).mitmproxy 下,p12结尾的证书就是windows平
台下的。
这里的密码填空就行了
使用方式:
浏览器设置代理mitnweb对应的监听端口
1
mitmproxy --listen-port 8888 -s test.py
test.py是自己编写的
浏览器依旧是设置8080
抓包⼯具设置上层代理(代理端⼝就是mitmproxy监听的端⼝),监听 8080
设置*就是所有⽹站都⾛代理,测试中建议只设置⽬标系统的地址⾛代理就⾏了
正向(请求出去):
- 浏览器发起 HTTP/HTTPS 请求,浏览器代理配置:
127.0.0.1:8080,请求发给 Burp - Burp 收到流量,Burp 配置了上游代理(Upstream Proxy)指向 127.0.0.1:8888
- Burp 把流量转发给 mitmproxy(监听 8888)
- mitmproxy 加载了
test.py脚本,触发脚本的request函数,自动修改请求(加 header、改参数等) - mitmproxy 将处理后的请求,发送到远端目标网站服务器
反向(响应回来):
- 目标网站返回响应数据包,发给 mitmproxy
- mitmproxy 触发脚本
response函数,可以修改返回内容 - mitmproxy 把修改后的响应包回传给 Burp
- Burp 接收响应,在 Burp 界面展示请求和响应,你可以手动查看 / 二次修改
- Burp 把响应交给浏览器
text.py示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# encoding=utf-8
import uuid
import hashlib
import json
from mitmproxy import ctx
import time
import random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.Padding import unpad
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
import base64
# 加解密处理
# 设置AES密钥和初始向量
Key= "7d8251684679455491db4d2e3c85e521"
key = b"7d8251684679455491db4d2e3c85e521"
iv = b"2098634618552512"
#AES加密函数
def aes_encrypt(data):
print('执行了加密')
"""
AES 加密
:param data: 需要加密的字符串,先进行AES加密然后再BS64加密
:return: 返回加密后字符串
"""
cipher = AES.new(key, AES.MODE_CBC, iv)
padded_data = pad(data.encode('utf-8'), AES.block_size, style='pkcs7')
encrypted_data = cipher.encrypt(padded_data)
return base64.b64encode(encrypted_data).decode('utf-8')
#AES解密函数
def aes_decrypt(data):
print('执行了解密')
"""
AES 解密
:param s: 加密后的字符串,先BS64解密再AES解密
:return: 返回解密后的字符串
"""
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted_data = base64.b64decode(data)
decrypted_data = cipher.decrypt(encrypted_data)
unpadded_data = unpad(decrypted_data, AES.block_size, style='pkcs7')
return unpadded_data.decode('utf-8')
#请求头AES密钥进行RSA加密
# 加载RSA公钥
public_key_pem = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCUIPx7My9gtdfVuobe1FDwCIjZ
LUg6T7e5XDoJyDzX4RgrM279jSOz98cXI8iLjqW7d+OTlA3K1yUL/w03eaLTEiqp
COkbK3HIWoJ5uK+oPzGYTvixHDIqn6z159nEZRlDOxRgYnf3b8G63fmIUv9ZBCYq
1gsl/f6se9QX6Z/SzwIDAQAB
-----END PUBLIC KEY-----
"""
public_key = serialization.load_pem_public_key(public_key_pem.encode(), backend=default_backend())
# RSA加密函数
def RSA_Encrypt(data):
encrypted_data = public_key.encrypt(
data.encode(),
padding.PKCS1v15()
)
return base64.b64encode(encrypted_data).decode('utf-8')
class ReplayBreakdown:
def __init__(self):
pass
def request(self, flow):
if "ctzhgd.eshore.cn" == flow.request.host:
# nonce生成
uuids = str(uuid.uuid4())
nonce = uuids.replace("-", "") # 去掉UUID中的`-`
# print("nonce:", nonce)
# timestamp生成
timestamp = str(int(round(time.time() * 1000)))
# print("timestamp:", timestamp)
# 获取原始请求的未加密参数
data = flow.request.get_text()
datas = "data="+data+"&nonce="+nonce+"×tamp="+timestamp
#生成sign签名
# 创建一个SHA-1哈希对象,实现sha1加密
sha1 = hashlib.sha1()
sha1.update(datas.encode('utf-8') if isinstance(datas, str) else datas)
signature=sha1.hexdigest()
print("signature:", signature)
flow.request.headers["Nonce"] = nonce
flow.request.headers["Timestamp"] = timestamp
flow.request.headers["Key"] = RSA_Encrypt(Key)
# flow.request.headers["Key"] = 'kU6iZsk/Xi/+IfeKBm7GTbKZSHOq673Hb0c+qixTyNZNkb7TU2Mh1HEeYAJV/VeHYvZW1EOdo3xOPNv1IcP1OpCTzYsWWpTkAhbmdvZ5TBAgU85FvgKVqE6IdEulpkfx7zU6e+7ra0YgTq2tumBmyrojBAC5HUYQdBLQlyBij9E='
flow.request.headers["Signature"] =signature
#请求数据处理
ecrypt_data = aes_encrypt(data)
print("ecrypt_data:", ecrypt_data)
# 修改请求体
flow.request.content = f'{{"data":"{ecrypt_data}"}}'.encode()
# def response(self, flow):
# # 检查是否需要解密,例如检查URL或其他条件
# if "ctzhgd.eshore.cn" == flow.request.host:
# # 获取返回的加密数据
# response_data = json.loads(flow.response.content)
# # 对返回的加密数据进行解密
# data_rep = response_data.get("data")
# decrypt_data = aes_decrypt(data_rep)
# # 打印解密后的数据
# flow.response.content = decrypt_data.encode('utf-8') # 确保是字节格式
addons = [ReplayBreakdown()]
















