В коде оператора есть комментарий, который обесценивает месяц его собственной работы. Он посчитал сам и записал результат прямо в исходник: 89,4 % живых серийных номеров отдают открытый канал, не требующий аутентификации вообще. Записал – и продолжил перебирать пароли.
С 17 июня по 22 июля 2026 года один человек собрал больше 14 530 камер Dahua. Тремя путями сразу: перебором учёток по порту 37777, связкой из двух байпасов авторизации образца 2021 года и облачным релеем самой Dahua, которому от камеры нужен только серийный номер. Подтверждённые компрометации легли в Украину и Россию, с перевесом в сторону Украины.
Известно это с такой точностью не потому, что кто-то поймал его трафик. 23 июля он поднял в собственном домашнем каталоге python3 -m http.server и отдал наружу 407 мегабайт рабочего окружения: тулкит, логи, историю шелла, записи кампании.
Читать это можно двумя способами. Как отчёт о взломе четырнадцати тысяч камер – и как разбор чужого инженерного решения, в котором лучше всего сделана ровно та часть, которая была нужна меньше всего.
Первое, что оператор сделал на машине 17 июня, – измерил аплинк. Спидтест, и сразу за ним настройка скорости masscan: он выяснял, сколько пакетов в секунду вытянет канал, прежде чем вбивать цифру в сканер. Мелочь, но она сразу задаёт уровень – так не поступает человек, запускающий чужой скрипт с дефолтами.
Сама машина новой не была. История SSH-отпечатков на 154.86.119.60 показывает три поколения системы: Ubuntu 22.04 с апреля по май 2025 года, короткий эпизод Ubuntu 24.04 в июне 2026-го на нестандартном порту 20001 и Debian 13 с 18 июня 2026 года. Порт 80 на этом адресе открыт с мая 2022-го, за четыре года до кампании. Инфраструктуру под операцию не поднимали – её перепрофилировали, и первый слой её жизни к камерам отношения не имеет.
Цель сканирования – TCP/37777, порт бинарного протокола управления Easy4IP. Список целей оператор не составлял: файл cidrs.txt в его каталоге оказался переименованной ru-aggregated.zone от IPDeny. Первая фаза шла по российскому адресному пространству на десяти миллионах пакетов в секунду, вторая – по всему IPv4 на скорости пониже. Параметры сохранил чекпойнт-файл masscan:
seed = 9870541109226383968
rate = 10000000.000000
adapter-ip = 154.86.119.60
adapter-port = 57280-57296
ports = 37777
range = 0.0.0.0-255.255.255.254
Строка adapter-ip здесь весит больше остальных: она подтверждает, что хост был живым источником сканирования, а не пассивным коллектором чужих результатов.
Перед тем как трогать очередной блок CIDR, отдельный модуль брал из него один адрес, определял местное время и, если оно выпадало из окна с 09:00 до 16:59, блок пропускался целиком.
Расчёт читается сразу. Камера в три часа ночи отдаёт чёрный прямоугольник, а место на диске, время сканера и трафик тратятся на него ровно так же, как на дневной кадр. Та же мысль продолжена на выходе: после снятия картинки работает второй фильтр, выбрасывающий тёмное и лишённое деталей.
У решения есть и вторая сторона, и Hunt прямо отказывается выбирать за оператора, какая из них была мотивом: сканирование, идущее только в рабочие часы, растворяется в дневном трафике лучше, чем ночной всплеск на одном порту. Код различить эти два намерения не позволяет – в нём нет ни строки про шум. Гадать не будем и мы; отметим только, что решение работает в обе стороны одновременно, а такое обычно получается у человека, который считает стоимость, а не только выхлоп.
Параллельно с перебором адресов оператор собирал вторую валюту – серийники. Двенадцатью запросами к Shodan и чтением DDNS-поддоменов, не касаясь при этом ни одной камеры.
SHODAN_QUERIES = [
'hostname:".dahuaddns.com" "Serial Number"',
'hostname:".quickddns.com" "Serial Number"',
'hostname:".dahuaddns.com" port:37777',
'hostname:".quickddns.com" port:37777',
]
SHODAN_P2P_QUERIES = [
'product:"Dahua" "Serial Number"',
'"Dahua Technology" port:37777',
'http.html:"Serial Number" "Dahua"',
'http.title:"WEB SERVICE" "Dahua"',
]
Механика бесплатная и полностью на стороне вендора: камера, зарегистрированная в облаке Dahua, сама поднимает себе поддомен вида {серийник}.dahuaddns.com. Достаточно перечислить поддомены и вычитать из них идентификаторы – ни один пакет к устройствам при этом не уходит, и на стороне жертвы не остаётся ничего.
Зрелым этот список на момент краула не был. Из всех накопленных префиксов живые пробы имелись у двух: AH1058FPAG шёл с оценкой 0,0055, унаследованной от более раннего прогона, нашедшего 2246 живых устройств, а 5E00107PAJ получил ноль на тридцати двух пробах. Это оперативный срез в середине работы, а не готовая база целей.
Третий канал разведки оператор не писал вовсе – взял чужой CCTV-Scanner, который не эксплуатирует ничего: опознаёт Dahua и Hikvision по заголовку страницы и щупает RTSP, 37777 и хиквижновский 8000. В конвейере он стоит раньше всех собственных инструментов.
18 июня в дело пошёл перебор. Движок – asyncio-фреймворк, публично доступный под именем asleep_scanner, и копия оператора ссылается на исходный репозиторий из собственного README.
По коду сплошь идут русские комментарии, и написаны они в характерном регистре: человек документирует не замысел, а собственные грабли. Перед тем как открыть хоть одно соединение, тулза поднимает лимиты файловых дескрипторов и conntrack прямо из кода, а рядом стоит пояснение – без этого тысячи соединений молча роняет ядро. Это не комментарий для читателя. Это запись об отказе, который уже случился в проде и стоил ему прогона.
Конкурентность адаптивная: 300 воркеров по умолчанию, автоматический разгон до потолка в 4000 на списках длиннее миллиона хостов. И вот решение, которое здесь надо назвать сильным вслух: очередь ограниченная, завершение – по одному часовому на каждого воркера. Память при этом остаётся O(concurrency), а не O(hosts).
Разница не косметическая. Очевидная альтернатива – загрузить список целей в память и раздавать воркерам из него – на списке в миллион с лишним адресов превращает сканер в машину, которая либо не стартует, либо умирает на середине, а гонка при завершении оставляет недописанные результаты, и найденное придётся искать заново. Заплатил он за это чуть более сложным путём остановки: один часовой на воркера вместо простого флага. Для инструмента, которому предстоит месяц молотить интернет, размен верный. Так не собирает скрипты человек, который взял первый ответ из поисковой выдачи.
Протокол он тоже не взял из библиотеки. dahua.py реализует бинарный Easy4IP на TCP/37777 из реверснутой структуры пакета: опкод 0xA0 – логин, 0xA8 – перечисление каналов, 0x11 – снятие кадра, 0xA4 – общий запрос, различаемый по байту субкода.
Важнее опкодов то, что движок делает с ответом. Читая пару 0x01/0x04, он опознаёт срабатывание блокировки и бросает хост после пяти неудач, вместо того чтобы выжигать учётку до конца.
Это второе решение, которое стоит назвать сильным, и цена у него считается точно. Альтернатива очевидна – долбить дальше, пока список паролей не кончится. Заплатил бы он тем, что камера уходит в лок и перестаёт быть достижимой вообще: ни подбором, ни чем-то ещё. А CVE-проход, который случится только 12 июля, три недели спустя, придёт как раз на выживших. То есть он берёг цель для инструмента, которого у него на тот момент ещё не было в работе. Это не осторожность, это планирование.
Кадры на выходе фильтруются двумя признаками: средней яркостью и плотностью границ по Канни. Тонкость сидит в условии – картинка выбрасывается, только если она одновременно тёмная и лишённая деталей. Тёмная, но детализированная остаётся. Он отбирал не работающие камеры, а сцены с содержимым, и это разные задачи.
Каждое попадание, пережившее фильтр, немедленно улетает в Telegram. В шаблоне уведомления зашита ссылка на сообщество во «ВКонтакте» – она уходит с каждым сообщением, в каждый канал, всю кампанию напролёт. Идентификатор, приклеенный к операции добровольно и навсегда.
Вторая деталь того же файла обошлась дороже. В bot.py на 147-й строке стоит delete=False, и из-за этого очистка через rmtree() не отрабатывает ни разу. Каждый снятый JPEG остаётся лежать на диске рядом с учёткой, к которой относится.
Здесь стоит посчитать. Из 407 мегабайт, уехавших к Hunt, 371 – каталог логов брутфорсера. Девяносто один процент корпуса улик, по которому восстановлена вся кампания целиком, – это ровно то, что должно было удаляться и не удалялось из-за одного дефолтного значения флага.
Альтернатива стоила одного символа: дать rmtree() отработать и оставить единственную копию в телеграм-канале, который он и так читает. Измерение здесь не абстрактная «наблюдаемость», а мегабайты: без этих 371 краул открытого каталога дал бы тулкит и историю шелла, но не географию, не хронологию попаданий и не статистику прогонов. Дальше видно, что человек, аккуратно считавший память в очереди воркеров, ни разу не посчитал, сколько накапливает его собственный лог.
Логи покрывают 24 прогона с 17 июня по 13 июля. Больше трети вернули ноль: девять прогонов не дали ни одного устройства, причём пять из них уложились в одну сорокаминутную сессию по одному и тому же списку. По двенадцати результативным Hunt оценивает добычу примерно в 13 000 учётных записей устройств.
Стабильности нет никакой, и это видно на одном вечере: три подряд попытки по идентичному списку примерно на 55 000 хостов дали ноль, затем 0,044 %, затем 0,38 %. Один и тот же список, один и тот же инструмент, разброс почти на порядок.
География плавала тоже. Крупнейший единичный улов 18 июня лёг в мексиканские и вьетнамские потребительские диапазоны, а к концу июня доминировать стали российские и снговские телеком-блоки. И популяция оказалась шире, чем «камеры»: домофоны, программный NVR-клиент на обычном ПК, отпечаток которого не отличается от железного устройства, и целый ряд OEM-перемаркировок – RVi, ST-XVR, QVC, AC-D, SNR, все говорящие тем же самым протоколом.
Сборка у него при этом своя, разошедшаяся с публичным проектом. README документирует инвертированный дефолт снятия кадра и умалчивает о двух флагах производительности, которые в коде есть. Три из четырёх определённых опкодов Dahua – мёртвый код, конвейер их не вызывает: их вытеснила более лёгкая асинхронная реализация. А у самых крупных ранних уловов отсутствуют экспорты в SMART PSS, и правдоподобнее всего потому, что этот шаг не имел обработки ошибок и молча падал на больших результатах, пока в начале июля не был починен.
Про экспорт стоит сказать отдельно, потому что он торчит из чисто технической истории в сторону денег. export.py пакует попадания в XML под SMART PSS – собственную корпоративную платформу управления камерами от Dahua. Разбиение по 255 устройств на файл – это её же потолок на импорт, то есть выход собран так, чтобы грузиться в вендорский продукт без единой доработки: 13 229 записей превратились в 52 файла. Hunt честно оговаривает, что формат сам по себе не различает личное использование и передачу – выгодны оба сценария.
Перебор достаёт только тех, у кого пароль слабый. 12 июля оператор занёс инструмент, которому пароль не нужен вообще.
p2pwn – скомпилированный Go-бинарь, и в его конфигурации включены ровно две вещи: два байпаса авторизации и установка бэкдора. Релея среди его методов нет, вопреки названию: это отдельный тулкит с собственным запуском и собственной историей разработки. Два пути структурно разные, а не опции одного инструмента, и путать их значит потерять половину картины.
CVE-2021-33044 бьёт в безусловное доверие к клиенту, представившемуся аппаратной клавиатурой NetKeyboard: если clientType равен NetKeyboard, поле пароля прошивка просто не вычисляет. На его месте едет литеральная строка Not Used.
CVE-2021-33045 бьёт в другое место – прошивка читает адрес источника из тела запроса, а не из TCP-соединения. Клиент объявляет себя пришедшим с 127.0.0.1, и прошивка верит на слово.
Детект-правило Hunt формулирует через эти же поля: в легитимном трафике клиентов Dahua ни того, ни другого не встречается.
{"clientType": "NetKeyboard"}
{"loginType": "Loopback", "ipAddr": "127.0.0.1"}
Обе возвращают полную административную сессию без аутентификации и отрабатывают меньше чем за секунду. Обе закрыты Dahua в октябре 2021 года адвайзори SA-2021-0130. То есть свежих дыр в этой кампании нет ни одной – есть пять лет непоставленных прошивок.
После любого из двух байпасов p2pwn заводит на камере учётку p2pwn с паролем p2password, и делает это по RPC. Хранится она независимо от админского пароля, поэтому его смену переживает, а на большинстве прошивок, по данным Hunt, переживает и сброс к заводским настройкам.
Оговорку здесь надо поставить честно. Публичный репозиторий p2pwn подтверждает наблюдаемое: инструмент принимает серийники Dahua, проверяет обе CVE и несёт конфигурацию с дефолтной учёткой. Но ни счёт в 1923 камеры, ни живучесть после factory reset из репозитория не следуют – это утверждение лаборатории, снятое с корпуса оператора, и на нём оно и держится.
Отдельная история – как эта техника подписана в самом инструменте. p2pwn вешает на неё ярлык CVE-2024-39943, который в действительности принадлежит command injection в Rejetto HTTP File Server: другой вендор, другое семейство продуктов, ничего общего. Второй ярлык, CVE-2025-31702, унаследован от авторов PoC, лёгшего в основу релейного компонента, и описывает совсем узкую вещь – пост-авторизационное повышение привилегий, CWE-732, CVSS 6.8, которого в KEV нет и близко. Техники при этом рабочие обе, врут только ярлыки. Полезное наблюдение для тех, кто строит детект по номерам CVE, вычитанным из чужого тулкита.
Более поздний краул достал исходники на Go, и картина усложнилась. p2pwn – не один эксплойт, а три поверхности атаки, мультиплексированные поверх одного туннеля: сырой канал SDK для байпасов и посадки бэкдора, HTTP-канал CGI, проксирующий во встроенный веб-сервер устройства, и консольный канал JSON-RPC, имитирующий внутренний протокол NetKeyboard.
Третий выходит за пределы атакуемой камеры. После логина он выполняет OnvifUser -u и снимает ONVIF-учётки – те самые, которые камера закешировала, чтобы разговаривать с видеорегистратором. Это уже сбор кредов вбок, за границу устройства, и оператор получает его бесплатным приложением к байпасу.
И деталь, связывающая два его инструмента в один узел: SDK-креды, зашитые в этом исходнике, побайтово совпадают с теми, что лежат в релейном тулките. Обе кодовые базы черпают из одного и того же восстановленного материала Dahua, а не собраны независимо.
Собирал он тоже в двух местах: из распакованного архива в /root и через git clone в /var/xdwd/. Второй каталог лежит за пределами раздаваемого веб-рута и краулу невидим – единственное место во всей истории, где оператор о разделении подумал.
Тринадцать запусков p2pwn оставили одиннадцать папок, доживших до краула: RU, RU2, RU3, RUNODIN, UA, UA1, UA2, UA4, UA5, dom и 123. Имена наклеены задним числом, по тому, куда легли скомпрометированные устройства: сам скан различается только серийным префиксом и никакой географии не знает.
Перевес украинский и заметный. UA5 – около 690 устройств, UA2 – около 640, UA – 301; в выдаче UA5 чаще всего попадается модель DH-IPC-K35P. Папка dom – холостой прогон, ноль. UA4 несёт метку CGI Brute, которой больше нет нигде: там отработал обычный HTTP-перебор в обход CVE-пути. UA3 и финальный сводный прогон в истории шелла присутствуют, а на диске их нет – провалились они или были подчищены, по корпусу не определить. Итого около 1900 компрометаций за девятнадцать дней.
Одна закономерность в этих папках заслуживает того, чтобы её вынести отдельно, потому что она говорит не об операторе, а о защищающейся стороне. Несловарный пароль I0949488055 повторяется на одиннадцати разных устройствах и адресах в одном только прогоне UA; VIT0662082161 и 01131418s – по десять раз каждый. Подбором такое не берётся: это не словарь, это выглядит как один монтажник, поставивший один и тот же кастомный дефолт на множестве независимых объектов. В словаре оператора эта строка уже лежала и совпала сразу, без итераций. Смена «admin/admin» на что-то своё защищает ровно до того момента, пока это своё ставится с одного листочка на всю клиентскую базу.
Оба пройденных пути требуют маршрутизируемого адреса. Третий отказывается от этого допущения целиком.
Облачный релей Dahua, easy4ipcloud[.]com:8800, существует ровно для того, чтобы приложение дотянулось до камеры за NAT по одному серийному номеру. Аутентификация к релею опирается на креды, зашитые одинаково в каждый когда-либо выпущенный клиент Dahua: предъявил их – и ты для облака доверенное приложение вендора.
Дальше начинается протокол, который в отчёте разобран до байта, потому что на том же сервере лежал независимо добытый исследовательский проект по PTCP. Каждый пакет несёт фиксированный 24-байтовый заголовок: ASCII-магия PTCP и четыре 32-битных поля с порядком байтов от старшего – отправлено, принято, идентификатор пакета и пара message-ID для подтверждений. Однобайтовое поле типа выбирает назначение: 0x00 SYN, 0x10 туннелированные данные, 0x11 запрос bind, 0x12 уведомление о соединении, 0x13 heartbeat, а 0x19, 0x1a и 0x1b образуют обмен аутентификации.
Единого сервера за этим релеем нет. В логе живого тестирования оператора восемь серийников одного префикса развели по семи разным бэкендам:
[5E00107PAJ01FFF] P2P SERVER - 165.154.164[.]157:8803
[5E00107PAJ02C6E] P2P SERVER - 165.154.165[.]25:8803
[5E00107PAJ06CD2] P2P SERVER - 165.154.165[.]24:8803
[5E00107PAJ0892A] P2P SERVER - 128.14.224[.]137:8803
Клиент сначала спрашивает у облачного брокера, какой P2P-сервер обслуживает данный серийник, затем идёт на релей, затем на агентский сервер за сессионным токеном – и только после этого пытается достучаться до устройства. Пробивает NAT он техникой, которую восстановленный диссектор Wireshark подписывает как Inverted STUN: пакет формы STUN, у которого перед отправкой инвертирован каждый бит.
Кадр 0x11 всегда адресован 127.0.0.1 самого устройства. Он не открывает путь снаружи – он велит камере соединиться самой с собой на заданном порту и вернуть полученные байты. Именно поэтому одна реализация туннелирует RTSP на 554, а другая правкой в одну строку – управляющий порт: тулкит оператора и есть такой форк, биндящий 37777 вместо 554 и проталкивающий в туннель собственноручно собранный пакет логина.
Модель доверия здесь перевёрнута относительно интуиции. Устройство не аутентифицирует того, кто к нему пришёл. Оно аутентифицирует сессию – по токену, который облако выдало ещё до того, как до устройства кто-либо дотянулся. Единственный настоящий барьер на этом пути – знать серийный номер, ровно то, что конвейер разведки и производит пачками.
Строго говоря, дотянуться не значит взять под контроль: релей открывает путь к управляющему интерфейсу, а дальше всё ещё нужны либо валидные креды, либо один из двух байпасов, и Dahua это разграничение подтверждает прямо. Вот только по логам самого оператора большинство выставленных устройств не требовало ни того, ни другого: девять из десяти живых серийников отвечали открытым каналом. Масштаб этой кампании держится не на взломе паролей, а на том, что пароля никто не спросил.
В этом протоколе работают три разных криптографических механизма, и смешивать их значит переврать устройство системы.
Первый – фиксированный ключ AES-256-OFB с фиксированным IV, та самая пара, что вшита в каждый клиент Dahua. Расшифровывает она ровно одну вещь: JSON-блоб, который облако возвращает с солью конкретного устройства. Второй – сессионный ключ AES-256-OFB, выведенный через PBKDF2-HMAC-SHA256 с двадцатью тысячами итераций поверх учётных данных устройства; он шифрует метаданные соединения и включается только там, где аутентификация устройства действительно требуется. Третий – WSSE-HMAC поверх тех же фиксированных кредов, которым подписывается каждый запрос к облачному брокеру.
Ни один из трёх не защищает туннелированные данные приложения. После установления соединения они идут обычными TCP-байтами, завёрнутыми в кадры PTCP.
Здесь суждение адресуется не оператору, а вендору, потому что это проектное решение Dahua, а не чей-то тактический выбор. Двадцать тысяч итераций PBKDF2 – дорогая, добросовестно сделанная деталь: она стоит вычислительного времени на каждом соединении и покупает устойчивость к перебору вывода ключа. Стоит эта деталь непосредственно за идентичностью, которая роздана всем: логин приложения к релею одинаков в каждом выпущенном бинаре, и достаётся он вскрытием любого клиента, включая мобильное приложение из магазина. Асимметрия ядра и обвязки в чистом виде – дорогое ядро за дверью, у которой ключ висит на гвозде снаружи.
Альтернатива на этом месте существует и не экзотична: идентичность приложения на инсталляцию или на устройство, выдаваемая при регистрации в облаке, а не компилируемая в клиент. Цена – реестр идентичностей на стороне вендора и процедура их отзыва, то есть работающая PKI вместо константы в бинаре. Дорого. Ровно на эту сумму сегодня и застрахованы все камеры, до которых можно дотянуться по серийному номеру, а таких, по логам оператора, девять из десяти. И признак того, что это именно вендорский материал, а не операторская самоделка: тот же AES-набор независимо всплыл во втором, никак не связанном исследовательском проекте на том же сервере.
Дальше конструкция становится интереснее, чем просто «дотянуться до камеры».
Скрипт asfefwq.py из каталога seria2 воспроизводит офлайн часть собственной процедуры восстановления доступа Dahua. Он генерирует кандидатов в серийные номера, проверяет каждого на живость через облако и для каждого живого выводит код восстановления.
Порядок здесь важнее самого факта. Код генерируется только для серийника, прошедшего проверку живости; мёртвые кандидаты записываются как просканированные и выбрасываются, кода не получая никогда. Оператор строит не спекулятивный список, а проверенный инвентарь. Вывод раскладывается по датированным каталогам на каждый префикс: append-only список серийников, JSON-карта кодов и XML с учётками, пакуемые по шестьдесят четыре штуки.
И главное свойство: вывод кода полностью офлайновый и детерминированный. Имея серийный номер, код получают без единого обращения к устройству. Именно это делает результат передаваемым – каждая запись погашается через штатную процедуру восстановления пароля Dahua, и знать текущие креды для этого не нужно.
Отсюда следует вывод, который Hunt делает со средней уверенностью и который стоит держать именно в этой формулировке: конструкция похожа на построенную для передачи доступа третьей стороне. Подпирают эту версию две вещи – передаваемость кодов и экспортный конвейер под вендорскую платформу. До подтверждённой коммерческой операции доказательства не дотягивают, и лаборатория это оговаривает сама.
От себя добавим наблюдение, которое версию усиливает от противного. Брутфорс-движок при каждом попадании стучит в Telegram. Здесь исследователи специально искали такой же шаг выгрузки в Telegram или Discord – и не нашли ни одного: все сетевые вызовы этого инструмента read-only, проверка живости и ничего больше, весь вывод остаётся на диске. Инструмент, который производит быстрые попадания, кричит о них немедленно; инструмент, который производит актив, молчит и копит. Захардкоженный заголовок консоли CCTV Scanner | discord.gg/cctv при этом никуда не ведёт – это статичный баннер, выдающий происхождение тулзы из сообщества по эксплуатации Dahua, а не рабочий канал.
Рядом лежит русскоязычный список целей, раскладывающий модели Dahua и OEM-перемаркировки по серийным префиксам и ранжирующий их по наблюдаемой отдаче. В его комментариях зафиксировано то, что владельцам стоит прочитать дважды: сканирование работает независимо от бренда, потому что Amcrest, Lorex, Annke и Swann сидят на том же облачном бэкенде Dahua под своей наклейкой. Покупка «не китайской» камеры от этого пути не спасает вообще.
Тулкит оператора собран, а не написан, и это видно на одном файле, сохранившемся в трёх копиях.
Оригинал закомментирован целиком по-испански: жёстко зашитое число потоков, никакой блокировки вокруг записи результатов – настоящая гонка, теряющая находки. Вторая копия, в каталоге cve-project, – ручной рефакторинг: число потоков вынесено в параметр, вокруг записи появился лок, таймауты укорочены. И в этот всё ещё испанский код вставлены русские строки времени выполнения:
print(f"[{num:06d}] Probando {candidate_serial}... | Найдено: {found_count[0]}")
Запущенная без аргументов, та же копия спрашивает уже полностью по-русски, сколько потоков использовать. Третья копия русифицирована до конца. Это три стадии одного человека, переписывающего заимствованный инструмент слоями, по одному языковому пласту за раз – и попутно чинящего в нём реальный дефект, который автор оригинала не заметил.
Тот же тулкит достаёт из устройств сохранённые пароли процедурой, которая выводит ключ расшифровки исключительно из того, что у атакующего уже есть: префикса класса устройства и серийного номера. Никакого секрета с устройства для этого не требуется. Остаточный испанский комментарий в этом коде подтверждает общее происхождение с оригинальным брутфорсером.
Есть и четвёртая линия, на английском, ссылающаяся в докстроке на публичный проект по протоколу Dahua, с MIT-лицензией на имя «Purple Ghosts», которое больше нигде в коде оператора не встречается. Полный конвейер по префиксу – scanner.py – перечисляет кандидатов, проверяет каждого на релее и для каждого живого поднимает локальный туннель и прогоняет цепочку байпасов, пробуя сначала вход без аутентификации, затем короткий список кредов. Состояние он держит по префиксам и умеет продолжать: один лог показывает возобновление скана после более чем девяти тысяч уже проверенных серийников.
Оркестратор поверх всего этого предельно лаконичен: пять живых устройств как цель, двадцать раундов как потолок, окно кандидатов расширяется на две тысячи за раунд от базового смещения в восемь тысяч. THANKS.md в p2pwn благодарит четверых: авторов реализации P2P-протокола, варианта туннеля PTCP, алгоритма генерации серийников и собственно эксплойта бэкдора. Запуск чужих инструментов устанавливает факт использования, но не авторства, и Hunt это разграничение держит аккуратно на протяжении всего отчёта.
22 июля прошла крупнейшая волна кампании. А потом человек, месяц считавший память в очередях воркеров, сделал вот это.
История шелла фиксирует три подряд усиливающихся полноинтернетных прогона masscan по порту 37777 – семьдесят тысяч пакетов в секунду, затем сто двадцать, затем сто пятьдесят. Он всё ещё подбирал скорость. И сразу за ними, дважды подряд:
cd /root && sudo python3 -m http.server 80 --bind 0.0.0.0
23 июля в 11:45 UTC краулер Hunt забрал с этого HTTP-сервера 2616 файлов в 234 подкаталогах – 407 мегабайт целиком. Тулкит, логи, историю шелла, папки кампании, коды восстановления, экспорты под SMART PSS.
Разрыв между двумя половинами этой истории стоит проговорить прямо, потому что он и есть её содержание. Ограниченная очередь с часовыми на выходе, детект блокировки по паре байтов ответа, фильтр кадра по плотности границ, ручной реверс бинарного протокола – это работа человека, понимающего, что делает. python3 -m http.server из домашнего каталога на 0.0.0.0 – это отсутствие даже той модели угроз, которую имеет любой, кто хоть раз выкладывал файл на сервер. Одна и та же голова.
На том же хосте лежала ещё одна вещь, к камерам отношения не имеющая: UPX-упакованный 32-битный виндовый бинарь, переименованный из xeno.exe в 1.exe, рядом с PowerShell-скриптом, отключающим Defender пятью независимыми способами сразу и на весь диск C:. Hunt квалифицирует это как отдельную возможность, стоящую на том же сервере, и рекомендует считать отдельным инцидентом, а не частью кампании по камерам. Мы следуем этой квалификации и здесь её не разворачиваем: имя файла намекает на одно семейство, единственная сэндбокс-метка называет другое, и достоверного разрешения этого противоречия в корпусе нет.
Отдельного разговора заслуживает то, чего в этом отчёте нет, – и по нашу сторону клавиатуры это едва ли не самое поучительное.
28 июля на 443-м порту хоста оператора поднялся сертификат с субъектом rbc.ru, побайтово совпадающий с тем, что стоит на настоящем Qrator-эдже РБК. Баннер на порту тоже читается как QRATOR. Материал для громкого заголовка готов – взломщик камер маскируется под крупный российский финансово-медийный холдинг.
Дальше начинается работа. Указанный в сертификате издатель – Let's Encrypt, а Let's Encrypt выдаёт только после подтверждения контроля над доменом; ничто не говорит, что этот сервер контролирует rbc.ru. Значит, поле издателя скопировано вместе с остальными байтами. Запрос по базе сертификатов даёт этот же хеш на более чем одиннадцати тысячах адресных наблюдений по всему миру, на портах, типичных для прокси: 443 и 8443 в основном, но и 1080, 9443, 2053, 2083. У настоящего эджа РБК при идентичных байтах сертификата совершенно другой JARM – так выглядит сертификат, снятый и переигрываемый как статический файл, а не склонированный стек. И решающее: десятки посторонних адресов предъявили этот сертификат на восемнадцать часов раньше, чем в том же датасете появился настоящий эдж РБК. Кража приватного ключа таким расписанием не объясняется.
Что это на самом деле, показал соседний порт. В тот же день на 8080 поднялся React-SPA с заголовком Telemt Panel; поиск по этому заголовку вывел на упоминания telemt как Rust-компонента прокси в стеке обхода блокировок MTProto для Telegram. Сертификат rbc.ru – дефолтная маскировочная идентичность этого инструмента, а не трофей и не признак компрометации у РБК или Qrator. На соседнем адресе того же /24, для полноты картины, лежит такой же правдоподобный клон сертификата Yahoo.
Той же дисциплиной сделана и вещь, которую в отчётах видишь редко: девять адресов релейного бэкенда, всплывших в логах оператора, Hunt намеренно не публикует в индикаторах. Причина названа прямо – опубликовать их значит заставить защитников заблокировать собственный легитимный релей Dahua и оборвать связь с камерами у реальных клиентов. Три из них к тому же несут побайтово одинаковые отпечатки на одинаковом наборе портов, что выдаёт не независимых арендаторов, а один образ, раскатанный по нескольким машинам. Отсутствие вредоносной метки у всех девяти – это и есть находка, а не пробел в покрытии.
Суждение здесь адресуется исследователю, и оно в его пользу. Выбор был между громким индикатором и тихой оговоркой. Публикация девяти адресов и заголовка про подмену сертификата РБК дала бы отчёту охват и цитируемость; заплатили бы за это чужие люди, у которых перестали бы работать камеры, и служба безопасности РБК, поднятая по ложной тревоге. Альтернатива стоила лаборатории отдельного раздела с объяснением, почему найденное не является тем, чем выглядит, – то есть работы, которую никто не похвалит. Выбрали второе, и в отчёте про камеры это оказалось лучшим куском.
Кампания кончилась, а её результат нет.
Учётка p2pwn живёт на примерно 1923 камерах ровно до тех пор, пока администратор не откроет список аккаунтов и не удалит её руками. Смена админского пароля её не трогает. Коды восстановления живут дольше самой учётки и снимаются только обновлением прошивки: Dahua заявляет, что после обновления новые коды не генерируются, а ранее выданные периодически перевыпускаются и со временем становятся недействительными. Удаление аккаунта эту вторую половину не закрывает вообще – и именно поэтому её надо держать в голове отдельным пунктом.
Практическая часть короткая. Любую камеру Dahua, отвечавшую на 37777 в июне и июле, разумно считать скомпрометированной; в списке её учёток искать p2pwn, а все хранящиеся на ней креды считать вынесенными и менять. P2P отключать везде, где он не нужен по делу: устройство с включённым P2P достижимо по одному серийному номеру, а релей ходит через пул бэкендов, так что блокировка по адресам тут не работает в принципе. И прошивку обновлять – SA-2021-0130 закрыл обе CVE ещё в октябре 2021 года.
Общая же оценка калибра этой операции складывается из вещей, которые по отдельности выглядят противоречиво. Ограниченная очередь, детект локаута с отходом ради будущего CVE-прохода, фильтр кадров по содержимому, ручной реверс Easy4IP – это верхняя половина квалификации, и мы её назвали. Незакрытый rmtree, ссылка на сообщество в каждом уведомлении, и наконец раздача домашнего каталога всему интернету – нижняя, и она обошлась ему в кампанию целиком. Но самое дорогое здесь не опсек. Самое дорогое – то, что месяц вычислительной работы был потрачен на путь, обходивший тот, который в его же коде отмечен цифрой 89,4 %. Инженер построил хороший инструмент для задачи, которая перед ним не стояла.
А общий грунт под всем этим шире одного оператора. Тот же байпас NetKeyboard встречается у отдельного проирански-ориентированного кластера, параллельно идёт кампания по видеорегистраторам Dahua вообще без единой CVE, на одном телнете, и сам тулкит собран из работы как минимум шести посторонних разработчиков. Эксплуатация Dahua никого не опознаёт. Это общая площадка, на которой одновременно работают люди, друг о друге не знающие, и вход на неё стоит ровно столько же, сколько знание серийного номера.
Разбор кампании, реверс протокола PTCP и инфраструктурный анализ – Hunt.
Автор: hacker@shifry.local