
CVE-2020-2551 POC для использования в Интернете
Тестовый POC (можно использовать для внешнего тестирования)
python CVE-2020-2551.py [HOST] [IP]
Попытка изменить codebase для удалённой загрузки классов (провал)
python CVE-2020-TEST.py [HOST] [IP]
(Впервые опубликовано на Anquanke, ссылка на оригинал)
Для изучения этой уязвимости требуются некоторые предварительные знания, например CORBA и RMI.
Кратко опишу:
CORBA — это технический стандарт, разработанный OMG, для распределённых приложений; в нём используется IDL для обеспечения кросс-языковой поддержки, а взаимодействие между клиентом и сервером осуществляется по протоколу IIOP.
RMI — это другая технология распределённых приложений; в Java её можно упростить с помощью JNDI. Клиент и сервер общаются по протоколу JRMP, однако в WebLogic RMI использует протокол T3, о котором ранее уже было немало уязвимостей.
RMI-IIOP объединяет преимущества RMI и CORBA, позволяя развёртывать RMI-приложения через протокол IIOP.
В официальной документации также упоминается:
Серверные объекты RMI могут использовать протокол IIOP и взаимодействовать с CORBA-клиентскими объектами, написанными на любом языке.
Пока оставим WebLogic в стороне и сосредоточимся на том, как написать пример RMI-IIOP.
Код клиента можно посмотреть в тестовом проекте из статьи Заметки о RMI, JNDI, LDAP, JRMP, JMX, JMS в Java (часть 1). Можно самостоятельно скомпилировать HelloClient и HelloServer, либо использовать уже скомпилированные версии из тестового проекта.
Запустите сервер имён в командной строке (входит в состав Java):
start orbd -ORBInitialPort 1050
Запустите серверный HelloServer из командной строки и настройте удалённую отладку. О том, как настроить удалённую отладку в IDEA, можно посмотреть в методе, описанном в начале этой статьи.
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 HelloServer
Конечно, можно не использовать удалённую отладку, а просто запустить и посмотреть результат:
java HelloServer
Запустите клиент в командной строке:
Java HelloClient
В этот момент появится калькулятор; при успешной удалённой отладке можно увидеть следующий стек вызовов:
Команды выполняются в EvilMessage.readObejct():

Кстати, статьи по установке и отладке WebLogic.
Что насчёт RMI-IIOP в WebLogic? В статье О RMI-IIOP в Java упоминается эксплуатация RMI-IIOP в WebLogic; на её основе я провёл некоторые исследования. Using WebLogic’s RMI over IIOP описывает несколько способов использования WebLogic в качестве клиента RMI-IIOP, включая:
Разница между первыми двумя способами, судя по всему, лишь в настройке JNDI_FACTORY.

Ранее при изучении T3-десериализации в WebLogic я разворачивал приложение Helloserver, у которого был доступный метод sayhello(). Я попробовал использовать оба JNDI_FACTORY и через второй JNDI_FACTORY успешно вызвал метод sayHello().

Тогда я немного изменил POC для протокола T3 в WebLogic — фактически просто заменил RMI на IIOP — и обнаружил, что цепочка эксплуатации jtaTransactionManager успешно сработала и отправила jrmp-запрос локальному jrmplisten.

Посмотрим на трафик: при вызове метода remove() отправляется запрос remove__java_lang_Object, в трафике есть вредоносные данные, но магического заголовка aced не найдено.

Предполагаю, что на сервере данные сначала проходят специальный разбор, а затем десериализуются. Если взглянуть на стек вызовов, видно, что вторая половина цепочки выполнения очень похожа на цепочку из ранее рассмотренного нативного RMI-IIOP: в том случае запуск происходил из CDRInputStream.read_value(), а здесь — из IIOPInputStream.read_value() в WebLogic (эта точка read_value также упоминалась в докладе 2019 года).

Здесь запрос сначала обрабатывается в clusterableServerRef.invoke(), который в зависимости от типа invoker вызывает this.invoker.invoke(). В данном случае вызывается Mejb_dj5nps_HomeImpl_WLSkel.invoke(), и поскольку операция — "remove", выполняется ветвь case 6, где вызывается IIOPInputStream.readObject(); в методе read_value() разбираются данные IIOPInputStream и запускается десериализация. Это и есть POC с использованием метода remove().
В [статье Lucifaer](https://lucifaer.com/2020/02/25/WebLogic WLS核心组件RCE分析(CVE-2020-2551)/?from=timeline&isappinstalled=0#2-2-Weblogic解析流程) упоминается использование метода bind() — это также основной способ эксплуатации в интернете. Проследим стек вызовов.

Как и раньше, запрос сначала обрабатывается в clusterableServerRef.invoke(), который в зависимости от типа invoker вызывает this.invoker.invoke(); в данном случае вызывается CobraServerRef.invoke(), а затем в _NamingContextAnyImplBase._invoke(), поскольку va1 равно "bind_any", выполняется ветвь case 0, где вызывается IIOPInputStream.read_any(); далее снова вызывается IIOPInputStream.read_value(), что запускает десериализацию. Ранее я говорил, что в трафике не видно магического заголовка aced — это потому, что в IIOPInputStream есть собственный способ разбора. Hex-value форма IIOPInputStream выглядит следующим образом и содержит имя класса и информацию о полях:

В итоге вызывается метод readObejct() вредоносного класса.

Посмотрел на патч и обнаружил, что он находится в том же месте, что и патч для эксплуатации T3-десериализации в 2015 году.

В тестах из анализа уязвимости WebLogic CVE-2020-2551 видно, что позиция фильтрации классов для CVE-2020-2551 также находится в классе weblogic.iiop.Utils.

Однако при локальном тестировании WebLogic 10.3.6 с установленным патчем 2015 года функция isBlacklisted() не сработала (хотя в MsgAbbrevInputStream и InboundMsgAbbrev есть вызовы isBlacklisted() для проверки чёрного списка... странно).

Патч для CVE-2020-2551 добавляет метод verifyclassermitted() для фильтрации в weblogic.iiop.Utils.LoadClass().

Чёрный список отфильтровывает вредоносные классы, включая родительский класс JtaTransactionManager — com.bea.core.repackaged.springframework.transaction.support.AbstractPlatformTransactionManager. Этот класс входит в состав WebLogic и очень опасен. При просмотре патча у меня возникла мысль: поскольку проверка в строке 606 выполняется после LoadClass(), то если при загрузке className произойдёт загрузка класса и выполнение вредоносного статического блока, разве это не позволит обойти защиту? Об этом поговорим позже.
POC, написанный на Java, имеет сетевые проблемы: работает только против локального сервиса WebLogic, но не работает против docker-контейнера или машины во внешней сети. Статьи с анализом этой проблемы:
Пошаговое руководство по решению сетевых проблем POC для WebLogic CVE-2020-2551
Обсуждение WebLogic-CVE-2020-2551
Далее займёмся отладкой POC. Можно опираться на предыдущий вариант с remove или на версию от Y4er. В двух упомянутых выше статьях предлагаются два способа решения:
Я попробовал оба варианта. После пересборки WebLogic возникает ошибка java.lang.NoSuchMethodError:weblogic.security.subject.SubjectManager.installCESubjectManager, но решения я так и не нашёл.
Поэтому я решил имитировать протокол IIOP. Сначала поставил точку останова в POC для отладки.

Обнаружил, что при вызове new InitialContext(env) в EndPointImpl.sendReceive() отправляются и принимаются два пакета.

LocateReply содержит информацию IOR. Здесь нужно понимать, что такое IOR. Его роль — предоставлять host и port, необходимые для IIOP-взаимодействия между RMI-IIOP клиентом и серверным объектом. Кроме того, Object_key в красной рамке используется для различения разных объектов на сервере.

При имитации протокола IIOP ключевое внимание нужно уделить Object_key; host и ip на самом деле не влияют. Когда я начинал тестирование, просто повторно воспроизводил все пакеты; при отправке resolve_any возвращался location forward.

В официальной документации GIOP говорится, что location forward означает, что Object_key меняется: при разных запросах возвращаемый Object_key может быть разным (Object_key здесь — это key address в пакете данных). Как уже упоминалось, этот Object_key при использовании протокола IIOP служит для определения того, с каким объектом ведётся связь, и его значение нужно динамически получать из LocateReply.

В итоге я решил имитировать не метод remove(), а IIOP-запрос, отправляемый методом bind(), поскольку таких запросов меньше. Посмотрим на пакет данных при локальной успешной эксплуатации.

Отправляем LocateRequest, получаем data и через регулярное выражение извлекаем key address из LocateReply.

Вручную задаю адрес вредоносного jrmp-сервера (rmi://...) и отправляю пакеты bind_any с интервалом в 1 секунду. Поскольку цепочка эксплуатации здесь отправляет jrmp-запрос не через DGCClient, на неё не влияет JEP290, и её можно эксплуатировать через jrmplisten.

POC успешно протестирован в среде docker. Можно использовать vulhub с окружением SSRF, указав IP хост-машины; docker успешно получает jrmp-запрос от хост-машины. Код выложен на Github.

Ранее упоминалась идея использования codebase для загрузки удалённого кода в обход детектирования. Тем, кто изучал JNDI-атаки, должно быть известно, что codebase можно использовать для указания местоположения удалённых классов. Если codebase контролируем и программа разрешает удалённую загрузку классов, то можно загрузить удалённый вредоносный класс и выполнить вредоносный код в статическом блоке.
Из чтения кода видно, что второй параметр weblogic.iiop.Utils.lodaClass() — это codebase; этот параметр считывается в IIOPInputStream.read_value(), это параметр var8. На строке 1659 вызывается readIndirectingRepositoryId(var8), который в итоге вызывает weblogic.iiop.Utils.lodaClass(). Чтобы выполнились строки 1644 и 1659, необходимо (va4r & 1)=1 и (va4 & 6)=2, поэтому значение var4 должно быть равно 3.
Стек вызовов от readIndirectingRepositoryId до getClassFromId, в конце которого выполняется loadclass() на строке 304.

Посмотрим на пакет bind_any: он состоит из GIOP Header и GIOP Request. GIOP Request также включает key address (совпадает с LocateReply), ServiceContextList и stub_data. Значение var4 — это \x7f\xff\xff\x02 из stub_data, поэтому (va4r & 1)=0, (va4 & 6)=2, и строка 1644, задающая codebase, не выполняется.

Мы модифицируем первый выделенный блок \x7f\xff\xff\x02 на \x00\x00\x00\x03, а во втором блоке добавляем длину и значение codebase. Конкретный код находится на Github. Можно заметить, что там выполняется ещё и операция выравнивания — это подводный камень. Дело в том, что перед чтением последующей информации о классе проверяется, является ли позиция следующего байта кратной 4; если нет, часть байтов пропускается. Например, если позиция следующего байта равна 1, будут пропущены 3 байта, и чтение начнётся с байта на позиции 4. Позиция здесь отсчитывается относительно всего пакета bind_any; если позиция не кратна 4, выполняется дополнение нулями.

Есть ещё одна проблема. Посмотрим на предыдущий стек вызовов от readIndirectingRepositoryId до getClassFromId: между ними находится функция findClassInfo(). Если какой-то класс уже был загружен, информация об ID класса сохраняется, и при вызове findClassInfo() сразу возвращается информация о классе, без входа в weblogic.iiop.Utils.getClassFromID().

Поэтому при тестировании каждый раз приходится менять имя класса.

Как бы то ни было, в итоге удалось успешно имитировать протокол IIOP, изменить значение codebase и выполнить функцию weblogic.iiop.Utils.getClassFromId().
К сожалению, при получении RMIURLClassFinder возвращается NULL, а функция RMIEnvironment.getEnvironment().isNetworkClassLoadingEnabled() возвращает false.

Причина в том, что параметр _NetworkClassLoadingEnable в ServerMBeanImpl имеет значение False.

Хотел посмотреть, в каком конфигурационном файле WebLogic задаётся этот параметр, но так и не нашёл...
На самом деле в процессе изучения этой уязвимости я понял, что требуется много предварительных знаний, таких как Java-десериализация, RMI, JNDI и тому подобное. Для изучения можно обратиться к этому тематическому разделу статей. Хотя в итоге эксплуатация с изменением codebase провалилась, я всё равно вынес много полезного.