จะตรวจสอบข้อมูลรับรองโดเมนได้อย่างไร?


87

ฉันต้องการตรวจสอบความถูกต้องของชุดข้อมูลรับรองกับตัวควบคุมโดเมน เช่น:

Username: STACKOVERFLOW\joel
Password: splotchy

วิธีที่ 1. สืบค้น Active Directory ด้วยการแอบอ้างบุคคลอื่น

ผู้คนจำนวนมากแนะนำให้ค้นหา Active Directory สำหรับบางสิ่ง หากเกิดข้อยกเว้นแสดงว่าคุณทราบว่าข้อมูลรับรองไม่ถูกต้องตามที่แนะนำในคำถาม stackoverflowนี้

อย่างไรก็ตามมีข้อเสียที่ร้ายแรงบางประการสำหรับแนวทางนี้ :

  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 จะเปิดใช้งานสำหรับผู้ใช้ทุกคน แต่ผู้ดูแลระบบบางคนอาจปิดใช้งานสำหรับทุกคน

แจก "การทำหน้าที่เป็นส่วนหนึ่งของระบบปฏิบัติการ " สิทธิพิเศษไม่ใช่สิ่งที่คุณต้องการจะทำจำใจ - ไมโครซอฟท์ชี้ให้เห็นในบทความฐานความรู้ :

... กระบวนการที่เรียก LogonUser ต้องมีสิทธิ์ SE_TCB_NAME (ในตัวจัดการผู้ใช้นี่คือสิทธิ์ " ทำหน้าที่เป็นส่วนหนึ่งของระบบปฏิบัติการ ") สิทธิ์ SE_TCB_NAME มีประสิทธิภาพมากและ ไม่ควรมอบให้กับผู้ใช้ใด ๆ โดยพลการเพียงเพื่อให้สามารถเรียกใช้แอปพลิเคชันที่ต้องตรวจสอบข้อมูลรับรองได้

นอกจากนี้การโทรหาLogonUser()จะล้มเหลวหากระบุรหัสผ่านว่างไว้


วิธีที่เหมาะสมในการรับรองความถูกต้องชุดข้อมูลรับรองโดเมนคืออะไร?


ฉันเกิดขึ้นจะได้รับการเรียกร้องจากการจัดการรหัส แต่นี้เป็น AA คำถามของ Windows ทั่วไป สามารถสันนิษฐานได้ว่าลูกค้าได้ติดตั้ง. NET Framework 2.0


1
ผู้อ่านควรทราบว่าใน Windows XP LogonUser ไม่ต้องใช้ SE_TCB_NAME อีกต่อไป (เว้นแต่คุณจะเข้าสู่บัญชี Passport)
Harry Johnston

คำตอบ:


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สิ่งที่ฉันสนใจที่จะรู้และพบในคำตอบนี้
Rudi Visser

1
@RudiVisser tvanfosson ได้แนะนำให้คุณ "ตรวจสอบตัวสร้าง PrincipalContext ที่กำหนดพารามิเตอร์สำหรับตัวเลือกอื่น ๆ " - อ่านเอกสารเสมออย่าใช้เพียงแค่คำพูดของอินเทอร์เน็ตเท่านั้น! :)
gbjbaanb

4
@gbjbaanb ใช่แน่นอน แต่การให้บริการเช่นการทำงานมากกว่าการเชื่อมโยงและข้อเสนอแนะในการอ่านที่อื่น ๆ เป็นมนต์ StackOverflow ที่ว่าทำไมเรายอมรับหลายส่งคำตอบ: D เพียงแค่บอกว่านี้ไม่ให้มากขึ้น
Rudi Visser

มีใครรู้บ้างว่าเราทำสิ่งที่คล้ายกันในแอป UWP ได้อย่างไร? (กับโฆษณาปกติและไม่ใช่กับ 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" ที่อธิบายไว้ในคำถาม ... ดังนั้น ... ไม่ได้ตอบคำถามจริงๆ
Robert Levy

1

วิธีระบุผู้ใช้ภายในเครื่องมีดังนี้

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

แก้ไขโดย Ian Boyd

คุณไม่ควรใช้ NTLM อีกต่อไปเลย มันเก่ามากและแย่มากที่ Application Verifier ของ Microsoft (ซึ่งใช้เพื่อตรวจจับข้อผิดพลาดในการเขียนโปรแกรมทั่วไป) จะส่งคำเตือนหากตรวจพบว่าคุณใช้ 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 ของ wrapper ระดับที่สูงกว่า)

  • ชื่อเป้าหมายในการเรียกใช้ InitializeSecurityContext เป็น NULL

  • ชื่อเป้าหมายในการเรียกใช้ InitializeSecurityContext ไม่ใช่ชื่อโดเมนสไตล์ SPN, UPN หรือ NetBIOS ที่มีรูปแบบถูกต้อง

สองกรณีหลังจะบังคับให้ Negotiate ถอยกลับไปที่ NTLM ไม่ว่าโดยตรง (กรณีแรก) หรือโดยอ้อม (ตัวควบคุมโดเมนจะส่งคืนข้อผิดพลาด "ไม่พบหลัก" ในกรณีที่สองซึ่งทำให้ Negotiate ถอยกลับ)

ปลั๊กอินยังบันทึกคำเตือนเมื่อตรวจพบการดาวน์เกรดเป็น NTLM ตัวอย่างเช่นเมื่อไม่พบ SPN โดย Domain Controller สิ่งเหล่านี้บันทึกเป็นคำเตือนเท่านั้นเนื่องจากมักเป็นกรณีที่ถูกต้องตัวอย่างเช่นเมื่อตรวจสอบสิทธิ์กับระบบที่ไม่ได้เข้าร่วมโดเมน

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;
    }
}

Kashif Mushtaq ออตตาวาแคนาดา


System.DirectoryServices.AccountManagement namespace ใหม่ใน. NET 3.5
Jeremy Grey

1
ฉันรู้ว่านี่เกือบ 4 ปีแล้ว แต่ถ้าคุณกำลังตรวจสอบความถูกต้องของผู้ใช้ภายในคุณจะต้องแน่ใจว่าคุณตั้งค่า ContextType เป็น ContextType.Machine เมื่อคุณสร้าง PrincipalContext มิฉะนั้นจะคิดว่าชื่อเครื่องที่ระบุในตัวแปรโดเมนเป็นโดเมนเซิร์ฟเวอร์จริงๆ
SolidRegardless
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.