Jak zweryfikować poświadczenia domeny?

86

Chcę zweryfikować zestaw poświadczeń względem kontrolera domeny. na przykład:

Username: STACKOVERFLOW\joel
Password: splotchy

Metoda 1. Zapytanie do usługi Active Directory z personifikacją

Wiele osób sugeruje wyszukanie czegoś w usłudze Active Directory. Jeśli zostanie zgłoszony wyjątek, to wiesz, że poświadczenia są nieprawidłowe - jak sugeruje to pytanie o przepływie stosu .

Takie podejście ma jednak kilka poważnych wad :

  1. Nie tylko uwierzytelniasz konto domeny, ale również przeprowadzasz niejawną kontrolę autoryzacji. Oznacza to, że odczytujesz właściwości z usługi AD przy użyciu tokenu personifikacji. Co się stanie, jeśli konto, które w przeciwnym razie działa, nie ma uprawnień do odczytu z reklamy? Domyślnie wszyscy użytkownicy mają dostęp do odczytu, ale zasady domeny można ustawić tak, aby wyłączyć uprawnienia dostępu dla kont z ograniczeniami (i / lub grup).

  2. Powiązanie z usługą AD wiąże się z poważnymi narzutami, pamięć podręczna schematu usługi AD musi zostać załadowana po stronie klienta (pamięć podręczna ADSI u dostawcy ADSI używana przez DirectoryServices). Jest to zarówno sieć, jak i serwer AD, które pochłaniają zasoby - i są zbyt kosztowne dla prostej operacji, takiej jak uwierzytelnianie konta użytkownika.

  3. Polegasz na niepowodzeniu wyjątku w przypadku nietypowym i zakładasz, że oznacza to nieprawidłową nazwę użytkownika i hasło. Inne problemy (np. Awaria sieci, awaria łączności AD, błąd alokacji pamięci itp.) Są następnie błędnie interpretowane jako awaria uwierzytelniania.

Metoda 2. LogonUser Win32 API

Inni sugerowali użycie LogonUser()funkcji API. Brzmi to nieźle, ale niestety użytkownik wywołujący czasami potrzebuje pozwolenia, które zwykle jest udzielane tylko samemu systemowi operacyjnemu:

Proces wywołujący LogonUser wymaga uprawnienia SE_TCB_NAME. Jeśli proces wywołujący nie ma tego uprawnienia, LogonUser kończy się niepowodzeniem i GetLastError zwraca ERROR_PRIVILEGE_NOT_HELD.

W niektórych przypadkach proces, który wywołuje LogonUser, musi mieć również włączone uprawnienie SE_CHANGE_NOTIFY_NAME; w przeciwnym razie LogonUser nie powiedzie się i GetLastError zwraca ERROR_ACCESS_DENIED. To uprawnienie nie jest wymagane w przypadku lokalnego konta systemowego ani kont należących do grupy administratorów. Domyślnie SE_CHANGE_NOTIFY_NAME jest włączony dla wszystkich użytkowników, ale niektórzy administratorzy mogą go wyłączyć dla wszystkich.

Udostępnianie uprawnienia „ Działaj jako część systemu operacyjnego ” nie jest czymś, co chcesz robić, kiedy nie chcesz - jak wskazuje Microsoft w artykule z bazy wiedzy :

... proces, który wywołuje LogonUser, musi mieć uprawnienie SE_TCB_NAME (w Menedżerze użytkowników jest to uprawnienie „ Działaj jako część systemu operacyjnego ”). Przywilej SE_TCB_NAME ma bardzo duże możliwości i nie powinien być nadawany żadnemu dowolnemu użytkownikowi tylko po to, aby mógł on uruchomić aplikację wymagającą weryfikacji poświadczeń.

Ponadto wywołanie LogonUser()nie powiedzie się, jeśli zostanie określone puste hasło.


Jaki jest właściwy sposób uwierzytelniania zestawu poświadczeń domeny?


Tak się składa, że ​​dzwonię z kodu zarządzanego, ale jest to ogólne pytanie dotyczące systemu Windows. Można założyć, że klienci mają zainstalowany .NET Framework 2.0.

Ian Boyd
źródło
1
Czytelnicy powinni pamiętać, że od systemu Windows XP LogonUser nie wymaga już SE_TCB_NAME (chyba że logujesz się na konto Passport).
Harry Johnston

Odpowiedzi:

130

C # w .NET 3.5 przy użyciu System.DirectoryServices.AccountManagement .

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

Spowoduje to weryfikację w bieżącej domenie. Sprawdź sparametryzowany konstruktor PrincipalContext, aby uzyskać inne opcje.

tvanfosson
źródło
@tvanfosson: czy DirectoryServices nie używa AD?
Mitch Wheat
1
Tak. Jednak dokumentacja wskazuje, że jest to szybki sposób sprawdzenia poświadczeń. Różni się również od metody wiązania wspomnianej w pytaniu, ponieważ nie odczytujesz żadnych właściwości z obiektu. Zauważ, że metoda dotyczy kontekstu, a nie obiektu katalogu.
tvanfosson
Poprawka: System.DirectoryServices.AccountManagement wymaga platformy .NET 3.5. ( msdn.microsoft.com/en-us/library/… )
Ian Boyd
19
Działa również z lokalnymi użytkownikami, jeśli new PrincipalContext(ContextType.Machine)zamiast tego użyłeś .
VansFannel
Czy ktoś wie, czy to działa na buforowanych poświadczeniach, czy wymaga połączenia z kontrolerem domeny? Muszę to wiedzieć, jeśli chodzi o implementację, nad którą teraz pracuję i nie jestem obecnie w żadnej domenie do przetestowania
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);
        }
    }
}
kantanomo
źródło
7
czy zawiera to jakąkolwiek istotną różnicę w stosunku do odpowiedzi @ tvanfosson 3 lata wcześniej?
gbjbaanb
5
@gbjbaanb Tak, ponieważ zawiera Domainparametr podczas tworzenia PrincipalContext, coś, co mnie interesowało i znalazłem w tej odpowiedzi.
Rudi Visser
1
@RudiVisser tvanfosson zasugerował Ci „Sprawdź sparametryzowany konstruktor PrincipalContext dla innych opcji” - zawsze czytaj dokumentację, nigdy nie wierz po prostu na słowo internetu! :)
gbjbaanb
4
@gbjbaanb Tak oczywiście, ale zapewnienie pracy przykład zamiast linku i sugestię, aby przeczytać gdzie indziej jest mantra StackOverflow, dlatego przyjmują wiele zgłoszeń odpowiedzi: D Wystarczy powiedzieć, że ten ma zapewnić bardziej.
Rudi Visser
Czy ktoś wie, jak moglibyśmy zrobić coś podobnego w aplikacji UWP? (ze zwykłą usługą AD, a nie z usługą Azure AD). Zadałem tutaj pytanie: stackoverflow.com/questions/42821447
slayernoah
7

Używam następującego kodu do weryfikacji poświadczeń. Przedstawiona poniżej metoda potwierdzi, czy poświadczenia są poprawne, a jeśli nie, to hasło wygasło lub wymaga zmiany.

Szukałem czegoś takiego od wieków ... Mam nadzieję, że to komuś pomoże!

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;
        }
    }
Kevinrr3
źródło
to jest "metoda 2" opisana w pytaniu ... więc ... nie bardzo odpowiadam na pytanie
Robert Levy
1

Oto jak określić użytkownika lokalnego:

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

Edycja przez Iana Boyda

Nie powinieneś już w ogóle używać NTLM. Jest tak stary i tak zły, że weryfikator aplikacji firmy Microsoft (który służy do wychwytywania typowych błędów programistycznych) wyświetli ostrzeżenie, jeśli wykryje, że używasz NTLM.

Oto rozdział z dokumentacji Application Verifier o tym, dlaczego mają test, jeśli ktoś omyłkowo używa NTLM:

Dlaczego potrzebna jest wtyczka NTLM

NTLM to przestarzały protokół uwierzytelniania z lukami, które mogą zagrozić bezpieczeństwu aplikacji i systemu operacyjnego. Najważniejszą wadą jest brak uwierzytelniania serwera, który może pozwolić napastnikowi nakłonić użytkowników do połączenia się ze sfałszowanym serwerem. W następstwie braku uwierzytelniania serwera aplikacje korzystające z NTLM mogą być również podatne na atak zwany atakiem „refleksyjnym”. Ta ostatnia umożliwia atakującemu przechwycenie rozmowy uwierzytelniającej użytkownika na legalny serwer i użycie go do uwierzytelnienia atakującego na komputerze użytkownika. Luki w zabezpieczeniach NTLM i sposoby ich wykorzystania są celem rosnącej działalności badawczej w społeczności zajmującej się bezpieczeństwem.

Chociaż protokół Kerberos był dostępny od wielu lat, wiele aplikacji jest nadal napisanych wyłącznie z myślą o używaniu protokołu NTLM. To niepotrzebnie zmniejsza bezpieczeństwo aplikacji. Kerberos nie może jednak zastąpić NTLM we wszystkich scenariuszach - głównie w tych, w których klient musi uwierzytelnić się w systemach, które nie są przyłączone do domeny (sieć domowa jest prawdopodobnie najpopularniejszym z nich). Pakiet zabezpieczeń Negotiate pozwala na kompromis ze wsteczną zgodnością, który używa protokołu Kerberos, gdy tylko jest to możliwe, i przywraca NTLM tylko wtedy, gdy nie ma innej opcji. Przełączenie kodu na używanie Negotiate zamiast NTLM znacznie zwiększy bezpieczeństwo naszych klientów, wprowadzając niewiele lub żadnych kompatybilności aplikacji. Negocjowanie samo w sobie nie jest srebrną kulą - istnieją przypadki, w których osoba atakująca może wymusić przejście na NTLM, ale są one znacznie trudniejsze do wykorzystania. Jednak jednym z natychmiastowych ulepszeń jest to, że aplikacje napisane w celu poprawnego używania Negotiate są automatycznie odporne na ataki odbicia NTLM.

Na koniec ostrzeżenie przed używaniem NTLM: w przyszłych wersjach systemu Windows będzie można wyłączyć korzystanie z NTLM w systemie operacyjnym. Jeśli aplikacje mają twardą zależność od NTLM, po prostu nie zostaną uwierzytelnione, gdy NTLM jest wyłączone.

Jak działa wtyczka

Wtyczka Verifier wykrywa następujące błędy:

  • Pakiet NTLM jest określany bezpośrednio w wywołaniu AcquireCredentialsHandle (lub interfejsu API otoki wyższego poziomu).

  • Nazwa docelowa w wywołaniu InitializeSecurityContext to NULL.

  • Nazwa docelowa w wywołaniu InitializeSecurityContext nie jest poprawnie sformułowaną nazwą SPN, UPN lub nazwą domeny w stylu NetBIOS.

W dwóch ostatnich przypadkach Negotiate powróci do NTLM bezpośrednio (pierwszy przypadek) lub pośrednio (w drugim przypadku kontroler domeny zwróci błąd „Nie znaleziono jednostki głównej”, powodując powrót Negotiate).

Wtyczka rejestruje również ostrzeżenia w przypadku wykrycia obniżenia wersji do NTLM; na przykład, gdy nazwa SPN nie zostanie znaleziona przez kontroler domeny. Są one rejestrowane tylko jako ostrzeżenia, ponieważ często są to uzasadnione przypadki - na przykład podczas uwierzytelniania w systemie, który nie jest przyłączony do domeny.

NTLM zatrzymuje się

5000 - Aplikacja ma jawnie wybrany pakiet NTLM

Ważność - błąd

Aplikacja lub podsystem jawnie wybiera NTLM zamiast Negotiate w wywołaniu AcquireCredentialsHandle. Nawet jeśli klient i serwer mogą uwierzytelniać się za pomocą protokołu Kerberos, zapobiega temu jawny wybór NTLM.

Jak naprawić ten błąd

Rozwiązaniem tego błędu jest wybranie pakietu Negotiate zamiast NTLM. Sposób wykonania tego zależy od konkretnego podsystemu sieciowego używanego przez klienta lub serwer. Poniżej podano kilka przykładów. Powinieneś zapoznać się z dokumentacją dotyczącą konkretnej biblioteki lub zestawu API, którego używasz.

APIs(parameter) Used by Application    Incorrect Value  Correct Value  
=====================================  ===============  ========================
AcquireCredentialsHandle (pszPackage)  “NTLM”           NEGOSSP_NAME “Negotiate”
Alan Nicholas
źródło
-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;
    }
}

Kashif Mushtaq Ottawa, Kanada

Markus Safar
źródło
Przestrzeń nazw System.DirectoryServices.AccountManagement była nowa w .NET 3.5
Jeremy Gray
1
Wiem, że ma to prawie 4 lata, ale jeśli walidujesz lokalnego użytkownika, musisz upewnić się, że ustawiłeś ContextType na ContextType.Machine podczas konstruowania PrincipalContext. W przeciwnym razie pomyśli, że nazwa komputera podana w zmiennej domeny jest w rzeczywistości serwerem domeny.
SolidRegardless,