Аудит sudo и root-команд
Скомпрометированный сервер выдаёт себя не сразу. Один из самых надёжных
маркеров — кто и какие команды запускает через sudo. Если внезапно
sudo запускает пользователь, которого нет в списке админов, идёт прямой
вход под root или сыплются ошибки аутентификации — узнать об этом надо в ту же
секунду, а не из post-mortem.
Этот сценарий про эскалацию привилегий и отличается от
уведомлений о SSH-входах: там мы ловим сам факт
входа, здесь — что человек делает под sudo/root уже внутри.
Что мы будем делать
Заголовок раздела «Что мы будем делать»Поднимаем долгоживущий watcher (systemd-сервис с Restart=always), который
читает журнал в реальном времени и реагирует на строки sudo::
- sudo от неожиданного пользователя (не из allowlist
alice,bob) — 🚨 высокий приоритет (9); - ошибка аутентификации sudo (
authentication failure,NOT in sudoers) — ⚠️ высокий приоритет (8); - прямой вход/сессия под
root— 🚨 высокий приоритет (9); - ожидаемый админ запустил sudo — 🔑 тихий/низкий приоритет (3) для дайджеста (можно вовсе отключить).
В сообщение кладём вызвавшего пользователя, TTY и саму команду (COMMAND=).
На Debian/Ubuntu записи sudo идут в /var/log/auth.log, на systemd/RHEL —
в журнал (journalctl) и в /var/log/secure. Watcher универсален: если есть
journalctl — читаем его, иначе tail -F по auth.log.
Сохраните как /usr/local/bin/notifly-sudo-audit:
#!/usr/bin/env bashset -euset -a; source /etc/notifly.env; set +a
HOST=$(hostname -s)
# Кто из пользователей имеет право повышать привилегии.# Всё, что вне этого списка, — повод для громкого алерта.ALLOWLIST="alice bob"
# Читаем журнал sudo в реальном времени. journalctl (systemd) или auth.log (Debian).tail_source() { if command -v journalctl >/dev/null 2>&1; then # SYSLOG_IDENTIFIER=sudo покрывает и systemd, и RHEL (/var/log/secure) journalctl SYSLOG_IDENTIFIER=sudo -f -o cat --since now elif [ -f /var/log/auth.log ]; then tail -F -n0 /var/log/auth.log else tail -F -n0 /var/log/secure fi}
in_allowlist() { for u in $ALLOWLIST; do [ "$1" = "$u" ] && return 0 done return 1}
tail_source | while read -r line; do # Нас интересуют только строки сессий sudo echo "$line" | grep -q "sudo:" || continue
# Ошибка аутентификации / нет в sudoers — самое подозрительное if echo "$line" | grep -qiE "authentication failure|NOT in sudoers|incorrect password"; then USER=$(echo "$line" | grep -oP 'sudo:\s*\K\S+' | head -1) TTY=$(echo "$line" | grep -oP 'TTY=\K\S+' | head -1) /usr/local/bin/notifly-send \ "⚠️ Ошибка sudo: ${USER:-?}@$HOST" \ "Пользователь: ${USER:-?}TTY: ${TTY:-?}Хост: $HOSTСтрока: $line" 8 || true continue fi
# Обрабатываем только строки с реально запущенной командой echo "$line" | grep -q "COMMAND=" || continue
USER=$(echo "$line" | grep -oP 'sudo:\s*\K\S+' | head -1) TTY=$(echo "$line" | grep -oP 'TTY=\K\S+' | head -1) RUNAS=$(echo "$line" | grep -oP 'USER=\K\S+' | head -1) COMMAND=$(echo "$line" | grep -oP 'COMMAND=\K.*' | head -1)
# Прямая эскалация до root — всегда громко if [ "${RUNAS:-root}" = "root" ] && ! in_allowlist "${USER:-?}"; then /usr/local/bin/notifly-send \ "🚨 Неизвестный sudo→root: ${USER:-?}@$HOST" \ "Пользователь: ${USER:-?} (нет в allowlist)TTY: ${TTY:-?}Команда: ${COMMAND:-?}Хост: $HOST" 9 || true elif ! in_allowlist "${USER:-?}"; then # sudo от кого-то вне списка админов /usr/local/bin/notifly-send \ "🚨 Чужой sudo: ${USER:-?}@$HOST" \ "Пользователь: ${USER:-?} (нет в allowlist)Как: ${RUNAS:-?}TTY: ${TTY:-?}Команда: ${COMMAND:-?}" 9 || true else # Ожидаемый админ — тихий дайджест (при желании закомментируйте) /usr/local/bin/notifly-send \ "🔑 sudo: ${USER:-?}@$HOST" \ "Команда: ${COMMAND:-?}TTY: ${TTY:-?}" 3 || true fidoneСделайте исполняемым:
sudo chmod +x /usr/local/bin/notifly-sudo-auditЗапуск как systemd-сервис
Заголовок раздела «Запуск как systemd-сервис»Чтобы watcher жил постоянно и сам поднимался после перезагрузки и падений,
оформляем его сервисом с Restart=always.
/etc/systemd/system/notifly-sudo-audit.service:
[Unit]Description=Notify about suspicious sudo/root activityAfter=network-online.target
[Service]ExecStart=/usr/local/bin/notifly-sudo-auditRestart=alwaysRestartSec=10s
[Install]WantedBy=multi-user.targetАктивируем:
sudo systemctl daemon-reloadsudo systemctl enable --now notifly-sudo-auditПроверка:
sudo -ksudo id # ожидаемый админ — придёт тихое 🔑sudo -u nobody id # чужой пользователь — прилетит 🚨Прямой вход под root
Заголовок раздела «Прямой вход под root»Прямой root-вход (например, через консоль или su -) виден по сессиям PAM.
Добавьте вторую подписку в watcher или отдельный pam_exec в
SSH-сценарии, где USER=root уже помечается
приоритетом 9. Идеальная политика — PermitRootLogin no в sshd плюс алерт на
любую строку session opened for user root в журнале.
Windows: PowerShell + Event Log
Заголовок раздела «Windows: PowerShell + Event Log»Аналог для Windows-серверов: следим за Security Event Log через подписку в Task Scheduler. Нужные коды:
- 4672 — специальным привилегиям назначен вход (т.е. вошёл администратор);
- 4720 — создана новая учётная запись пользователя.
Оба события — классические маркеры эскалации привилегий и «тихого» создания
бэкдор-аккаунта. Используем общую функцию Send-Notifly.
Скрипт-нотификатор
Заголовок раздела «Скрипт-нотификатор»param([int]$EventId = 4672). C:\scripts\Notifly.ps1
# Кто ожидаемо имеет админ-права. Всё вне списка — повод для алерта.$AllowList = @("alice", "bob", "Administrator")
# Берём последнее событие из Security Log$ev = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=$EventId} -MaxEvents 1if (-not $ev) { return }
# Разбираем EventData$xml = [xml]$ev.ToXml()$data = @{}$xml.Event.EventData.Data | ForEach-Object { $data[$_.Name] = $_.'#text' }
if ($EventId -eq 4672) { $user = $data.SubjectUserName # Игнорируем системные учётки и компьютерные аккаунты if ($user -in @("SYSTEM", "LOCAL SERVICE", "NETWORK SERVICE")) { return } if ($user -like "*$") { return }
$prio = if ($user -in $AllowList) { 3 } else { 9 } $icon = if ($user -in $AllowList) { "🔑 Админ-вход" } else { "🚨 Чужой админ-вход" }
Send-Notifly ` -Title "$icon: $user@$env:COMPUTERNAME" ` -Message "Пользователь: $user`nДомен: $($data.SubjectDomainName)`nСервер: $env:COMPUTERNAME`nВремя: $($ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'))" ` -Priority $prio}else { # 4720 — создан новый пользователь (всегда громко) $newUser = $data.TargetUserName $by = $data.SubjectUserName Send-Notifly ` -Title "🚨 Создан пользователь: $newUser на $env:COMPUTERNAME" ` -Message "Новый аккаунт: $newUser`nКем создан: $by`nСервер: $env:COMPUTERNAME`nВремя: $($ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'))" ` -Priority 9}Подписка на события
Заголовок раздела «Подписка на события»# 4672 — назначены специальные привилегии (админ-вход)$xml = @"<QueryList> <Query Id="0" Path="Security"> <Select Path="Security"> *[System[EventID=4672]] </Select> </Query></QueryList>"@$Trigger = New-ScheduledTaskTrigger -AtStartup$Trigger.Subscription = $xml$Action = New-ScheduledTaskAction -Execute "powershell.exe" ` -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\Notifly-Privilege.ps1 -EventId 4672"Register-ScheduledTask -TaskName "Notifly Privilege Watch" -Trigger $Trigger -Action $Action ` -Principal (New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest) ` -Description "Notifly: аудит админ-входов"
# 4720 — создана новая учётная запись$xml2 = $xml -replace '4672', '4720'$T2 = New-ScheduledTaskTrigger -AtStartup$T2.Subscription = $xml2$A2 = New-ScheduledTaskAction -Execute "powershell.exe" ` -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\Notifly-Privilege.ps1 -EventId 4720"Register-ScheduledTask -TaskName "Notifly New Account Watch" -Trigger $T2 -Action $A2 ` -Principal (New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest) ` -Description "Notifly: аудит новых учёток"- Видно любое повышение привилегий. Чужой
sudoили прямойroot— это ровно тот момент, когда атака превращается в захват машины. - Ошибки аутентификации — ранний сигнал.
NOT in sudoersчасто означает, что кто-то уже сидит под чужой учёткой и пробует расширить права. - Allowlist гасит шум. Пока команды запускают только
aliceиbob, вам приходят тихие 🔑 (или ничего), а громкие 🚨 звучат только на аномалиях.
Что улучшить дальше
Заголовок раздела «Что улучшить дальше»- Отправлять полный контекст (
last,who, окружение) через полеextras, чтобы разбирать инцидент прямо с телефона. - Приоритеты по priority: ожидаемый админ — 3, чужой sudo и ошибки — 8–9, создание пользователя root/wheel — 10.
- Связать с уведомлением о срабатывании Fail2ban —
bruteforce снаружи и подозрительный
sudoвнутри часто идут в паре.