Способы эксплуатации уязвимости Log4j

Эксплуатация уязвимости Log4j

В предыдущей статье мы говорили о мерах защиты от уязвимости Log4j. Лег­кость экс­плу­ата­ции Log4j породи­ла нас­тоящий бум в сети, все­воз­можные логи начали пух­нуть от наличия в них записей содер­жащих завет­ные ${jndi:ldap://}. Сегодня продолжим изучать эту инте­рес­ную и опас­ную, с точ­ки зре­ния пос­ледс­твий, уяз­вимость.

Еще по теме: Интересные уязвимости PostgreSQL

Эксплуатация уязвимости Log4j

Да­вайте на деле разберемся насколько опасна данная уязвимость.

Статья в образовательных целях и предназначается для пентестеров (белых хакеров). Взлом и несанкционированный доступ уголовно наказуем. Ни редак­ция spy-soft.net, ни автор не несут ответс­твен­ность за ваши действия.

RCE через Log4j

Что­бы выпол­нить про­изволь­ный код, необ­ходимо под­нять сер­вер, который передаст полез­ную наг­рузку на ата­куемую машину. При получе­нии пей­лоада Java выпол­нит десери­али­зацию и про­изой­дет вызов ука­зан­ных клас­сов, что в ито­ге при­ведет к запус­ку про­изволь­ной коман­ды.

Для авто­мати­зации экс­плу­ата­ции такого вида атак написа­но мно­жес­тво ути­лит — ysoserial, marshalsec, rogue-jndi.

Возь­мем rogue-jndi для раз­нооб­разия.

Ком­пилиру­ем ути­литу с помощью maven.

И запус­каем ее, в качес­тве аргу­мен­та command ука­зыва­ем коман­ду, которую хотим выпол­нить.

За­пуск ути­литы RogueJndi для экс­плу­ата­ции уяз­вимос­ти CVE-2021-44228 в log4j
За­пуск ути­литы RogueJndi для экс­плу­ата­ции уяз­вимос­ти CVE-2021-44228 в log4j

В сос­таве идет нес­коль­ко пей­лоадов, нас инте­ресу­ет RemoteReference. Это клас­сичес­кая ата­ка через JNDI, которая ведет к RCE через уда­лен­ную заг­рузку клас­сов.

Ука­зыва­ем адрес в теле нашего пей­лоада.

Ус­пешная экс­плу­ата­ция уяз­вимос­ти CVE-2021-44228 в log4j
Ус­пешная экс­плу­ата­ция уяз­вимос­ти CVE-2021-44228 в log4j

Ес­ли вы не уви­дели каль­кулятор, то поз­драв­ляю, у вас новая Java. Дело в том, что в вер­сии выше 8u191 по дефол­ту зап­рещена уда­лен­ная заг­рузка клас­сов. Одна­ко, это не меша­ет экс­плу­ати­ровать уяз­вимость, исполь­зуя локаль­ные цепоч­ки гад­жетов. Java — язык биб­лиотек и фрей­мвор­ков и ред­ко встре­чают­ся ситу­ации где исполь­зует­ся чис­тый код на Java.

Эксплуатация Log4j в Spring Boot RCE на Java

Рас­смот­рим популяр­ный фрей­мворк Spring. Уже готовое уяз­вимое при­ложе­ние мож­но взять из репози­тория log4shell-vulnerable-app.

За­пус­каем его при помощи gradle.

В качес­тве пей­лоада выбира­ем Tomcat. Для экс­плу­ата­ции исполь­зует небезо­пас­ный reflection в клас­се:

Этот класс из Tomcat содер­жит логику для соз­дания Beans с помощью реф­лексии. Если вы хотите почитать под­робнее об этой тех­нике, то доб­ро пожало­вать в статью Миха­ила Сте­пан­кина (Michael Stepankin) Exploiting JNDI Injections in Java.

В резуль­тате получа­ется такой пей­лоад:

От­прав­ляем его в качес­тве заголов­ка X-Api-Version.

И вуаля, наб­люда­ем каль­кулятор.

Экс­плу­ата­ция Log4j
Экс­плу­ата­ция Log4j

Другие способы эксплуатации Log4j

Вы­пол­нение кода — это конеч­но замеча­тель­но, но уяз­вимость и без это­го сулит мно­го проб­лем.

Да­вайте вспом­ним какое количес­тво резол­веров, помимо jndi, были объ­явле­ны в variableResolver:

Все их мож­но исполь­зовать таким же обра­зом. Рас­смот­рим, нап­ример, резол­вер env. Он поз­воля­ет получать дос­туп к перемен­ным окру­жения.

Поп­робу­ем что‑нибудь безобид­ное, нап­ример ${env:OS}.

По­луче­ние дос­тупа к перемен­ным окру­жения через log4j
По­луче­ние дос­тупа к перемен­ным окру­жения через log4j

Это хорошо, но как нам получить зна­чение этой перемен­ной уда­лен­но?

Да­вайте вер­немся к методу substitute. Раз­берем перемен­ную substitutionInVariablesEnabled.

По дефол­ту она уста­нов­лена в true — зна­чит, что конс­трук­ции ${} могут сами быть динами­чес­кими, то есть содер­жать дру­гие перемен­ные.

Та­ким обра­зом при пар­синге про­веря­ется истинность substitutionInVariablesEnabled и если зна­чение истинно, находит­ся начало еще одной конс­трук­ции ${. Ее позиция записы­вает­ся, а зна­чение nestedVarCount инкре­мен­тиру­ется.

Пос­ле того как вся стро­ка обра­бота­на, про­исхо­дит рекур­сивный вызов метода substitute начиная с самой глу­боко вло­жен­ной перемен­ной.

То есть пре­обра­зова­ние конс­трук­ции вида ${${${var}}} начина­ется с перемен­ной ${var}. Такое поведе­ние откры­вает для ата­кующе­го прос­то колос­саль­ное количес­тво раз­личных век­торов для ата­ки.

С эти­ми зна­ниями воз­вра­щаем­ся к нашему воп­росу: как получить зна­чение перемен­ной уда­лен­но?

Пер­вое, что при­ходит в голову, — это прос­то передать дан­ные в URI или в качес­тве парамет­ров на наш сер­вер. Давайте поп­робу­ем это сде­лать. Для получе­ния нуж­ной информа­ции необ­ходимо передать валид­ное LDAP-при­ветс­твие кли­енту, в качес­тве которо­го выс­тупа­ет уяз­вимая машина.

Сэмули­руем его, прос­то передав нуж­ную байт‑стро­ку через echo:

В пей­лоад добавим нуж­ную перемен­ную окру­жения.

Пе­реда­ча перемен­ных окру­жения на сер­вер ата­кующе­го
Пе­реда­ча перемен­ных окру­жения на сер­вер ата­кующе­го

Прос­тор для твор­чес­тва здесь колос­саль­ный. Нап­ример, мож­но передать клю­чи дос­тупа от AWS.

Да­же если на уда­лен­ной машине зап­рещены TCP-кон­некты, чаще все­го DNS зап­росы ходят нор­маль­но. В таком слу­чае пей­лоад при­обре­тает сле­дующий вид.

От­прав­ка содер­жимого перемен­ной окру­жения через DNS-зап­рос
От­прав­ка содер­жимого перемен­ной окру­жения через DNS-зап­рос

Манипуляции с пейлоадом и обходы WAF

Те­перь сто­ит ска­зать пару слов о раз­личных тех­никах обхо­дах WAF.

В пер­вую оче­редь обра­тим вни­мание на обра­бот­ку пре­фик­са для опре­деле­ния резол­вера.

При выпол­нении фун­кции toLowerCase все сим­волы, про­ходя­щие через нее, при­водят­ся к ука­зан­ным реги­ональ­ным нас­трой­кам (Locale.US). Это поз­воля­ет брать похожие литеры из дру­гих алфа­вит­ных сис­тем и они будут пре­обра­зова­ны в мак­сималь­но под­ходящие англий­ские. Тех­ника называ­ется Best-fit Mappings.

Та­кой век­тор тоже отлично отра­баты­вает.

Ис­поль­зование похожих сим­волов из дру­гих алфа­витов при экс­плу­ата­ции уяз­вимос­ти в log4j
Ис­поль­зование похожих сим­волов из дру­гих алфа­витов при экс­плу­ата­ции уяз­вимос­ти в log4j

По­мимо это­го, воз­можность исполь­зовать вло­жен­ные перемен­ные откры­вает широкий прос­тор для соз­дания уни­каль­ных пей­лоадов. Пос­мотрим на резол­веры lower и upper.

Как вы поняли из наз­вания, они пре­обра­зуют текст в ниж­ний и вер­хний регистр соот­ветс­твен­но. Мож­но ука­зывать один или нес­коль­ко сим­волов для тран­сфор­мации. Исполь­зуя эти резол­веры, полез­ную наг­рузку мож­но тран­сфор­мировать в сле­дующий вид:

Толь­ко не исполь­зуйте вер­хний регистр в схе­ме ( ldap), так как она регис­тро­зави­сима и LDAP://127.0.0.1 уже не при­ведет кон­нект к вашему сер­веру.

Те­перь еще раз вер­немся к про­вер­ке зна­чения перемен­ной в методе substitute.

Здесь, конс­трук­ция valueDelimiterMatcher.isMatch про­веря­ет содер­жимое перемен­ной на наличие дво­ето­чия и минуса. Они исполь­зуют­ся для того, что­бы ука­зать дефол­тное зна­чение, если резол­вер вер­нет false. Нап­ример, перемен­ная окру­жения, которую мы зап­рашива­ем, не сущес­тву­ет.

Переда­ча перемен­ной с дефол­тным зна­чени­ем име­ет сле­дующий фор­мат.

Мой при­мер с несущес­тву­ющей перемен­ной окру­жения будет выг­лядеть как‑то так:

При­чем количес­тво дво­ето­чий во вто­ром слу­чае может быть любым.

Ука­зание дефол­тных зна­чений в стро­ковых перемен­ных ${}
Ука­зание дефол­тных зна­чений в стро­ковых перемен­ных ${}

Так­же мож­но сов­сем не ука­зывать резол­вер. Тог­да метод resolveVariable поп­робу­ет при­менить резол­вер по умол­чанию — MapLookup. И если он вер­нет null, то будет исполь­зовать­ся дефол­тное зна­чение, которое мы переда­ли.

Де­фол­тное зна­чение перемен­ной без исполь­зования резол­вера в log4j
Де­фол­тное зна­чение перемен­ной без исполь­зования резол­вера в log4j

Ров­но такое же поведе­ние будет при несущес­тву­ющем резол­вере.

Де­фол­тное зна­чение перемен­ной при исполь­зовании несущес­тву­юще­го резол­вера в log4j
Де­фол­тное зна­чение перемен­ной при исполь­зовании несущес­тву­юще­го резол­вера в log4j

Тог­да ата­кующий пей­лоад может при­обре­тать сов­сем безум­ный вид.

И такое тоже успешно отра­баты­вает.

Экс­плу­ата­ция уяз­вимос­ти в log4j при помощи обфусци­рован­ной полез­ной наг­рузки
Экс­плу­ата­ция уяз­вимос­ти в log4j при помощи обфусци­рован­ной полез­ной наг­рузки

Бло­киро­вать что‑то подоб­ное WAF-сис­темами, которые осно­ваны на регуляр­ках, сам понима­ете, занятие не из при­ятных.

Патчи для уязвимости Log4j

Пришло вре­мя погово­рить о зап­латках для уяз­вимос­ти.

Пер­вое, что рекомен­довали, — это уста­новить флаг formatMsgNoLookups или перемен­ную окру­жения LOG4J_FORMAT_MSG_NO_LOOKUPS в true, что­бы перемен­ные в логиру­емых событи­ях не обра­баты­вались. Такое решение под­ходит для Log4j вер­сий 2.10 и выше.

Фикс уяз­вимос­ти в Log4j при помощи фла­га formatMsgNoLookups
Фикс уяз­вимос­ти в Log4j при помощи фла­га formatMsgNoLookups

Это дей­стви­тель­но помога­ет, толь­ко вот далеко не всег­да. При­чина в том, что, в Log4j все еще сущес­тву­ют мес­та в коде, где может про­исхо­дить обра­бот­ка перемен­ных в логиру­емых событи­ях.

Нап­ример, если при­ложе­ние исполь­зует конс­трук­ции вида Logger.printf(level, "%s", userInput) или свой кас­томный класс для логиро­вания, где не реали­зует­ся StringBuilderFormattable.

Об­ход фик­са уяз­вимос­ти в Log4j. Успешная экс­плу­ата­ция с уста­нов­ленным фла­гом formatMsgNoLookups
Об­ход фик­са уяз­вимос­ти в Log4j. Успешная экс­плу­ата­ция с уста­нов­ленным фла­гом formatMsgNoLookups

Мо­гут сущес­тво­вать и дру­гие век­торы атак, так что исполь­зовать этот метод фик­са не рекомен­дует­ся.

Пер­вый офи­циаль­ный патч появил­ся в вер­сии 2.15. В ней воз­можность исполь­зовать перемен­ные в сооб­щени­ях по дефол­ту отклю­чили, но в кон­фигах это по‑преж­нему работа­ет. Для JNDI-кон­нектов был вве­ден механизм белых спис­ков, который по умол­чанию раз­реша­ет толь­ко localhost.

Если исполь­зует­ся кас­томный шаб­лон логиро­вания, где поль­зователь­ские дан­ные каким‑то обра­зом попада­ют в Thread Context Map (MDC), то экс­плу­ата­ция уяз­вимос­ти все еще воз­можна.

Рас­смот­рим на при­мере. Возь­мем форк репози­тория log4shell-vulnerable-app Кая Мин­дерма­на (Kai Mindermann).

Рас­коммен­тируйте стро­ку в build.gradle, что­бы исполь­зовать новую вер­сию Log4j.

В этом фор­ке нем­ного из­менен шаб­лон логиро­вания.

И метод логиро­вания поль­зователь­ских дан­ных.

Те­перь зна­чение из заголов­ка X-Api-Version переда­ется в перемен­ную apiversion через ThreadContext. В таком слу­чае экс­плу­ата­ция все так же воз­можна. Единс­твен­ное, что оста­нав­лива­ет от пол­ноцен­ного RCE, — это огра­ниче­ние кон­нектов через JNDI толь­ко к локаль­ным адре­сам. Но сво­еоб­разный LCE (Local Code Execution :-)) все еще мож­но при­менять, нап­ример, как век­тор для под­нятия при­виле­гий.

Об­ход фик­са уяз­вимос­ти в Log4j 2.15. Успешная экс­плу­ата­ция через ThreadContext
Об­ход фик­са уяз­вимос­ти в Log4j 2.15. Успешная экс­плу­ата­ция через ThreadContext

Эта воз­можность экс­плу­ата­ции получи­ла отдель­ный иден­тифика­тор CVE-2021-45046.

Пос­ле того, как раз­работ­чики поняли, что патч получил­ся не сов­сем удач­ным, выш­ла оче­ред­ная вер­сия Log4j — 2.16. Казалось бы, на этот раз все дол­жно быть исправ­лено как нуж­но.

Но прис­таль­ное вни­мание со сто­роны иссле­дова­телей быс­тро дало свои пло­ды — нашел­ся спо­соб выз­вать отказ в обслу­жива­нии. Эта уяз­вимость сно­ва получа­ет свой иден­тифика­тор CVE-2021-45105. Экс­плу­ата­ция сно­ва воз­можна толь­ко ког­да исполь­зует­ся нес­тандар­тные шаб­лоны логиро­вания.

Об­ратим­ся все к тому же фор­ку log4shell-vulnerable-app. Здесь шаб­лон уяз­вим и для этой ата­ки.

Ес­ли ата­кующий передаст ${ctx:apiversion}, это вызовет бес­конеч­ные попыт­ки пре­обра­зова­ния перемен­ной и в работе при­ложе­ния про­изой­дет исклю­чение.

Об­новля­ем вер­сию Log4j в кон­фиге и тес­тиру­ем уяз­вимость.

Экс­плу­ата­ция DoS уяз­вимос­ти CVE-2021-45046 в Log4j 2.16.0
Экс­плу­ата­ция DoS уяз­вимос­ти CVE-2021-45046 в Log4j 2.16.0

В оче­ред­ном бил­де — вер­сии 2.17 — основные проб­лемы, кажет­ся, закон­чились. Была обна­руже­на еще одна уяз­вимость с иден­тифика­тором CVE-2021-44832, но усло­вия для ее успешной экс­плу­ата­ции доволь­но суровы.

Ата­кующе­му нужен дос­туп для изме­нения кон­фигура­ций логиро­вания. В таком слу­чае он может сге­нери­ровать кон­фигура­цию, где мож­но выпол­нить про­изволь­ный код через JDBC Appender с источни­ком дан­ных, ссы­лающим­ся на JNDI URI. Об этом я, пожалуй, рас­ска­жу как‑нибудь в дру­гой раз, а вы ско­рее обновляйтесь на самую пос­леднюю вер­сию log4j 2.17.1.

Заключение

Log4Shell по пра­ву стал самой горячей уяз­вимостью ухо­дяще­го года. Думаю, что мы еще дол­гое вре­мя будем видеть на прос­торах баг­баун­ти репор­ты где эта проб­лема всплы­вает в самых неожи­дан­ных мес­тах.

Уди­витель­но, как баг с таким прос­тым век­тором экс­плу­ата­ции оста­вал­ся в тени широкой общес­твен­ности целых восемь лет, ведь пер­вая уяз­вимая вер­сия Log4j 2.0 beta9 была выпуще­на аж в кон­це сен­тября 2013 года.

Для нас это оче­ред­ное напоми­нание о том, что иног­да дос­таточ­но лишь прис­таль­нее приг­лядеть­ся к коду, который у всех на виду, что­бы обна­ружить неч­то инте­рес­ное.

Еще по теме:

ВКонтакте
OK
Telegram
WhatsApp
Viber

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *