
Этот репозиторий содержит результаты моего исследования прошивок IPC/NVR от Tiandy, проведённого в августе 2020 года. Я обнаружил две уязвимости, которые можно использовать для удалённого восстановления пароля администратора и получения root-доступа к устройству.
Этот репозиторий содержит результаты моего исследования прошивок IPC/NVR от Tiandy, проведённого в августе 2020 года (эти устройства также продаются под брендом OMNY). Это «исследование» не было исчерпывающим, но я нашёл несколько способов удалённо восстановить пароль администратора, включить telnet и изменить пароль root.
Трудно сказать, какие именно версии затронуты, поскольку мы можем скачать только недавно выпущенные. Все эти загружаемые версии затронуты:
DVRS_V9.12.7.20200422
DVRS_V11.7.4.20200721
NVSS_V13.6.1.20200723
NVSS_V22.1.0.20200722
Существуют не только разные ветки прошивок для разных устройств, но и некоторые компоненты имеют собственные версии и обновляются отдельно, например веб-API, в котором я нашёл обход аутентификации, работающий только для версий, выпущенных с середины 2019 года, независимо от номера версии прошивки. Если вам известно больше о затронутых версиях, буду признателен за помощь.
Я публикую здесь полное раскрытие, но в разумных пределах. Патч от вендора (маловероятный, поскольку они не отвечают) не заставил бы проблему исчезнуть, особенно когда ни одно устройство в интернете не имеет последней прошивки (даже близко). Настоящая уязвимость заключается в том, что эти устройства открыты в интернет. И это то, что должны исправить конечные пользователи, а не Tiandy.
В качестве бонуса я также прилагаю распаковщик прошивки и немного информации о том, как получить доступ к потокам через RTSP/RTMP (удачи найти это в руководстве).
Сначала я представляю скрипты:
Затем я кратко объясняю, что делают эти скрипты и почему. Я не повторяю код, но стараюсь дать достаточно контекста, чтобы вы могли понять код:
Наконец, материал становится относительно техническим:
Вам понадобится Python 3 с PyCrypto.
Сначала попробуйте recover.py. Для этого требуется доступность порта 3001:
python3 recover.py [HOST]
если всё пройдёт хорошо, будут выведены учётные данные администратора.
Если этот порт недоступен, может сработать веб-порт. Для этого понадобится URL:
python3 cgi_recover.py http://123.45.67.89
python3 cgi_recover.py https://123.45.67.89
Если ничего из вышеперечисленного не работает, проверьте, включён ли telnet. Если он включён, вы можете получить root на устройстве напрямую, просто взломав этот хэш:
support:$1$$AErA9BQgLjrxTJB1748k71:501:501:Linux User,,,:/home/support:/bin/sh
(обязательно откройте PR, если вам действительно удастся его взломать :D)
В более старой прошивке NVR V7 вы можете выполнять команды напрямую:
python3 ftpupdate.py [host] [adminpass] '[cmd]'
но она не выводит результат. Чтобы упростить задачу, я включил этот сокращённый вариант:
python3 ftpupdate.py [host] [adminpass] adduser [username] [password]
Это добавит ещё одного пользователя с uid 0.
Сначала включите telnet с помощью:
python3 telnet.py [host] [adminpw]
или для недавних устройств:
python3 cgi_recover.py [host] telnet
Затем вы можете перезаписать /etc/passwd (предполагаю, вы знаете, как это работает). Сначала попробуйте filetransport.py (для NVR):
python3 filetransport.py [host] [adminpass] put /etc/passwd <[source_file]
это не даёт обратной связи, нужно проверить, попытавшись войти...
Для моделей IPC, где filetransport.py не работает, попробуйте upgrade_rw.py:
python3 upgrade_rw.py [url] [adminpass] /etc/passwd <[source_file]
(это тоже не даёт обратной связи)
Наконец, для ещё более новых устройств это можно сделать через веб-API:
python3 cgi_recover.py [url] write /etc/passwd <[source_file]
Если ни один из вышеперечисленных способов не сработал (проверьте, можете ли вы войти), повторите все эти методы, но перезапишите вместо этого /config/etc/passwd. В некоторых версиях прошивки /etc/passwd — это симлинк на него. Наконец, можно также попробовать перезаписать /tdfs/etc/passwd, но после этого, возможно, потребуется перезагрузка устройства; чтобы перезагрузить его, используйте:
python3 reboot.py [host] [adminpass]
Старая прошивка V7 (IPC и NVR), судя по всему, не затронута, но если у вас есть пароль администратора, для NVR существует аутентифицированная RCE (ftpupdate.py), а для IPC скрипт upgrade-rw.py может использоваться для перезаписи /etc/passwd.
В более поздних версиях прошивки NVR (V9 и V11) есть учётная запись по умолчанию, которая в сочетании с «пассивным» повышением привилегий позволяет восстановить пароль администратора. Затем мы можем перезаписать /etc/passwd с помощью filetransport.py.
Хотя в прошивке IPC нет учётной записи по умолчанию, появляется другой метод восстановления — метод PSW. Это механизм восстановления пароля, в котором нет вообще никакой безопасности. Он присутствует во всех загружаемых версиях прошивки, начиная с V9. В то время как filetransport.py работает только на NVR, upgrade_rw.py достигает той же цели на IPC, используя механизм обновления, так что мы всё равно можем получить root-доступ.
Прошивка 2019 года представляет ещё один вектор атаки — обход аутентификации через веб-API. Экспортировав файл конфигурации без аутентификации, мы можем восстановить пароль и подготовить пакет обновления для перезаписи произвольных файлов.
Что касается уязвимостей, их четыре:
Обратите внимание, что я не исследовал «облачные» функции, то есть возможность перечисления устройств и, соответственно, подключения к устройствам, не открытым в интернет (как это делается с устройствами Xiongmai).
В старых версиях telnet включён по умолчанию, и вот что мы можем найти в файле /etc/passwd:
support:$1$$AErA9BQgLjrxTJB1748k71:501:501:Linux User,,,:/home/support:/bin/sh
(пароль root обновляется динамически; кроме того, я не взломал этот хэш, так что пул-реквесты более чем приветствуются :D)
Пользователь support (на самом деле присутствующий во всех версиях прошивки) может показаться непривилегированным, но, разумеется, у него достаточно прав, чтобы прочитать пароль Admin и перезаписать доступные для записи всеми init-скрипты в /etc/init.d или даже создать новые :)
Концептуально метод очень прост. Мы просто отправляем пакет входа и читаем ответ. И всё, потому что ответ об успешном входе содержит учётные данные всех пользователей, независимо от наших привилегий. Хотя так было и в V7, практически этот метод стал полезен только после появления учётной записи по умолчанию в прошивке NVR. Неудаляемая учётная запись «Default» не имеет удалённых привилегий, так что с ней ничего нельзя сделать. Ну, разве что прочитать пароль администратора...
Хотя это звучит тривиально, реализовать было не так просто. Для связи используется собственный протокол, а пароли зашифрованы с помощью DES, но с перевёрнутыми битами (самым сложным было это понять), а ключ передаётся сервером. Поскольку деривации ключа нет, подслушивающий легко может расшифровать всё. Тем не менее, нужно ещё догадаться, что биты перевёрнуты, либо переписать всё с нуля...
Реализацию см. в функции recover_with_default в файле recover.py.
При анализе бинарника сложно не заметить этот механизм. Вся его цель —... сделать возможным восстановление пароля, и он действительно хорошо это делает. Слишком хорошо, я бы сказал...
Что здесь происходит? Я думаю, это должно было быть механизмом восстановления пароля, предположительно созданным для того, чтобы вендоры могли предоставить владельцам устройств способ восстанавливать свои пароли.
Можно предположить, что процесс должен был выглядеть так:
Всё это хорошо, но чего-то не хватает... где же безопасность? Как выясняется, её нет. Ничто не мешает нам отправить этот пакет, получить код безопасности и восстановить пароль любого доступного устройства.
Я нахожу это поразительным, потому что дело не в каком-то изъяне механизма, который сводит на нет безопасность. Безопасности просто нет вообще. Здесь нечего исправлять, но мне это также не кажется очевидным бэкдором. Механизм оставляет следы в журналах и имеет 3 различные схемы получения кода, каждая сложнее предыдущей. На его реализацию действительно потребовалось время...
Возвращаясь к методу: чтобы он сработал, устройство должно иметь привязанный телефон/email. Изучая более старую версию, я увидел, что сделать это может только администратор, но затем я нашёл обход. Удивительно, но в более новой прошивке этот обход больше не нужен, поскольку явно можно изменить email устройства без аутентификации. Вот, здесь, я думаю, и должен был быть бэкдор :)
С технической стороны этот механизм довольно сложен, и его труднее всего было реверсировать и переписать. Существует 3 версии этого механизма, каждая использует свой алгоритм получения кода безопасности. Помимо DES с перевёрнутыми битами, используется собственный шифр подстановки с жёстко заданным ключом. Но всё это напрасно, потому что Tiandy не сможет сделать симметричное шифрование с жёстко заданным ключом безопасным, как бы они ни старались.
Я заметил, что поскольку код безопасности меняется каждую минуту, существует вероятность, что исходный процесс может завершиться неудачей только из-за того, что код изменился между пакетом на шаге 3 и пакетом на шаге 5, независимо от того, как мало времени прошло между их отправкой. Я учёл это, поэтому мой скрипт повторяет процесс, если код оказывается недействительным.
Весь процесс, включая установку email (который нам не нужно контролировать), реализован в файле recover.py.
Он работает с более новыми версиями прошивки (2019 года и новее), у которых «современный» веб-интерфейс (тот, что с «картой». Мне нравится эта карта, хотя Австралия кажется немного искажённой).
Этот обход прост. Хотя большинство конечных точек API требуют аутентификации, есть некоторые исключения. Однако проверка того, нужно ли пропустить аутентификацию, и сопоставление того, какую конечную точку активировать, реализованы в разных местах. В большинстве случаев аутентификация пропускается, когда путь URL равен заданной строке, что безопасно.
Но в недавних версиях появляется ещё одно исключение, которое активируется, когда строки Record/DownLoad и ID= просто присутствуют где-то в пути URL.
Для конечных точек, где сопоставляется полный путь, это по-прежнему безопасно. Поскольку Security/users — одна из таких конечных точек, напрямую восстановить пароль мы не можем. К счастью для нас, конечная точка экспорта конфигурации выбирается путём проверки, начинается ли путь URL с заданной строки (с помощью strncmp), так что мы можем просто добавить эти строки к пути и экспортировать файл конфигурации.
Восстановление пароля с помощью этой уязвимости реализовано в cgi_recover.py.
Теоретически администратор может обновить прошивку, а прошивка не подписана и не зашифрована. Но действительно ли нам нужно готовить собственный пакет прошивки? Иногда нет. Иногда да, и я был достаточно безумен, чтобы реально это реализовать...
Если у вас есть пароль, вы можете использовать уязвимость внедрения команд — скрипт ftpupdate.py. Суть в том, что мы сообщаем устройству, что нужно получить обновление по ftp, и для этой задачи используется ftpget. Неудивительно, что наши параметры попадают прямо в функцию system().
В прошивке NVR бинарный протокол содержит команду FILETRANSPORT, которая делает ровно то, что написано. Собственно, больше сказать нечего, потому что можно просто скачать SDK и использовать ту же команду. Конечно, мне хотелось переписать её, поэтому, чтобы увидеть, как это работает, смотрите filetransport.py.
В моделях IPC этой команды нет, но, как я уже говорил, прошивку можно обновить. Хотя подготовка всей прошивки целиком непрактична, пакеты обновления Tiandy позволяют заменять отдельные файлы, а это именно то, что нам нужно (см. распаковку прошивки).
Но не всё так просто. Файловый формат «box» содержит некоторые метаданные, а затем массив файлов. Первый файл должен называться ProductModule и содержать соответствующие параметры устройства, иначе обновление не продолжится. Нужны не только значения этих параметров, но и сами параметры. Кроме того, проверяется и часть с метаданными (включая версию box-файла).
Собирать это вручную кажется непрактичным, но, к счастью, есть другой способ. Экспорт файлов конфигурации использует тот же формат «box», со всеми подходящими метаданными и включённым файлом ProductModule, за исключением поля типа обновления.
Мне удалось выяснить, как заполнить это поле, и тем самым реализовать этот процесс. Хотя этот механизм присутствует и в прошивке NVR, он работает не так же, но дальнейший анализ бессмысленен, поскольку у NVR есть команда FILETRANSPORT, описанная ранее.
Скрипт upgrade_rw.py использует процесс обновления. Однако название предполагает нечто большее... Это потому, что при экспорте файла конфигурации мы указываем, какие файлы экспортировать по имени, и, как несложно догадаться, подойдёт любой файл, так что мы можем скачать box, а затем прочитать этот файл с помощью того же кода, который используется для извлечения прошивки.
Как экспорт, так и обновление можно выполнить и через веб-API. В этом случае нельзя прочитать произвольные файлы. Мне всё равно хотелось реализовать это, потому что API кажется более стабильным, см. cgi_recover.py.
В моделях, поддерживающих FTP, возможно, можно внедрить shell-команды в пароль пользователя (когда выполняется команда, добавляющая этого пользователя, чтобы он мог войти по FTP).
Устройства Tiandy имеют открытый порт 3001. Это порт, необходимый для запуска не-веб-методов. У более новых моделей RTSP доступен на порту 9100 в дополнение к порту 554, а также есть RTMP на порту 1935. Модели IPC используют порт 8082 для ONVIF. HTTP и HTTPS работают на стандартных портах.
Устройства со старым веб-интерфейсом (использующим нашу любимую технологию ActiveX) содержат в своём HTTP-ответе одно из следующего:
<title>Net Video Browser</title>
tdvideo.css
Устройства с более новым веб-интерфейсом (на этот раз использующим... Flash) содержат в ответе следующее:
res/app-0.1.0.css
(заголовок Last-Modified любезно сообщает нам точную дату выпуска)
Мы также можем идентифицировать эти устройства по сертификату, хотя HTTPS включён не всегда. Для всех устройств используются только два сертификата, и их можно найти в скачанной прошивке, что делает пиннинг бесполезным.
Более старый:
C=CN, ST=Tianjin, O=Tiandy Tech, CN=dvr_ui
Более новый:
C=CN, ST=Tianjin, L=Tianjin, O=Tiandy Tech Ltd, CN=NetDevice
Кстати... поддержка HTTPS реализована через отдельный процесс stunnel. Это работает, но, как несложно догадаться, IP-адрес теряется в процессе, поэтому в журналах всегда указан 127.0.0.1.
Tiandy утверждает, что их устройства поддерживают RTSP и RTMP. Это здорово, но в руководстве нельзя найти, как на самом деле использовать эти протоколы, потому что там нет нужных URL RTSP и RTMP. К счастью, у меня есть эта информация как побочный продукт анализа, так что я могу поделиться ею.
Для NVR:
Для просмотра прямой трансляции канала C (начиная с 1) с типом потока S (1, 2, 3):
rtsp://username:password@host/C/S
Для IPC:
Для просмотра прямой трансляции с типом потока S:
rtsp://username:password@host/S
URL RTMP не так просты, потому что для аутентификации запроса требуется специальный хэш. Кроме того, RTMP позволяет воспроизводить записанный контент.
URL для прямой трансляции:
rtmp://host/live/C/S/authstring
где C — канал, S — тип потока.
URL для воспроизведения:
rtmp://host/vod/START-STOP/C/S/authstring
где и START, и STOP — метки времени Unix.
authstring вычисляется следующим образом:
base64("username:"+md5("username:password")+":unix_timestamp")
Инструмент rtmpauth.py может сгенерировать его:
python3 rtmpauth.py username password
Поскольку эта метка времени проверяется и разница не может превышать 2 дней, есть ограничения:
Обновления прошивки упакованы в проприетарный файловый формат «box», который не подписан и не зашифрован. С точки зрения распаковщика, этот формат на самом деле очень прост. Есть заголовок, который мы пропускаем, и массив файлов для распаковки, где каждый файл имеет заголовок фиксированного размера, содержащий имя файла и размер (дважды), а затем идут данные.
Инструмент unbox.py распаковывает файл в каталог, названный по имени box-файла, или в указанный каталог:
python3 unbox.py [box_file]
python3 unbox.py [box_file] [target_dir]
Этот инструмент должен быть безопасным в использовании (я написал эту строку, затем снова проверил инструмент и нашёл уязвимость... упс), потому что абсолютные пути превращаются в относительные, .. заменяется на __, и нет символических ссылок.
Иногда вам придётся запустить этот инструмент дважды, так как вы заметите, что файл внутри .box — это ещё один .box.