ICMP timestamp - старейшая строка в отчётах сканеров: «ICMP Timestamp Request Remote Date Disclosure», CVE-1999-0524, уровень Low. Номер года намекает, сколько ей лет. Её привычно пролистывают, и чаще всего правильно делают. Но у неё есть свойство, из-за которого стоит потратить десять минут: за CDN она прячется. Скан по домену не находит ничего, и строка из отчёта пропадает, хотя сервер как отвечал, так и отвечает.
Разбираю на своём сервере: что именно утекает, почему это нельзя выключить через sysctl, и почему на хосте с Docker «просто добавить правило в файрвол» - не просто.
Что это вообще такое
В ICMP кроме знакомого echo (type 8 и 0, то есть обычный ping) есть пара type 13 и 14 - timestamp request и timestamp reply. Задумывалась она для грубой синхронизации часов и замера задержки в те времена, когда NTP ещё не было. Сегодня её не использует никто, а ядро Linux отвечает на неё по умолчанию.
В ответе лежат три поля: когда отправитель послал запрос, когда получатель его принял и когда ответил. Время - в миллисекундах от полуночи UTC. То есть удалённая машина честно сообщает, сколько на ней сейчас времени.
Как это выглядит на живом сервере
Мой сайт стоит за Cloudflare. Сканирую домен проверкой именно по timestamp:
$ sudo nmap -sn -PP -n derristy.com
Note: Host seems down. If it is really up, but blocking
our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 2.07 seconds
Чисто. Если бы аудит ограничился доменом, находки бы не было вовсе. Теперь то же самое по адресу origin-сервера (сам адрес я ниже прячу - ровно по той причине, которая разбирается в следующем разделе):
$ sudo hping3 --icmp --icmptype 13 -c 2 <origin-ip>
len=40 ip=<origin-ip> ttl=51 icmp_seq=1 rtt=48.4 ms
ICMP timestamp: Originate=37309312 Receive=37309339 Transmit=37309339
len=40 ip=<origin-ip> ttl=51 icmp_seq=2 rtt=48.3 ms
ICMP timestamp: Originate=37310312 Receive=37310339 Transmit=37310339
Отвечает. Receive=37309339 - это 37309 секунд от полуночи UTC, то есть 10:21:49. Ровно столько на сервере и было. Edge Cloudflare на те же пакеты не ответил ни разу: два отправлено, ноль получено.
Вывод, который здесь важнее самой находки: прокси закрыл её от сканера, но не с сервера. Отчёт стал зелёным, поведение машины не изменилось.
Почему origin всё равно находят
Возражение напрашивается: адрес origin никто не публикует, значит и спрашивать некого. На практике он утекает скучными путями:
- история DNS - до подключения CDN домен месяцами указывал прямо на сервер, и это записано в сервисах пассивного DNS;
- логи прозрачности сертификатов - там видны все имена, на которые выпускались сертификаты, включая служебные поддомены;
- непроксированные записи - почта, панель, ftp, staging обычно остаются серыми, потому что через прокси они не работают;
- заголовки исходящих писем -
Receivedс адресом отправляющего хоста; - сканы всего адресного пространства по SNI - находят сервер, отвечающий вашим сертификатом, без всякого DNS.
То есть прятать origin за прокси как меру защиты рассматривать не стоит. Прокси решает другие задачи, а настройки самой машины должны быть такими, будто её адрес известен.
Почему sysctl не поможет
Первая мысль - поискать ручку в ядре, по аналогии с net.ipv4.icmp_echo_ignore_all. Вот всё, что есть по ICMP на Ubuntu 24.04:
$ sysctl -a | grep icmp
net.ipv4.icmp_echo_enable_probe = 0
net.ipv4.icmp_echo_ignore_all = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_errors_use_inbound_ifaddr = 0
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.icmp_msgs_burst = 50
net.ipv4.icmp_msgs_per_sec = 1000
net.ipv4.icmp_ratelimit = 1000
net.ipv4.icmp_ratemask = 6168
Про echo - три штуки, про timestamp - ни одной. Выключателя ответов на type 13 в ядре нет, и дискуссии об этом в списках рассылки заканчиваются одним: фильтруйте файрволом. Единственное, что относится к делу косвенно, - icmp_ratemask, но он ограничивает частоту, а не сам факт ответа.
Правило, которое закрывает
Через iptables:
iptables -A INPUT -p icmp --icmp-type timestamp-request -j DROP
iptables -A INPUT -p icmp --icmp-type timestamp-reply -j DROP
Через nftables одной строкой:
nft add rule inet filter input icmp type \
{ timestamp-request, timestamp-reply } drop
Вторую половину - про timestamp-reply - часто забывают, а сканеры её проверяют отдельно: ответ на входящий reply машина тоже может дать. В IPv6 этих типов нет вовсе, там закрывать нечего.
Где это перестаёт быть однострочником
На пустой машине правило добавляется и работает. На моём сервере всё интереснее, и это типичный случай.
Политика INPUT - ACCEPT. Файрвола как набора правил тут нет: порты закрыты тем, что на них никто не слушает, плюс фильтрация у провайдера. Добавить одно правило в такую конфигурацию можно, но оно останется единственным, и человек, который придёт через год, не поймёт, почему в пустых цепочках висит drop для type 13.
Правилами уже владеет Docker. В iptables -S полтора десятка строк в цепочках DOCKER, DOCKER-USER, DOCKER-FORWARD, которые демон переписывает при каждом старте. Свои правила надо держать там, где он их не затрёт: либо в DOCKER-USER (но она про FORWARD, а не про INPUT), либо в отдельной таблице inet со своим хуком. Второе чище: таблицы в nftables независимы, drop в любой из них побеждает, и Docker в чужую таблицу не лезет.
ufw установлен, но выключен. Соблазн «включу ufw и добавлю правило туда» здесь опасен вдвойне. Во-первых, включение ufw мгновенно меняет политику с ACCEPT на deny, и это надо делать с открытой второй сессией и пониманием, какие порты нужны. Во-вторых, ufw и Docker известно не дружат: опубликованные порты контейнеров проходят через DNAT в PREROUTING и правил ufw в INPUT не видят вовсе. То есть ufw создаст ощущение закрытого периметра, которого не будет.
Правило должно пережить перезагрузку. nftables.service на машине в состоянии disabled, iptables-persistent не стоит. Значит любое правило, добавленное руками, живёт до первого ребута - и это худший исход: в отчёте закрыто, в реальности откатилось.
Чего точно не делать
Регулярно встречающийся «фикс» - зарубить ICMP целиком: -p icmp -j DROP. Сканер замолкает, а вместе с ним ломается следующее:
- type 3 code 4, fragmentation needed - это Path MTU Discovery. Заблокировали - и соединения с клиентами за каналами с меньшим MTU начинают виснуть на больших ответах: рукопожатие проходит, страница не грузится. Диагностируется мучительно;
- type 11, time exceeded - перестаёт работать traceroute до вас, и любой разбор сетевой проблемы с вашим участием становится гаданием;
- type 8, echo - умирает внешний мониторинг по ping и проверки доступности у хостера.
Если уж закрывать, то точечно: два типа из тридцати с лишним, а не весь протокол.
Стоит ли оно вообще
Честно: сама по себе утечка часов - это Low не для галочки. Злоумышленнику она даёт немного - понимание, что машина не синхронизирована по NTP, грубый ориентир по часовому поясу и ещё один признак для определения операционной системы. Чтобы эти три миллисекунды во что-то превратились, нужно, чтобы рядом было что-то действительно времязависимое: токены с коротким окном, одноразовые пароли, подписи с проверкой свежести.
Реальный мотив закрыть обычно другой: строка в отчёте пентеста или комплаенс-скана, которую у вас попросят убрать, и спорить будет дороже, чем поправить. Это нормальная причина, просто её стоит называть своим именем.
У себя я эту находку пока не закрыл, и это осознанно. Серьёзность низкая, адрес origin нигде не опубликован, а трогать файрвол на хосте, где правилами распоряжается Docker, а ufw выключен и политика ACCEPT, между делом не стоит: такой правке нужна отдельная сессия, персистентность через nftables.service и проверка, что ничего кроме двух типов не задето. Это очередь, а не отказ.
Как проверить себя
Главное - проверять адрес сервера, а не домен, если перед вами CDN. Иначе вы проверяете настройки Cloudflare, а не свои.
# быстрый ответ: отвечает или нет
sudo nmap -sn -PP -n <ip>
# с расшифровкой часов
sudo hping3 --icmp --icmptype 13 -c 2 <ip>
Если hping3 показал три числа - сервер выдаёт своё время. Миллисекунды от полуночи UTC делятся на 1000 и переводятся в часы: 37309339 это 10:21:49.