В прошлой части нашего цикла мы рассмотрели работу протоколов семейства NTLM, отметив ряд их существенных недостатков, которые невозможно решить в рамках протокола. Поэтому вместе с Active Directory на смену им пришел более совершенный протокол Kerberos, который продолжает успешно применяться до сих пор. В данном материале мы рассмотрим общие принципы функционирования данного протокола, что позволит получить начальные знания в этой области самым широким массам читателей.
Онлайн-курс по устройству компьютерных сетей
На углубленном курсе «Архитектура современных компьютерных сетей» вы с нуля научитесь работать с Wireshark и «под микроскопом» изучите работу сетевых протоколов. На протяжении курса надо будет выполнить более пятидесяти лабораторных работ в Wireshark.
Протокол Kerberos был разработан в Массачусетском технологическом институте (MIT) в рамках проекта «Афина» в 1983 году и долгое время использовался в качестве образовательного, пока версия 4 не была сделана общедоступной. В настоящий момент принята в качестве стандарта и широко используется следующая версия протокола Kerberos 5.
Основным недостатком протокола NTLM служит то, что он не предусматривает взаимную аутентификацию клиента и сервера, это во многом обусловлено тем, что протокол изначально разрабатывался для небольших сетей, где все узлы считаются легитимными. Несмотря на то, что в последних версиях протокола сделаны серьезные улучшения безопасности, но направлены они в основном на усиление криптографической стойкости, не устраняя принципиальных недостатков.
В доменных сетях протоколы NTLM вызывают повышенную нагрузку на контроллеры домена, так как всегда обращаются к ним для аутентификации пользователя. При этом также отсутствует взаимная идентификация узлов и существует возможность накопления пакетов для последующего анализа и атаки с их помощью.
В отличии от NTLM Kerberos изначально разрабатывался с условием, что первичная передача информации будет производиться в открытых сетях, где она может быть перехвачена и модифицирована. Также протокол предусматривает обязательную взаимную аутентификацию клиента и сервера, а взаимное доверие обеспечивает единый удостоверяющий центр, который обеспечивает централизованную выдачу ключей.
Перед тем как продолжить, следует прийти к соглашению по поводу используемых в статье терминов. Так под термином клиент будет рассматриваться любой узел сети, обращающийся к любому иному узлу — серверу — с целью доступа к любому из его ресурсов.
Основой инфраструктуры Kerberos является Центр распространения ключей (Key Distribution Center, KDC), который является доверенным центром аутентификации для всех участников сети (принципалов). Область Kerberos (Realm) — пространство имен для которых данный KDC является доверенным, как правило это область ограниченная пространством имен домена DNS, в Active Directory область Kerberos совпадает с доменом AD. Область Kerberos записывается в виде соответствующего ему доменного имени DNS, но в верхнем регистре. Принципалом или учетной записью Kerberos является любой участник отношений безопасности: учетная запись пользователя, компьютер, сетевая служба и т.д.
Центр распространения ключей содержит долговременные ключи для всех принципалов, в большинстве практических реализаций Kerberos долговременные ключи формируются на основе пароля и являются так называемым «секретом для двоих». С помощью такого секрета каждый из его хранителей может легко удостовериться, что имеет дело со своим напарником. При этом долговременные ключи не при каких обстоятельствах не передаются по сети и располагаются в защищенных хранилищах (KDC), либо сохраняются только на время сеанса.
Для принципалов, которые не являются членами домена AD, могут использоваться специальные keytab-файлы, которые содержат сведения о клиенте, домене и его долговременный ключ. В целях безопасности keytab-файл нельзя передавать по незащищенным каналам, а также следует обеспечить его безопасное хранение у принципала.
В структуре Active Directory центр распространения ключей располагается на контроллере домена, но не следует путать эти две сущности, каждая из них является самостоятельной и выполняет свои функции. Так Kerberos отвечает только за аутентификацию клиентов, т.е. удостоверяет, что некто является именно тем, за кого себя выдает. Авторизацией, т.е. контролем прав доступа, занимается контроллер домена, в свою очередь разрешая или ограничивая доступ клиента к тому или иному ресурсу.
Рассмотрим каким образом происходит аутентификация клиента по протоколу Kerberos.
Желая пройти проверку подлинности в сети, клиент передает KDC открытым текстом свое имя, имя домена и метку времени (текущее время клиента), зашифрованное долговременным ключом клиента. Метка времени в данном случае выступает в роли аутентификатора — определенной последовательности данных, при помощи которой узлы могут подтвердить свою подлинность.
Получив эти данные KDC извлекает долговременный ключ данного пользователя и расшифровывает метку времени, которую сравнивает с собственным текущим временем, если оно отличается не более чем на 5 минут (значение по умолчанию), то метка считается действительной. Эта проверка является дополнительной защитой, так как не позволяет использовать для атаки перехваченные и сохраненные данные.
Убедившись, что метка времени действительна KDC выдает клиенту сеансовый ключ и билет (тикет) на получение билета (ticket granting ticket, TGT), который содержит копию сеансового ключа и сведения о клиенте, TGT шифруется с помощью долговременного ключа KDC и никто кроме него билет расшифровать не может. Сеансовый ключ шифруется с помощью долговременного ключа клиента, а полученная от клиента метка времени возвращается обратно, зашифрованная уже сеансовым ключом. Билет на получение билета является действительным в течении 8 часов или до завершения сеанса пользователя.
Клиент в первую очередь расшифровывает сеансовый ключ, затем при помощи этого ключа метку времени и сравнивает ее с той, что он отправил KDC, если метка совпала, значит KDC тот, за кого себя выдает, так как расшифровать метку времени мог только тот, кто обладает долговременным ключом. В этом случае клиент принимает TGT и помещает его в свое хранилище.
Чтобы лучше понять этот механизм приведем небольшой пример. Если злоумышленник перехватил посланный KDC запрос, то он может на основе открытых данных послать клиенту поддельный сеансовый ключ и TGT, но не сможет расшифровать метку времени, так как не обладает долговременным ключом. Точно также, он может перехватить отправленные клиенту TGT и сеансовый ключ, но также не сможет расшифровать последний, не имея долговременного ключа. Перехватить долговременный ключ он не может, так как они по сети не передаются.
Успешно пройдя аутентификацию, клиент будет располагать сеансовым ключом, которым впоследствии он будет шифровать все сообщения для KDC и билетом на получение билета (TGT).
Теперь рассмотрим ситуацию, когда клиент хочет обратиться к серверу.
Для этого он снова обращается к KDC и посылает ему билет на получение билета, зашифрованную сеансовым ключом метку времени и имя сервера. Прежде всего KDC расшифровывает предоставленный ему TGT и извлекает оттуда данные о клиенте и его сеансовый ключ, обратите внимание, что сам KDC сеансовые ключи не хранит. Затем сеансовым ключом он расшифровывает данные от клиента и сравнивает метку времени с текущим. Если расхождения нет, то KDC формирует общий сеансовый ключ для клиента и сервера.
Теоретически теперь данный ключ следует передать как клиенту, так и серверу. Но KDC имеет защищенный канал и установленные доверительные отношения только с клиентом, поэтому он поступает по-другому. Экземпляр сеансового ключа для клиента он шифрует сессионным ключом, а копию сеансового ключа для сервера он объединяет с информацией о клиенте в сеансовый билет (session ticket), который шифрует закрытым ключом сервера, после чего также отправляет клиенту, дополнительно зашифровав сессионным ключом.
Таким образом клиент получает сессионный ключ для работы с сервером и сессионный билет. Получить содержимое билета, как и TGT, он не может, так как не располагает нужными долговременными ключами.
Теперь, имея новый ключ и билет, клиент обращается непосредственно к серверу:
Он предъявляет ему сеансовый билет и метку времени, зашифрованную сессионным ключом. Сервер расшифровывает билет, извлекает оттуда свой экземпляр ключа и сведения о клиенте, затем расшифровывает метку времени и сравнивает ее с текущим. Если все нормально, то он шифрует полученную метку своим экземпляром сессионного ключа и посылает назад клиенту. Клиент расшифровывает ее своим сеансовым ключом и сравнивает с тем, что было послано серверу. Совпадение данных свидетельствует о том, что сервер тот, за кого себя выдает.
Как можно заметить, сеансовые ключи никогда не передаются по незащищенным каналам и не передаются узлам, с которыми нет доверительных отношений. У KDC нет доверительных отношений с сервером, и он не может передать ему сессионный ключ, так как нет уверенности, что ключ будет передан кому надо. Но у него есть долговременный ключ этого сервера, которым он шифрует билет, это гарантирует, что никто иной не прочитает его содержимое и не получит сессионный ключ.
Клиент имеет билет и свой экземпляр ключа, доступа к содержимому билета у него нет. Он передает билет серверу и ждет ответ в виде посланной метки времени. Сервера, как и KDC, не хранят сеансовые ключи, а, следовательно, расшифровать метку времени сервер может только в том случае, если сможет расшифровать билет и получить оттуда сеансовый ключ, для чего нужно обладать долговременным ключом. Получив ответ и расшифровав его, клиент может удостоверить подлинность сервера, так как прочитать аутентификатор и извлечь из него метку времени сервер сможет только при условии расшифровки билета и получения оттуда сеансового ключа.
Несмотря на то, что мы рассмотрели крайне упрощенную модель протокола Kerberos, надеемся, что данная статья поможет устранить пробелы и получить первоначальные знания, которые затем можно расширить и углубить уже осмысленно подойдя к прочтению более серьезных материалов.
Онлайн-курс по устройству компьютерных сетей
На углубленном курсе «Архитектура современных компьютерных сетей» вы с нуля научитесь работать с Wireshark и «под микроскопом» изучите работу сетевых протоколов. На протяжении курса надо будет выполнить более пятидесяти лабораторных работ в Wireshark.
Сегодня мы с вами рассмотрим протокол аутентификации Kerberos. Пройдемся по самому протоколу и рассмотрим его работу в рамках Microsoft Active Directory.
Kerberos – протокол взаимной аутентификации сервера и клиента, основан на использовании симметричных ключей и может использоваться в незащищенной среде. По сути, протокол выполняет роль третьей стороны, благодаря которой клиент и сервер могут однозначно идентифицировать друг друга.
История
Самая первая версия протокола была разработана в далеком 1988 году в MIT для проекта «Афина». Версии с 1 по 3 использовались исключительно внутри MIT.
4 версия была разработана Стивеном Миллером и Клиффордом Нейманном, она хоть и была публичной, но для очень ограниченного количества компаний, включая проект «Афина». И только 5 версия протокола Kerberos, которая вышла в 1993 году уже стала по настоящему публичной и была описана в RFC 1510.
Microsoft Windows Server и Active Directory используют расширенный протокол Kerberos 5 версии, описанный в RFC 4120.
Kerberos в Active Directory
Kerberos сейчас считается основным протоколом при работе в домене Microsoft Windows и состоит из набора компонентов:
Компонент |
Описание |
Область Kerberos |
Сеть, в которой расположен наш KDC. Если мы говорим про Active Directory – то в качестве области может восприниматься домен. |
KDC (Key Distribution Centre) |
Центр выдачи ключей. Роль состоит из AS и TGS и занимается выдачей билетов. KDC располагается на контроллере домена. |
AS (Authentication Server) |
Выполняет авторизацию пользователя и затем передает данные в KDC для выдачи билета. |
TGS (Ticket Granting Service) |
Билет сервиса. Данный билет клиент предоставляет сервису для проверки. Он зашифрован сервисным ключом. |
TGT (Ticket Granting Ticket) |
Билет клиента, предоставленный KDC после успешной аутентификации. |
Описание работы протокола Kerberos
Для общего понимания для начала нарисуем схему и потом пройдем по всем пунктам. Схема работы протокола представлена ниже:
Рассмотрим каждый шаг более подробно:
1. AS_REQ (Preauthentication Request). Данный этап мы проходим, когда авторизуемся на нашей рабочей станции в домене (вводим наш логин и пароль). Формат отправляемого запроса выглядит следующим образом:
Client Name – имя пользователя;
Service Name – обращение к сервису krbtgt на стороне контроллера домена;
Client Time – метка времени с рабочей станции, откуда мы пытаемся авторизоваться. Метка шифруется хэшем пароля пользователя для предотвращения атак с повторной передачей.
По умолчанию, разница во времени между клиентом и KDC должна быть не более 5 минут, иначе запрос будет отклонен;
2. AS_REP. После того, как контроллер домена получит наш пакет – он сразу же попытается расшифровать присланную метку времени используя локальную копию хэша пароля пользователя из базы Active Directory. Если провести операцию не получается, то такой запрос будет отклонен, а клиенту возвращена ошибка. Если же все проходит успешно, то формируется сообщение AS_REP. Формат данного сообщения приведен ниже:
Client Name – имя пользователя;
Session Key – случайно сгенерированный криптографический ключ, который в дальнейшем будет использоваться для взаимодействия между пользователем и AD;
Token Information – токен пользователя, содержащий все данные по пользователю – состав групп, права и т.д.;
Lifetime/Expiry – время жизни TGT;
По умолчанию время жизни TGT составляет 10 часов.
В сообщении AS_REP у нас 2 блока – один шифруется хэшем пароля пользователя, второй – шифруется секретом KDC. Секрет KDC хранится в AD на контроллере. Клиент расшифровывает свою часть сообщения, а TGT просто сохраняется в памяти. Именно внутри блока TGT у нас хранится Token Information.
После успешно принятого сообщения AS_REP нам уже не требуется пароль, поскольку дальнейшее взаимодействие между клиентом и контроллером AD будет происходит посредством защищенного Session Key;
3. TGS_REQ. Вот и настал момент, когда нам необходимо обратиться к какому – либо сервису по Kerberos. В случае с AD это практически любой сервис (Exchange, SharePoint, файловые серверы и т.д.). Для того, чтобы мы смогли запросить доступ к сервису, данный сервис должен иметь свой SPN.
Итак, мы хотим обратиться к сервису через Kerberos – для этого мы посылаем на KDC сообщение TGS_REQ следующего содержания:
Service Principal – SPN запрашиваемого сервиса;
Client Name – имя пользователя;
Time Stamp – метка времени;
Как видно по изображению выше – имя пользователя и метка времени у нас зашифрованы сессионным ключом, который мы получили еще на этапе аутентификации. Блок TGT, который мы получили ранее, также передается на сервер KDC для обработки;
4. TGS_REP. Если на предыдущем этапе у нас все прошло хорошо, то в ответ KDC формирует нам TGS_REP пакет со следующим содержимым:
Service Principal – SPN сервиса;
Time Stamp – метка времени;
Service Session Key – это сессионный ключ, который будет использоваться уже при общении со службой;
Client Name – имя пользователя.
Здесь у нас также ответ представлен из двух блоков: первый блок – шифруется сессионным ключом клиента, который был получен на этапе AS_REP, второй блок – шифруется хэшем пароля службы;
5. AP_REQ. После того, как клиент получит ответ TGS_REP, он «идет» с ним к целевому сервису уже с запросом AP_REQ следующего содержания:
Client Name – имя пользователя;
Time Stamp – метка времени;
Service Principal – SPN запрашиваемого сервиса;
Service Session Key – это сессионный ключ, который будет использоваться уже при общении со службой;
Token Information – токен пользователя, содержащий все данные по пользователю – состав групп, права и т.д.;
Здесь у нас также представлены 2 блока – один блок шифруется сессионным ключом службы, второй – секретом службы;
6. После поступления запроса AP_REQ служба дешифрует свой блок, получая Service Session Key, которым затем дешифрует первый блок, обеспечивая проверку подлинности;
7. AP_REP. Данный пакет у нас на рисунке выше помечен пунктирной стрелкой. По сути, после получения AP_REQ и его расшифровки у службы нет обязательного требования отправки данного пакета. Он может быть отправлен в случае, например, возникшей ошибки или если была запрошена взаимная проверка подлинности.
Итог
В этой статье мы с вами расмотрели, как происходит взаимодействие по протоколу Kerberos в среде Active Directory. Естественно, сам протокол не ограничивается только этими пакетами. Гораздо больше технической информации можно найти в RFC 1510 и RFC 4120.
Kerberos is the native authentication protocol in Active Directory, and it’s essential to understand how it works to get a grasp of more advanced concepts in networking such as authentication and delegation.
You might be surprised to find out that many people that work with Active Directory don’t know how Kerberos works, which can complicate troubleshooting when things go wrong.
We have summarized some key information about how Kerberos works in Windows Active Directory, as well as some useful information about how the whole process works.
You will have a general idea of how all of these components work by the time you finish reading this article, so let’s get started.
What Problems Does Kerberos Solve?
The main reason that you would want to use Kerberos is for security. If you think about it, the process of authenticating users to services often must happen over unsecured networks.
But what is an unsecured network?
We normally think of an unsecured network as being something like a public Wi-Fi Access Point, but the truth is that any network can be considered as being unsecured.
The reason is that users and resources need to be managed and audited effectively to ensure that nothing is accessed by users that should not be able to.
So, even though you know the computers and users on your network, there is no way to be completely sure that there are no bad actors or external threats targeting your network.
This means that a secure sign-on implementation like Kerberos is vital for modern networks, which is why Active Directory uses it to keep things accessible and safe at the same time.
Understanding Kerberos Basics
Kerberos is a secure authentication protocol, and it is most used as the basis for single sign-on and allows information to be transmitted securely over a network.
Kerberos can also be used as effective access control, as only users that are authenticated can access resources on the network.
Active Directory leverages these characteristics and makes it easier to administer thanks to user and security groups — making it much quicker to give access to resources on a larger scale.
This is all well and good, but how does Kerberos authentication work with Active Directory?
Related: How to Design Your First Database.
Basic Authentication with Kerberos
In almost all authentication processes there are three main players. These are:
-
The Client or User that is trying to access a resource on the network.
-
The Resource that needs to be accessed by the user.
-
The Key Distribution Center (KDC) in Windows environments — we know this as the Domain Controller.
With Kerberos, the client takes on the majority of the processing burden, which distributes that authentication workload across the network in a way that’s secure and trustworthy.
The client constructs an authenticator, which includes a date and time, and some other information. This is sent to the KDC or domain controller, which can then verify the user’s identity.
Kerberos uses the user’s password as an encryption key, and the domain controller can see the key in clear text. If you can decrypt the Authenticator, you don’t need to use it anymore; instead, you can create a ticket-granting ticket (TGT).
The domain controller is going to encrypt the user’s information and send it to the client. The client is going to store the data in a special area of memory called the Kerberos Tray.
Transmitting Data with Kerberos
The client logs on and requests a ticket from the Key Distribution Center. The Key Distribution Center uses its key to decrypt the ticket and gives the client a ticket for the file server.
The client keeps a copy of the ticket in its Kerberos tray and sends a copy to the domain controller, which generates a ticket, which is then sent to the file server, which generates a ticket, along with a request to the resource.
The file server accepts a ticket from a user and uses the client’s username and group membership to decide what rights to give the user. The client must resend the file server a copy of their certificate every time they want to use the file server.
That is all there really is to it. It is a complicated system but when broken down into a basic step-by-step overview, it becomes very easy to understand without getting lost in the technicalities that are under the hood.
There’s a command-line utility called KerbTray that lets you view a Kerberos tray, which is how the whole Kerberos protocol works. This is especially handy if you want to understand how the authentication process with Kerberos works.
Final Thoughts
It is always helpful to understand the basic flow of authentication on any given system that you must work with, and Kerberos is no exception.
By visualizing how users are (or aren’t) accessing data and resources on the network, you can more quickly troubleshoot, and find when things go wrong, even with a basic understanding of how the authentication flow works.
Введение
Несмотря на то, что уже существует множество различных статей про Kerberos, я всё‑таки решил написать ещё одну. Прежде всего эта статья написана для меня лично: я захотел обобщить знания, полученные в ходе изучения других статей, документации, а также в ходе практического использования Kerberos. Однако я также надеюсь, что статья будет полезна всем её читателям и кто‑то найдёт в ней новую и полезную информацию.
Данную статью я написал после того, как сделал собственную библиотеку, генерирующую сообщения протокола Kerberos, а также после того, как я протестировал «стандартный клиент» Kerberos в Windows — набор функций SSPI. Я научился тестировать произвольные конфигурации сервисов при всех возможных видах делегирования. Я нашёл способ, как выполнять пре‑аутентификацию в Windows как с применением имени пользователя и пароля, так и с помощью PKINIT. Я также сделал библиотеку, которая умещает «стандартный» код для запроса к SSPI в 3–5 строк вместо, скажем, 50-ти.
Хотя в названии статьи фигурирует «простыми словами» однако эта статья предполагает, что читатель уже имеет какое‑то представление о Kerberos.
Что есть в данной статье
-
Простое объяснение основ работы Kerberos;
-
Простое объяснение аутентификации между доменами;
-
Описание реализации передачи сообщений Kerberos как по UDP, так и по TCP;
-
Объяснение что такое «сервис» в понятии Kerberos и почему у «сервиса» нет пароля;
-
Объяснение почему в абсолютном большинстве случаев SPN служит просто для идентификации пользователя, из‑под которого запущен «сервис»;
-
Как система понимает, к какому типу относить то или иное имя принципала;
-
Рабочий код на С++, реализующий алгоритм шифрования Kerberos с AES (код реализован с применением CNG);
-
Как передать X.509 сертификат в качестве credentials в функцию AcquireCredentialsHandle (PKINIT);
-
Как сделать так, чтобы «сервис» понимал чужие SPNs (одновременная работа с credentials от нескольких пользователей);
-
Почему GSS‑API является основой для всего процесса аутентификации в Windows (да и в других ОС тоже);
-
Описание (со скриншотами из Wireshark) того, как именно передаётся AP‑REQ внутри различных протоколов (LDAP, HTTP, SMB, RPC);
-
Простое описание для каждого из типов делегирования Kerberos;
-
Код для тестирования каждого из типов делегирования, который может быть выполнен на единственной рабочей станции, без какой‑либо конфигурации кучи вспомогательных сервисов;
-
Описание User‑To‑User (U2U), а также код с возможностью протестировать U2U;
-
Ссылки на различный мой код, как то: реализация «whoami на S4U», реализация klist, библиотеки XKERB для запросов к SSPI и прочее;
Для чего нужен Kerberos
Для начала представим, что на компьютере (любом) в сети запущена программа. Эта программа может общаться с другими программами в сети с помощью какого‑то произвольного протокола. Представим также, что эта программа может предоставлять какой‑то «сервис» другим программам: например, выполнить сложение «2+2» и затем переслать результат обратно. Пока «сервис», предоставляемый этой командой, ограничивается только контекстом самой программы, то всё просто. Однако теперь представим, что в качестве «сервиса» эта программа позволяет считывать какой‑то локальный файл и затем передать содержимое этого файла по сети. В этом случае эта программа уже столкнётся с понятием доступа к необходимому файлу. Как известно, в Windows доступ к файлу определяется на основе security descriptor, где прописаны какие именно пользователи и группы имеют возможность выполнять определённые операции над данным файлом. Информация же о текущем конкретном пользователе и его группах содержится в так называемом access token (далее просто «токен»), который присваивается каждому отдельному процессу и потоку. Обычно такой токен получается пользователем при первичном логине на рабочую станцию. И все процессы, которые будут запущены от имени залогиненного пользователя, будут «наследовать» этот токен.
Теперь вернёмся обратно к той самой программе, которая запущена на локальном компьютере и предоставляет «сервис» по чтению контекста локального файла. Предположим, что ей поступил запрос на чтение локального файла А. Сама программа выполняется из‑под пользователя, который находится, например, в группе «Users» на данном компьютере. Однако доступ к файлу А возможен только для пользователей из группы «Administrators». То есть в том случае, если эта программа будет использовать свой «базовый» токен, то для неё доступ к файлу А будет закрыт. Но ведь эта программа предоставляет «сервис» каким‑то другим программам, которые, в свою очередь, могут быть запущены уже под пользователями, входящими в группу «Administrators». То есть вроде как логично, что если «сервис» запрашивается от имени пользователя, которому разрешён доступ к удалённому файлу, то он ему должен быть предоставлен. Что же нужно для того, чтобы это стало возможным?
На самом деле всё, что нужно — это сделать новый access token на имя того пользователя, от которого пришёл внешний запрос на этот «сервис». Далее программа, предоставляющая данный «сервис», должна просто сделать новый поток и присвоить этому новому потоку вновь сделанный access token. Теперь для локального компьютера всё, что будет сделано в рамках работы данного потока будет сделано от имени удалённого пользователя, который первично сделал запрос на «сервис». Но так или иначе для создания нового access token на «сервис» в качестве входных данных должна прийти вся информация, необходимая для этого.
Теперь рассмотрим ещё более сложную ситуацию. Допустим, на «сервис», который запущен на компьютере А пришёл запрос «запроси сервис на компьютере Б». В этом случае, опять же, есть две возможности: либо запросить сервис Б от имени пользователя, под которым запущен сервис А, или запросить сервис Б от имени пользователя, который сформировал первичный удалённый запрос к сервису А. Если мы хотим, чтобы был реализован второй вариант, то получается, что мы должны «делегировать» сервису А возможность представляться пользователем, который сформировал к нему запрос. Конечно, и в ранее рассмотренном случае с доступом к локальному файлу также был факт «делегирования», но там был случай «локального делегирования», только в рамках локального компьютера. Здесь же сервису А должно быть делегировано полномочие представлять какого‑то пользователя в рамках взаимодействия с другими сервисами по сети. Так что для возможности «делегирования» в протоколе «сервиса» также должны быть какие‑то данные.
Kerberos является протоколом аутентификации. Есть три различных понятия: идентификация, аутентификация и авторизация. Идентификация — это когда ты подходишь к какому‑то пропускному пункту и говоришь охраннику «я Вася Пупкин». Аутентификация — это когда кто‑то «авторитетный» заверяет вашу идентификацию, например рядом стоящий другой охранник говорит первому «да, я знаю этого парня, это точно Вася Пупкин». Авторизация — это когда этот самый охранник посмотрит твои документы, потом сверится со своим личным списком лиц, которым разрешён проход, и скажет «Васи Пупкина нет в списке, вы не проходите». То есть в данном примере идентификация прошла успешно, аутентификация также была выполнена, а при авторизации произошёл отказ.
В Kerberos для аутентификации (подтверждения идентификационной информации) используется передача зашифрованных сообщений. Если конечный получатель сообщения смог корректно расшифровать это сообщение, то однозначно считается, что информация, которую содержит сообщение, является верной. Существуют механизмы дополнительной проверки корректности этой информации в виде нескольких типов «подписей», но для текущего повествования это маловажно. Для шифрования идентификационных сообщений используются ключи шифрования, которые формируются специальным образом на основе текстовых паролей конечных получателей. Предполагается, что эти пароли известны только конечным получателям идентификационных сообщений, а также некоторому общему для домена центру хранения паролей. Таким образом, для того, чтобы пользователь А был идентифицирован пользователем Б пользователь А просит центр хранения паролей сформировать идентификационное сообщений, которое было бы зашифровано на шифровальном ключе пользователя Б. Центр хранения паролей формирует такое сообщение, и пересылает его обратно пользователю А. Пользователь А не может расшифровать или изменить это сообщение (оно зашифровано на ключе пользователя Б). Далее пользователь А может переслать это идентификационное сообщение пользователю Б, он его сможет успешно расшифровать и прочитать хранящуюся в данном сообщении идентификационную информацию.
В качестве «идентификационной информации» вполне достаточно передавать только уникальное имя пользователя. Однако в Windows пользователь идентифицируется не только своим именем, а также набором групп пользователей и набором привилегий. Можно было бы сделать так, чтобы сервис после расшифровки идентификационного сообщения сам посылал куда‑то запросы, на которые бы ему возвращались все дополнительные данные пользователя. Однако был выбран путь включения в идентификационное сообщение полной информации о «предмете идентификации»: полный список групп, а также другой вспомогательной информации. Привилегии «выдаются» каждому пользователю или группе на каждом компьютере отдельно, так что добавление привилегий в access token происходит уже в процессе формирования access token на машине «сервиса». Кстати, в Active Directory «предмет идентификации» называется «принципал» (principal).
В Active Directory пользователями идентификационных сообщений могут быть любые сущности, у которых возможно существование пароля. К таким сущностям относятся как пользователи домена, так и компьютеры, которые в данный домен входят. Использование компьютеров как пользователей идентификационных сообщений удобно так как нужно, чтобы «сервисы» работали даже тогда, когда ни один пользователь не работает за компьютером.
Также в Active Directory возможна аутентификация между пользователями из «дружественных» доменов. Технически это реализуется путём добавления в каждый из доменов специальных пользователей, имена которых совпадают с именем дружественного домена. При необходимости пользователя из домена А получить доступ к сервисам из домена Б пользователь сначала посылает запрос к своему контроллеру домена. Там формируется аутентификационная информация, однако она шифруется не на ключе начального контроллера домена, а на ключе того пользователя, имя которого совпадает с именем «дружественного» домена. Далее пользователь просто пересылает полученную аутентификационную информацию уже контроллеру домена Б и процесс аутентификации продолжается стандартным образом.
Техническая реализация Kerberos
Теперь опишем реальную структуру, которая обслуживает протокол идентификации Kerberos в Windows Active Directory. В Active Directory для каждого домена существует Key Distribution Center (KDC). Именно эта сущность отвечает за хранение паролей всех пользователей и компьютеров в данном домене. В документе RFC 4120 применяется также разделение на Authentication Service (AS) и Ticket‑Granting Service (TGS), однако в Active Directory все эти сущности объединены. Для простоты будем считать, что все коммуникации осуществляются с KDC.
Все сообщения в Kerberos кодируются с помощью ASN.1. Описание схем ASN.1 для различных видов сообщений можно найти в RFC 4120 и других документах. Коммуникации с KDC в Active Directory осуществляются либо с использованием протокола TCP (KDC слушает на порту 88), либо с использованием протокола UDP (и опять KDC слушает на порту 88). Найти на каком именно адресе работает сервис Kerberos можно с помощью команды «nslookup ‑type=SRV _kerberos._tcp.<имя_домена>» или для UPD с помощью команды «nslookup ‑type=SRV _kerberos._udp.<имя_домена>». Если сообщения передаются по протоколу TCP, то перед каждым сообщением передаётся 4 байта, содержащие общую длину сообщения. Если передача происходит по UDP, то передаётся просто чистое сообщение, без информации о длине.
Все коммуникации с KDC начинаются с посылки сообщения KDC‑AS‑REQ. В данном сообщении присутствует информация о различных флагах (об этом позже), имени принципала, имени домена, имени принципала, который является специальным и отвечает за выдачу идентификационной информации (оно всегда имеет значение «krbtgt»), а также о списке поддерживаемых схем шифрования. Насчёт «схем шифрования» необходимо дать дополнительное пояснение. Дело в том, что в KDC‑AS‑REQ передаётся набор тех схем шифрования, которые может поддержать (зашифровать/расшифровать) данный конкретный клиент. На KDC, в свою очередь, может быть какой‑то другой набор поддерживаемых схем шифрования, и, потенциально, они могут не совпасть со схемами на клиенте. В этом случае коммуникации между таким клиентом и KDC будут невозможны.
В ответ на получение сообщения KDC‑AS‑REQ KDC может возвратить как зашифрованную идентификационную информацию для упомянутого в запросе принципала (сообщение KDC‑AS‑REP), так и ошибку (KRB‑ERROR). В KRB‑ERROR будет передаваться, прежде всего, код ошибки, а также возможна передача дополнительной информации. Обычно в современных системах код ошибки будет 25: требуется пре‑идентификация. Под пре‑идентификацией подразумевается процесс, в ходе которого запрашивающая сторона проверяется на то, что она действительно знает ключ шифрования того принципала, для которого запрашивается идентификационная информация. Также в случае кода ошибки 25 в KRB‑ERROR передаётся дополнительная информация, как то: 1) специальное текстовое сообщение, которое используется при генерации ключа шифрования (salt); 2) идентификатор предпочтительной схемы шифрования; 3) перечень способов пре‑идентификации, которые поддерживаются данным KDC. Необходимо заметить, что обычно KDC поддерживает несколько способов пре‑идентификации, таких как пре‑идентификация по паролю, пре‑идентификация по сертификату (PKINIT), пре‑идентификация с использованием расширения FAST (RFC 6113) и так далее. Однако базовым способом пре‑идентификации является идентификация по паролю. Для того, чтобы клиент прошёл эту пре‑идентификацию, он должен зашифровать специально сформированные данные, содержащие текущую временную метку. То есть клиент просто должен каким‑то образом получить текущее время, затем сформировать специальное сообщение, и затем зашифровать его с использованием пароля того принципала, для которого он запрашивает у KDC идентификационную информацию. В случае с использованием пре‑идентификации по сертификату (PKINIT) процесс практически такой же за исключением того, что шифрование производится с использованием закрытого ключа сертификата принципала.
После формирования пре‑идентификационного сообщения формируется новый вариант сообщения KDC‑AS‑REQ, который содержит всю информацию из первого варианта сообщения плюс информацию о зашифрованном текущем времени на клиенте. Эта информация располагается внутри массива PA‑DATA (pre‑authentication data).
В ответ на KDC‑AS‑REQ в котором содержится пре‑идентификационная информация KDC, присылает KDC‑AS‑REP, который содержит идентификационную информацию по запрошенному принципалу. Эта идентификационная информация зашифрована на ключе KDC и служит для реализации функционала Single Sign‑On (SSO). В большинстве случаев эту идентификационную информацию называют «Ticket‑Granting Ticket» (TGT). То есть для всех дальнейших запросов к KDC пользователю не нужно будет всегда использовать пароль: он будет просто посылать свою идентификационную информацию KDC, затем KDC сможет её расшифровать и использовать эту информацию во вторичных запросах. Нужно заметить, что в KDC‑AS‑REP присылается как идентификационная информация, которую клиент не может расшифровать, так и дополнительная информация, зашифрованная на ключе клиента и к которой он имеет доступ. В этой дополнительной информации, в частности, находится случайно сгенерированный сессионный ключ, который клиенту теперь будет необходимо использовать в дальнейших коммуникациях с KDC. Использование сессионного ключа вместо заранее известного ключа, генерируемого из пароля пользователя, потенциально повышает безопасность коммуникаций. Кстати, на самом деле KDC нигде не хранит этот сессионный ключ. Вместо этого он просто добавляет его в ту идентификационную информацию, которую присылает в KDC‑AS‑REP. Когда клиент выполнит следующий запрос к KDC он пошлёт эту же идентификационную информацию, которую KDC сможет расшифровать и достать оттуда нужный сессионный ключ.
Флаги в KDC options
Основные флаги, поддерживаемые в Windows, описаны в документе по следующей ссылке. Здесь я расскажу о наиболее используемых флагах.
Начнём с флага CANONICALIZE. Если данный флаг установлен в запросе от клиента, то это означает, что клиент ожидает (и может обработать) изменение имени клиента и сервиса, а также изменение типа этого имени в ответном сообщении. То есть при посылке TGS‑REQ с именем клиента «client@server» в TGS‑REP может быть уже «server\client».
Флаг RENEWABLE устанавливается на KDC при формировании первичных TGT. Если это флаг установлен, то клиент может обновлять свой TGT, без полного прохождения процедуры пре‑аутентификации. Делается это путём повторной посылки сообщения TGS‑REQ к сервису «krbtgt», установленным флагом RENEWABLE в запросе и первичным TGT в PA‑DATA. Далее KDC просто декодирует старый TGT, скопирует его содержимое в новый TGT, обновить время действия нового TGT, а также сформирует новый сессионный ключ.
Флаг OK‑AS‑DELEGATE находится в поле «flags» зашифрованной части TGS‑REP (не в kdc_options). Флаг OK‑AS‑DELEGATE формируется на стороне KDC, и приходит в TGS‑REP когда выполняется запрос к сервису, для которого включён режим «неограниченного делегирования» (более подробно про данный режим далее в статье). При получении TGS‑REP с установленным OK‑AS‑DELEGATE стандартный Kerberos клиент Windows автоматически формирует TGS‑REQ к сервису «krbtgt» с установленными флагами FORWARDABLE и FORWARDED. В этом новом TGS‑REQ также прикладывается первичный TGT клиента. В ответ KDC формирует TGS‑REP, в котором содержится TGT с установленным флагом FORWARDED и именно этот новый TGT используется далее для формирования AP‑REQ к необходимому сервису. Таким образом, если у вас есть TGT с установленным флагом FORWARDABLE и есть сессионный ключ, соответствующий данному TGT, то можно сформировать запрос, который возвратит вам новый TGT с установленным FORWARDABLE. Флаг FORWARDABLE необходим при реализации делегирования на сервисах, без этого флага KDC будет отвергать запросы от сервисов на получение билетов на имя клиента.
Далее для продолжения рассказа о технических аспектах реализации Kerberos необходимо прерваться и рассказать о том, как реализованы «сервисы» в Active Directory.
Сервисы
Под «сервисом» понимается какая‑то программа, предоставляющая какой‑то функционал удалённым или локальным (по отношению к компьютеру, на котором этот «сервис» запущен) пользователям. Нужно заметить, что в Windows есть даже отдельная оснастка для MMC, которая показывает «Сервисы». Но на самом деле те «сервисы», которые отображаются в этой оснастке, не относятся к «сервисам» в понятии Kerberos. Для Kerberos сервисом будет считаться любая программа, даже запущенная из‑под обычного пользователя. То есть «сервис» из оснастки MMC может быть также «сервисом» для Kerberos, однако это не является обязательным условием.
Как я описал ранее, для аутентификации в Kerberos используются зашифрованные сообщения. Для шифрования в адрес какого‑то принципала необходимо, чтобы у этого принципала был какой‑то пароль. Пароли в домене Active Directory могут быть только у пользователей или компьютеров. «Сервисы» сами по себе паролей не имеют. Вместо этого «сервис» использует идентификационную информацию (имя пользователя и пароль) того пользователя, из‑под которого этот «сервис» в настоящий момент запущен. И, как следствие, все «сервисы», которые запущены из‑под одного и того же пользователя, будут использовать один и тот же пароль. Если «сервис А» запущен из‑под пользователя А на компьютере 1, то если запустить «сервис Б» на компьютере 2 и запустить его опять из‑под пользователя А, то оба сервиса будут использовать один и тот же пароль. Для более «продвинутых» читателей приведу ещё пример: все сервисы, запущенные на любом компьютере из‑под одного и того же аккаунта gMSA используют один и тот же пароль.
Для Kerberos «сервис» тоже является принципалом. То есть «сервис» может использовать идентификационную информацию других принципалов, а также передавать свою идентификационную информацию. Однако так как «сервис» использует идентификационную информацию того пользователя, из‑под которого он запущен, то фактически «сервис» можно назвать «фантомным» принципалом. В первичных стандартах Kerberos «сервис» имеет собственное имя (Service Principal Name, SPN), однако в Active Directory это имя, фактически, используется только для поиска пользователя, который в настоящий момент владеет данным SPN и вместо имени «сервиса» может быть использовано имя того принципала, из‑под которого данный сервис запущен.
В Active Directory имя «сервиса» (SPN) также используется, но только для того, чтобы найти имя того принципала, из‑под которого данный «сервис» запущен. Для того, чтобы это было возможно SPN должно быть уникальным в рамках всего домена. Так как обычно «сервисы» запускаются из‑под учётных записей компьютеров, то имя SPN для любого такого «сервиса» должно (по правилам Microsoft) содержать имя компьютера, на котором этот «сервис» запускается. Но если вы для компьютера COMP1 добавите новый «сервис» и зададите ему SPN вида «service1/COMP2», то Active Directory это тоже пропустит. Главное, чтобы такого имени ранее в домене не существовало. Возможно, кто‑то подумает, что если SPN=service1/COMP2, то теперь KDC при запросах также будет использовать и пароль для COMP2 — нет, KDC будет использовать пароль того пользователя, у которого в атрибуте servicePrincipalName находится искомое SPN. Кстати, не смотря на официальную документацию, в которой описывается как именно должны именоваться SPN в Active Directory (ссылка) у меня получалось добавлять (и в дальнейшем использовать) SPN вида «S1/S1». Повторюсь — главное, чтобы имя «сервиса» было уникальным в масштабе текущего домена.
Любой «сервис» может быть запущен как из‑под системной учётной записи (записи компьютера), так и из‑под учётной записи произвольного пользователя. Но если меняется пользователь, из‑под которого запускается какой‑то «сервис», то для корректной работы Kerberos необходимо просто удалить SPN этого сервиса из атрибута servicePrincipalName у одного пользователя/компьютера и добавить этот SPN в аналогичный атрибут другого пользователя/компьютера. Отличием между запуском из‑под системной учётной записью и записью обычного пользователя будет только набор привилегий, которыми обладает пользователь: для полноценной работы «сервисам» зачастую необходимо, чтобы пользователь, в контексте которого выполняются «сервисы», имел такие привилегии как SeTcbPrivilege и SeImpersonatePrivilege. Однако если выдать права на эти привилегии какому‑то другому пользователю на данном компьютере, то «сервис», выполняемый от его имени, будет иметь точно такие же возможности, как если бы он выполнялся от имени системной учётной записи.
Также необходимо заметить, что некоторые имена «сервисов» являютс предопределёнными, и другие «сервисы» ожидают, что «сервис» с таким именем будет предоставлять определённые функции. Так «сервис» с именем «krbtgt/<имя домена>» будет предоставлять сервисы KDC, «сервис» с именем «host/<имя компьютера>» предоставляет возможность логина на компьютер, «сервис» с именем «cifs/<имя компьютера>» предоставляет услуги доступа к файловой системе на определённом компьютере по протоколу SMB и так далее. Список таких «предопределённых имён» можно посмотреть тут. Посмотреть, какие «сервисы» зарегистрированы для определённого компьютера можно с помощью команды «setspn.exe ‑L <ServerName>». Посмотреть, какие «сервисы» зарегистрированы для определённого пользователя можно с помощью команды «setspn.exe ‑L <domain\user>
».
Общие правила составления имён принципалов
Перед правилами именования принципалов необходимо пояснить какие же типы имён принципалов бывают в Active Directory. В файле «NTSecAPI.h» объявлены следующие типы имён принципалов:
-
KRB_NT_UNKNOWN
-
KRB_NT_PRINCIPAL
-
KRB_NT_PRINCIPAL_AND_ID
-
KRB_NT_SRV_INST
-
KRB_NT_SRV_INST_AND_ID
-
KRB_NT_SRV_HST
-
KRB_NT_SRV_XHST
-
KRB_NT_UID
-
KRB_NT_ENTERPRISE_PRINCIPAL
-
KRB_NT_ENT_PRINCIPAL_AND_ID
-
KRB_NT_MS_PRINCIPAL
-
KRB_NT_MS_PRINCIPAL_AND_ID
Если в имени присутствует «@», то всё, что находится после «@» считается именем домена (realm name). Имя перед «@» делится по группам на основе присутствия символа «/». Если этих групп 1 (то есть нет символа «/»), то общему имени присваивается тип KRB_NT_PRINCIPAL. Если таких групп 2, то общему имени присваивается тип KRB_NT_SRV_INSTANCE. Если таких групп 3 и более, то вроде как общему имени должен присваиваться тип KRB_NT_SRV_XHST, однако на деле присваивается также KRB_NT_SRV_INSTANCE. Если в общем имени присутствует символ «\», то имени присваивается тип KRB_NT_MS_PRINCIPAL_NAME. Если в имени нет ни одного из символов «@», «\» или «/», то этому имени назначается тип KRB_NT_PRINCIPAL и имя домена берётся из текущих значений. Таким образом, если хотите запросить билет для пользователя, то формируйте имя в виде «name@domain» или «domain\name». Если хотите запросить билет для сервиса, то формируйте имя в виде «part1/[part N]@domain» или просто «part 1/[part N]». Всё решает прямой или обратный backslash.
Все типы имён, кончающихся на «_ID» представляют собой обычный тип (без «_ID»), но с добавленным SID в качестве последнего элемента массива имён в виде строки. Данный SID должен быть в стандартной форме, вроде «S-1–5–500».
Описание алгоритма шифрования Kerberos
На самом деле, как я и писал ранее, Kerberos поддерживает множество различных схем шифрования. Также Kerberos может использовать CMS (Cryptographic Message Syntax, RFC5652), как минимум на этапе пре‑идентификации. Однако в настоящее время наиболее используемыми схемами шифрования являются схемы на основе использования стандарта шифрования AES. Само по себе шифрование применяется с использованием стандартного алгоритма AES. Однако формирование ключа шифрования в Kerberos выполняется по достаточно сложной схеме. Реализацию на языке С++ с использованием CNG (Cryptography API: Next Generation) можно найти по этой ссылке. Для общего понимания необходимо знать, что для дополнительного усложнения Kerberos использует так называемые key usage numbers: 4-байтовые значения, которые специфичны для каждой фазы передачи сообщений. Значения для этих key usage numbers приведены в RFC4120, item 7.5.1. Таким образом, для фазы посылки первичного запроса AS‑REQ с encrypted timestamp значение для key usage number будет равно 1 (или если писать в виде 4-х байтов, то 0×00 000 001), для фазы передачи AS‑REP значение key usage number будет уже 2, и так далее. Знание key usage number критически важно для взлома ключей Kerberos — если указать неверный номер, то ключ никогда не будет взломан, или будет получено ошибочное выходное значение.
Обратно к процессу получения идентификационной информации
Итак, раннее я остановился на том, что клиент Kerberos получил от KDC зашифрованную идентификационную информацию. Но всё, что возможно сделать имея эту идентификационную информацию, это идентифицироваться на KDC. Что же делать, чтобы наш клиент смог идентифицироваться на других принципалах, доступных в домене? Для этого KDC предоставляет сервис, формирующий идентификационную информацию, зашифрованную на ключах необходимых принципалов. То есть процесс аутентификации для данного клиента на произвольном принципале в домене состоит в следующем. Изначально клиент идентифицируется на KDC. Полученная на данном этапе идентификационная информация зашифрована на ключе KDC. Далее «прикладывая» к сообщению эту аутентификационную информацию клиент формирует дополнительный запрос к KDC, в котором указывает имя того принципала, к которому ему необходимо получить идентификационную информацию. Всё, что далее делает KDC это расшифровывает аутентификационную информацию, которую «приложил» клиент, достаёт оттуда идентификационные данные клиент и затем зашифровывает эту же идентификационную информацию, но уже на ключе того принципала, идентификацию для которого запросил клиент. То есть процесс крайне прост: расшифровал данные на одном ключе, скопировал их и зашифровал на другом ключе. Далее вновь зашифрованная идентификационная информация пересылается обратно клиенту, и теперь клиент может переслать её (произвести процесс аутентификации) другому принципалу. Это принципал, в свою очередь, сможет эту аутентификационную информацию расшифровать. Теперь для этого принципала наш клиент считается аутентифицированным.
Нужно также понимать одну особенность KDC: он выдаёт аутентификационную информацию для любого принципала в системе, вне зависимости от того имеет ли конкретный клиент Kerberos туда доступ или нет. Это происходит потому, что Kerberos является протоколом аутентификации, а вопрос доступа уже относится к понятию авторизации.
Технически запрос к KDC на предоставление аутентификационной информации для другого принципала формируется с помощью сообщения KDC‑TGS‑REQ. Идентификационная информация, которую ранее получил клиент в сообщении KDC‑AS‑REP, сохраняется в KDC‑TGS‑REQ в поле «additional‑tickets». Также для Active Directory возможно передать эту идентификационную информацию в PA‑DATA используя тип PA‑TGS‑REQ. Имя принципала, для которого клиент хочет получить идентификационную информацию, сохраняется в поле «sname» (service name). Также в KDC‑TGS‑REQ пересылается ещё одна внутренняя структура, которая имеет тип Authenticator. Эта структура имеет в своём составе время формирования KDC‑TGS‑REQ, а также продублированное имя принципала из другой части KDC‑TGS‑REQ. Также Authenticator шифруется на сессионном ключе, который клиент получает вместе с KDC‑AS‑REP. Использование Authenticator позволяет в какой‑то мере противостоять атакам, связанным с повторной передачей данных. Для этого на KDC ведётся учёт всех ранее полученных данных из Authenticator, и если приходит сообщение, в котором все значения совпадают с ранее полученными, то KDC возвращает в ответ ошибку.
В ответ на KDC‑TGS‑REQ сервис KDC присылает сообщение KDC‑TGS‑REP, в котором содержится идентификационная информация для нужного принципала. Далее будем обозначать эту идентификационную информацию как «Service Ticket» (ST).
Теперь необходимо пояснить, как же именно происходит передача ST на сторону нужного принципала (сервиса).
Передача service ticket для идентификации на сервисе
Как я уже говорил, «сервисом» может был любая программа, предоставляющая возможность по запросу выполнять заранее известный функционал. Любой «сервис» может осуществлять коммуникации по любым протоколам. Один сервис «слушает» запросы только по HTTP, другой только по SMB, третий — по какому‑то собственному проприетарному протоколу. Однако хотелось бы, чтобы можно было выполнять аутентификацию клиента как можно проще и унифицировано. Одним из способов такой унификации является использование Generic Security Service Application Program Interface (GSS‑API, RFC 2743, RFC 4121). На самом деле в GSS‑API описан просто набор общих функций, их параметров, а также некие общие заголовки передаваемых сообщений. Также описывается, какой именно результат должен быть достигнут от выполнения определённой функции с определёнными параметрами. То есть вроде «если выполнишь сначала такую, а потом вот такую функции, то аутентифицируешь клиента». Всё максимально просто. Собственно внутренняя реализация функций GSS‑API остаётся за производителем операционной системы. В Windows реализация GSS‑API известна под названием Security Support Provider Interface (SSPI). С помощью SSPI можно работать не только с Kerberos (и GSS‑API изначально сделан с возможностью поддержки множества протоколов аутентификации). Можно даже писать свои собственные «провайдеры», обслуживающие аутентификацию по произвольным протоколам.
Однако вернёмся к работе с Kerberos с помощью SSPI. Сам по себе SSPI является неким набором стандартных функций, реализация же этих функций называется SSP (Security Support Provider). По сути, в операционной системе Windows SSP реализует функционал «стандартного клиента Kerberos», а также «стандартного сервера/сервиса Kerberos», и в Windows больше нет никаких других API, которые бы работали с Kerberos. Для «стандартного клиента» достаточно использовать только две функции: AcquireCredentialsHandle (реализация GSS_Acquire_cred из RFC 2743) и InitializeSecurityContext (реализация функции GSS_Init_sec_context из RFC 2743). Для «стандартного сервиса» достаточно также только двух функций: ранее упомянутой AcquireCredentialsHandle, и AcceptSecurityContext (реализация GSS_Accept_sec_context из RFC 2743). Функции InitializeSecurityContext и AcceptSecurityContext реализованы так, чтобы воспринимать выходную информацию друг друга. То есть на клиенте вызывается InitializeSecurityContext, затем сама эта функция внутри, если нужно, запрашивает TGT для нужного пользователя, затем запрашивается ST (service ticket) для нужного принципала, из‑под которого выполняется нужный «сервис». Далее выходная информация из функции InitializeSecurityContext каким‑то образом (безразлично каким конкретно, позже я покажу реальные реализации такой коммуникации) попадает на сторону «сервиса». Внутри «сервиса» выполняется вызов функции AcceptSecurityContext, в которую без изменений передаётся весь бинарный массив данных, ранее сформированный на клиенте с помощью InitializeSecurityContext. В качестве результата работы функции AcceptSecurityContext возвращаются различные флаги. С помощью этих флагов можно определить, смогли ли внутренние алгоритмы AcceptSecurityContext идентифицировать клиента или нет. Также есть специальная функция QueryContextAttributes с помощью которой можно получить различные переменные «контекста», как то SPN того ST, который поступил на вход, имя клиента, которое существует в данном ST и так далее. В некоторых случаях может случиться так, что AcceptSecurityContext потребуется дополнительная информация со стороны клиента. В этом случае AcceptSecurityContext формирует свой выходной бинарный буфер, который также должен быть переслан обратно на сторону клиенте (безразлично каким именно способом). Ну и далее по кругу: InitializeSecurityContext — AcceptSecurityContext, до тех пор, пока AcceptSecurityContext не возвратит флаг ошибки или успешно идентифицирует клиента.
На самом деле все «выходные бинарные буфера» для обеих функций реализуют стандартное кодирование информации, принятое в GSS‑API. Опишу заголовок данных при передаче сообщений Kerberos. Заранее упомяну, что обычно стандартные клиенты Kerberos использует SPNEGO (RFC4178), реализация которого есть в Windows в SSP (Security Support Provider) с именем «Negotiate». Однако SPNEGO это просто «надстройка», позволяющая выбрать при авторизации NTLM или Kerberos. В реальности можно вместо SPNEGO применять напрямую SSP Kerberos. Итак, в GSS‑API все внутренние сообщения протокола сначала идентифицируются OID (Object Identifier) в формате ASN.1. Для Kerberos данный OID имеет значение «1.2.840.113 554.1.2.2». После OID идёт 2 байта типа внутреннего сообщения Kerberos. Эти байты могут иметь следующие значения:
-
0×0001 = KERB‑AP‑REQUEST
-
0×0002 = KERB‑AP‑REPLY
-
0×0003 = KERB‑ERROR
-
0×0004 = KERB‑TGT‑REQUEST
-
0×0104 = KERB‑TGT‑REPLY
Последние два типа сообщений могут применяться только в том случае, когда «сервис» работает по схеме U2U (сервис не имеет имени и пароля пользователя и может обмениваться запросами только по предварительно согласованным сессионным ключам). Более подробно про U2U я напишу позже в статье.
За двумя байтами типа идут собственно данные Kerberos. То есть если тип GSS‑API сообщения равен 0×0001 (KERB‑AP‑REQUEST), то далее хранится обычный AP‑REQ. Если тип GSS‑API сообщения равен 0×0003 (KERB‑ERROR), то далее хранится обычное сообщение KRB‑ERROR, кодированное в ASN.1. В случае использования GSS‑API (а в Windows всегда используется GSS‑API) внутри AP‑REQ будет формироваться специальным образом сформированный authenticator. Особенным является поле checksum. Вместо использования стандартного алгоритма Kerberos для формирования checksum, в GSS‑API применяется набор дополнительных флагов. Именно эти флаги регулируют, будет ли информация из AP‑REQ использована для делегирования, ожидает ли клиент каких‑то дополнительных сообщений (mutual authentication), информацию по channel binding. С полным перечнем этих флагов можно ознакомиться в RFC4121, item 4.1.1.
Разберём теперь более подробно параметры, которые необходимо передавать для каждой из упомянутых функций. Первая функция в нашем списке AcquireCredentialsHandle. Функция служит для последующей передачи в «security context» (контекст безопасности) информации по используемым реквизитам для входа (credentials). В качестве реквизитов для входа может использоваться как пустые значения, имя пользователя и пароль, или специальным образом кодированный идентификатор сертификата пользователя.
В случае пустых значений реквизиты для входа берутся из уже объявленных в текущей «logon session» (сессии входа в систему). Нужно заметить, что все данные, с которыми работают функции SSPI, а также все артефакты, которые они сохраняют на локальном компьютере, сохраняются в разрезе «logon sessions». На компьютере таких сессий может быть множество, для множества различных пользователей. Идентифицируются «logon sessions» по специальному идентификатору «Logon Unique IDentifier» (LUID), полный список существующих на компьютере сессий можно получить с помощью функции LsaEnumerateLogonSessions. Для того, чтобы в текущей logon session существовали реквизиты для входа необходимо, чтобы до этого они были явно в эту сессию добавлены. Это делается либо через вызов AcquireCredentialsHandle, либо с помощью вызова функции LsaLogonUser (которая, в свою очередь, неявно вызывает всё ту же AcquireCredentialsHandle). Если в текущей сессии уже есть реквизиты для входа, то можно реализовать «фантастический хак» — вызвать AcquireCredentialsHandle без указания пользователя и пароля и всё заработает.
Использование AcquireCredentialsHandle с указанием имени пользователя и пароля является тривиальным, и я пропущу объяснение этого варианта.
Гораздо более интересным является вариант указания реквизитов для входа с помощью идентификатора сертификата пользователя. Для реализации этого варианта необходимо, чтобы в хранилище сертификатов «Personal» (MY) текущего пользователя существовал сертификат, выданный центром сертификации текущего домена, в котором в качестве SAN (Subject Alternative Name) был бы указан UPN (User Principal Name) в формате «user@domain». Этот сертификат также должен иметь связанный закрытый ключ. Для формирования идентификатора сертификата, который можно передать в функцию AcquireCredentialsHandle, понадобится использовать функцию CredMarshalCredentialW, в которую должна быть передана структура типа CERT_CREDENTIAL_INFO. Основной информацией, которую содержит CERT_CREDENTIAL_INFO, является SHA-1 хэш необходимого нам сертификата. Такой хэш можно получить, например, с помощью функции CryptHashCertificate. Как результат вызова CredMarshalCredentialW мы получим строку, подобную этой: «@@BRi6zZsVIijBXbuYDYub3SKfcO7H». Пример программы, формирующей подобную строку из произвольного сертификата, можно найти по этой ссылке.
Эту строку можно передать в значение имени пользователя для функции AcquireCredentialsHandle. Если у используемого сертификата установлен PIN для доступа к закрытому ключу, то его также будет необходимо указать в значении «пароль пользователя». После указания подобного идентификатора сертификата ближайший вызов InitializeSecurityContext или AcceptSecurityContext будет формировать запрос на получение TGT с использованием PKINIT, а не с использованием стандартного способа шифрования на пароле. В результате использования сертификата для реквизитов входа можно получить сессию, в которой отсутствуют имя пользователя и пароль. Однако, как я уже писал ранее, Kerberos базируется на том, что для успешной коммуникации с такой сессией необходимо, чтобы была возможность использования пароля. Как же всё работает при использовании сертификатов (PKINIT)? В этом случае используется специальное расширение протокола, называемое «User‑To‑User» (U2U). При использовании этого расширения коммуникации производятся без помощи шифрования на пароле, а только с использованием согласованных сессионных ключей. Более подробно про это расширение я напишу позднее.
Как оказалось, результат вызова CredMarshalCredentialW можно передавать во многих случаях, когда требуется информация о реквизитах доступа. То есть, например, возможно указать строковый идентификатор сертификата при указании имени пользователя, из‑под которого запуститься произвольный сервис. Или можно локально зайти в систему, вообще не зная имени пользователя и пароля. Однако, как я уже ранее писал, в этих случаях необходимо, чтобы используемый сертификат содержался в хранилище «Personal» для текущего пользователя. И в случае логина сервиса (здесь под «сервисом» понимается программа, запускаемая в оснастке «Services»), или выполнения первичного входа в систему, таким текущим пользователем будет SYSTEM. Для того, чтобы добавить произвольный сертификат в «Personal» для SYSTEM понадобится использовать любой «token stealer» и с его помощью запустить «mmc.exe». Там добавить оснастку «Certificates» и выполнить импорт сертификата. После этого будет доступен как бы функционал смарт‑карт, но без использования самих смарт‑карт, просто с использованием чистых сертификатов: вводите в поле «User name» строку, которую получили с помощью CredMarshalCredentialW и нажимаете Enter. Кстати, запуск сервисов без использования паролей полностью исключает использование атаки вида «Silver Ticket» так как даже если злоумышленник узнал пароль учётной записи для данного «сервиса» всё равно все коммуникации с этим сервисом будут проведены с помощью сессионного ключа и PAC всё равно будет сформирован на KDC.
Перейдём к следующей функции: InitializeSecurityContext. Данная функция предназначена для создания первичного запроса к удалённому «сервису» и, если необходимо, передачи дополнительной информации, необходимой для окончательной идентификации клиента на удалённой системе. Как я уже писал ранее, все функции из SSPI работают с данными из текущей «logon session». Если в текущей сессии нет ранее полученного TGT, то первый вызов InitializeSecurityContext попытается этот TGT получить. Если же в сессии уже есть TGT, то будет просто сформирован запрос KDC‑TGS‑REQ, содержащий этот TGT и информацию о сервисе, ST (service ticket) для которого необходимо получить. После получения требуемого ST он сохраняется во внутренних структурах текущей «logon session». Если в дальнейшем любой программе, запускаемой в рамках той же «logon session», понадобится ST для того же сервиса, и этот ST будет иметь корректный временной интервал использования, то этот ST будет получен из «кэша», без запроса к KDC вообще. После получения требуемого ST функция InitializeSecurityContext формирует выходной бинарный буфер (его формат я уже ранее описывал) и ожидает, что этот буфер будет каким‑то образом передан на сторону «сервиса». Примеры передачи подобного буфера представлены ниже.
Если при первом запуске для InitializeSecurityContext был использован флаг ISC_REQ_MUTUAL_AUTH, то функция будет ожидать ответный буфер, сформированный на стороне «сервиса» с помощью AcceptSecurityContext. Со своей стороны я рекомендую всегда использовать флаг ISC_REQ_MUTUAL_AUTH так как сервис может вернуть какую‑то ошибку, которую на клиенте можно обработать. Например, «сервис» может вернуть ошибку с кодом, указывающим на то, что требуется прохождение идентификации на основе сессионного ключа (U2U). И если клиент не ожидает входящего сообщения, то коммуникация с приходом этой ошибки будет полностью прекращена. Значение всех используемых в SSPI флагов можно найти по этой ссылке.
Работа функции AcceptSecurityContext в начале аналогична работе InitializeSecurityContext: функция AcceptSecurityContext также может получить TGT для «сервисной» части, если это будет нужно. В этой функции также можно инициализировать контекст безопасности с использованием явно заданных имени пользователя, пароля или сертификата. Однако в отличие от InitializeSecurityContext функция AcceptSecurityContext просто ждёт входящих сообщений. После получения входящего сообщения (в виде бинарного буфера, сформированного на клиенте с помощью InitializeSecurityContext) функция AcceptSecurityContext может либо вернуть какой‑то свой бинарный буфер, который подлежит отправке на сторону клиента, либо завершается с кодом, соответствующим успешному прохождению идентификации для удалённого клиента. В последнем случае «сервис» может, например, выделить отдельный процесс и в нём вызвать функцию ImpersonateSecurityContext — в этом случае этот новый процесс будет использовать «access token» (токен безопасности) в котором будут представлены все данные по идентификационной информации удалённого клиента (все его группы, привилегии и так далее). Важно заметить, что если функция AcceptSecurityContext смогла успешно идентифицировать удалённого клиента, то это автоматически приводит к тому, что на локальном для «сервиса» компьютере появляется новая «logon session» для удалённого пользователя. Это необходимо, в частности, для того, чтобы «сервис» в дальнейшем мог организовать «делегирование» идентификационной информации (об этом позже в статье). В этой новой «logon session» по умолчанию будет отсутствовать какая‑либо информация по реквизитам доступа: там не будет TGT, не будет сохранённого имени пользователя и пароля. Всё, что будет в этой новой сессии это «access token», который можно использовать, например, для проверки прохождения авторизации на локальных ресурсах. Таким образом, в общем случае функция AcceptSecurityContext в качестве своей конечной выходной информация позволяет создавать на локальной системе новую «logon session», а также новый «access token», содержащий всю идентификационную информацию для удалённого клиента. Необходимо сказать, что AcceptSecurityContext создаёт такой «access token» используя исключительно только ту идентификационную информацию, которая поступила этой функции на вход. То есть AcceptSecurityContext самостоятельно не выполняет никаких обращений к KDC, за исключением вызова функций проверки «подписей» внутри PAC. Ещё одной особенностью AcceptSecurityContext является тот факт, что у этой функции нет никакого параметра, в который бы передавалось имя того «сервиса», от имени которого эта функция в настоящий момент работает. То есть этой функции нет разницы работает она от имени сервиса «host/server», или «cifs/server» — всё, что ей нужно это реквизиты входа (credentials) текущего «сервиса». Вот здесь можно почитать про то, как человек переписывался с Microsoft по этому вопросу, и ему подтвердили, что фактически имя сервиса не является каким‑то влияющим на аутентификацию параметром.
Дополнительной особенностью «сервисного контекста» в SSPI является то, что в этом контексте может существовать одновременно несколько credentials. То есть на самом деле один «сервис» может корректно обрабатывать запросы, направляемые в адрес нескольких различных пользователей/сервисов. Например, если у «user_1» сконфигурирован SPN_1, а у user_2 сконфигурирован SPN_2, то можно сделать так, что «сервисный контекст» (посредством функции AcceptSecurityContext) сможет корректно обработать входящие service tickets как для SPN_1, так и для SPN_2. Реализуется подобный функционал с помощью вызовов функции LsaCallAuthenticationPackage с типами сообщений KerbAddExtraCredentialsMessage или KerbAddExtraCredentialsExMessage. Добавление дополнительных идентификационных данных будет работать только в текущей logon session, в других сессиях дополнительные идентификационные данные будут отсутствовать.
Теперь, когда я описал основные функции, которые создают изменения в «контексте безопасности» (security context) необходимо также рассказать о функциях получения информации из «контекста безопасности». Таких функций всего две: QueryContextAttributes и LsaCallAuthenticationPackage. Описание первой функции можно найти здесь. С её помощью можно получить крайне ограниченную информацию из контекста безопасности. Например, на стороне «сервиса» после успешной идентификации с помощью AcceptSecurityContext функция QueryContextAttributes может получить непосредственное значение «access token», получить значение SPN из ST, или имя пользователя из ST. Функция LsaCallAuthenticationPackage является гораздо более интересной. Данная функция служит неким общим интерфейсом для вызова внутреннего функционала произвольного SSP. Реализуется подобная универсальность посредством существования у каждого «security provider» собственных наборов входных и выходных параметров, которые приводятся к нейтральным общим типам и передаются в LsaCallAuthenticationPackage. Перечень входных параметров для Kerberos можно найти по этой ссылке. Однако в официальном описании отсутствуют описания типов входных и выходных структур, передаваемых вместе с каждым из типов сообщений. Ниже я привожу собственную таблицу таких соответствий. Для более прозрачной работы я реализовал специализированные функции и типы внутри библиотеки XKERB.
Тип |
Тип структуры входных данных |
Тип структуры выходных данных |
Реализовано в XKERB |
KerbDebugRequestMessage |
? |
? |
Нет |
KerbQueryTicketCacheMessage |
KERB_QUERY_TKT_CACHE_REQUEST |
KERB_QUERY_TKT_CACHE_RESPONSE |
Да |
KerbChangeMachinePasswordMessage |
? |
? |
Нет |
KerbVerifyPacMessage |
? |
? |
Нет |
KerbRetrieveTicketMessage |
KERB_RETRIEVE_TKT_REQUEST |
KERB_RETRIEVE_TKT_RESPONSE |
Да |
KerbUpdateAddressesMessage |
? |
? |
Нет |
KerbPurgeTicketCacheMessage |
KERB_PURGE_TKT_CACHE_REQUEST |
null |
Да |
KerbChangePasswordMessage |
KERB_CHANGEPASSWORD_REQUEST |
null |
Да |
KerbRetrieveEncodedTicketMessage |
KERB_RETRIEVE_TKT_REQUEST |
KERB_RETRIEVE_TKT_RESPONSE |
Да |
KerbDecryptDataMessage |
KERB_DECRYPT_REQUEST |
KERB_DECRYPT_RESPONSE |
Нет |
KerbAddBindingCacheEntryMessage |
KERB_ADD_BINDING_CACHE_ENTRY_REQUEST |
null |
Да |
KerbSetPasswordMessage |
KERB_SETPASSWORD_REQUEST |
null |
Да |
KerbSetPasswordExMessage |
KERB_SETPASSWORD_EX_REQUEST |
null |
Нет |
KerbAddExtraCredentialsMessage |
KERB_ADD_CREDENTIALS_REQUEST |
null |
Да |
KerbVerifyCredentialsMessage |
? |
? |
Нет |
KerbQueryTicketCacheExMessage |
KERB_QUERY_TKT_CACHE_REQUEST |
KERB_QUERY_TKT_CACHE_EX_RESPONSE |
Да |
KerbPurgeTicketCacheExMessage |
KERB_PURGE_TKT_CACHE_EX_REQUEST |
null |
Нет |
KerbRefreshSmartcardCredentialsMessage |
KERB_REFRESH_SCCRED_REQUEST |
null |
Нет |
KerbQuerySupplementalCredentialsMessage |
? |
? |
Нет |
KerbTransferCredentialsMessage |
KERB_TRANSFER_CRED_REQUEST |
null |
Нет |
KerbQueryTicketCacheEx2Message |
KERB_QUERY_TKT_CACHE_REQUEST |
KERB_QUERY_TKT_CACHE_EX2_RESPONSE |
Да |
KerbSubmitTicketMessage |
KERB_SUBMIT_TKT_REQUEST |
null |
Да |
KerbAddExtraCredentialsExMessage |
KERB_ADD_CREDENTIALS_REQUEST_EX |
null |
Да |
KerbQueryKdcProxyCacheMessage |
KERB_QUERY_KDC_PROXY_CACHE_REQUEST |
KERB_QUERY_KDC_PROXY_CACHE_RESPONSE |
Да |
KerbPurgeKdcProxyCacheMessage |
KERB_PURGE_KDC_PROXY_CACHE_REQUEST |
KERB_PURGE_KDC_PROXY_CACHE_RESPONSE |
Да |
KerbQueryTicketCacheEx3Message |
KERB_QUERY_TKT_CACHE_REQUEST |
KERB_QUERY_TKT_CACHE_EX3_RESPONSE |
Да |
KerbCleanupMachinePkinitCredsMessage |
KERB_CLEANUP_MACHINE_PKINIT_CREDS_REQUEST |
null |
Нет |
KerbAddBindingCacheEntryExMessage |
KERB_ADD_BINDING_CACHE_ENTRY_EX_REQUEST |
null |
Да |
KerbQueryBindingCacheMessage |
KERB_QUERY_BINDING_CACHE_REQUEST |
KERB_QUERY_BINDING_CACHE_RESPONSE |
Да |
KerbPurgeBindingCacheMessage |
KERB_PURGE_BINDING_CACHE_REQUEST |
null |
Да |
KerbPinKdcMessage |
KERB_PIN_KDC_REQUEST |
null |
Да |
KerbUnpinAllKdcsMessage |
KERB_UNPIN_ALL_KDCS_REQUEST |
null |
Да |
KerbQueryDomainExtendedPoliciesMessage |
KERB_QUERY_DOMAIN_EXTENDED_POLICIES_REQUEST |
KERB_QUERY_DOMAIN_EXTENDED_POLICIES_RESPONSE |
Да |
KerbQueryS4U2ProxyCacheMessage |
KERB_QUERY_S4U2PROXY_CACHE_REQUEST |
KERB_QUERY_S4U2PROXY_CACHE_RESPONSE |
Да |
KerbRetrieveKeyTabMessage |
KERB_RETRIEVE_KEY_TAB_REQUEST |
KERB_RETRIEVE_KEY_TAB_RESPONSE |
Да |
KerbRefreshPolicyMessage |
KERB_REFRESH_POLICY_REQUEST |
KERB_REFRESH_POLICY_RESPONSE |
Да |
KerbPrintCloudKerberosDebugMessage |
KERB_CLOUD_KERBEROS_DEBUG_REQUEST |
KERB_CLOUD_KERBEROS_DEBUG_RESPONSE |
Нет |
Как можно заметить, с помощью LsaCallAuthenticationPackage можно как просто получать информацию, так и запрашивать действия со стороны «security provider». Для автоматизации определённых вызовов LsaCallAuthenticationPackage я написал вспомогательный код, который можно найти здесь.
Также здесь можно упомянуть, что в Windows существует стандартная утилита «klist», с помощью которой можно посмотреть все существующие в текущей «logon session» TGT и service tickets. С помощью LsaCallAuthenticationPackage я реализовал аналог функциональности программы «klist», выводящий информацию по кэшированным тикетам Kerberos, код приведён здесь.
Ещё одной, играющей вспомогательную роль, является функция LsaLogonUser. Эта функция также запрашивает новый TGT для пользователя по его реквизитам для входа. Напомню, что в этой функции также можно использовать текстовое представление идентификатора сертификата. Однако в отличие от ранее рассмотренных функций LsaLogonUser после успешного получения TGT автоматически создаёт новую «logon session». Как я ранее писал, такой функциональностью из ранее рассмотренных функций обладает только AcceptSecurityContext. Так как все действия в SSPI так или иначе привязаны к «logon session», то лично я считаю, что если необходимо в программе реализовать какой‑то «сервис» имея предопределённые реквизиты для входа, то крайне полезным будет сначала явно сделать «logon session», и уже потом в рамках этой новой сессии вызывать функции InitializeSecurityContext/AcceptSecurityContext. С использованием данного подхода я реализовал некий «универсальный клиент‑сервисный код»: в одной функции вызываются как клиентские, так и сервисные функции. Это позволяет протестировать клиент‑сервисное взаимодействие без использования каких‑либо коммуникаций. Данный код находится здесь.
На этом я заканчиваю рассмотрение базового функционала, и перехожу ещё к одной большой теме — делегированию в Kerberos.
Делегирование
Под «делегированием» в Kerberos понимается процесс, когда клиент каким‑либо образом передаёт «сервису» возможность действовать, используя идентификационную информацию клиента. Это бывает необходимо в тех случаях, когда клиент поручает «сервису» выполнить операцию, которая, в свою очередь, требует обращения к другим «сервисам». В этом случае первичному «сервису» будет необходимо получить возможность самостоятельно получать Service Ticket (ST) от имени клиента к другим «сервисам». Конечно, можно было бы как‑то организовать обратную коммуникацию с клиентом, запрос у него ST для другого «сервиса», передача этого ST обратно и так далее. Однако технически это зачастую сложно реализуемо. Например, если «сервис» является HTTP сервисом, то у него очень ограниченная возможность обратной коммуникации с клиентом по своей инициативе. Поэтому Kerberos предоставляет функционал, позволяющий делегировать идентификационную информацию клиентов на определённые «сервисы».
Существуют следующие виды делегирования:
-
Неограниченное делегирование;
-
Ограниченное делегирование с использование только протокола Kerberos;
-
Ограниченное делегирование с использование произвольного протокола;
Перед описание каждого вида делегирования подробно приведу одно общее замечание. Чтобы «сервис» мог кешировать (и в дальнейшем использовать) TGT от других пользователей данный «сервис» должен исполняться из‑под пользователя, у которого активна привилегия SeEnableDelegationPrivilege (SE_ENABLE_DELEGATION_NAME).
Неограниченное делегирование
В случае неограниченного делегирования клиент передаёт на сервис свой TGT вместе с его текущим сессионным ключом. Имея эту информацию «сервис» в дальнейшем может самостоятельно запросить Service Ticket (ST) к любому существующему «сервису». Коммуникации никак не ограничены.
Ограниченное делегирование с использованием только Kerberos
В случае ограниченного делегирования вместо TGT на сторону «сервиса» передаётся только ST. Далее «сервис» может, используя расширение S4U2Proxy, запросить от имени пользователя ST к другому «сервису». Набор «сервисов», к которым данный «сервис» может получить ST от имени пользователя, заранее определён и ограничен.
Несмотря на то, что механизм ограниченного делегирования с использованием только Kerberos реализован в Windows уже очень давно, и, вроде как, должен быть полностью отлажен у меня в тестовых сервисах не получилось реализовать делегирование с использованием этого режима. После аутентификации клиента на сервисе 1 в сессии возникал соответствующий service ticket от этого клиента к сервису 1. И этот service ticket даже имел установленный FORWARDABLE флаг. Однако если я пытался из этой сессии выполнить запрос к сервису 2, то функции SSPI не включали уже имеющийся service ticket и вместо этого пытались заново запросить TGT для клиента с использованием S4U2Self. В ответ формировался TGT, однако без флага FORWARDABLE. И, как результат, запрос на получение service ticket для сервиса 2 отклонялся.
Ограниченное делегирование с использованием произвольного протокола
Здесь клиент может присылать на сервис только service ticket, однако «сервис» имеет дополнительную возможность самостоятельно, вообще без какого‑либо участия клиента запрашивать его идентификационную информацию. Повторюсь, что в случае «делегирование без ограничения протоколов» пользователь всё также может передать ST с на сторону «сервиса», эта функциональность в данном виде делегирования осталась без изменений.
Для реализации запроса «сервисом» идентификационной информации для произвольного клиента было реализовано расширение Service for User to Self (S4U2self). Представим, что есть какой‑то «сервис», который выполняет идентификацию пользователей по биометрии, скажем на основании анализа лица пользователя. И для этого «сервис» использует специфическое оборудование, специальные вызовы драйверов и так далее. База данных лиц пользователей также будет какой‑то специализированной. Также предположим, что этот «сервис» должен выполнять аутентификацию пользователей домена, и использовать для этого Kerberos. В случае подобной задачи внутри «сервиса» будет нужна дополнительная база, сопоставляющая имена пользователей домена с их идентификационными данными, специфичными для данного «сервиса» (в нашем случае — с изображениями их лиц). Таким образом, после успешной идентификации пользователя по его лицу «сервис» теперь может получить имя пользователя в домене. Далее, используя расширение S4U2Self, данный «сервис» сможет запросить аутентификационную информацию пользователя по протоколу Kerberos зная только его имя и далее получить возможность аутентификации данного пользователя на других «сервисах» (логин на рабочую станцию, доступ к файлам по сети и так далее).
На самом деле использовать S4U2Self может абсолютно любой пользователь, который уже прошёл идентификацию в домене. То есть если у пользователя есть TGT, то он может запросить идентификационную информацию по любому другому пользователю, машине или даже «сервису» (по имени «сервиса»). Нет никаких ограничений на то, от имени кого будут приходить запросы S4U2Self, запросы может делать как полноценный «сервис», так и обычный пользователь.
Расширение S4U2Self реализуется с помощью всего двух дополнительных структур: PA‑FOR‑USER и PA‑S4U‑X509-USER. Использование этих двух структур является взаимоисключающим (можно использовать только один из двух типов в одном запросе). При использовании PA‑FOR‑USER можно запрашивать информацию только на основе имени пользователя. При использовании PA‑S4U‑X509-USER можно запрашивать информацию как на основе имени пользователя, так и на основе X.509 сертификата пользователя. В MS‑SFU также упоминается, что если текущий домен поддерживает схемы шифрования с использованием AES-128 и выше, то рекомендуется всегда использовать PA‑S4U‑X509-USER.
Реализация SSP Kerberos в Windows поддерживает использование S4U2Self, но только опосредованно, через вызов LsaLogonUser с параметрами KERB_S4U_LOGON или KERB_CERTIFICATE_S4U_LOGON. На основании использования этой функция я создал некий «whoami на S4U», позволяющий выводить всю информацию любого пользователя — как то его группы, SID, набор привилегий. Код данного проекта доступ по этой ссылке.
Использование S4U2Self позволяет получить идентификационную информацию для любого пользователя, однако это позволяет идентифицировать пользователя только локально на той машине, на которой запущен «сервис». Для того, чтобы реализовать возможность делегирования идентификационных данных пользователей на других «сервисах» было реализовано другое расширение Kerberos — S4U2Proxy.
Расширение S4U2Proxy позволяет запрашивать у KDC service ticket от имени любого пользователя к ограниченному набору сервисов. Внутри расширение S4U2Proxy реализовано просто в виде двух дополнительных флагов: флага «resource‑based constrained delegation» внутри PA‑PAC‑OPTIONS, и флага «cname‑in‑addl‑tkt» внутри KDC_OPTIONS. Использование флага CNAME‑IN‑ADDL‑TKT позволяет запросить ST для того пользователя, имя которого содержится в «additional tickets». Использование флага «resource‑based constrained delegation» позволяет использовать возможности RBCD (Resource‑Based Constrained Delegation).
Как ни странно, несмотря на официальную документацию Microsoft (MS‑SFU), флаг CNAME‑IN‑ADDL‑TKT на самом деле не используется. То есть ни одна функция SSPI не выставляет данный флаг и не учитывает его при приёме сообщений. Как минимум так было у меня во всех возможных проводимых тестах. Вместо этого, похоже, имя клиента автоматически берётся из additional ticket если это поле содержит какое‑то значение.
Использование RBCD позволяет более тонко конфигурировать получение service tickets для каждого из «сервисов». При использовании RBCD каждый пользователь, из‑под которого запускается какой‑либо «сервис», может сконфигурировать свой список других пользователей, которым доступна возможность делегирования на этот «сервис». То есть в атрибуте msDS‑AllowedToActOnBehalfOfOtherIdentity для данного пользователя может содержаться полноценный security descriptor (SD), в котором будут прописаны полноценные Access Control Lists (ACLs). Представим, что клиент сформировал запрос для «сервиса 1». В процессе деятельности «сервис 1» должен выполнить какие‑то действия на «сервис 2» используя аутентификационную информацию для клиента. При использовании RBCD «сервис 2» должен иметь в msDS‑AllowedToActOnBehalfOfOtherIdentity такой security descriptor, который бы позволял доступ для пользователя, из‑под которого запускается «сервис 1». Также необходимо понимать крайне важную особенность RBCD: если для какого‑то пользователя сконфигурирован SD, который позволяет выполнить делегирование для «сервис 1», то режим делегирования для «сервис 1» не учитывается. То есть если «сервис 1» сконфигурирован так, чтобы не позволять делегирование (режим «Do not trust this user for delegation»), то он всё равно сможет выполнить делегирование к «сервис 2», если у «сервис 2» сконфигурирован security descriptor, позволяющий доступ для «сервис 1». Также KDC не учитывает ограничения делегирования по именам. То есть если для «сервис 1» задано ограниченное делегирование, и «сервис 2» не присутствует в списке, то делегирование на «сервис 2» всё равно будет выполнено, если на «сервис 2» задан корректный security descriptor, позволяющий доступ для «сервис 1».
User-To-User (U2U)
Как уже было сказано, протокол Kerberos использует шифрование на заранее известных ключах, генерируемых на основе текстовых паролей. Однако существуют варианты, когда пользователь будет запускать какой‑либо «сервис», не обладая знанием о собственном пароле. Это может быть, например, в случае использования X.509 сертификата для первичной аутентификации. В этом случае необходимо иметь возможность так или иначе обмениваться аутентификационной информацией между клиентами и подобным «сервисом». Для реализации подобного функционала была придумана схема U2U.
При использовании U2U работа с «сервисом» для клиента начинается с посылки стандартного сообщения AP‑REQ. Однако, так как «сервис» не имеет информации о собственном пароле, то «сервис» в ответ формирует ошибку с кодом KRB_AP_ERR_USER_TO_USER_REQUIRED (0×45). Для того, чтобы клиент смог получить эту ошибку на «сервисе» должен быть использован флаг ASC_REQ_EXTENDED_ERROR, а на клиенте должен быть использован флаг ISC_REQ_MUTUAL_AUTH чтобы клиент ожидал какой‑то информации со стороны «сервиса». Можно также явно задать флаги ISC_REQ_USE_SESSION_KEY и ASC_REQ_USE_SESSION_KEY, но в этом случае общая система теряет универсальность: клиент сразу начинает с запроса TGT для «сервиса».
Итак, после получения ошибки клиент понимает, что необходимо реализовать общение с использованием не постоянных, а сессионных ключей. Для этого клиент сперва формирует запрос на «сервис» для получения TGT этого сервиса. Данный запрос формируется на уровне GSS‑API, и представляет собой, по сути, специальный Object Identificator (OID). В ответ «сервис» возвращает свой TGT. Передача TGT в данном случае условно безопасна, так как вместе с TGT не передаётся его сессионный ключ. После получения этого TGT клиент формирует специальный запрос к KDC на получение service ticket, в котором устанавливается флаг ENC‑TKT‑IN‑SKEY, а также в поле additional tickets добавляется TGT, полученный со стороны «сервиса». В свою очередь KDC расшифровывает TGT из additional ticket, получает оттуда session key, а затем формирует новый service ticket, шифруя его уже на этом session key. После получения нового service ticket (ST) клиент далее может переслать этот ST заново необходимому «сервису». Теперь «сервис» может расшифровать этот полученный ST и провести аутентификацию клиента стандартным образом.
Тестирование Kerberos
Теперь, когда я описал все основные возможности стандартных клиента и серверной части Kerberos в Windows, можно перейти к описанию схемы тестирования этого функционала. Так как в Windows только в SSPI реализована возможность работы с Kerberos, то естественно, что для тестирования будут использоваться только стандартные функции SSPI. Например, вот здесь приведены ссылки на тестовые примеры по SSPI от Microsoft. В этих примерах существует однозначное разделение на программу, выполняющую клиентскую часть, и программу, которая реализует функции «сервиса». Однако всё можно упростить, если убрать передачу между «клиентом» и «сервисом» по какому‑либо стороннему каналу (вроде pipe, как в примерах от Microsoft). Вместо этого достаточно, чтобы выходной массив из функции InitializeSecurityContext был передан на вход функции AcceptSecurityContext. Для реализации полноценных «клиента» и «сервиса» на самом деле достаточно использовать двух обычных пользователей домена, у одного из которых будет непустой атрибут servicePrincipalNames (то есть будут SPNs). Изменяя параметры делегирования для «сервисного» пользователя, можно изучать различные виды этого делегирования, а также иметь возможность проконтролировать каждый из параметров как на стороне «клиента», так и «сервиса». Можно инициализировать «сервис» через сертификат (PKINIT) и получить сервисный контекст, в котором будут отсутствовать данные о логине и пароле, а обмен с таким «сервисом» будет возможен только через U2U. Можно добавлять дополнительные credentials в «сервисный» контекст и получить возможность корректно принимать «не свои» билеты: то есть если у нас в тестовом окружении будут два «сервисных» пользователя, и мы добавим credentials от второго пользователя в контекст первого, то теперь сервисный контекст первого пользователя сможет корректно обработать все AP‑REQ к SPNs от второго пользователя, абсолютно прозрачно. Можно добавлять tickets в «клиентский» или «сервисный» контекст (Push The Ticket, PTT). Можно получать какие‑то дополнительные данные из любого контекста, и так далее.
Для более удобной работы со всем этим функционалом я реализовал библиотеку XKERB, а также набор тестовых функций, позволяющий тестировать произвольный функционал SSPI. Библиотека написана на чистом С++ с использованием функционала последнего стандарта С++ 23. При работе применяются «умные указатели» и, как следствие, пользователю библиотеки нет необходимости заботиться о менеджменте памяти: все необходимые функции будут вызваны в нужное время и память будет корректно освобождена. Также в библиотеке применяется «Aggregate initialization», позволяющая передавать в функции параметры по их имени, а также прозрачно использовать опциональные параметры. Реализация библиотеки находится по этой ссылке.