抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

开局一个登录框,漏洞全靠捡。
图片

随便输入一个工号,点击密码登录查看数据包,看看数据包的情况。
图片

可以查看到请求包中有两个参数:requestData和encrypted,响应包存在encrypted和responseData。通过名字分析可以猜测:encrypted中的内容就是加密当前请求包的密钥,和请求内容一同发送给服务端进行解密。响应包同理。

判断加密思路后可以开始追前端加密逻辑了,常见的方法有通过文件流程断点、代码标签断点、XHR提交断点。不过我一般喜欢简单粗暴的直接搜索关键字,如:encrypt/encode。

图片

如上图所示一般不会存在很多包含此关键字的匹配行。刚开始学习可以每条都点进去打上断点,然后输入工号点击断点,通过是否能够断点断住来判断此处代码是否参与了加密。熟悉之后就可以根据代码来判断此处是否为加密点,个人的经验一般为:进入后查看上下文代码,观察是否存在key、iv、mode、Pkcs7、Pkcs5、RSA、AES、SM4、ECB、CBC等关键字,存在再观察一下逻辑,是否将key、iv传入了encrypt加密函数中,基本可以很顺利的找到大部分的加密点。

断点的位置一般都是断在明文请求体进入加密函数后,出来变成密文的位置。
图片

从上图可以获得的信息,首先是代码方面

  1. 加密的方式:AES
  2. 加密的模式:ECB
  3. 加密的填充方式:Pkcs7

从作用域来看,可获得的信息有:

  1. 明文请求包
  2. AES的加密密钥
  3. 加密后的结果

现在就来验证我们的猜测是否正确

问题一:如何判断返回值即为加密后的请求体数据?

将此数据包发送到Burpsuite,查看responseData是否与返回值一致即可判断。

图片

问题二:如何判断推测的加密信息是否正确?
使用Tscan中的加解密模块,将我们获取到的所有信息放入其中再次进行加密,查看加密结果是否与作用域中的返回值是否一致。

图片

确认加密模式没错之后就可以去寻找加密点了,此处只是调用了加密函数进行加密,还没有看到具体的加密逻辑,寻找具体的加密点可通过步入,继续执行、或者翻找上下文找到具体加密点,此处通过继续执行JS代码,发现具体加密点。

图片

1
2
3
4
5
6
7
8
9
10
11
12
encodeData(e, t) {
var n = this.randomString(16)
, r = this.Encrypt(e, n)
, a = new JSEncrypt;
a.setPublicKey(t);
var o = a.encrypt(n)
, i = {
requestData: r,
encrypted: o
};
return i
}

根据上图可获得的信息有

  1. AES 密钥的生成方式,随机生成的16位字符串
  2. RSA 的公钥
  3. encrypted的生成逻辑

到这里也就捋清楚了整体的加密解密逻辑

用户端请求操作:
用户输入–> AES 使用随机密钥加密用户输入 –> requestData –> 使用 RSA 加密随机生成的AES密钥 –> encrypted

服务器端处理用户请求逻辑:
服务器收到用户传来的 requestData 和 encrypted –> 使用 RSA 私钥解密 encrypted 获取随机密钥 –> 解密 requestData 数据 –> 后端处理

服务器响应用户请求逻辑:
后端处理完成 –> AES 使用随机密钥加密服务器响应包 –> responseData –> 使用 RSA 加密随机生成的AES密钥 –> encrypted

用户接收到响应操作:
接收到服务器响应数据 –> 浏览器使用 RSA 私钥解密 encrypted 获取随机密钥 –> 使用解密出的密钥解密 responseData 数据 –> 展示给用户

现在又到了验证环节:

问题一: AES生成方式是否为随机生成的16位字符串

1
2
3
4
5
6
7
8
n = this.randomString(16)

randomString: e => {
e = e || 32;
for (var t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678", n = t.length, r = "", a = 0; a < e; a++)
r += t.charAt(Math.floor(Math.random() * n));
return r
}

问题二:AES密钥是否为 RSA 加密后的结果

1
2
3
4
n = this.randomString(16)
a = new JSEncrypt
a.setPublicKey(t)
a.encrypt(n)

根据setPublicKey,就能知道公钥的值,RSA 的公私钥通常以 —–BEGIN xxx KEY—– 和 —–END xxx KEY—– 包裹的格式(PEM 格式)存储,这是 PEM (Privacy-Enhanced Mail) 标准的标记方式。而本次RSA使用的加密 JSEncrypt 必须使用 PEM 格式。

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
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
import base64

# 你的公钥(PEM 格式)
public_key_pem = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9baE8HsiYyy1IL35kjEsNTzfCGu9cgEG1aAQh3BUSsUQIVtwDeHcKzhjqMcPnPM1h
92HZAlt2UOyXj1YdkT/b7IL9mFTanNIQFR8jgqDH2l0PvRVyfjoEuRDCdBgmt8X03QI/JK4Gcg6hD42Zr2HCTH0emygdKaRKHM8kTGXQfwIDAQAB
-----END PUBLIC KEY-----"""

# 要加密的字符串
plaintext = "ABpR7ExGX2A7p2jH"

def rsa_encrypt(public_key_pem, message):
# 加载公钥
public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8'))
# 使用RSA加密
encrypted = public_key.encrypt(
message.encode('utf-8'),
asym_padding.PKCS1v15()
)
return base64.b64encode(encrypted).decode('utf-8')

print("加密后的结果 (Base64):")
print(rsa_encrypt(public_key_pem,plaintext))

因为 PKCS1v15 填充方案在加密时引入了随机数,所以每次对同一个AES密钥加密都会出现不同的结果。但只要用正确的私钥解密,结果一定是原始明文。

图片

验证上述的分析逻辑是否通顺,此处为了方便观察结果,提前将响应包解密了。

图片

可以从上图看到我们的思路没有问题,下一步就是实现自动化。此处选择使用 mitmproxy 来作为burpsuite下游代理。整体思路如下:

burpsuite发送明文数据 –> mitmproxy加密成密文 –> 服务器响应密文 –> mitmproxy解密成明文 –> burpsuite响应体明文

此处无法实现下述逻辑:

浏览器密文 –> burpsuite解密成明文 –> mitmproxy加密成密文 –> 服务器响应密文 – mitmproxy解密成明文 –> burpsuite响应体明文

经个人实验得出的结论为:
浏览器和服务器端一共存在两套rsa公私钥。
浏览器掌握着:浏览器公钥+服务器私钥
服务器掌握着:服务器公钥+浏览器私钥

将浏览器加密后的requestData和encrypted用浏览器的公钥和服务器的私钥是无法解密的。

最终的mitmproxy脚本

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
111
112
113
114
115
116
117
from mitmproxy import http
import json
import base64
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
import random
import string

public_key_pem = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9baE8HsiYyy1IL35kjEsNTzfCGu9cgEG1aAQh3BUSsUQIVtwDeHcKzhjqMcPnPM1h
92HZAlt2UOyXj1YdkT/b7IL9mFTanNIQFR8jgqDH2l0PvRVyfjoEuRDCdBgmt8X03QI/JK4Gcg6hD42Zr2HCTH0emygdKaRKHM8kTGXQfwIDAQAB
-----END PUBLIC KEY-----"""

private_key_pem = """-----BEGIN RSA PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAIE3Ctx/ZRvjTuAPnjtvquyEEDazVJNgAo909M9BX2umwBOwVXE+hGyH
EXPDjZ+h4qJ2lTUc+B3UiVz0TnBlvGGuOQdvImWwfylHtkG+pzrTusE45AEy+AsQpfPDKrypIhk0bhQLGkEhoRWg04rcqrXXpzIoC0P8a+vlScz8LQ4NAgMBAAECgYAX74ZHiiHEpLq7rqj1AZ576YrHVzjXg/V1dYjTy5xNaLoz63ooXBhTskF9XEAjze0ZgzXofNFJVVGMsoTFNVNL0YKKBWTrtw58iXFDEn0JwD1LxYwZQsc+V1da/OzaL+0gDcuMY4YmLqOUGTmQer1LLt1oNobI3ZYga7Q1iPz40QJBALh/346m0J6R8tafrJgnmNLfNh8DXGq0CEChoP3tIeOf2tbcX9efcqEW+igTlm+8BtDHzHo2Yjf+bJEpp1e+mM8CQQCzSmpC2zYCRowOyIx3uX3KY1hue4i3uPrUSG2sBgmBdBLHmsSAA+wtKlAsS1kh0voCjvKLL2bHO4UxyDqeuipjAkBcWMzqFv8Gz6CP4p4+DlvE+KqbPVBtrC0RRJVTY/T5fRLJRsbGI235yYlus9cxmBiFOexUI5Jn2nY29nVnSuQrAkEAmFgH+K0ZtE9LnRgtu2GjEEDgGGjhn/MPNyggAIbUtunxNyg8BebPXQVSQID5yLLjex8J2ti5RVs+7zELFmprrwJAPe8jVXfSq+5nvHMiUO4m8/MR4YmMsvG2G/bVN74+v4SthceLFmLNPjTGuBvcuFgX4B5HP3B2M90Kr0KWmET99w==
-----END RSA PRIVATE KEY-----"""

def random_string(length=16):
chars = string.ascii_letters + string.digits
return ''.join(random.choices(chars, k=length))

def encrypt(plain_text, key):
key_bytes = key.encode('utf-8')
plain_bytes = plain_text.encode('utf-8')
padder = padding.PKCS7(128).padder()
padded_data = padder.update(plain_bytes) + padder.finalize()
cipher = Cipher(algorithms.AES(key_bytes), modes.ECB())
encryptor = cipher.encryptor()
cipher_text = encryptor.update(padded_data) + encryptor.finalize()
return base64.b64encode(cipher_text).decode('utf-8')

def rsa_encrypt(public_key_pem, message):
public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8'))
encrypted = public_key.encrypt(
message.encode('utf-8'),
asym_padding.PKCS1v15()
)
return base64.b64encode(encrypted).decode('utf-8')

def decrypt(cipher_text, key):
key_bytes = key.encode('utf-8')
cipher_bytes = base64.b64decode(cipher_text)
cipher = Cipher(algorithms.AES(key_bytes), modes.ECB())
decryptor = cipher.decryptor()
decrypted_padded = decryptor.update(cipher_bytes) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
decrypted = unpadder.update(decrypted_padded) + unpadder.finalize()
return decrypted.decode('utf-8')

def rsa_decrypt(private_key_pem, encrypted_message):
private_key = serialization.load_pem_private_key(
private_key_pem.encode('utf-8'),
password=None
)
decrypted = private_key.decrypt(
base64.b64decode(encrypted_message),
asym_padding.PKCS1v15()
)
return decrypted.decode('utf-8')

class CryptoProxy:
def request(self, flow: http.HTTPFlow):
if flow.request.method in ["POST", "PUT", "PATCH"]:
try:
data = json.loads(flow.request.content.decode('utf-8'))
print("[DEBUG] 原始请求内容:", data)

# 新增判断逻辑
if 'requestData' in data and 'encrypted' in data:
print("[INFO] 请求已包含加密字段,跳过处理")
return

n = random_string(16)
encrypted_data = encrypt(json.dumps(data), n)
rsa_encrypted_key = rsa_encrypt(public_key_pem, n)

new_request_body = {
'requestData': encrypted_data,
'encrypted': rsa_encrypted_key
}

flow.request.content = json.dumps(new_request_body).encode('utf-8')
flow.request.headers['Content-Length'] = str(len(flow.request.content))
flow.request.headers['Content-Type'] = 'application/json'

except json.JSONDecodeError:
print("[WARNING] 请求不是有效的JSON格式")
except Exception as e:
print(f"[ERROR] 请求处理失败: {str(e)}")

def response(self, flow: http.HTTPFlow):
try:
raw_content = flow.response.content.decode('utf-8', errors='ignore')
response_data = json.loads(raw_content)

encrypted_key = response_data.get('encrypted') or response_data.get('encryptedKey')
encrypted_data = response_data.get('responseData') or response_data.get('data')

if encrypted_key and encrypted_data:
decrypted_key = rsa_decrypt(private_key_pem, encrypted_key)
decrypted_data = decrypt(encrypted_data, decrypted_key)
print("[SUCCESS] 解密后的数据:", decrypted_data)

flow.response.content = decrypted_data.encode('utf-8')
flow.response.headers['Content-Length'] = str(len(flow.response.content))
flow.response.headers['Content-Type'] = 'application/json'
else:
print("[WARNING] 响应中缺少加密字段")
except json.JSONDecodeError:
print("[ERROR] 响应不是有效的JSON格式")
except Exception as e:
print(f"[FATAL ERROR] 响应处理失败: {str(e)}")

addons = [CryptoProxy()]

启动 mitmproxy

1
mitmdump -s encrypt.py --listen-port 8484

最终实现的效果
图片