
Всем привет! Я Андрей, в Яндексе работаю над IoT‑железками в Городских сервисах. Но сегодня речь пойдёт не о них. Эта история началась с неожиданной находки в новой квартире — с обычной, на первый взгляд, трубки домофона. Кажется: ну трубка и трубка. Но вот что бросилось в глаза: она была подключена по Ethernet. А если есть Ethernet, значит, внутри что‑то с TCP/IP, то есть уже маленький компьютер.
Любопытство победило, и я решил разобраться, как же устроена эта железка и что она умеет. Провёл небольшое техническое расследование: от осмотра корпуса до анализа сетевых пакетов и выяснения, какие протоколы вообще использует домофон.
Я давно увлекаюсь железками и автоматизацией дома — эдакий «полу‑умный» дом, который вроде бы и работает сам, но иногда всё же требует вмешательства человека. Поэтому, обнаружив Ethernet‑домофон, я и решил разобраться в нём.
Основная цель эксперимента — научить трубку открывать дверь по решению автоматизации в Home Assistant. Побочная — внести вклад в сообщество и, возможно, помочь самому вендору, если в процессе вскроются любопытные особенности или проблемы.
Знай врага в лицо
Сначала меня удивила клавиатура — у моей трубки были все возможные кнопки, кроме той самой, что открывает дверь. Уже интрига.
Информации о модели в интернете оказалось немного, но производитель всё‑таки указал несколько ключевых характеристик:
веб‑интерфейс;
кнопка «Консьерж»;
режим громкой связи;
поддержка протоколов SIP и TCP/IP;
питание DC 5V/2A или PoE 802.3af;
Веб‑интерфейс — уже хороший знак: значит, можно будет посмотреть настройки. Оставалось лишь найти способ заглянуть внутрь. И тут удача — со стороны крепления кто‑то заботливо подписал IP‑адрес трубки маркером прямо на корпусе. Видимо, устройство работает со статической адресацией. Отличная точка входа для исследования.

Подключаемся к сети домофонов
С точки зрения протоколов трубка оказалась вполне предсказуемой: она работает как обычный SIP VoIP‑телефон, точно такие же стоят, например, в офисах Яндекса. Значит, взаимодействовать с ней можно привычными средствами.
В моём случае трубка питалась прямо по Ethernet — через PoE. Этот стандарт часто используется в офисах для точек доступа и IP‑телефонов: питание идёт по тому же кабелю, что и данные, что избавляет от лишних проводов. Если вы сейчас на работе — посмотрите на потолок: возможно, там висит коробочка, питающаяся тем же способом и раздающая вам Wi‑Fi.
К счастью, мой роутер MikroTik RB5009UPr+S+ умеет запитывать устройства по стандарту 802.3af, так что вопрос с питанием решился сам собой.
Дальше — немного магии RouterOS. В терминологии MikroTik «бридж» — это программный коммутатор второго уровня.То есть нам нужно сделать отдельный мост между трубкой и сетью домофонов, чтобы не мешать основной сети.
Создаём новый бридж, называем его
intercom.Перевешиваем на него два порта — трубку и порт uplink, — чтобы изолировать от локальной сети.
Добавляем маршрут до сети домофонов. Маску мы не знаем, но можно взять /16 от IP, написанного на корпусе трубки. Например:
/ip/route/add dst-address=10.17.0.0/16 gateway=eth-intercom-uplink
Эти же действия можно было выполнить и с помощью обычного неуправляемого коммутатора. Но под рукой был только MikroTik.
После всех манипуляций втыкаем оба провода в роутер. Теперь он работает как прозрачный коммутатор между трубкой и сетью домофонов. Снимаем трубку — слышен гудок. Отлично: значит, устройство подключено к SIP‑серверу и связь работает.
Что там в админке
Маршруты настроены, связь есть — пора заглянуть внутрь. Обычно в такой момент кулхацкеры первым делом сканируют устройство на открытые порты. Но поскольку я уже знаю, что у трубки есть веб‑интерфейс, начнём с него. Вбиваю IP‑адрес в браузер — и вижу страницу с авторизацией.

Для начала нужно понять, как реализована авторизация. Открываю инструменты разработчика, пробую наугад логин admin и пароль admin. Браузер немного думает… и вот я попадаю внутрь.

Креды по умолчанию — admin/admin. Ну что ж, зато ничего ломать не пришлось.
На главной странице админки сразу отображаются базовые характеристики устройства:
ROM: 1,5/16 M
RAM: 1/23 M
Не густо, Python, конечно, не запустишь, но жить можно.
Что удалось найти внутри
Полазив по разделам, вытаскиваю несколько действительно полезных вещей:
Настройки сети. Тут, наконец, выяснилось, что маска подсети —
/23.Параметры SIP‑подключения. Имя пользователя и номер телефона формируются просто:
6 + номер квартиры.DTMF‑код открытия двери. Это те самые тональные сигналы, которые мы все слышали в голосовом меню колл‑центров. В моём случае комбинация
123#отправляется на SIP‑сервер — и тот понимает, что дверь нужно открыть.
Кроме того, трубка унаследовала от своих старших братьев из линейки Fanvil богатую функциональность: поддержку OpenVPN, 802.1X‑аутентификации и других корпоративных штук, явно избыточных для обычной квартиры.
Пароль от SIP‑сервера, к сожалению, увидеть не удалось — в интерфейсе он скрыт звёздочками. Я проверил HTML‑код на всякий случай, но и там: value="****".
На удивление секурно!
Больше настроек богу настроек
На этом этапе я немного застрял: пароль от SIP‑сервера админка упрямо не показывала, а значит, подключиться напрямую без самой трубки не получится. В голове уже крутилась идея: а не послушать ли трафик между трубкой и сервером с помощью Wireshark? Тем более, RouterOS позволяет делать такие штуки буквально одним кликом: включил сниффер, направил на Wireshark, и вот тебе весь SIP‑трафик как на ладони.
Но прежде чем привлекать тяжёлую артиллерию, я вспомнил, что где‑то в интерфейсе промелькнула опция импорта и экспорта настроек. Если есть экспорт — значит, где‑то должен лежать файл конфигурации. А если есть файл, то, возможно, там и спрятан тот самый пароль.
Пора посмотреть, что именно отдаёт устройство и в каком формате сохраняет настройки.

Жмём на ссылку и получаем XML‑файлик — никакого хитрого шифрования. Внутри очень много настроек, но нам нужен всего лишь пароль…
<line index="1">
<PhoneNumber>6149</PhoneNumber>
<DisplayName>6149</DisplayName>
<SipName></SipName>
<RegisterAddr>10.17.0.1</RegisterAddr>
<RegisterPort>5060</RegisterPort>
<RegisterUser>6149</RegisterUser>
<RegisterPswd>6149</RegisterPswd>
<RegisterTTL>3600</RegisterTTL>
</line>
<!-- и еще 1500 строк -->
..а пароль (RegisterPswd) совпадал с юзернеймом SIP‑аккаунта. Сработавший admin/admin ничему меня не научил:(
Копаем глубже
Итак, все данные сервера у меня есть. У самурая два пути:
полностью выкинуть трубку и подключиться к SIP‑серверу напрямую с помощью Asterisk — всё для этого уже готово;
заставить трубку делать всю работу через её собственный API (если он есть) — то есть писать софт для трубки.
Я выбрал второй вариант. Думаю, написать софт прямо для трубки домофона — это весело.
Вернёмся к XML с параметрами. И там — прекрасное.
<web>
<EnableTelnet>0</EnableTelnet> <!-- ← это же Telnet! -->
<TelnetPort>23</TelnetPort>
<TelnetPrompt></TelnetPrompt>
<LogonTimeout>30</LogonTimeout>
<account index="1">
<Name>admin</Name>
<Password>admin</Password>
<Level>10</Level>
</account>
</web>
Telnet — это очень простой, как палка, протокол из 1972 года. Используется для удалённого подключения к шеллу. Галочку, включающую Telnet, в админке я не видел. Но на всякий случай захожу в раздел с настройками портов и проверяю инпуты в коде страницы.

В коде просто спрятали опцию через CSS. Очевидно, S в CSS — это Safety & Security. Убираем стили, нажимаем галочку и подключаемся по Telnet.

admin/admin не подходитТеперь знаем две важные вещи:
Трубка работает на Linux, о чём свидетельствует using Poky 13.0 — это значит, что прошивка сделана на референсном дистрибутиве Yocto Project.
Название платформы, на которой эта трубка построена, — DSPG DVF97. Под этим названием скрывался SoC для VoIP‑телефонов (внезапно).
Тем временем я нашёл подробный мануал для другого телефона, но с такой же прошивкой. В мануале честно указано наличие Telnet в виде скрытой настройки.


Пользователем оказался root, а пароля вовсе не было. Такая безопасность, конечно, удручает, но и упрощает реверсерам жизнь. Железка окончательно повержена.
Грызем кактус и юзерспейс
Теперь есть полное понимание, что за железо стоит в трубке:
Слабенький одноядерный ARMv5-процессор.
Из 24 МБ ОЗУ свободно 1,5 МБ, а доступно около 6 МБ (в чём разница, объясняется на этой странице).
Корневая ФС примонтирована как read‑only, используется SquashFS, а значит, «поправить» систему не получится.
Даже не знаю, что больше всего добавляет сложностей — настолько малый объём памяти или столь древняя архитектура.

Сразу отметаем интерпретируемые языки. В основном я пишу на Go, но он слишком прожорлив — получаются толстые бинарники, которые не поместились бы на флешку. C и C++ для такой задачи я отбросил: слишком больно и громоздко.
Остаётся только Rust. Он весьма экономичен по памяти (no GC, zero cost abstractions), и вокруг Rust сложилась экосистема — крутая система сборки и море пакетов. А ещё он поддерживает нужную мне архитектуру.
Сборка "Hello, world!"
Кросс‑компиляция (сборка под архитектуру, отличную от хоста) подразумевает установленные тулчейны. К счастью, в репозитории Fedora всё есть. Цели (target) для сборки бинарников задаются триплетами (triplet) — строками вида «архитектура‑платформа‑ОС‑ABI».
Для трубки подошёл таргет armv5te-unknown-linux-musleabi.
# добавляем целевую платформу для Rust
rustup target add armv5te-unknown-linux-musleabi
# и устанавливаем утилиты работы с бинарниками под ARM
# оттуда мне нужен только линковщик (ld)
# => Fedora
dnf install -y gcc-arm-linux-gnueabi
# => macOS
brew install arm-linux-gnueabihf-binutils
В настройки Cargo (.cargo/config.toml) подкидываем пути до линкера, иначе он попробует собрать всё с помощью линкера хоста.
[target.armv5te-unknown-linux-musleabi]
linker = "arm-linux-gnueabihf-ld"
ar = "arm-linux-gnueabihf-ar"
Написать «Hello, world!» в консоль — моветон. Я решил собрать HTTP‑сервер, да чтоб на модном асинхронном фреймворке. HTTP‑сервер нам ещё пригодится, но об этом позже.
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
println!("Listening on port 3000");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Собираем бинарник с --release‑флагом, скачиваем его в /tmp трубки (через wget). И запускаем.


Автозапуск в read-only-системе
Рабочий HTTP‑сервер — это замечательно, но надо бы научить его запускаться вместе с системой. Я планирую использовать MQTT для автоматизации, поэтому процесс должен работать фоново. Очевидно, что трубка где‑то хранит свои настройки. Узнать точки монтирования поможет команда mount.
root@dvf97:~# mount
rootfs on / type rootfs (rw)
/dev/root on / type squashfs (ro,relatime)
devtmpfs on /dev type devtmpfs (rw,relatime,size=12124k,nr_inodes=3031,mode=755)
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
tmpfs on /run type tmpfs (rw,nosuid,nodev,mode=755)
tmpfs on /var/volatile type tmpfs (rw,relatime)
devpts on /dev/pts type devpts (rw,relatime,gid=5,mode=620)
/dev/mtdblock11 on /mnt type jffs2 (rw,relatime)
/dev/mtdblock7 on /base type squashfs (ro,relatime)
/dev/mtdblock9 on /userdata type squashfs (ro,relatime)
/dev/mtdblock8 on /usr type jffs2 (rw,relatime)
Тут что‑то странное:
/userdata, которая, казалось бы, должна хранить данные пользователя, примонтирована как RO. Там хранится кастомизация админской панели и рингтоны.А вот
/usr, где хранятся бинарники, смонтирована на запись. Используется JFFS2 — простая журналируемая ФС. Но свободно там всего лишь 32 КБ.В
/mntтоже можно писать. И места много — свободно 1,4 МБ.
Чёрт ногу сломит!
К слову, подробную таблицу про разделы я оставил в своём Гитхабе.
Раз уж вендор нам дал возможность писать в /usr, то можно заменить какой‑нибудь бинарник или подредактировать скрипт. А свой бинарник положу в /mnt, так как в /usr очень мало свободного места.
Патчим жертву
К счастью, найти жертву‑бинарник оказалось очень просто. Вот список процессов:
root@dvf97:~# ps -
PID USER TIME COMMAND
1 root 0:16 init [5]
< ... kernel threads ... >
329 root 38:02 {afterUpgrade.sh} /bin/sh /bin/afterUpgrade.sh
350 root 0:00 /sbin/getty -L 115200 ttyS
351 root 0:00 /sbin/getty 38400 tty1
361 root 234:23 {callManager} XGUI
411 root 228:01 app_dsp
508 root 0:41 {ipwatchd-script} /bin/sh /usr/sbin/ipwatchd-script
597 root 0:01 telnetd -p 23
25323 root 0:00 -sh
29050 root 0:00 sleep 60
29067 root 0:00 -sh
29156 root 0:00 sleep 1
На трубке крутится пара процессов (XGUI и app_dsp), которые отвечают за не��осредственные функции телефона. Я не разбирался, как между ними реализован IPC и какие функции у каждого бинарника. Но в чём я уверен — их точно не стоит трогать.
А вот потрогать /bin/sh /usr/sbin/ipwatchd-script — звучит как идеальный план. Он лежит в /usr, куда можно писать. И этот скрипт написан на unix shell, а значит, можно его поправить, ничего не сломав.
Оригинальный скрипт
#!/bin/sh
CM_FIFO=/tmp/ipconflict_fifo
DEVICE=$1
IP=$2
MAC=$3
while true
do
ipStr=`ifconfig eth0 | grep "inet addr" | awk '{ print $2}' | awk -F: '{print $2}'`
if [ "$ipStr" ]; then
arping -D -c 1 -w 1 $ipStr
if [ $? == 1 ]; then
echo "ip_conflict \"$ipStr\"" > $CM_FIFO
fi
fi
sleep 60
done
exit 0Скрипт раз в минуту проверяет ARP записи по IP‑адресу телефона. Если произошла коллизия, то выполняется запись в FIFO. Зачем это трубке и какой процесс это читает — не знаю.
Я не стал доверять жизненному циклу ipwatchd-script и решил использовать его как трамплин для своего скрипта. В начало добавим запуск своего init‑скрипта и отвяжемся от ipwatchd-script:
if [ -f /mnt/userdata/hottubes-init.sh ]; then
/mnt/userdata/hottubes-init.sh &
fi
# ... старый код
Теперь записываю init‑скрипт в /mnt/userdata/hottubes-init.sh:
Код init скрипта
#!/bin/sh
PIDFILE="/run/hottubes.pid"
if [ -f "$PIDFILE" ]; then
PID=$(cat "$PIDFILE")
if kill -0 "$PID" 2>/dev/null; then
echo "hottubes is already running (PID: $PID). Exiting."
exit 0
else
echo "stale PID file found; removing it."
rm -f "$PIDFILE"
fi
fi
# ignore SIGHUP so that the child process isn't killed when this shell exits.
trap '' HUP
/mnt/userdata/hottubes </dev/null >/run/hottubes.log 2>&1 &
child_pid=$!
echo "$child_pid" > "$PIDFILE"
echo "started /mnt/userdata/hottubes detached with PID $child_pid."Осталось переместить бинарник из /tmp в /mnt/userdata/hottubes и проверить в бою. Перезагружаем трубку. Всё работает!
Что делать дальше, я уже примерно представлял:
в админке задать вебхуки на localhost, чтобы понимать, в каком состоянии трубка;
дёргать какую‑нибудь ручку для ответа на вызов и отправки DTMF‑кода;
всё это обернуть в конечный автомат;
писать в топики по MQTT и объяснить Home Assistant, как с ними работать.
Почти что вебхуки
Вернёмся в самое начало — в админку. В разделе Phone → Action можно задать что‑то вроде вебхуков. На целевое устройство прилетает GET‑запрос без полезных данных. Но нужные события тут есть: Idle, Ringing и Talking.
Вот для этого и пригодился HTTP‑сервер! Прописываем вебхуки до 127.0.0.1 (localhost трубка не хочет принимать):

Снял трубку и увидел запросы в консоли — обратные вызовы работают!
Нажимаем кнопки
Для управления трубкой есть всего одна ручка. Ручка кривая, но что поделать. Как ни странно, её достаточно. Мануал нашёл здесь.
Трубка принимает GET /cgi-bin/ConfigManApp.com. Используется Basic‑аутентификация. В параметре key указывается клавиша, которую нужно «нажать». Последовательность простая:
F_ACCEPT— принять вызов.1;2;3;POUND— набрать DTMF‑код123#. Это же действие совершает кнопка с человечком на трубке.F_RELEASE— положить трубку.
MQTT
Теперь бинарник знает о состоянии звонка и умеет открывать дверь. Кайф!

Для этого идеально подходит MQTT — лёгкий протокол обмена сообщениями, изначально созданный для умных устройств с ограниченными ресурсами. Он работает по принципу publisher/subscriber: устройство публикует события в определённые топики, а другие системы (например, Home Assistant) на них подписываются.
В центре этой схемы стоит брокер — сервер, который принимает сообщения и раздаёт их подписчикам. Самый популярный брокер — Mosquitto, его можно запустить хоть на Raspberry Pi.
При старте бинарник собирает информацию о железке — модель, IP и MAC‑адрес — и отправляет в специальный топик homeassistant/device/{device_id}/config сообщение с длинным JSON.
Вот как формируется JSON
json!({
"device": {
"name": metadata.model,
"manufacturer": metadata.vendor,
"model": metadata.model,
"sw_version": metadata.firmware_version,
"configuration_url": metadata.ip_address.as_ref().map(|address| format!("http://{}", address)),
"identifiers": [state.client_id],
"connections": [["mac", metadata.mac_address]],
},
"origin": {
"name": env!("CARGO_PKG_NAME"),
"sw": env!("CARGO_PKG_VERSION"),
"url": env!("CARGO_PKG_HOMEPAGE"),
},
"components": {
"phone": {
"unique_id": format!("{}-phone-status", state.client_id),
"platform": "sensor",
"name": "Phone Status",
"device_class": "enum",
"icon": "mdi:phone-ring",
"options": [
"IDLE",
"TALKING",
"RINGING",
],
"state_topic": format!("{}/phone", state.client_id),
},
"open": {
"unique_id": format!("{}-open-button", state.client_id),
"platform": "button",
"name": "Open Door",
"icon": "mdi:door",
"command_topic": format!("{}/open", state.client_id),
},
"ring": {
"unique_id": format!("{}-ring-trigger", state.client_id),
"automation_type": "trigger",
"platform": "device_automation",
"type": "phone",
"subtype": "ring",
"topic": format!("{}/phone", state.client_id),
"payload": "RINGING",
},
},
});Последний шаг к победе: реализовать запись в топики и подписаться на команду /open. Finita la commedia!
Что в итоге
С одной стороны, это история о том, как небрежность в разработке и настройке может сделать уязвимым даже простейшее устройство. С другой — отличный пример того, как из подобных ситуаций рождаются проекты, где можно исследовать железо, экспериментировать и искать творческие решения.
В итоге получился отличный пет‑проект, где можно было поковыряться в железе, покодить на Rust под древнюю архитектуру, покопать сетевые стеки, — и сделать наконец умный домофон, который реально что‑то умеет.
Без хаков, конечно, ничего бы не вышло. Но когда железку можно взломать логином admin/admin и запустить Telnet галочкой, спрятанной через CSS, — понимаешь, в каком прекрасном мире мы живём.
Миссия выполнена. Дверь открыта.
pnetmon
Если уж хакерить то - а позвонить соседям? Хотя по логам могут и найти.
Непонятен смысл действий.
Открывать автоматически входную дверь по входящему звонку - так где идентификация входящего? Это как admin, admin.
А для более сложных задач эта трубка SIP телефонии уже не нужна.
AdrianoVisoccini
нужно сказать, что домофон без видео это буквально дыра в безопасности. Слишком уж просто он взламывается социальным индинирингом, начиная от "это ваш сосед с этажа Х, забыл ключи пустите пожалусто" до "Добрый день, доставка воды/скорая/почта/все-что-угодно пустите"
так что на самом деле проверять смысла нет никакого. Тот кто захочет зайти - зайдет. Как пруф- тысячи закладкичков которые ежедневно обкладывают подъезды наркотиками без каких либо проблем