かの有名なキヌガワマサトさんのXSSチャレンジ。
XSS Challenge 2020-06
なんかのきっかけで見つけたけれど、さっぱりわからんくて放置してた。
けど、以下のツイートを見つけて、復習できた。
(8) おまどんさんはTwitterを使っています 「結局解けなかったやつ。解法めちゃ納得した ・同一オリジンであれば親ページ下のiframeにdocument.writeで書き込める ・長い文字列はiframeのnameに入れて文字数制限を回避 ・邪魔な "Invalid code:" は /**/ でコメントアウト」 / Twitter
参考Writeups
- (8) おまどんさんはTwitterを使っています 「結局解けなかったやつ。解法めちゃ納得した ・同一オリジンであれば親ページ下のiframeにdocument.writeで書き込める ・長い文字列はiframeのnameに入れて文字数制限を回避 ・邪魔な "Invalid code:" は /**/ でコメントアウト」 / Twitter
- たぶんこのツイートが無ければ理解できなかった。観点をまとめてくれてて感謝
- 公式解法(XSS発動します。Alertだけだけど)
- 必要最小で分かりやすい。さすがです。
- XSS Challenge 2020-06 | Solution(XSS発動します。Alertだけだけど)
- 唯一見つけた、文章で書かれたWriteup
攻撃の流れ
- Frame1, Frame2を用意して、Frame1には普通の攻撃先サイトを読み込んで、Frame2は空にしておく
- Frame2に対してcodeパラメタを使って、その先のJSONPっぽいやつを呼び出す
- 呼び出した結果、Frame1に文字列が書き込まれる
- 全部書き込み切ったらFrame1でスクリプト発動。Frame2のnameに入っているJSコードを実行して、Alert読み出し達成
テク抽出と疑問解消(できてないのもあるけど)
XFS: Cross-Frame Scripting
同じオリジンであれば、親サイトが別オリジンであってもフレーム間通信ができる。
同じようなテクを使う問題をこの前見た。
Chaining No impact(N/A) Bugs to get High impact
このテクの名前ってXFSで正しいんかな?
公式解説のHTMLでは、以下のようにフレームを2つ用意して、フレームYでXSS(XSSではないのかも)を起こして、その結果としてフレームXに書き込みをしている。
<iframe src="https://vulnerabledoma.in/xss_2020-06/" name="x" onload="go()"></iframe> <iframe id="y" name="alert(document.domain)"></iframe>
JSONPっぽいURLの生成方法に弱点
ソースコードでは以下のようにURLを作成している。
s.src = `/xss_2020-06/check_code.php?callback=callback&code=${encodeURI(code)}`;
公式解説では、これのcodeに[payload]&callback=top.x.document.write
をInjectionしている。
2つ弱点を利用している。
- encodeURIはエスケープとして不適切
- escape と encodeURI と encodeURIComponent を正しく使い分ける
- ここに説明があるが、encodeURIは
&
はエスケープされない
- GET parameterは複数回指定することで上書き可能
- なるほど。今回は最後に入力したものが採用されるみたいだけど、確か実装依存。前調べたときのメモには以下のように書いてあった
?username=abc&username=efg
$_GET["username"]
->efg
new URL(location).searchParams.get("username")
->abc
document.writeを使ったコード追記
これは初めて見た。
公式解説では、↑のテクを使って、フレームXに対して以下をdocument.writeしている。
await loadIframe("<script>/*"); await loadIframe("*/eval(/*"); await loadIframe("*/top[1]/*"); await loadIframe("*/.name)//"); await loadIframe("<\/script>");
これの結果、フレームXには以下のように書き込まれる。
<body>Invalid code: '<script>/*'Invalid code: '*/eval(/*'Invalid code: '*/top[1]/*'Invalid code: '*/.name)//'Invalid code: '</script>'</body>
コメントが色々挟まってるのはcheck_code.php
からの応答にInvalid codeが入るから。
仮にこれが完成して、実行されれば、XSSが成立しそうな感じがする。
- なぜ小分けにして書き込んでいる?
- 今回の問題特有であるが、
check_code.php
のcodeには文字数制限があるので、小分けにしている
- 今回の問題特有であるが、
document.write
はクリアして書き込みするんじゃない?- Document.write() - Web API | MDN
- 恐らくだけど、初回呼び出し時は自動でdocument.openが呼び出されてクリアされるけど、closeしてないので、次回以降は追記になる
- なんで書き込み後に実行される?
- 色々な記事を見つけたけれど、まあ、再レンダリングされて実行されるんだろう。とても複雑そうな話に見えた
- 【翻訳】document.writeでSCRIPTを書き出すなやで! - MOL
- script タグ を document.write したときの実行順序【JavaScript】 | Design Hack and Slash
- 完璧に揃ったら実行してくれるんでしょう(よくわからん)
XFSでは同じオリジンであれば、フレームのnameを取得可能
個人的にはここが一番直感に反するのだが、XFSでフレームの中身を同じオリジンなら相互参照可能なのは、まあ、分かる。
しかし、そのフレームを定義している別オリジンのサイトのフレームのnameは参照可能であるようだ。
非直感といえど、フレーム間参照時にtop.x.document.write
みたいに、別オリジンの構造を思いっきり使ってるんだけれど、
どういう風に分別しているんだろう…
最終的にはalert(document.domain)
を実行したいので、これをnameに入れておいて、
<script>eval(top[1].name)</script>
を実行する。
- nameから引っ張ってこなくても、document.writeを使ったコード追記で直接alert書き込めばいいのでは?
alert(document/**/.domain);
のようにちょうどいい所にコメントが入るのはちゃんと動くけどalert(docum/**/ent.domain);
のようにキーワードをぶった切る場合は動かない- documentが最も長いキーワードであるが、文字数制限があり、これを一回では書けないので直接は無理
他
- XSS Challenge 2020-06 | Solution(XSS発動します。Alertだけだけど)
- こっちもほぼ同じ解法
- 最後を自動呼出しじゃなくて、明示的に呼び出しているくらいの違い