欢迎光临
我们一直在努力

精灵数据资讯接口 —— AES/ECB/PKCS7 响应体解密

目录

  • 一、分析
  • 二、Python 实现
  • 三、总结

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

一、分析

目标地址:

https://www.jinglingshuju.com/articles

本案例需要抓取精灵数据的资讯列表,循环采集前 3 页,提取每条新闻的标题、发布时间、新闻来源和详情页链接。

F12 打开 DevTools,切到 Network 面板,清空请求记录。列表页数据通常会通过 Ajax 动态加载,因此先过滤 Fetch/XHR,再通过翻页触发请求,定位到目标数据包:

目标接口是一个 POST 请求:

POST https://vapi.jinglingshuju.com/Data/getNewsList

多次翻页后对比 Form Data:

page=3&num=20
page=4&num=20
page=5&num=20

参数含义如下:

参数含义说明
page 页码 翻页时变化的参数
num 每页条数 固定为 20

请求头中没有看到自定义签名、时间戳或加密参数。实际测试时,即使不携带 Cookie 也能请求成功,服务端对请求头校验不严格;保留 User-Agent 即可拿到响应。为了让请求更接近浏览器环境,正式脚本中仍建议保留 Referer、Origin、X-Requested-With 等常见头。

接口外层返回的是普通 JSON,但 data 字段是一段密文:

{
"code": 0,
"msg": "success",
"data": "Ou1c1U7XU3pQRu0GosMzTD85Gpd/…"
}

接下来需要定位前端在哪里把这段密文还原成业务数据。响应解密通常有两条定位思路:

  • Hook JSON.parse,从 解密后会被 parse 成对象 这个位置向上回溯;
  • 在 Sources 中搜索 decrypt、AES、CryptoJS 等关键字。
  • 本案例优先采用第二种方式。全局搜索 decrypt 后出现两处相关代码:

    两处代码分别是:

    // 第 1 处:Cookie 相关解密
    return c.a.AES.decrypt(t, d.cookieKey).toString(c.a.enc.Utf8)

    // 第 2 处:接口响应 data 解密
    var e = y.a.AES.decrypt(data, z, {
    iv: y.a.enc.Utf8.parse(j.substr(0, 16)),
    mode: y.a.mode.ECB,
    padding: y.a.pad.Pkcs7
    });

    第一处使用的是 cookieKey,更像是本地 Cookie 或用户信息解密。第二处变量名直接是 data,并且后面紧跟 JSON.parse,更符合接口响应解密的特征。为了确认判断,可以分别打断点,然后翻页触发 getNewsList 请求。

    翻页后命中第二处断点,说明当前接口响应确实走到了这里:

    为了避免只凭断点位置下结论,可以继续做两步验证。第一步,对比 Network 面板中接口返回的 data 密文,与断点处函数入参 data 是否一致。如果二者一致,就可以确认这里处理的正是接口返回的密文。注意接口响应 JSON 中的 / 可能会显示为 \\/,这是 JSON 转义,不是密文本身发生了变化。

    第二步,在 JSON.parse(e.toString(y.a.enc.Utf8)) 附近继续断住,然后在 Console 中执行同样的表达式,查看返回对象是否就是页面上的资讯数据。

    整理后的核心解密逻辑如下:

    var j = "DXZWdxUZ5jgsUFPF";
    var z = y.a.enc.Utf8.parse(j);

    var e = y.a.AES.decrypt(data, z, {
    iv: y.a.enc.Utf8.parse(j.substr(0, 16)),
    mode: y.a.mode.ECB,
    padding: y.a.pad.Pkcs7
    });

    return JSON.parse(e.toString(y.a.enc.Utf8));

    其中 y.a 是打包后的 CryptoJS 对象。展开对象可以看到 AES、enc、mode、pad 等模块。这里的关键参数是:

    项目值
    算法 AES
    模式 ECB
    填充 Pkcs7
    Key DXZWdxUZ5jgsUFPF
    IV 代码中传入了 j.substr(0, 16),但 ECB 模式实际不使用 IV

    也就是说,复现时只需要对响应中的 data 做 Base64 解码,再用 AES-ECB-PKCS7 解密,最后对明文 JSON 执行 json.loads 即可。先用一个最小 Python 片段验证算法:

    # -*- coding: utf-8 -*-
    """
    @File : live_jinglingshuju_demo.py
    @Author : XAMO Lab
    @Date : 2026/6/12 21:58
    @Blog : https://blog.csdn.net/xw1680
    @Tool : PyCharm
    @Desc :
    """

    import base64
    import json

    import requests
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import unpad

    headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
    }
    url = "https://vapi.jinglingshuju.com/Data/getNewsList"
    data = {
    "page": "3",
    "num": "20"
    }
    response = requests.post(url, headers=headers, data=data)
    print(response.status_code)
    print(response.text)
    enc_data = response.json().get("data")
    aes_obj = AES.new(b'DXZWdxUZ5jgsUFPF', AES.MODE_ECB)
    print(json.loads(unpad(aes_obj.decrypt(base64.b64decode(enc_data)), AES.block_size).decode('utf-8')))

    执行结果如下:

    二、Python 实现

    前面的最小验证代码已经能证明算法可行,但直接用于采集还不够完整:缺少封装、并发、异常隔离、日志输出和字段结构化处理。这里把代码整理成面向对象版本,使用 ThreadPoolExecutor 并发请求前 3 页,并用 loguru 输出每页请求和解密状态。

    这里保留前面抓包测试时使用的请求头,尤其是 user-agent,同时补充 Referer、Origin、X-Requested-With 等常见浏览器请求头,让请求更接近页面环境。

    需要解析的字段包括:

    字段来源字段说明
    标题 title 资讯标题
    发布时间 news_time / time_str 优先使用接口中的 news_time
    新闻来源 news_source 文章来源
    详情页链接 detail / article_id 接口通常直接返回 detail,缺失时用 article_id 拼接

    完整代码如下:

    # -*- coding: utf-8 -*-
    """
    @File : jinglingshuju_articles_spider.py
    @Author : XAMO Lab
    @Date : 2026/6/12 23:17
    @Blog : https://blog.csdn.net/xw1680
    @Tool : PyCharm
    @Desc : 精灵数据资讯列表采集(AES/ECB/PKCS7 响应解密 + 多线程)
    """

    import base64
    import json
    import time
    import warnings
    from concurrent.futures import ThreadPoolExecutor, as_completed
    from typing import Any, Dict, List

    warnings.filterwarnings(
    "ignore",
    message=r"urllib3 .* or chardet .*charset_normalizer .* doesn't match a supported version!",
    )

    import requests
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import unpad
    from loguru import logger

    class JinglingshujuArticlesSpider:
    """精灵数据资讯列表爬虫"""

    API_URL = "https://vapi.jinglingshuju.com/Data/getNewsList"
    SITE_URL = "https://www.jinglingshuju.com/articles"
    NEWS_URL = "https://news.jinglingshuju.com"
    AES_KEY = b"DXZWdxUZ5jgsUFPF"

    def __init__(self, page_size: int = 20, max_workers: int = 3, retries: int = 3) > None:
    """
    :param page_size: 每页条数,页面默认 20
    :param max_workers: 并发线程数
    :param retries: 单页请求失败后的重试次数
    """

    self.page_size = page_size
    self.max_workers = max_workers
    self.retries = retries
    self.session = requests.Session()
    self.session.headers.update({
    "Accept": "application/json, text/plain, */*",
    "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
    "Origin": "https://www.jinglingshuju.com",
    "Referer": self.SITE_URL,
    # 保留抓包测试时使用的 User-Agent,不删减请求头。
    "user-agent": (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/149.0.0.0 Safari/537.36"
    ),
    "X-Requested-With": "XMLHttpRequest",
    })

    def _decrypt(self, ciphertext: str) > Dict[str, Any]:
    """Base64 密文 -> AES/ECB/PKCS7 解密 -> dict"""
    raw = base64.b64decode(ciphertext)
    cipher = AES.new(self.AES_KEY, AES.MODE_ECB)
    plaintext = unpad(cipher.decrypt(raw), AES.block_size).decode("utf-8")
    return json.loads(plaintext)

    def _parse_item(self, item: Dict[str, Any], page: int, index: int) > Dict[str, str]:
    """提取文章字段,统一输出结构。"""
    article_id = str(item.get("article_id") or "")
    detail_url = item.get("detail") or ""
    if not detail_url and article_id:
    detail_url = f"{self.NEWS_URL}/article/{article_id}"

    return {
    "page": str(page),
    "index": str(index),
    "title": item.get("title", ""),
    "publish_time": item.get("news_time") or item.get("time") or item.get("time_str", ""),
    "source": item.get("news_source", ""),
    "detail_url": detail_url,
    "article_id": article_id,
    }

    def fetch_page(self, page: int) > List[Dict[str, str]]:
    """请求单页、解密响应并返回结构化资讯列表。"""
    payload = {
    "page": str(page),
    "num": str(self.page_size),
    }

    for attempt in range(1, self.retries + 1):
    try:
    response = self.session.post(self.API_URL, data=payload, timeout=20)
    response.raise_for_status()

    result = response.json()
    if result.get("code") != 0:
    logger.warning("page={} 接口异常 code={} msg={}", page, result.get("code"), result.get("msg"))
    return []

    data = self._decrypt(result["data"])
    rows = data.get("list", [])
    articles = [self._parse_item(item, page, idx) for idx, item in enumerate(rows, 1)]
    logger.success("page={} 解密成功,获取 {} 条资讯", page, len(articles))
    return articles
    except Exception as exc:
    if attempt >= self.retries:
    raise
    logger.warning("page={} 第 {} 次请求失败,准备重试: {}", page, attempt, exc)
    time.sleep(0.5 * attempt)

    return []

    def run(self, pages: int = 3) > List[Dict[str, str]]:
    """并发采集前 pages 页资讯。"""
    logger.info("开始采集精灵数据资讯 | 共 {} 页 | 每页 {} 条 | 并发 {}", pages, self.page_size, self.max_workers)
    all_articles: List[Dict[str, str]] = []

    with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
    future_map = {executor.submit(self.fetch_page, page): page for page in range(1, pages + 1)}
    for future in as_completed(future_map):
    page = future_map[future]
    try:
    all_articles.extend(future.result())
    except Exception as exc:
    logger.error("page={} 采集失败: {}", page, exc)

    all_articles.sort(key=lambda row: (int(row["page"]), int(row["index"])))
    logger.info("采集完成,共 {} 条资讯", len(all_articles))
    return all_articles

    if __name__ == "__main__":
    spider = JinglingshujuArticlesSpider(page_size=20, max_workers=3, retries=3)
    articles = spider.run(pages=3)

    for idx, article in enumerate(articles, 1):
    logger.info(
    "[{}] page={} | {} | {} | {} | {}",
    idx,
    article["page"],
    article["publish_time"],
    article["source"],
    article["title"],
    article["detail_url"],
    )

    运行脚本后,日志会先输出每一页的解密状态,再按页码和页内顺序打印结构化结果。当前测试前 3 页共获取 60 条数据:

    三、总结

    这个案例整体难度不高,主要考察的是接口定位和响应体解密。请求参数本身是明文的,分页逻辑也很直观,真正需要处理的是响应中的 data 字段。

    分析时可以先通过 getNewsList 定位业务接口,再搜索 decrypt / AES 找到统一响应拦截器中的解密逻辑。确认密文入参与 Network 返回值一致后,就可以提取 Key、模式和填充方式,用 Python 复现。

    最终流程可以概括为:

    POST /Data/getNewsList
    → 返回 code/msg/data
    → data 为 Base64 形式的 AES 密文
    → AES/ECB/PKCS7 解密
    → json.loads 得到资讯列表

    本题没有复杂签名、动态参数或混淆控制流,适合作为 AES 响应解密类案例的入门练习。

    赞(0)
    未经允许不得转载:171主机测评 » 精灵数据资讯接口 —— AES/ECB/PKCS7 响应体解密
    分享到: 更多 (0)

    评论 抢沙发

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