Почитав в сети подробности о нескольких обнародованных критических CVE, связанных с маршрутизаторами Asus, мы решили проанализировать уязвимую прошивку этих устройств и, быть может, написать подходящий эксплойт «n-day». В итоге в процессе поиска уязвимой части программы и написания эксплойта для получения возможности удалённого выполнения кода мы также обнаружили, что в реальных устройствах приписываемое названным уязвимостям свойство «Unauthenticated Remote» в зависимости от конфигурации устройства может не действовать.
▍ Вступление
Прошлый год стал очень бурным в плане безопасности IoT и роутеров. Взлому подверглось множество устройств, и в системе CVE было зарегистрировано множество уязвимостей. Ну а поскольку мы с @suidpit любим реверс-инжинирить IoT-устройства, а в отношении большинства новых уязвимостей ещё не публиковались подтверждающие их действительность детали, у нас была возможность применить подход CVE North Stars, разработанный clearbluejar.
Для этого мы выбрали следующие CVE, влияющие на различные роутеры Asus SOHO:
В описаниях этих CVE были сделаны достаточно громкие утверждения, но мы вспомнили несколько уязвимостей тех же устройств, обнародованных за несколько месяцев до этого (например, CVE-2023-35086), где описывалась другая строка формата в точно таком же сценарии:
«Неаутенцифицированный удалённый атакующий может эксплуатировать эту уязвимость без возможности выполнения произвольного кода».
Внимательно читайте приводимые утверждения, так как далее мы будем использовать их в качестве основы наших допущений.
Исходя из описания этих CVE, мы уже можем вывести некоторую интересную информацию, такую как подверженные им устройства и версии их прошивок. Перечисленные ниже версии содержат патчи для каждого устройства:
- Asus RT-AX55: 3.0.0.4.386_51948 или новее.
- Asus RT-AX56U_V2: 3.0.0.4.386_51948 или новее.
- Asus RT-AC86U: 3.0.0.4.386_51915 или новее.
Кроме того, мы можем выяснить, что уязвимостью предположительно является строка формата, и что ей подвержены модули set_iperf3_cli.cgi
, set_iperf3_srv.cgi
и apply.cgi
.
Поскольку у нас не было опыта работы с устройствами Asus, мы начали со скачивания уязвимой и исправленной версий прошивки с сайта производителя.
▍ Сравнение патчей с помощью BinDiff
Скачав прошивки, мы извлекли их с помощью Unblob.
В результате быстрого поиска с помощью find
/ripgrep
мы выяснили, что уязвимыми модулями являются не файлы CGI, как предполагалось, а скомпилированные функции внутри бинарника /usr/sbin/httpd
.
Затем мы загрузили новый и старый исполняемые файлы httpd
в Ghidra, проанализировали их и экспортировали нужную информацию командой BinExport
из BinDiff, чтобы сравнить патчи.
Цель сравнения — выделить изменения между уязвимой и пропатченой версиями, чтобы обнаружить какие-либо странности, а также появление или исчезновение некой функциональности.
При сравнении httpd
мы выявили ряд изменений, но ни одно из них не представляло для нашей задачи интереса. В частности, если взглянуть на обработчиков уязвимых модулей CGI, то мы увидим, что они вообще не менялись.
Интересно, что все они имеют общий паттерн — вводные данные функции notify_rc
не были фиксированными, а поступали из управляемого пользователем запроса JSON.
Функция notify_rc
прописана в /usr/lib/libshared.so
, и это объясняет, почему сравнение бинарника httpd
оказалось неэффективным.
При сравнении libshared.so
мы обнаружили кое-что интересное: в первых строчках функции notify_rc
был добавлен вызов новой функции с именем validate_rc_service
. В итоге мы сочли, что именно эта функция отвечает за патч уязвимости строки формата.
Она выполняет проверку синтаксиса JSON-поля rc_service
. Декомпилированный Ghidra код не так легко прочесть. По сути, эта функция возвращает 1
, если строка rc_service
содержит только цифры, буквы, пробелы или символы _
и ;
. В противном случае возвращается 0
.
Очевидно, что в нашей уязвимой прошивке мы можем эксплуатировать уязвимость строки формата, управляя тем, что окажется в поле rc_service
. У нас пока не было устройства, чтобы это подтвердить, но мы не хотели тратить время и деньги, опасаясь, что эта догадка окажется ложной. Почему бы не использовать эмуляцию!
▍ Эмуляция с помощью Qiling
Если вы нас знаете, то вам также наверняка известно, что мы любим Qiling, поэтому первым делом подумали: «А что, если попытаться сэмулировать прошивку с помощью Qiling и воспроизвести уязвимость в нём?»
При запуске в голом проекте Qiling процесс httpd
, к сожалению, даёт сбой и сообщает о разных ошибках.
В частности, дело в том, что устройства Asus используют память NVRAM для хранения множества конфигураций. Ребята из firmadyne написали библиотеку для эмуляции этого поведения, но нам не удалось заставить её работать, поэтому мы решили реализовать его сами в собственном скрипте Qiling.
Этот скрипт создаёт структуру в куче, после чего перехватывает все функции, используемые httpd
для записи/чтения NVRAM, перенаправляя их в эту самую структуру.
После этого нам осталось лишь исправить реализацию системных вызовов и хуков. Сделав это, мы смогли загружать веб-интерфейс роутера из своих браузеров.
Между делом мы также разобрали функции do_set_iperf3_srv_cgi
/do_set_iperf3_cli_cgi
, чтобы понять, какой ввод нужно отправлять в строке формата.
Как оказалось, для эксплуатации конечной точки set_iperf3_srv.cgi
достаточно следующего содержимого JSON:
{
'iperf3_svr_port': '8888',
'rc_service': '%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
}
После чего мы получили в консоли Qiling следующий вывод:
Теперь уязвимость строки формата была подтверждена, и мы знали, как активировать её через эмуляцию прошивки с помощью Qiling. Более того, мы знали, что исправление включало вызов validate_rc_message
в функции notify_rc
, экспортируемой общей библиотекой libshared.so
. С целью написать рабочий эксплойт «n-day» для реального устройства мы купили одно из таких устройств (Asus RT-AX55) и начали анализировать уязвимость, чтобы понять её причину и найти возможность управлять ей.
▍ Поиск причины
Поскольку исправление было добавлено в функцию notify_rc
, мы начали реверс-инжиниринг этой функции в старой, уязвимой версии.
Вот фрагмент её псевдокода:
int notify_rc(char *message)
{
// ...
pid = getpid();
psname(pid,proc_name,0x10);
pid = getpid();
cprintf("<rc_service> [i:%s] %d:notify_rc %sn",proc_name,pid,message);
pid = getpid();
logmessage_normal("rc_service","%s %d:notify_rc %s",proc_name,pid,message);
// ...
}
Похоже, эта функция отвечает за логирование сообщений из разных мест через один централизованный канал вывода.
Функция logmessage_normal
является частью той же библиотеки, и её код довольно просто поддаётся реверс-инжинирингу:
void logmessage_normal(char *logname, char *fmt, ...)
{
char buf [512];
va_list args;
va_start(args, fmt);
vsnprintf(buf,0x200,fmt_string,args);
openlog(logname,0,0);
syslog(0,buf); // буфер может управляться пользователем!
closelog();
va_end(args);
return;
}
И хотя Ghidra не может ✨автомагически✨ распознать список переменных этой функции, она является обёрткой вокруг syslog
и отвечает за открытие выбранного журнала, отправку в него сообщения и последующее закрытие.
Уязвимость находится в этой функции, а конкретно в использовании функции syslog
со строкой, которой может управлять атакующий. Чтобы понять почему, рассмотрим сигнатуру этой функции в мануале libc
:
void syslog(int priority, const char *format, ...);
Согласно сигнатуре, syslog
ожидает список аргументов, напоминающий семейство *printf
. В результате поиска мы поняли, что эта функция является известным местом появления уязвимостей строк формата.
▍ Эксплуатация — поиск необходимых средств на месте
Уязвимости строки формата весьма полезны для атакующих, и обычно предоставляют произвольные примитивы чтения/записи. В данном сценарии, поскольку вывод логируется в системный журнал, который видят только администраторы, мы предполагаем, что неаутентифицированный удалённый атакующий не должен иметь возможности его прочесть, а значит теряет в эксплойте примитив «чтения».
В ОС роутера включена функция ASLR (address space layout randomization, случайное распределение адресного пространства), и ниже показаны средства противодействия, реализуемые в исполняемом файле на этапе компиляции:
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)
Согласно этому сценарию, типичный способ разработки эксплойта заключается в нахождении подходящей для переписывания GOT (global offset table, глобальная таблица смещений), отыскивании функции, получающей управляемый пользователем ввод, и перенаправлении её в system
.
Тем не менее, следуя принципу «поиска нужных средств на месте», мы какое-то время искали другой подход, который бы не повреждал внутренности процесса, а задействовал уже реализованную в бинарнике логику для заполучения чего-то полезного (а именно, оболочки).
В исполняемом файле одной из первых целей поиска стало место вызова функции system
. Мы хотели найти удачные точки внедрения, куда можно было бы направить наш мощный примитив записи.
Среди множества полученных результатов особо интересным выглядел один фрагмент кода:
void sys_script(char *script)
{
int cmp;
char *pcVar1;
char buf [64];
char *cmd;
undefined4 local_10c;
snprintf(buf,0x40,"/tmp/%s",script);
cmp = strcmp(script,"syscmd.sh");
if (cmp == 0) {
if (SystemCmd[0] != '') {
snprintf((char *)&cmd,256,
"%s > /tmp/syscmd.log 2>&1 && echo 'XU6J03M6' >> /tmp/syscmd.log &n",SystemCmd);
system((char *)&cmd);
strlcpy(SystemCmd,&DAT_0007e451,0x80);
return;
}
f_write_string("/tmp/syscmd.log",&DAT_0007e451,0);
return;
}
// ...
}
Давайте вкратце прокомментируем этот код, чтобы понять его важные участки:
SystemCmd
— это глобальная переменная, которая содержит строку.sys_script
— при вызове с аргументомsyscmd.sh
передаёт в функциюsystem
команду, находящуюся вSystemCmd
, после чего снова обнуляет глобальную переменную.
Это место выглядит хорошей целью для эксплойта при условии, что мы в роли атакующих сможем:
- Переписать содержимое
SystemCmd
. - Активировать функцию
sys_script("syscmd.sh")
.
Первый пункт выполняется за счёт уязвимости строки формата: поскольку бинарник не является позиционно-независимым, адрес глобальной переменной SystemCmd
прописан в нём жёстко, и для записи в него нам не требуются утечки. В нашей уязвимой прошивке SystemCmd
находится в смещении 0x0f3ecc
.
Что касается пункта 2, то некоторые конечные точки веб-интерфейса используются для легитимного выполнения команд через функцию sys_script
. Эти конечные точки вызывают показанную ниже функцию ej_dump
при каждом выполнении GET-запроса:
int ej_dump(int eid,FILE *wp,int argc,char **argv)
{
// ...
ret = ejArgs(argc,argv,"%s %s",&file,&script);
if (ret < 2) {
fputs("Insufficient argsn",wp);
return -1;
}
ret = strcmp(script,"syscmd.sh");
if (ret == 0) {
sys_script(script);
}
// ...
}
Итак, после переписывания глобальной переменной SystemCmd
наш эксплойт будет активироваться простым посещением Main_Analysis_Content.asp
или Main_Netstat_Content.asp
.
▍ Оболочка для ваших размышлений
Мы избавим вас от подробного объяснения эксплуатации строк формата, просто помните, что с помощью спецификатора %n
в адрес, указанный его смещением, можно записывать количество прочитанных к моменту обнаружения %n
символов.
Как оказалось, перед нами стоял ряд ограничений, некоторые из которых были типичны для эксплойтов строк формата, а другие относились конкретно к нашему сценарию.
Первая проблема заключалась в том, что полезную нагрузку необходимо отправлять в объект JSON, поэтому нам нужно постараться не «сломать» тело JSON, иначе парсер выдаст ошибку. К счастью, можно использовать комбинацию сырых байтов, внедряемых в это тело (получаемое парсером), двойное кодирование (%25
вместо %
для внедрения спецификаторов формата) и закодировать с помощью UTF завершающий адрес нулевой байт (u0000
).
Вторая сложность была в том, что после декодирования наша полезная нагрузка сохранялась в строке С, поэтому нулевой байт завершал её рано. Это значит, что у нас может быть только один нулевой байт, и он должен находиться в конце строки формата.
Третья проблема заключалась в ограничении длины строки формата. Но её можно обойти, записывая несколько байтов разом с помощью %hn
.
Четвёртая загвоздка была в том, что в строке формата нашему вводу предшествует переменное число символов, которые %hn
будет считать и затем записывать в целевой адрес. Причина в том, что функция logmessage_normal
вызывается с аргументами в виде имени процесса (httpd
либо httpsd
) и pid
(от 1 до 5 символов).
Наконец, наша полезная нагрузка была готова, всё было идеально налажено, и пришло время выполнять эксплойт, чтобы захватить оболочку нашего устройства…
Подождите-ка, что???
▍ Быть или не быть аутентифицированным
Отправка полезной нагрузки без куки приводит к перенаправлению на страницу авторизации.
Мы были крайне шокированы. Про эти CVE сказано, что они эксплуатируются «неаутентифицированным удалённым атакующим», и наш эксплойт для их сэмулированной в Qiling версии прекрасно работал без какой-либо аутентификации. Что же пошло не так?
В ходе эмуляции ещё до покупки реального устройства мы скачали из интернета дамп состояния NVRAM. Если процесс httpd
загружал ключи, которые отсутствовали в дампе, мы автоматически устанавливали их на пустые строки, а некоторые в случае явного сбоя/segfault корректировали вручную.
Оказалось, что важный ключ x_Setting
определяет, настроен ли роутер. Исходя из этого, доступ к большинству конечных точек CGI предоставляется либо нет. В состоянии NVRAM, которое мы использовали в Qiling, x_Setting
был установлен на 0
, а на реальном устройстве (настроенном обычным образом) — на 1
.
Но и это ещё не всё!
Мы отыскали ранее обнародованные CVE строк формата, относящиеся к другим конечным точкам, чтобы протестировать их с нашей конфигурацией. Мы нашли в сети эксплойты, которые прописывали в заголовках Referer и Origin целевой хост, в то время, как другие вместо POST-запроса с телом JSON отправляли GET-запросы. Наконец, чтобы максимально точно воспроизвести их конфигурацию, мы даже сэмулировали прошивку других устройств (например, Asus RT-AX86U).
В итоге ни один эксплойт в среде c x_Setting=1
не сработал.
И знаете что? Если роутер настроен, WAN-интерфейс не предоставляется для удалённого доступа, что делает его недоступным для атакующих.
▍ Заключение
Проведённое исследование оставило неприятное послевкусие.
Мы выяснили, что наверняка:
- Есть дополнительная уязвимость для обхода аутентификации, которая до сих пор не исправлена, в связи с чем при сравнении не проявляется.
- Упомянутый в отчётах о CVE «неаутентифицированный удалённый атакующий» подразумевает сценарий с межсайтовой подделкой запросов.
- Все прежние исследователи находили эти уязвимости путём эмуляции прошивки без учёта содержимого NVRAM.
Как бы то ни было, мы опубликовали наш доказательный код эксплойта и скрипт эмулятора Qiling в репозитории GitHub.
Если вам известно о неисправленной уязвимости, позволяющей обходить аутентификацию (😉) в устройствах Asus, напишите нам в X (бывший Twitter) или BlueSky. Также хотелось бы получить поясняющие комментарии от представителей Asus и/или исследователей, сообщивших о CVE.
Автор: Bright_Translate