Как проверить учетные данные домена?


87

Я хочу проверить набор учетных данных на контроллере домена. например:

Username: STACKOVERFLOW\joel
Password: splotchy

Метод 1. Запрос Active Directory с олицетворением

Многие люди предлагают запросить что-нибудь в Active Directory. Если генерируется исключение, значит, вы знаете, что учетные данные недействительны - как предлагается в этом вопросе о переполнении стека .

Однако у этого подхода есть несколько серьезных недостатков :

  1. Вы не только аутентифицируете учетную запись домена, но также выполняете неявную проверку авторизации. То есть вы читаете свойства из AD с помощью токена олицетворения. Что, если действующая учетная запись не имеет прав на чтение из AD? По умолчанию все пользователи имеют доступ для чтения, но политики домена могут быть настроены на отключение разрешений на доступ для ограниченных учетных записей (и / или групп).

  2. Привязка к AD имеет серьезные накладные расходы, кэш схемы AD должен быть загружен на клиенте (кеш ADSI в провайдере ADSI, используемом DirectoryServices). Это потребляет ресурсы и сеть, и сервер AD, и это слишком дорого для такой простой операции, как аутентификация учетной записи пользователя.

  3. Вы полагаетесь на ошибку исключения в неисключительном случае и предполагаете, что это означает неверное имя пользователя и пароль. Другие проблемы (например, сбой сети, сбой подключения к AD, ошибка выделения памяти и т. Д.) Затем неверно интерпретируются как сбой аутентификации.

Способ 2. LogonUser Win32 API

Другие предложили использовать LogonUser()функцию API. Звучит неплохо, но, к сожалению, вызывающему пользователю иногда требуется разрешение, которое обычно предоставляется только самой операционной системе:

Для процесса, вызывающего LogonUser, требуется привилегия SE_TCB_NAME. Если вызывающий процесс не имеет этой привилегии, LogonUser завершается ошибкой, а GetLastError возвращает ERROR_PRIVILEGE_NOT_HELD.

В некоторых случаях для процесса, вызывающего LogonUser, также должна быть включена привилегия SE_CHANGE_NOTIFY_NAME; в противном случае LogonUser завершается ошибкой, а GetLastError возвращает ERROR_ACCESS_DENIED. Эта привилегия не требуется для локальной системной учетной записи или учетных записей, которые являются членами группы администраторов. По умолчанию SE_CHANGE_NOTIFY_NAME включен для всех пользователей, но некоторые администраторы могут отключить его для всех.

Предоставление привилегии « Действовать как часть операционной системы » - это не то, что вы хотите делать волей-неволей, как Microsoft указывает в статье базы знаний :

... процесс, вызывающий LogonUser, должен иметь привилегию SE_TCB_NAME (в диспетчере пользователей это право « Действовать как часть операционной системы »). Привилегия SE_TCB_NAME очень мощная и не должна предоставляться произвольным пользователям только для того, чтобы они могли запускать приложение, которому необходимо проверять учетные данные.

Кроме того, вызов LogonUser()не удастся, если указан пустой пароль.


Как правильно аутентифицировать набор учетных данных домена?


Я случиться , чтобы быть звонок из управляемого кода, но это аа общий вопрос для Windows. Можно предположить, что у клиентов установлена ​​.NET Framework 2.0.


1
Читатели должны отметить, что начиная с Windows XP LogonUser больше не требует SE_TCB_NAME (если вы не входите в учетную запись Passport).
Гарри Джонстон

Ответы:


130

C # в .NET 3.5 с использованием System.DirectoryServices.AccountManagement .

 bool valid = false;
 using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
 {
     valid = context.ValidateCredentials( username, password );
 }

Это будет проверяться на текущий домен. Проверьте параметризованный конструктор PrincipalContext для других вариантов.


@tvanfosson: разве DirectoryServices не использует AD?
Mitch Wheat

1
Да. Но в документации указано, что это быстрый способ проверки учетных данных. Он также отличается от метода привязки, упомянутого в вопросе, поскольку вы не читаете никаких свойств из объекта. Обратите внимание, что метод находится в контексте, а не в объекте каталога.
tvanfosson

Исправление: System.DirectoryServices.AccountManagement требует .NET 3.5. ( msdn.microsoft.com/en-us/library/… )
Ян Бойд,

19
Он также работает с локальными пользователями, если вы их использовали new PrincipalContext(ContextType.Machine).
VansFannel

Кто-нибудь знает, работает ли это с кешированными учетными данными или требуется подключение к DC? Мне нужно знать это для некоторой реализации, над которой я сейчас работаю, и в настоящее время я не нахожусь ни в одном домене для тестирования
Jcl

21
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security;
using System.DirectoryServices.AccountManagement;

public struct Credentials
{
    public string Username;
    public string Password;
}

public class Domain_Authentication
{
    public Credentials Credentials;
    public string Domain;

    public Domain_Authentication(string Username, string Password, string SDomain)
    {
        Credentials.Username = Username;
        Credentials.Password = Password;
        Domain = SDomain;
    }

    public bool IsValid()
    {
        using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, Domain))
        {
            // validate the credentials
            return pc.ValidateCredentials(Credentials.Username, Credentials.Password);
        }
    }
}

7
содержит ли это какое-либо существенное отличие от ответа @tvanfosson 3 года назад?
gbjbaanb

5
@gbjbaanb Да, поскольку он содержит Domainпараметр при создании PrincipalContext, то, что мне было интересно узнать и что я нашел в этом ответе.
Руди Виссер

1
@RudiVisser tvanfosson действительно посоветовал вам «Проверить параметризованный конструктор PrincipalContext для других опций» - всегда читайте документацию, никогда не верьте простому слову Интернета! :)
gbjbaanb

4
@gbjbaanb Да, конечно, но и обеспечивает рабочий пример , а не ссылку и предложение прочитать в другом месте мантра StackOverflow, поэтому мы принимаем несколько представлений ответов: D Просто сказать , что это действительно обеспечивает более.
Руди Виссер

Кто-нибудь знает, как мы можем сделать что-то подобное в приложении UWP? (с обычным AD, а не с Azure AD). Я задал вопрос здесь: stackoverflow.com/questions/42821447
slayernoah

7

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

Я давно искал что-то подобное ... Надеюсь, это кому-то поможет!

using System;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Runtime.InteropServices;

namespace User
{
    public static class UserValidation
    {
        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool LogonUser(string principal, string authority, string password, LogonTypes logonType, LogonProviders logonProvider, out IntPtr token);
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool CloseHandle(IntPtr handle);
        enum LogonProviders : uint
        {
            Default = 0, // default for platform (use this!)
            WinNT35,     // sends smoke signals to authority
            WinNT40,     // uses NTLM
            WinNT50      // negotiates Kerb or NTLM
        }
        enum LogonTypes : uint
        {
            Interactive = 2,
            Network = 3,
            Batch = 4,
            Service = 5,
            Unlock = 7,
            NetworkCleartext = 8,
            NewCredentials = 9
        }
        public  const int ERROR_PASSWORD_MUST_CHANGE = 1907;
        public  const int ERROR_LOGON_FAILURE = 1326;
        public  const int ERROR_ACCOUNT_RESTRICTION = 1327;
        public  const int ERROR_ACCOUNT_DISABLED = 1331;
        public  const int ERROR_INVALID_LOGON_HOURS = 1328;
        public  const int ERROR_NO_LOGON_SERVERS = 1311;
        public  const int ERROR_INVALID_WORKSTATION = 1329;
        public  const int ERROR_ACCOUNT_LOCKED_OUT = 1909;      //It gives this error if the account is locked, REGARDLESS OF WHETHER VALID CREDENTIALS WERE PROVIDED!!!
        public  const int ERROR_ACCOUNT_EXPIRED = 1793;
        public  const int ERROR_PASSWORD_EXPIRED = 1330;

        public static int CheckUserLogon(string username, string password, string domain_fqdn)
        {
            int errorCode = 0;
            using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domain_fqdn, "ADMIN_USER", "PASSWORD"))
            {
                if (!pc.ValidateCredentials(username, password))
                {
                    IntPtr token = new IntPtr();
                    try
                    {
                        if (!LogonUser(username, domain_fqdn, password, LogonTypes.Network, LogonProviders.Default, out token))
                        {
                            errorCode = Marshal.GetLastWin32Error();
                        }
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                    finally
                    {
                        CloseHandle(token);
                    }
                }
            }
            return errorCode;
        }
    }

это «метод 2», описанный в вопросе ... так что ... на самом деле не отвечая на вопрос
Роберт Леви

1

Вот как определить локального пользователя:

    public bool IsLocalUser()
    {
        return windowsIdentity.AuthenticationType == "NTLM";
    }

Редактировать Иэн Бойд

Вам вообще не следует использовать NTLM. Он настолько старый и настолько плохой, что Microsoft Application Verifier (который используется для выявления распространенных ошибок программирования) выдаст предупреждение, если обнаружит, что вы используете NTLM.

Вот глава из документации Application Verifier о том, почему у них есть тест, если кто-то по ошибке использует NTLM:

Зачем нужен подключаемый модуль NTLM

NTLM - это устаревший протокол аутентификации с недостатками, которые потенциально могут поставить под угрозу безопасность приложений и операционной системы. Самым важным недостатком является отсутствие аутентификации сервера, что может позволить злоумышленнику обманом заставить пользователей подключиться к поддельному серверу. Как следствие отсутствия аутентификации сервера, приложения, использующие NTLM, также могут быть уязвимы для типа атаки, известной как атака «отражения». Последний позволяет злоумышленнику перехватить диалог аутентификации пользователя с легитимным сервером и использовать его для аутентификации злоумышленника на компьютере пользователя. Уязвимости NTLM и способы их использования являются целью все возрастающей исследовательской активности в сообществе безопасности.

Хотя Kerberos существует уже много лет, многие приложения все еще написаны для использования только NTLM. Это без нужды снижает безопасность приложений. Однако Kerberos не может заменить NTLM во всех сценариях - особенно в тех, где клиенту необходимо пройти аутентификацию в системах, которые не присоединены к домену (домашняя сеть, возможно, является наиболее распространенной из них). Пакет безопасности Negotiate допускает компромисс с обратной совместимостью, который использует Kerberos, когда это возможно, и возвращается к NTLM только тогда, когда нет другого варианта. Переключение кода на использование Negotiate вместо NTLM значительно повысит безопасность для наших клиентов, в то же время обеспечивая небольшую совместимость приложений или вообще не обеспечивая их. Само по себе согласование - не панацея: бывают случаи, когда злоумышленник может принудительно перейти на NTLM, но воспользоваться ими значительно труднее. Однако одно немедленное улучшение заключается в том, что приложения, написанные для правильного использования Negotiate, автоматически становятся невосприимчивыми к атакам отражения NTLM.

В качестве последнего слова предостережения против использования NTLM: в будущих версиях Windows можно будет отключить использование NTLM в операционной системе. Если приложения жестко зависят от NTLM, они просто не смогут пройти аутентификацию, когда NTLM отключен.

Как работает плагин

Плагин Verifier обнаруживает следующие ошибки:

  • Пакет NTLM напрямую указывается в вызове AcquireCredentialsHandle (или API оболочки более высокого уровня).

  • Целевое имя в вызове InitializeSecurityContext равно NULL.

  • Целевое имя в вызове InitializeSecurityContext не является правильно сформированным именем SPN, UPN или доменным именем в стиле NetBIOS.

Последние два случая заставят Negotiate вернуться к NTLM либо напрямую (первый случай), либо косвенно (контроллер домена вернет ошибку «принципал не найден» во втором случае, вызывая откат Negotiate).

Подключаемый модуль также регистрирует предупреждения при обнаружении перехода на NTLM; например, когда имя участника-службы не найдено контроллером домена. Они регистрируются только как предупреждения, поскольку они часто являются законными случаями - например, при аутентификации в системе, не присоединенной к домену.

NTLM останавливает

5000 - приложение явно выбрало пакет NTLM

Серьезность - ошибка

Приложение или подсистема явно выбирает NTLM вместо Negotiate при вызове AcquireCredentialsHandle. Даже несмотря на то, что клиент и сервер могут пройти аутентификацию с использованием Kerberos, этому препятствует явный выбор NTLM.

Как исправить эту ошибку

Чтобы исправить эту ошибку, выберите пакет Negotiate вместо NTLM. Как это будет сделано, будет зависеть от конкретной сетевой подсистемы, используемой клиентом или сервером. Ниже приведены некоторые примеры. Вам следует обратиться к документации по конкретной библиотеке или набору API, которые вы используете.

APIs(parameter) Used by Application    Incorrect Value  Correct Value  
=====================================  ===============  ========================
AcquireCredentialsHandle (pszPackage)  “NTLM”           NEGOSSP_NAME “Negotiate”

-1
using System;
using System.Collections.Generic;
using System.Text;
using System.DirectoryServices.AccountManagement;

class WindowsCred
{
    private const string SPLIT_1 = "\\";

    public static bool ValidateW(string UserName, string Password)
    {
        bool valid = false;
        string Domain = "";

        if (UserName.IndexOf("\\") != -1)
        {
            string[] arrT = UserName.Split(SPLIT_1[0]);
            Domain = arrT[0];
            UserName = arrT[1];
        }

        if (Domain.Length == 0)
        {
            Domain = System.Environment.MachineName;
        }

        using (PrincipalContext context = new PrincipalContext(ContextType.Domain, Domain)) 
        {
            valid = context.ValidateCredentials(UserName, Password);
        }

        return valid;
    }
}

Кашиф Муштак Оттава, Канада


Пространство имен System.DirectoryServices.AccountManagement было новым в .NET 3.5
Джереми Грей,

1
Я знаю, что ему почти 4 года, но если вы проверяете локального пользователя, вам нужно будет убедиться, что вы установили ContextType на ContextType.Machine при создании PrincipalContext. В противном случае он будет думать, что имя компьютера, указанное в переменной Domain, на самом деле является сервером домена.
SolidRegardless
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.