Короткий ответ
Root-cron, который вызывает world-writable скрипт или backup.sh из PATH без абсолютного пути — готовый PATH hijack. Зафиксируйте PATH=/usr/sbin:/usr/bin:/bin, в crontab только /usr/local/sbin/backup.sh, режим 750 root:root (или группа бэкапа без write для other). /etc/cron.d и spool не writable для admin без нужды. Не чините сбой job через chmod 777. Хост host.example (10.0.20.10).
Симптомы и как отличить
- Скрипт в
/opt/scriptsrwxrwxrwx, cron root каждую ночь; - в crontab
python backup.pyбез пути; /etc/cron.dпринадлежитubuntu:ubuntu;- MAILTO молчит, hijack уже идёт.
| Место | Команда просмотра |
|---|---|
| user crontab | crontab -u admin -l |
| /etc/crontab | поле user |
| /etc/cron.d/* | run-parts правила имён |
| systemd timer | не cron |
Отличие от «cron не запускается»: там unit/PATH/mail. Здесь безопасность живого cron. Права деревьев: 777. Секреты в скрипте: секреты.
Возможные причины
- Деплой копирует job в
/optс umask 000. PATH=.или/home/admin/binпервым.- NFS no_root_squash + writable share.
wget https://…/install.sh | bashв weekly.- Пользовательский crontab с
sudoNOPASSWD. - World-writable
/etc/cron.dпосле «восстановления из tar».
Диагностика
systemctl status cron --no-pager
sudo ls -la /etc/crontab /etc/cron.d /etc/cron.daily /var/spool/cron/crontabs
sudo crontab -l
sudo crontab -u admin -lПрава целей:
sudo grep -Rvh '^#' /etc/crontab /etc/cron.d/* 2>/dev/null | awk 'NF'
# для каждой команды: type/stat
namei -l /usr/local/sbin/backup.sh
stat /usr/local/sbin/backup.sh
sudo find /etc/cron.d /etc/cron.daily /usr/local/sbin -perm -0002 -o -perm -0020 2>/dev/nullPATH в файлах:
sudo grep -n PATH /etc/crontab /etc/cron.d/* /var/spool/cron/crontabs/* 2>/dev/nullSUID рядом: SUID.
Решение
Сценарий A. Скрипт writable
sudo chown root:root /usr/local/sbin/backup.sh
sudo chmod 750 /usr/local/sbin/backup.shКаталог /usr/local/sbin — 755 root. Other не пишет. Если правит группа ops — 750 root:ops и без write для other.
В crontab абсолютный путь:
PATH=/usr/sbin:/usr/bin:/bin
0 2 * * * root /usr/local/sbin/backup.shНе backup.sh, не cd /opt && ./backup.sh если /opt writable.
Сценарий B. PATH hijack
В скрипте:
#!/bin/bash
set -eu
PATH=/usr/sbin:/usr/bin:/bin
export PATH
/usr/bin/tar ...Каждая внешняя команда — с полным путём или после безопасного PATH. Не вызывайте python без /usr/bin/python3.
Сценарий C. Ограничить кто имеет crontab
echo 'admin' | sudo tee /etc/cron.allow
sudo chmod 640 /etc/cron.allowПри наличии cron.allow остальные не ставят crontab (кроме root). cron.deny — другая модель, не смешивайте в голове: читайте man crontab вашей Ubuntu.
Сценарий D. Секреты и wget
Ключи БД — EnvironmentFile 640, не в скрипте 755 в git. Уберите | bash из cron: пакет/артефакт с checksum.
systemd timer + User=backup лучше root-cron, если не нужен raw disk: сервис User= по духу тот же принцип.
Широкий sudo из cron: sudo.
Проверка «кто может подменить job». Кроме mode файла смотрите владельца каждого компонента namei -l: если /opt 777, файл 750 бесполезен. То же для симлинков: cron следует по ссылке; writable symlink в /etc/cron.d = запись чужого задания.
/etc/cron.hourly и cron.daily: скрипты пакетов + ваши. Не кладите туда файлы с точкой в имени и не копируйте .bak. run-parts молча пропустит. Для безопасности важнее: эти каталоги 755 root:root. Если admin туда пишет без sudo — сузьте.
systemd timer как замена: User=backup, ReadWritePaths= на каталог бэкапа, без shell PATH. Старый cron удалите, чтобы не было двух запускающих с разным uid.
Логирование: MAILTO=admin@host.example или явный >> /var/log/backup.log 2>&1 с файлом 640. World-writable лог позволит подменить вывод и скрыть hijack. Не chmod 777 лог «чтобы видеть».
Интерактивный crontab -e под root на проде оставляет spool root без git. Лучше файл в /etc/cron.d/backup из ansible с mode 644 root:root (содержимое читают, не пишут). 644 на cron.d файле ок, если каталог не writable; содержимое без секретов.
Тест PATH hijack на копии: положите /tmp/tar executable в PATH до системного — в безопасном crontab с абсолютным /usr/bin/tar не вызовется. Если вызовется — PATH всё ещё дырявый.
На 24.04 пользовательский crontab лежит в том же spool; crontab -e создаёт файл mode 600. Не «выровняйте права» chmod 666, чтобы ansible читал без sudo. Ansible должен становиться root. Дублирование job в /etc/cron.d и user crontab — два PATH, два hijack-вектора; оставьте одно место.
Прогоните run-parts --test /etc/cron.daily и сверните список с ожидаемым: лишний скрипт с 777 виден здесь же.
Отдельно: anacron и /etc/cron.daily/apt-compat не должны быть world-writable. Пакетный файл 755 root. Если видите 777 — инцидент, не «чтобы apt заработал». Для таймеров apt смотрите systemd, не чините daily chmod.
EDITOR=vim crontab -e создаёт spool с umask пользователя: проверьте stat /var/spool/cron/crontabs/root — не 666. Дефолт обычно 600.
Как проверить, что проблема устранена
stat -c '%a %U:%G %n' /usr/local/sbin/backup.sh /etc/cron.d /var/spool/cron/crontabs
sudo find /etc/cron.d /usr/local/sbin -perm -0002
sudo crontab -l | grep PATH
sudo journalctl -t CRON --since 'today' | tailСкрипт не world-writable. Job отрабатывает (тестовый run от того же user). nobody не может дописать файл. cron.service active.
Если не помогло
- Job не стартует после 750: владелец не тот, что в поле user crontab.
- anacron vs hourly — читайте, какой планировщик.
- systemd timer дублирует cron — два запуска, не hijack.
- AppArmor DENIED на
/usr/local/sbin— профиль, не 777. - NFS: права на сервере экспорта.
Профилактика
- CI: запрет 0002 на cron-targets.
- Скрипты только из пакета/ansible с mode.
- Инвентарь crontab в git (без паролей).
- Baseline.
FAQ
Точка в PATH
Никогда в cron. Даже в конце опасна при пустых компонентах ::.
/etc/cron.daily и точки в имени
run-parts пропускает имена с точкой. Это доступность, не hijack, но «тихий» бэкап.
Нужен ли cron.allow если только root crontab
Да как слой. Пользовательские crontab — лишняя поверхность.
wget | sh один раз вручную
Тот же риск. Верифицируйте подпись/хеш.
Можно ли chmod 777 «на минуту»?
Минута с cron каждую минуту = окно. Нет.