全国新书网 —— AES/CBC 双向加解密

目录

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

一、分析

目标地址:https://cnpub.com.cn/information.html#/search?nav=1

抓取行业动态下的最新资讯,解析标题、作者、出自以及发布日期。

F12 打开 DevTools,切到 Network → Fetch/XHR,清空后翻页,捕获到数据接口:

text 复制代码
POST https://cnpub.com.cn/prod-api/api/index/newsList

观察发现 Request Payload 和 Response 都是密文,看着像十六进制编码(全是 0-9 a-f 字符)。

精简请求头 :逐步删除 Cookie 和多余 Header,测试发现必须保留 'Content-Type': 'application/json;charset=UTF-8',否则请求失败。

定位思路 :请求体只有一个 密文字符串,没有明显的 key 名可以搜索。换个角度------从响应解密入手。服务端返回密文后,前端一定会解密再 JSON.parse() 成对象。所以 Hook JSON.parse 来反向定位解密函数。

Hook 代码(在 Sources 面板打 Script 断点 → Ctrl+F5 刷新 → 断住后在 Console 注入):

javascript 复制代码
(function () {
    const nativeParse = JSON.parse;
    JSON.parse = function (text, reviver) {
        debugger;
        const result = nativeParse.call(JSON, text, reviver);
        console.log("JSON.parse 拦截:", result);
        return result;
    };
    JSON.parse.toString = function () {
        return "function parse() { [native code] }";
    };
    console.log("JSON.parse Hook 已注入");
})();

注入的时机

注入后取消 Script 断点,F8 恢复执行。触发翻页后 debugger 命中,观察 text 变量确认是页面数据。

回溯堆栈:向上一层找到关键代码:

javascript 复制代码
"string" == typeof e.data && (e.data = JSON.parse(Object(o["b"])(e.data)));

e.data 是密文,Object(o["b"]) 就是解密函数。点击 [[FunctionLocation]] 跳转到定义处:

javascript 复制代码
r = (e, t="16weizifuchuan16", i="1suibianshurude6") => {
    const a = s["enc"].Utf8.parse(t),
          o = s["enc"].Utf8.parse(i),
          r = s["AES"].decrypt(s["format"].Hex.parse(e), a, {
              iv: o,
              mode: s["mode"].CBC,
              padding: s["pad"].Pkcs7
          });
    return s["enc"].Utf8.stringify(r).toString()
}

算法确认 :AES / CBC / PKCS7,密文格式为 Hex。密钥 "16weizifuchuan16",IV "1suibianshurude6",都是硬编码的默认参数。s 就是 CryptoJS。

在同一闭包中找到加密函数:

javascript 复制代码
const o = (e, t="16weizifuchuan16", i="1suibianshurude6") => {
    const a = s["enc"].Utf8.parse(t),
          o = s["enc"].Utf8.parse(i);
    let r = s["enc"].Utf8.parse(JSON.stringify(e));
    const n = s["AES"].encrypt(r, a, {
        iv: o,
        mode: s["mode"].CBC,
        padding: s["pad"].Pkcs7
    });
    return n.ciphertext.toString()  // 输出 hex
}

断点观察加密函数的输入 e

json 复制代码
{"newsType":1,"pageNum":3,"pageSize":10,"title":""}

pageNum 是页码,其他字段固定。

二、JS 复现与验证

javascript 复制代码
// cnpub.js 这里我用的是离线的 CryptoJS
let CryptoJS = require('../../CryptoJS')
let t = "16weizifuchuan16"
let i = "1suibianshurude6"

function encrypt(obj) {
    const a = CryptoJS["enc"].Utf8.parse(t)
    const o = CryptoJS["enc"].Utf8.parse(i)
    let r = CryptoJS["enc"].Utf8.parse(JSON.stringify(obj))
    const n = CryptoJS["AES"].encrypt(r, a, {
        iv: o,
        mode: CryptoJS["mode"].CBC,
        padding: CryptoJS["pad"].Pkcs7
    });
    return n.ciphertext.toString()
}

function decrypt(data) {
    const a = CryptoJS["enc"].Utf8.parse(t)
    const o = CryptoJS["enc"].Utf8.parse(i)
    const r = CryptoJS["AES"].decrypt(CryptoJS["format"].Hex.parse(data), a, {
        iv: o,
        mode: CryptoJS["mode"].CBC,
        padding: CryptoJS["pad"].Pkcs7
    });
    return CryptoJS["enc"].Utf8.stringify(r).toString()
}

加密结果与浏览器一致,验证通过。注意服务端返回的密文字符串两端带引号 ",解密前需要 .replace('"', '') 去掉。

三、Python 实现

方式一:execjs 调用 JS

python 复制代码
import json
import subprocess
from functools import partial

import requests

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

headers = {
    'Content-Type': 'application/json;charset=UTF-8',
    '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',
}

ctx = execjs.compile(open('./cnpub.js', 'r', encoding='utf-8').read())

for page in range(1, 3):
    data = {"newsType": 1, "pageNum": page, "pageSize": 10, "title": ""}
    encrypted_data = ctx.call('encrypt', data)
    response = requests.post(
        'https://cnpub.com.cn/prod-api/api/index/newsList',
        headers=headers, data=encrypted_data
    )
    res_encrypted_data = response.text.replace('"', '')
    json_data = json.loads(ctx.call('decrypt', res_encrypted_data))
    for row in json_data.get('list', {}).get('rows', []):
        print({
            "标题": row.get('title'),
            "作者": row.get('author', ''),
            "出自": row.get('platformName', ''),
            "发布时间": row.get('publishTime').split(' ')[0],
        })

方式二:纯 Python(pycryptodome)

文件名:cnpub_pure_crypto.py

python 复制代码
import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import requests

KEY = b"16weizifuchuan16"
IV = b"1suibianshurude6"

headers = {
    'Content-Type': 'application/json;charset=UTF-8',
    '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',
}

for page in range(1, 3):
    # 加密: 每次必须新建 AES 对象(CBC 模式有状态,不能复用)
    cipher_enc = AES.new(KEY, AES.MODE_CBC, IV)
    plaintext = json.dumps(
        {"newsType": 1, "pageNum": page, "pageSize": 10, "title": ""},
        separators=(',', ':')  # 必须紧凑格式,否则加密结果与服务端不一致
    )
    encrypted_data = cipher_enc.encrypt(pad(plaintext.encode('utf-8'), AES.block_size)).hex()

    response = requests.post(
        'https://cnpub.com.cn/prod-api/api/index/newsList',
        headers=headers, data=encrypted_data
    )

    # 解密: 同样需要新建 AES 对象
    cipher_dec = AES.new(KEY, AES.MODE_CBC, IV)
    res_hex = response.text.replace('"', '')
    decrypted = unpad(cipher_dec.decrypt(bytes.fromhex(res_hex)), AES.block_size).decode('utf-8')
    json_data = json.loads(decrypted)

    for row in json_data.get('list', {}).get('rows', []):
        print({
            "标题": row.get('title'),
            "作者": row.get('author', ''),
            "出自": row.get('platformName', ''),
            "发布时间": row.get('publishTime')
        })

四、踩坑记录

坑一:json.dumps 的空格问题

Python 的 json.dumps 默认输出带空格:{"key": "value"}(冒号和逗号后有空格)。但 JS 的 JSON.stringify 默认输出紧凑格式:{"key":"value"}。加密时输入不同,密文就不同,服务端解密后校验会失败。

python 复制代码
# 默认格式,加密结果与网页不一致
json.dumps(data)                        # '{"newsType": 1, "pageNum": 1, ...}'

# 紧凑格式,与 JS 的 JSON.stringify 一致
json.dumps(data, separators=(',', ':')) # '{"newsType":1,"pageNum":1,...}'

坑二:AES CBC 模式不能复用对象

CBC 模式内部维护一个状态(上一个密文块作为下一轮的 IV)。加密完成后对象的内部状态已经变了,不能再拿来解密。每次加密/解密都必须 AES.new() 创建新对象。

python 复制代码
# 复用同一个对象,解密会得到乱码
aes = AES.new(KEY, AES.MODE_CBC, IV)
ct = aes.encrypt(...)
pt = aes.decrypt(...)  # 错误!内部 IV 已经变了

# 分别创建
cipher_enc = AES.new(KEY, AES.MODE_CBC, IV)
cipher_dec = AES.new(KEY, AES.MODE_CBC, IV)

坑三:响应密文带引号

服务端返回的响应体是 "81eff789..." 这样带双引号的字符串(因为 JSON 序列化了一次)。解密前需要去掉引号,否则 bytes.fromhex() 会报错。

五、总结

环节 要点
抓包 POST 请求,请求体和响应都是 hex 密文,必须带 Content-Type 头
定位 请求体无明显 key 名,改从响应入手 Hook JSON.parse 反向定位
算法 AES / CBC / PKCS7,密文格式为 Hex(不是 Base64)
密钥 Key "16weizifuchuan16"、IV "1suibianshurude6",硬编码明文
复现 标准 CryptoJS 无魔改,直接引库或 Python 重写

本案例的核心收获

  1. 当请求体没有明显的参数名可搜索时,从响应解密方向入手(Hook JSON.parse)是一个有效的替代策略。
  2. Python 重写对称加密时注意两个坑:json.dumpsseparators 参数、CBC 模式 AES 对象不能复用。
  3. 密文格式不一定是 Base64,也可能是 Hex------看密文字符集(全是 0-9 a-f 就是 Hex)。
相关推荐
如烟花的信页3 小时前
加速乐cookie逆向分析
javascript·爬虫·python·js逆向
Amo Xiang6 小时前
福建公共资源交易平台 —— MD5 签名 + AES 响应解密
js逆向·python爬虫·md5·cryptojs·前端加密·axios拦截器·aes解密
Amo Xiang1 天前
JS 逆向系统进阶路线:专栏总纲与文章导航
javascript·js逆向·前端加密·爬虫逆向·反爬虫
Amo Xiang1 天前
新华社客户端 —— 3DES 双向加解密
js逆向·python爬虫·cryptojs·3des·前端加密
冰履踏青云1 天前
十年饮冰,热血难凉 | JS逆向为爱发电
js逆向
嫂子的姐夫2 天前
047-MD5:飞卢网
爬虫·python·js逆向·逆向
如烟花的信页2 天前
某管理服务平台点选逆向分析
javascript·爬虫·python·js逆向
如烟花的信页5 天前
易盾点选逆向分析
javascript·爬虫·python·js逆向
如烟花的信页6 天前
易盾滑块逆向分析
javascript·爬虫·python·js逆向