ESPNNBA Rank 2026: The new top 10 features a razor-thin race for No. 1Daily MaverickANALYSIS: Economic fire — Interest rate adjustment makes for interesting braai chatThe Jerusalem PostPollster warns Likud that attacking Ofer Winter is a ‘very big gamble’ as party drops to 18 seatsPunchFIFA names Nigerian referee among 81 officials for U-17 World CupUN NewsThe Takeaway: UN General Assembly debate Day 2Bollywood HungamaForget Bollywood box office! Juhi Chawla has quietly built her EMPIRE from Rs. 4,600 cr to Rs. 7,790 cr in ONE YEARRTP DesportoMotoGP em Portugal pelo menos até 2029The Japan TimesA trumpet-led finale brings new spark to Seiji Ozawa Matsumoto FestivalAitnewsملخص مؤتمر Meta Connect 2026.. نظارات واقع افتراضي أخف ونماذج جديدة للذكاء الاصطناعيRTL BoulevardZechiël sluit aan bij Jong Oranje voor belangrijk duel met Noren7sur7La CIA met en garde: la Russie pourrait saboter des câbles essentiels à nos paiementsCBC NewsMarineland dolphin dies at Spain aquarium, 5th death after rescue transfer
The Daily Newsstand · Free, Always
Thursday, September 24, 2026

Бэкдор в системных файлах OPNsense: как расследовали внедрение и что из этого вышло

Translate

Хочу поделиться разбором инцидента, который заставил меня вместо привычного GUI окунуться в файловую систему OPNsense. Я расскажу, как с виду легитимный, но подозрительный процесс привёл к обнаружению бэкдора в системном файле, почему расследование зашло в тупик и какие выводы мы из него вынесли. Всё по существу.

TL;DR: В OPNsense обнаружен процесс php‑cgi, пожирающий 100% CPU. Оказался бэкдором, вшитым в системный файл system.inc через cron. Источник внедрения не установлен — помешал общий root‑пароль и отсутствие внешнего логирования. Выводы — в конце.

В один из рабочих дней я столкнулся с неприятным симптомом: веб‑интерфейс нашего межсетевого экрана OPNsense начал жутко тормозить. На тот момент (13.09.2026) на шлюзе была установлена версия OPNsense 26.7.1_1 (последней сборкой тогда была 26.7.4). Причина: утилизация процессора на OPNsense уперлась в потолок, при этом процессорное время тратилось по большей части на системные процессы.

Первая мысль — банальный баг в ПО. Ведь системный процесс обслуживания веб‑интерфейса (OPNsense построен на PHP) не должен утилизировать все ресурсы процессора, лишая ИБ‑задачи вычислительных ресурсов.

last pid: 89763;  load averages:   29.55,   27.45,   26.24  up 36+22:54:07  11:23:05 
1125 threads:  58 running, 1012 sleeping, 55 waiting
CPU:  2.6% user,  0.0% nice, 97.3% system,  0.0% interrupt,  0.0% idle
Mem: 379M Active, 5860M Inact, 21G Wired, 104K Buf, 20G Free
ARC: 18G Total, 1732M MFU, 16G MRU, 41M Anon, 203M Header, 198M Other
     17G Compressed, 99G Uncompressed, 5.84:1 Ratio
Swap: 8192M Total, 8192M Free

  PID USERNAME    PRI NICE   SIZE    RES STATE    C   TIME    WCPU COMMAND
 8544 root        125    0  9028K  1400K CPU23   23 349.4H  75.04% php-cgi
81716 root        125    0  9028K  1400K CPU7     7 348.4H  74.61% php-cgi
90499 root        126    0  9028K  1416K CPU9     9 348.8H  73.91% php-cgi
55209 root         59    0  9028K  1428K RUN      9 348.1H  73.65% php-cgi
94044 root         59    0  9028K  1404K RUN     18 348.0H  72.70% php-cgi
11272 root        125    0  9028K   956K CPU12   12  34:14  72.63% php-cgi
59994 root         59    0  9028K  1392K RUN     18 348.8H  72.58% php-cgi
...

Анатомия аномалии

Виновником аномалии оказался процесс php‑cgi. У него обнаружился ряд странных особенностей: полное отсутствие аргументов командной строки и единственный открытый дескриптор — Unix‑сокет (никаких внешних сетевых соединений).

$ procstat files 8544
  PID COMM                FD T V FLAGS    REF  OFFSET PRO NAME
8544 php-cgi           text v r r-------   -       - -   /usr/local/bin/php-cgi
8544 php-cgi            cwd v d r-------   -       - -   /usr/local/bin
8544 php-cgi           root v d r-------   -       - -   /
8544 php-cgi              0 s - rw------  11       0 UDS 0 0 /var/lib/php/tmp/php-fastcgi.socket-4
8544 php-cgi              1 v c rw---n--  32       0 -   /dev/null
8544 php-cgi              2 v c rw---n--  31       0 -   /dev/null
8544 php-cgi              3 v r rw------   6       0 -   -
8544 php-cgi              4 ? - --------   2       0 -   -

Поскольку процесс явно мешал работе и выглядел странно, я попытался завершить его принудительно (killall -9 php-cgi). Однако процессы автоматически создавались заново. Первое подозрение пало на cron — и оно подтвердилось.

# or /usr/local/etc/cron.d and follow the same format as
# /etc/crontab, see the crontab(5) manual page.
@reboot sleep 5; /usr/local/sbin/php-cgi
SHELL=/bin/sh
PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
REQUESTS_CA_BUNDLE=/usr/local/etc/ssl/cert.pem
#minute hour    mday    month   wday    command
1       *       *       *       *       (/usr/local/sbin/configctl -d syslog archive) > /dev/null
*/4     *       *       *       *       (/usr/local/sbin/ping_hosts.sh) > /dev/null
0       22      *       *       *       (/usr/local/sbin/configctl -d firmware changelog cron) > /dev/null
0,15,30,45      *       *       *       *       (/sbin/pfctl -qt 'virusprot' -T expire '3600') > /dev/null
0,15,30,45      *       *       *       *       (/sbin/pfctl -qt 'sshlockout' -T expire '3600') > /dev/null
*       *       *       *       *       (/usr/local/bin/flock -n -E 0 -o /tmp/updaterrd.lock /usr/local/opnsense/scripts/health/updaterrd.php) > /dev/null
0       1       *       *       *       (/usr/local/sbin/configctl -d system remote backup 3600) > /dev/null
*       *       *       *       *       (/usr/local/opnsense/scripts/nginx/ngx_autoblock.php) > /dev/null
1       3       1       *       *       (/usr/local/sbin/configctl -d filter schedule bogons) > /dev/null
*       *       *       *       *       (/usr/local/bin/flock -n -E 0 -o /tmp/filter_update_tables.lock /usr/local/opnsense/scripts/filter/update_tables.py --quick) > /dev/null

Поиск источника заражения

В нашей инфраструктуре кроме основного OPNsense есть и резервный. Они развёрнуты в кластере, имеют одинаковые наборы и версии пакетов. На втором OPNsense не был запущен этот процесс и отсутствовала команда php‑cgi в cron. Если бы это была легитимная строка из штатного обновления, она была бы и на втором узле.

# or /usr/local/etc/cron.d and follow the same format as
# /etc/crontab, see the crontab(5) manual page.
SHELL=/bin/sh
PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
REQUESTS_CA_BUNDLE=/usr/local/etc/ssl/cert.pem
#minute hour    mday    month   wday    command
1       *       *       *       *       (/usr/local/sbin/configctl -d syslog archive) > /dev/null
*/4     *       *       *       *       (/usr/local/sbin/ping_hosts.sh) > /dev/null
0       22      *       *       *       (/usr/local/sbin/configctl -d firmware changelog cron) > /dev/null
0,15,30,45      *       *       *       *       (/sbin/pfctl -qt 'virusprot' -T expire '3600') > /dev/null
0,15,30,45      *       *       *       *       (/sbin/pfctl -qt 'sshlockout' -T expire '3600') > /dev/null
*       *       *       *       *       (/usr/local/bin/flock -n -E 0 -o /tmp/updaterrd.lock /usr/local/opnsense/scripts/health/updaterrd.php) > /dev/null
0       1       *       *       *       (/usr/local/sbin/configctl -d system remote backup 3600) > /dev/null
*       *       *       *       *       (/usr/local/opnsense/scripts/nginx/ngx_autoblock.php) > /dev/null
1       3       1       *       *       (/usr/local/sbin/configctl -d filter schedule bogons) > /dev/null
*       *       *       *       *       (/usr/local/bin/flock -n -E 0 -o /tmp/filter_update_tables.lock /usr/local/opnsense/scripts/filter/update_tables.py --quick) > /dev/null

В работе с cron OPNsense имеет особенность. Он сам наполняет cron на основе пользовательской конфигурации (/usr/local/etc/cron.d или из GUI) и своих собственных, системных задач (для обновления сигнатур, базы GeoIP...). OPNsense не сохраняет изменения, внесённые через crontab ‑e. Каким путём команда оказалась в cron?

Директории с пользовательскими настройками были пусты, в GUI также ничего похожего замечено не было. Поиск совпадений по всем файлам дал подсказку: php‑cgi запускается через cron и попадает в него из системного файла /usr/local/etc/inc/system.inc, который ни один пользователь не менял руками.

    $crontab_contents = "# DO NOT EDIT THIS FILE -- OPNsense auto-generated file\n";
    $crontab_contents .= "#\n";
    $crontab_contents .= "# User-defined crontab files can be loaded via /etc/cron.d\n";
    $crontab_contents .= "# or /usr/local/etc/cron.d and follow the same format as\n";
    $crontab_contents .= "# /etc/crontab, see the crontab(5) manual page.\n";
    $crontab_contents .= "@reboot sleep 5; /usr/local/sbin/php-cgi\n";
    $crontab_contents .= "SHELL=/bin/sh\n";
    $crontab_contents .= "PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin\n";
    $crontab_contents .= "REQUESTS_CA_BUNDLE=/usr/local/etc/ssl/cert.pem\n";
    $crontab_contents .= "#minute\thour\tmday\tmonth\twday\tcommand\n";

Анализ угроз и локализация

После экспресс‑анализа ситуации и общения с коллегами сформировались две теории: мы имеем дело либо с майнером (из‑за большой нагрузки на процессор), либо с бэкдором (пусть и через socket). Первый вариант отпадал из‑за того, что не было никакого открытого соединения с внешним сервером, также нагрузка шла на System, а не на User CPU. Второй сценарий выглядел гораздо реалистичнее: даже в таком изолированном виде php‑cgi мог использоваться атакующими в качестве локального бэкдора для выполнения произвольного кода с правами root.

Я понял, что ситуация критичная, и принял решение:

  • Заблокировать весь трафик, исходящий от самого OPNsense в интернет;

  • Проверить целостность всех пакетов OPNsense на соответствие хешам из официального репозитория;

$ pkg check --checksums --all
Checking all packages:  42%
opnsense-26.7.1_1: checksum mismatch for /usr/local/etc/inc/system.inc
Checking all packages: 100%
  • Исправить руками системный файл и удалить из него php‑cgi;

$ sed -i '' '1146d' /usr/local/etc/inc/system.inc
  • Перезагрузить службу cron и убрать оставшиеся процессы php‑cgi из системы.

$ configctl cron restart
$ killall -9 php-cgi

Расследование

Проверка целостности после исправления показала, что все файлы пакетов целы.

$ pkg check --checksums --all
Checking all packages: 100%

Тем не менее, злоумышленник мог так же подменить генерируемые из шаблонов и пользовательские конфиги. pkg check это проверить не может.

Проверка логов также ничего не дала (/var/log/, dmesg, auth.log), так как root‑пароль знали все, кому было разрешено заходить на OPNsense под своей учётной записью. В таком случае любой вход в систему даже под root‑пользователем выглядит легитимно. К тому же злоумышленник мог запросто почистить за собой логи.

GUI и SSH на OPNsense не были открыты «наружу» в интернет, а правила исключали возможность взлома не сотрудниками компании.

Длительное наблюдение за логами трафика, исходящего от OPNsense, показало, что кроме NTP и DNS запросов наружу ничего не летит.

Можно было бы провести дальнейшее расследование и выяснить, как именно появился бэкдор, кто им пользовался, но для этого не хватало опыта и времени.

Расследование зашло в тупик, но инцидент заставил нас пересмотреть подход к безопасности инфраструктуры.

Выводы для себя

1. Коллективный root. Использование общего root‑пароля внутри команды — огромная брешь для ИБ. Невозможно определить, кто и когда внёс изменение. Доступ должен быть строго персонализирован, а вход под root — запрещён. OPNsense мы поставили не так давно и решили, что на всякий случай доступ должен быть у всех.

2. Внешнее логирование. Логам на самом OPNsense верить нельзя — при получении root‑прав атакующий может их удалить. Необходима постоянная репликация логов на удалённый сервер.

3. Контроль целостности файлов. В данном случае pkg check --checksums --all успешно обнаружил модификацию system.inc. Проблема в другом: эта проверка запускается вручную, а не постоянно, и проверяет не все файлы. Для своевременного обнаружения вмешательства нужны системы контроля целостности файлов (FIM) — например, Syscheck в Wazuh, которые отслеживают изменения автоматически и оповещают в реальном времени.

4. Своевременное обновление. В нашем случае отставание версии на пару релизов не сыграло большой роли, но обновляться всё равно необходимо вовремя.

Инцидент закрыт, бэкдор удалён, система обновлена. Кто и как его установил — мы так и не узнали. Буду рад, если эта статья поможет кому‑то решить похожую проблему.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.