最近在研究某政府医院采购招标平台的数据采集时,遇到一个典型的RSA+AES混合加密案例。这个案例很好地展示了现代Web应用常见的安全防护机制,也让我对前端加密逆向有了更深入的理解。下面就把整个分析过程和解决方案详细分享给大家。
这个案例来自深圳某三甲医院的采购公告平台,平台采用了前后端分离架构,关键接口都进行了加密处理。我们需要采集的是采购公告列表数据,但请求参数和响应内容都经过了加密处理。经过分析发现,平台采用了RSA非对称加密传输AES密钥,再用AES对称加密业务数据的混合加密方案。
首先打开浏览器开发者工具,查看采购公告列表接口的请求参数。发现接口需要携带四个加密参数:
通过全局搜索发现,这些参数只在当前接口使用,其他接口并未采用相同加密方案。这意味着我们需要针对这个接口进行专门的逆向分析。
提示:在分析加密参数时,建议先确认参数是否全局使用。如果是全局参数,可能在公共拦截器或工具类中实现;如果是接口独有,则需要针对特定接口进行分析。
通过XHR断点调试,我们追踪到请求参数在发送前被加密的过程。关键发现是:
在Chrome开发者工具的Sources面板中,我们找到了加密的核心代码段:
javascript复制function encryptRequest(data) {
// 生成随机AES密钥
const aesKey = generateRandomKey();
// 使用RSA公钥加密AES密钥
const encryptedKey = RSA.encrypt(aesKey, publicKey);
// 使用AES加密请求数据
const encryptedData = AES.encrypt(JSON.stringify(data), aesKey);
return {
data: encryptedData,
key: encryptedKey,
timestamp: Date.now(),
nonce: generateNonce()
};
}
进一步分析加密函数,确认了以下关键信息:
这种混合加密方案结合了两种加密方式的优势:
首先需要获取RSA加密使用的公钥。在浏览器控制台搜索"publicKey"或"RSA"等关键字,很快就能找到如下公钥定义:
javascript复制const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx6X7mZ...(省略部分内容)
-----END PUBLIC KEY-----`;
这是一个标准的PKCS#1格式的RSA公钥,可以直接用于后续的加密操作。
平台使用以下逻辑生成随机AES密钥:
javascript复制function generateRandomKey() {
const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
let result = '';
for (let i = 0; i < 32; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
注意到密钥生成排除了容易混淆的字符(如1/l/I等),这是一个良好的安全实践。
基于以上分析,我们可以用Python还原整个加密流程:
python复制import rsa
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import json
import time
import random
import string
# RSA公钥
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx6X7mZ...(省略部分内容)
-----END PUBLIC KEY-----"""
def generate_aes_key():
"""生成随机AES密钥"""
chars = string.ascii_letters + string.digits
exclude = {'l', 'I', 'O', '0', '1'}
chars = [c for c in chars if c not in exclude]
return ''.join(random.choice(chars) for _ in range(32))
def rsa_encrypt(data, public_key):
"""RSA加密"""
pub_key = rsa.PublicKey.load_pkcs1(public_key.encode())
return base64.b64encode(rsa.encrypt(data.encode(), pub_key)).decode()
def aes_encrypt(data, key):
"""AES加密"""
iv = b'0123456789ABCDEF' # 固定IV
cipher = AES.new(key.encode(), AES.MODE_CBC, iv)
padded_data = pad(data.encode(), AES.block_size)
encrypted = cipher.encrypt(padded_data)
return base64.b64encode(encrypted).decode()
def encrypt_request(params):
"""加密请求参数"""
aes_key = generate_aes_key()
encrypted_key = rsa_encrypt(aes_key, PUBLIC_KEY)
encrypted_data = aes_encrypt(json.dumps(params), aes_key)
return {
'data': encrypted_data,
'key': encrypted_key,
'timestamp': int(time.time() * 1000),
'nonce': ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))
}
接口返回的数据同样经过加密处理,结构如下:
json复制{
"success": true,
"data": "U2FsdGVkX1+3C7g...(加密数据)",
"key": "MIIBIjANBgkqhkiG...(加密的AES密钥)"
}
响应解密需要以下步骤:
Python实现代码如下:
python复制from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import base64
def aes_decrypt(encrypted_data, key):
"""AES解密"""
iv = b'0123456789ABCDEF' # 必须与加密时一致
encrypted_bytes = base64.b64decode(encrypted_data)
cipher = AES.new(key.encode(), AES.MODE_CBC, iv)
decrypted = cipher.decrypt(encrypted_bytes)
return unpad(decrypted, AES.block_size).decode()
def decrypt_response(response, aes_key):
"""解密响应数据"""
encrypted_data = response['data']
return json.loads(aes_decrypt(encrypted_data, aes_key))
结合上述加密解密逻辑,完整的请求实现如下:
python复制import requests
# 初始化加密参数
params = {
'pageNum': 1,
'pageSize': 10,
'noticeType': '采购公告'
}
# 加密请求参数
encrypted = encrypt_request(params)
# 发送请求
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Content-Type': 'application/json'
}
response = requests.post(
'https://zbcg.sznsyy.cn/api/notice/list',
json=encrypted,
headers=headers
).json()
# 解密响应(使用之前生成的AES密钥)
decrypted_data = decrypt_response(response, aes_key)
print(decrypted_data)
在实际操作中,有几个关键点需要注意:
IV一致性:AES-CBC模式需要保证加密和解密使用相同的IV,这里平台使用了固定IV0123456789ABCDEF
填充模式:JavaScript端默认使用PKCS7填充,Python中需要使用pad/unpad方法处理
密钥管理:实际场景中,AES密钥应该在加密请求后保存,用于解密对应响应
编码处理:Base64编码在不同语言间可能存在细微差异,需要确保编码解码方式一致
调试时常见的几个问题及解决方案:
这个案例中的加密方案虽然增加了逆向难度,但仍然存在一些安全弱点:
作为爬虫开发者,我们应该注意:
对于大规模采集任务,可以考虑以下优化:
健壮的爬虫需要完善的错误处理机制:
python复制def safe_request(url, params, retry=3):
for i in range(retry):
try:
encrypted = encrypt_request(params)
response = requests.post(url, json=encrypted, timeout=10)
response.raise_for_status()
return decrypt_response(response.json(), aes_key)
except Exception as e:
print(f"请求失败(尝试 {i+1}/{retry}): {str(e)}")
time.sleep(2 ** i) # 指数退避
return None
建议编写测试用例验证加密解密逻辑:
python复制import unittest
class TestEncryption(unittest.TestCase):
def test_encrypt_decrypt(self):
original = {'test': 'data'}
encrypted = encrypt_request(original)
self.assertIn('data', encrypted)
# 模拟响应
mock_response = {
'data': encrypted['data'],
'key': encrypted['key']
}
decrypted = decrypt_response(mock_response, aes_key)
self.assertEqual(decrypted, original)
if __name__ == '__main__':
unittest.main()
在进行网络数据采集时,必须注意:
在实际项目中,建议:
这个案例展示了现代Web应用常见的安全防护机制,也让我对混合加密方案有了更深入的理解。在实际逆向过程中,耐心和细致是关键,需要逐步验证每个环节的假设。希望这个详细的案例分析对大家有所帮助,如果有任何问题欢迎交流讨论。