欢迎光临
我们一直在努力

青岛市政府信箱 —— RSA + AES 混合加密

目录

  • 一、分析
  • 二、JS 复现与验证
  • 三、Python 实现
  • 四、总结

免责声明:本文内容仅用于合法授权范围内的技术学习、安全研究、逆向分析方法交流与风控防护理解,不针对任何网站、产品或服务提供绕过、攻击、滥用或破坏性使用建议。文中涉及的接口分析、参数加解密、调试定位、代码复现、数据请求等内容,仅用于说明相关技术原理和分析流程。读者应在遵守相关法律法规、平台规则、robots 协议、用户协议以及获得合法授权的前提下进行学习和实验。请勿将本文中的方法、脚本或思路用于未授权访问、批量采集、账号撞库、绕过风控、破坏验证码体系、规避平台限制、侵犯数据权益、商业化滥用或影响线上系统稳定性的行为。对于真实网站案例,读者不应直接复制代码对线上服务进行高频请求或非授权调用。若相关网站、产品方、权利方或平台认为本文内容存在不适宜公开展示之处,可通过评论区、私信或作者主页提供的联系方式联系我;核实后将及时删除、替换或调整相关内容。读者因不当使用本文内容造成的任何法律责任、业务风险或经济损失,均由使用者自行承担,与作者无关。

一、分析

目标地址:http://www.qingdao.gov.cn/mailboxpage-qd/city/index.html

抓取信箱列表数据,提取标题、回复单位、类型、回复时间。

F12 抓包找到数据接口:

POST http://www.qingdao.gov.cn/mailboxpage-qd/mailboxBase/getMailListPage

请求体大部分字段是固定的,但有一个 encrypted 字段是动态生成的密文。响应体的 data 字段也是密文(Base64 格式)。

定位 encrypted 生成逻辑:搜索 encrypted,定位到 index.js:

mailListDTO.encrypted = getEncrypted();

注意:mailListDTO.encrypted 在第一页时是空的,翻页时才有值(保留了上一次的值)。每次翻页会重新调用 getEncrypted() 覆盖。

进入 getEncrypted 函数:

function getEncrypted() {
while (!sun) {} // 等待密钥初始化完成
const encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
return encrypt.encrypt(String(new Date().getTime())); // RSA 加密当前时间戳
}

这是 RSA 非对称加密——用公钥加密当前毫秒时间戳。JSEncrypt 是一个标准的 RSA 加密库。

密钥来源:页面加载时请求 /encipher/selectPublicKey 接口获取:

$.ajax({
url: requestPrefix + "/encipher/selectPublicKey",
success: function (result) {
publicKey = data.publicKey; // RSA 公钥
refreshAesKeyAndIv(data); // 设置 AES 密钥和 IV
}
})

返回数据:

{
"aesKey": "eWF3ZWlzb2Z0YnBtMjAyMQ==",
"aesIv": "MjAyMWJwbXlhd2Vpc29mdA==",
"publicKey": "MIGfMA0GCSqGSIb3DQEBA…"
}

aesKey 和 aesIv 是 Base64 编码的,解码后:

  • aesKey → "yaweisoftbpm2021"(16字节)
  • aesIv → "2021bpmyaweisoft"(16字节)
  • 经测试这三个值目前是固定的,但代码设计上是动态获取的,建议每次都请求一次以防服务端轮换。

    响应解密:在设置 encrypted 的代码下方几行就能看到:

    success: function (result) {
    data = JSON.parse(aesDecrypt(data)); // 解密响应
    }

    进入 aesDecrypt:

    function aesDecrypt(data) {
    if (!AES_SECRET_KEY || !AES_SECRET_IV) return data;
    const encryptedHexStr = CryptoJS.enc.Base64.parse(data);
    const str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
    // 上面两行是冗余操作,Base64 parse 再 stringify 回来等于没做
    const decrypt = CryptoJS.AES.decrypt(str, AES_SECRET_KEY, {
    iv: AES_SECRET_IV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7
    });
    return decrypt.toString(CryptoJS.enc.Utf8);
    }

    标准 AES/CBC/PKCS7,密文是 Base64 格式。

    二、JS 复现与验证

    // qd_mailboxpage.js
    const JSEncrypt = require('./jsencrypt').default;
    const CryptoJS = require('../../CryptoJS')

    // RSA 加密时间戳 → encrypted 字段
    function getEncrypted(publicKey) {
    const encrypt = new JSEncrypt();
    encrypt.setPublicKey(publicKey);
    return encrypt.encrypt(String(new Date().getTime()));
    }

    // AES 解密响应
    function decrypt(str, aesKey, aesIv) {
    let AES_SECRET_KEY = CryptoJS.enc.Utf8.parse(aesKey);
    let AES_SECRET_IV = CryptoJS.enc.Utf8.parse(aesIv);
    const _decrypt = CryptoJS.AES.decrypt(str, AES_SECRET_KEY, {
    iv: AES_SECRET_IV,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
    });
    return _decrypt.toString(CryptoJS.enc.Utf8);
    }

    注意:jsencrypt.js 从浏览器复制过来后,需要在文件顶部补 let window = globalThis;,因为 JSEncrypt 内部引用了 window 对象。

    三、Python 实现

    方式一:execjs 调用 JS

    import json
    import base64
    import subprocess
    from functools import partial
    from concurrent.futures import ThreadPoolExecutor, as_completed

    import requests

    subprocess.Popen = partial(subprocess.Popen, encoding='utf-8')
    import execjs

    class QingdaoMailboxSpider:
    """青岛市政府信箱爬虫(execjs 版)"""

    KEY_URL = 'http://www.qingdao.gov.cn/mailboxpage-qd/encipher/selectPublicKey'
    API_URL = 'http://www.qingdao.gov.cn/mailboxpage-qd/mailboxBase/getMailListPage'

    def __init__(self, js_path='qd_mailboxpage.js'):
    self.session = requests.Session()
    self.session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
    '(KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36',
    })
    self.session.verify = False
    self.ctx = execjs.compile(open(js_path, 'r', encoding='utf-8').read())
    # 初始化密钥
    self._init_keys()

    def _init_keys(self):
    """请求服务端获取 RSA 公钥和 AES 密钥"""
    resp = self.session.get(self.KEY_URL)
    data = resp.json()['data']
    self.public_key = data['publicKey']
    self.aes_key = base64.b64decode(data['aesKey']).decode('utf-8')
    self.aes_iv = base64.b64decode(data['aesIv']).decode('utf-8')

    def fetch_page(self, page: int) > list:
    """请求单页数据"""
    encrypted = self.ctx.call('getEncrypted', self.public_key)
    payload = {
    'seekCode': '', 'title': '', 'pageSize': 15,
    'pageNo': str(page), 'appSign': 'szxx', 'userGuid': '',
    'questionType': '', 'startTime': '', 'endTime': '',
    'encrypted': encrypted,
    }
    resp = self.session.post(self.API_URL, json=payload)
    enc_data = resp.json().get('data', '')
    if not enc_data:
    return []

    decrypted = json.loads(self.ctx.call('decrypt', enc_data, self.aes_key, self.aes_iv))
    return [
    {
    '标题': r.get('title'),
    '回复单位': r.get('origUser'),
    '回复时间': r.get('mailContent', {}).get('respDate', '').replace('T', ' '),
    }
    for r in decrypted.get('records', [])
    ]

    def run(self, pages=3, max_workers=3):
    """并发采集"""
    all_data = []
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
    futures = {executor.submit(self.fetch_page, p): p for p in range(1, pages + 1)}
    for future in as_completed(futures):
    page = futures[future]
    try:
    result = future.result()
    all_data.extend(result)
    print(f'[Page {page}] 获取 {len(result)} 条记录')
    except Exception as e:
    print(f'[Page {page}] 失败: {e}')
    return all_data

    if __name__ == '__main__':
    import urllib3

    urllib3.disable_warnings()
    spider = QingdaoMailboxSpider()
    data = spider.run(pages=3)
    for item in data:
    print(item)

    方式二:纯 Python(pycryptodome + rsa)

    文件名:qingdao_mailbox_pure_crypto.py

    import json
    import time
    import base64
    from concurrent.futures import ThreadPoolExecutor, as_completed

    import requests
    from Crypto.Cipher import AES, PKCS1_v1_5
    from Crypto.PublicKey import RSA
    from Crypto.Util.Padding import unpad

    class QingdaoMailboxSpider:
    """青岛市政府信箱爬虫(纯 Python 版)"""

    KEY_URL = 'http://www.qingdao.gov.cn/mailboxpage-qd/encipher/selectPublicKey'
    API_URL = 'http://www.qingdao.gov.cn/mailboxpage-qd/mailboxBase/getMailListPage'

    def __init__(self):
    self.session = requests.Session()
    self.session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
    '(KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36',
    })
    self.session.verify = False
    self._init_keys()

    def _init_keys(self):
    """请求服务端获取密钥"""
    resp = self.session.get(self.KEY_URL)
    data = resp.json()['data']
    self.aes_key = base64.b64decode(data['aesKey'])
    self.aes_iv = base64.b64decode(data['aesIv'])
    # 构造 RSA 公钥对象
    pub_key_der = base64.b64decode(data['publicKey'])
    self.rsa_key = RSA.import_key(pub_key_der)

    def _rsa_encrypt_timestamp(self) > str:
    """RSA 公钥加密当前时间戳 → encrypted 字段"""
    cipher = PKCS1_v1_5.new(self.rsa_key)
    plaintext = str(int(time.time() * 1000)).encode('utf-8')
    encrypted = cipher.encrypt(plaintext)
    return base64.b64encode(encrypted).decode('utf-8')

    def _aes_decrypt(self, ciphertext: str) > str:
    """AES/CBC 解密响应(Base64 密文)"""
    cipher = AES.new(self.aes_key, AES.MODE_CBC, self.aes_iv)
    ct_bytes = base64.b64decode(ciphertext)
    pt = unpad(cipher.decrypt(ct_bytes), AES.block_size)
    return pt.decode('utf-8')

    def fetch_page(self, page: int) > list:
    """请求单页数据"""
    payload = {
    'seekCode': '', 'title': '', 'pageSize': 15,
    'pageNo': str(page), 'appSign': 'szxx', 'userGuid': '',
    'questionType': '', 'startTime': '', 'endTime': '',
    'encrypted': self._rsa_encrypt_timestamp(),
    }
    resp = self.session.post(self.API_URL, json=payload)
    enc_data = resp.json().get('data', '')
    if not enc_data:
    return []

    decrypted = json.loads(self._aes_decrypt(enc_data))
    return [
    {
    '标题': r.get('title'),
    '回复单位': r.get('origUser'),
    '回复时间': r.get('mailContent', {}).get('respDate', '').replace('T', ' '),
    }
    for r in decrypted.get('records', [])
    ]

    def run(self, pages=3, max_workers=3):
    """并发采集"""
    all_data = []
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
    futures = {executor.submit(self.fetch_page, p): p for p in range(1, pages + 1)}
    for future in as_completed(futures):
    page = futures[future]
    try:
    result = future.result()
    all_data.extend(result)
    print(f'[Page {page}] 获取 {len(result)} 条记录')
    except Exception as e:
    print(f'[Page {page}] 失败: {e}')
    return all_data

    if __name__ == '__main__':
    import urllib3

    urllib3.disable_warnings()
    spider = QingdaoMailboxSpider()
    data = spider.run(pages=3)
    for item in data:
    print(item)

    四、总结

    环节要点
    抓包 POST 请求,请求体 encrypted 字段动态生成,响应 data 为 Base64 密文
    定位 搜索 encrypted 直接找到赋值位置,解密函数在同一文件的 ajax success 回调里
    加密 RSA 公钥加密当前时间戳(JSEncrypt 库),用于请求体的 encrypted 字段
    解密 AES-128 / CBC / PKCS7,密钥和 IV 从服务端接口动态获取(Base64 编码)
    密钥 通过 /encipher/selectPublicKey 接口获取,目前固定但建议每次请求
    补环境 JSEncrypt 库需要 let window = globalThis

    本案例的核心收获:

  • 第一次遇到 RSA + AES 混合加密:RSA 负责加密一个 "凭证"(时间戳),AES 负责加解密实际数据。这是企业级应用的常见模式。
  • 密钥不是硬编码在 JS 里,而是从服务端动态获取。虽然目前是固定值,但代码应该按动态处理来写(每次请求前先获取密钥)。
  • 第三方加密库(JSEncrypt)可以直接从浏览器复制到本地使用,只需简单补一个 window = globalThis。
  • 纯 Python 版使用 Crypto.Cipher.PKCS1_v1_5 做 RSA 加密,Crypto.PublicKey.RSA.import_key 可以直接导入 Base64 编码的 DER 格式公钥。
  • 赞(0)
    未经允许不得转载:171主机测评 » 青岛市政府信箱 —— RSA + AES 混合加密
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址