某社区未授权获取阿里云 OSS STS 云凭证
前言
本次测试属于公益类型的 SRC
JavaScript 信息收集
起点页面 https://web.haojiao.cc/wiki/wiki_home/t2Ud5pOQlscKLbRC 返回的 HTML 只有 1589 字节,除了一堆 meta 就是一个空 div
1 | <html><head><meta charset="utf-8"> |
meta 已经把技术栈交代清楚:React + Redux + TypeScript + router5 + antd 4.21.6,SPA 外壳,真实逻辑全在 JS 里。响应头显示前面是 Tengine + 阿里云 CDN(Via: cache40.l2ea120-5、EagleId)
fdf61c7e604adccd8da1.js 只有 3921 字节,是 webpack 的运行时引导

重点分析生成 JS chunk URL 的代码部分
1 | d.src = function(e) { |
function(e) { ... }(e)传入当前要加载的 chunk id(变量e),返回完整的 JS 文件路径f.p = "/",所以所有 chunk 都从网站根目录开始加载"js/"表示 chunk 文件存放在/js/目录下
1 | ({ |
如果 chunk id 是 0、2、4,则使用对应的可读名称:
common、antv、echarts其他 chunk id(如 3、5、11…)没有命名,直接使用数字 id 作为文件名的一部分
最终生成 URL 格式:{公共路径}js/{chunk名称或id}.{哈希}.js
按名单批量下载,结果暴露了一个信息:大部分异步 chunk 已从服务器下线,只有三个还在
| 文件 | 大小 | 说明 |
|---|---|---|
js/fdf61c7e604adccd8da1.js |
3,921 B | webpack runtime + chunk 表 |
js/antd.c91e96c74fb0d99041de.js |
435,388 B | antd 组件库 |
js/vendors~app.20d8b078010b5c655dd3.js |
2,287,319 B | 第三方依赖(含埋点 SDK) |
js/app.00bef81e713d16ef6d72.js |
408,443 B | 业务代码主包(关键) |
js/common.0f9cc54654dc2e88a00c.js |
1,903,402 B | 公共模块 |
js/antv.179fc3d571fe8dc212c6.js |
1,772,902 B | 图表库 |
js/echarts.f39b212965ee769ac2a2.js |
1,067,439 B | 图表库 |
| chunk 3/5/6/7/11–18 | 1,589 B × 12 | 已下线,返回 index.html 兜底 |
在 app.*.js 里翻到一个 API 基址配置对象

1 | var a; |
后端是 api.haojiao.cc 上一个按服务名分区的网关
| 服务名 | 作用(据接口推断) |
|---|---|
wiki |
电竞百科主体:赛事 / 战队 / 选手 / 比赛 / 评分 / 预测 |
user_register_center |
注册、登录、发码、绑定、找回、注销 |
user_base_logic |
用户资料、徽章、JWT 签发 |
moment_new |
社区动态、帖子、评论、话题 |
message_center |
消息通知 |
config_center |
全站配置、OSS 凭证签发 |
cms_api |
内容管理 |
顺着这份地图,从 JS 里抽出 205 条接口路径
后端接口有鉴权,直接访问返回 403,回到 app.*.js 找请求拦截器
1 | // 请求拦截器:统一处理请求配置 |
追 p(),在同一个模块里找到定义

1 | // 生成自定义请求头:时间戳、随机数、OS、签名、版本 |
| 头 | 值 | 可预测性 |
|---|---|---|
x-hj-timestamp |
当前毫秒时间戳 | 完全可控 |
x-hj-nonce |
10 位随机字母数字 | 完全可控 |
x-hj-os |
固定 web |
常量 |
x-hj-sign |
SHA1(盐 + nonce + timestamp) |
盐已泄露 → 可算 |
x-hj-version |
前端版本号 | 常量 |
紧接着拦截器的响应处理分支
1 | // 响应拦截器:统一处理响应数据 |
| 项 | 值 |
|---|---|
| 密钥算法 | AES-CBC / PKCS7 |
| 密钥 | B-:9bzB8K%~q{Au?^>Pfl*)k(24 字节 → AES-192) |
| IV | 同一串,CryptoJS 实际只用前 16 字节 |
| 触发条件 | 响应 Content-Type 含 text/plain |
| 编码 | base64 |
把 salt 和 nonce/timestamp 按算法拼起来,第一次对照实验
1 | 不带签名头: GET /wiki/api/v1/game/menu → 403 (body 0 字节) |
拿到了密文响应解密得到明文
1 | {"code":200,"data":[{"game_id":"t2Ud5pOQlscKLbRC","game_name":"无畏契约", |
未授权访问
访问 /config_center/api/v2/global
1 | {"data_sum":"468e04b051a1bf7e38dd36b1923b8b62", |
桶名、endpoint、CDN 域、每个业务前缀,全部匿名可读
前端是这么调它的(t 表示「是否访客」)
1 | _ = function(scene, isVisitor){ |
即 上传时的场景名(enum: Default / Meme / Moment / MomentVideo / UserReport)经 p() 换算成一个数字 key 传过去
把 key 从小到大扫一遍
oss_auth_key |
HTTP | 结果 |
|---|---|---|
| (空) | 400 | 参数缺失 |
0 |
400 | 参数缺失(0 被当作零值) |
1 |
200 | 返回凭证 |
2 |
200 | 返回凭证 |
99999 |
200 | 返回凭证 |
abc |
400 | 类型错(ParseUint) |
-1 |
400 | 类型错(ParseUint) |
1.5 |
400 | 类型错(ParseUint) |
1、2、99999 都能拿到凭证 —— 说明服务端对这个 key 的校验很松
oss_auth_key=1 的响应(密文开头):
1 | 14RaEHcDpjpoW77hxylf/bguQB6MOrPps5O49WGk14OzP3g/j9abgdHQDACptjLXHrTmPR1X9xPOcX0f9KE4cnOCZyKhxMgnnkAXLOsexK9K8ayGfF88udBr... |
解密字段结构,三种凭证 全部返回:
1 | ['access_key_id', 'access_key_secret', 'security_token'] |

1 | 用户在前端做「上传」动作(发动态 / 传头像 / 传聊天文件 / 提交举报) |
