如何验证域凭证?


86

我想针对域控制器验证一组凭据。例如:

Username: STACKOVERFLOW\joel
Password: splotchy

方法1.使用模拟查询Active Directory

很多人建议在Active Directory中查询某些内容。如果抛出异常,则说明凭据无效-如该stackoverflow问题所建议。

但是,此方法有一些严重的缺点

  1. 您不仅在验证域帐户,而且还在进行隐式授权检查。也就是说,您正在使用模拟令牌从AD读取属性。如果原本有效的帐户无权读取广告怎么办?默认情况下,所有用户都具有读取访问权限,但是可以将域策略设置为禁用受限帐户(和/或组)的访问权限。

  2. 与AD绑定会产生很大的开销,必须在客户端上加载AD模式缓存(DirectoryServices使用的ADSI提供程序中的ADSI缓存)。这既是网络,又是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帐户)。
哈里·约翰斯顿

Answers:


130

使用System.DirectoryServices.AccountManagement的.NET 3.5中的C#。

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

这将针对当前域进行验证。检出参数化的PrincipalContext构造函数以获取其他选项。


@tvanfosson:DirectoryServices不使用AD吗?
米奇·麦特

1
是。但是文档表明这是一种验证凭据的快速方法。它也与问题中提到的绑定方法不同,因为您没有从对象中读取任何属性。请注意,该方法位于上下文中,而不是目录对象。
tvanfosson

更正:System.DirectoryServices.AccountManagement需要.NET 3.5。(msdn.microsoft.com/en-us/library/...
伊恩·博伊德

19
如果您new PrincipalContext(ContextType.Machine)改用本地用户,它也可以使用。
VansFannel 2014年

有谁知道这是否适用于缓存的凭据,还是需要连接到DC?我需要知道一些正在进行的实现,并且目前尚未在任何域上进行测试
Jcl,2016年

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
这与3年前@tvanfosson的答案有何显着差异?
gbjbaanb

5
@gbjbaanb是的,因为它包含Domain创建时的参数PrincipalContext,这是我有兴趣了解并在此答案中找到的。
Rudi Visser 2015年

1
@RudiVisser tvanfosson确实建议您“检查参数化的PrincipalContext构造函数以了解其他选项”-始终阅读文档,切勿只听互联网的话!:)
gbjbaanb 2015年

4
@gbjbaanb当然可以,但是提供一个工作示例而不是其他地方的链接和建议是StackOverflow的口头禅,这就是为什么我们接受多次提交答案的原因:D简单地说,这确实提供了更多的答案。
Rudi Visser 2015年

有谁知道我们可以在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” ...所以...未真正回答问题
Robert Levy

1

确定本地用户的方法如下:

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

伊恩·博伊德(Ian Boyd)编辑

您完全不应再使用NTLM。它太老了,太糟糕了,以至于Microsoft的Application Verifier(用于捕获常见的编程错误)将在检测到您使用NTLM时发出警告。

这是Application Verifier文档中的一章,内容涉及为什么他们要测试是否有人错误地使用NTLM:

为什么需要NTLM插件

NTLM是一种过时的身份验证协议,其缺陷可能会损害应用程序和操作系统的安全性。最重要的缺点是缺乏服务器身份验证,这可能使攻击者诱骗用户连接到欺骗性服务器。作为缺少服务器身份验证的必然结果,使用NTLM的应用程序也容易受到称为“反射”攻击的攻击。后者允许攻击者将用户的身份验证会话劫持到合法服务器,并使用它将攻击者身份验证到用户的计算机。NTLM的漏洞及其利用方法是安全社区中越来越多的研究活动的目标。

尽管Kerberos已有许多年可用,但许多应用程序仍仅使用NTLM编写。这不必要地降低了应用程序的安全性。但是,Kerberos不能在所有情况下都替换NTLM,主要是在客户端需要向未加入域的系统进行身份验证的情况下(家庭网络可能是其中最常见的)。Negotiate安全软件包允许向后兼容的折衷方案,该方案尽可能使用Kerberos,并且仅在没有其他选择时才还原为NTLM。将代码切换为使用协商而不是NTLM可以显着提高我们客户的安全性,同时几乎不引入应用程序兼容性。进行谈判本身并不是灵丹妙药,在某些情况下,攻击者可以强制降级为NTLM,但要利用它们显然要困难得多。但是,一项直接的改进是,为正确使用协商而编写的应用程序可以自动抵抗NTLM反射攻击。

最后要注意不要使用NTLM:在将来的Windows版本中,可以在操作系统上禁用NTLM。如果应用程序对NTLM有严格的依赖性,则在禁用NTLM时,它们将仅无法通过身份验证。

插件如何工作

验证程序插件检测到以下错误:

  • NTLM包直接在对AcquireCredentialsHandle(或更高级别的包装器API)的调用中指定。

  • 调用InitializeSecurityContext的目标名称为NULL。

  • 对InitializeSecurityContext的调用中的目标名称不是格式正确的SPN,UPN或NetBIOS样式的域名。

后两种情况将迫使“协商”直接回退到NTLM(第一种情况)或间接退回到NTLM(在第二种情况下,域控制器将返回“找不到主要”错误,导致“协商”回退)。

当插件检测到降级为NTLM时,也会记录警告。例如,当域控制器未找到SPN时。由于它们通常是合法情况,例如,当对未加入域的系统进行身份验证时,它们仅作为警告记录。

NTLM停止

5000 –应用程序已明确选择了NTLM软件包

严重性–错误

应用程序或子系统在对AcquireCredentialsHandle的调用中显式选择了NTLM而不是“协商”。即使客户端和服务器可能使用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命名空间是.NET 3.5中的新功能
Jeremy Gray

1
我知道这已经有将近4年的历史,但是如果您要验证本地用户,则在构造PrincipalContext时需要确保将ContextType设置为ContextType.Machine。否则,它将认为Domain变量中提供的计算机名称实际上是域服务器。
Solid无论2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.