【问题标题】:ASP.NET to host Active Directory RSAT powershell scripts--DC hangs upASP.NET 托管 Active Directory RSAT powershell 脚本--DC 挂起
【发布时间】:2013-02-03 11:45:40
【问题描述】:

我正在制作一个 ASP.NET (C#) 应用程序,该应用程序基本上是一个网关,用于为日常管理任务运行 Powershell 脚本。其中一些脚本使用 ActiveDirectory RSAT 模块,我发现其中一些 cmdlet 在通过网关调用时将无法正确运行,并且跟踪似乎暗示与域控制器的连接成功,但随后被关闭由 DC。

以下代码是一个 ASP.NET Web 表单,它有一个文本输入来指定用户名。基本上,它执行以下操作:

  • 假设网络用户的身份(确认被powershell继承)
  • 在该运行空间内创建一个 powershell 运行空间和管道
  • 调用 Get-ADUser cmdlet 并将用户名作为 Identity 参数传递
  • 通过将用户名读入表单上的输出元素来确认成功。

    protected void LookupButton_Click( object sender, EventArgs e ) {
      WindowsImpersonationContext impersonationContext = ((WindowsIdentity)User.Identity).Impersonate();
      Runspace runspace;
      Pipeline pipe;
      try {
        runspace = new_runspace();
        runspace.Open();
        pipe = runspace.CreatePipeline();
        Command cmd = new Command("Get-ADUser");
        cmd.Parameters.Add(new CommandParameter("Identity", text_username.Text));
        pipe.Commands.Add(cmd);
    
        PSObject ps_out = pipe.Invoke().First();
        output.Text = ps_out.Properties["Name"].Value.ToString();
      }
      catch( Exception ex ) {
        error.Text = ex.ToString();
      }
      finally {
        impersonationContext.Undo();
      }
    }
    
    private Runspace new_runspace( ) {
      InitialSessionState init_state = InitialSessionState.CreateDefault();
      init_state.ThreadOptions = PSThreadOptions.UseCurrentThread;
      init_state.ImportPSModule(new[] { "ActiveDirectory" });
      return RunspaceFactory.CreateRunspace(init_state);
    }
    

有趣的部分是在 catch 块中暴露的错误消息中的具体措辞(强调我的):

System.Management.Automation.CmdletInvocationException:无法 联系服务器。这可能是因为该服务器不存在,它 当前已关闭,或者它没有 Active Directory Web 服务运行。 ---> Microsoft.ActiveDirectory.Management.ADServerDownException:无法 联系服务器。这可能是因为该服务器不存在,它 当前已关闭,或者它没有 Active Directory Web 服务运行。 ---> System.ServiceModel.CommunicationException: 套接字连接被中止。这可能是由错误引起的 处理您的消息或接收超时被超过 远程主机或底层网络资源问题。本地套接字 超时为“00:01:59.6870000”。 ---> System.IO.IOException:读取 操作失败,请参阅内部异常。 ---> System.ServiceModel.CommunicationException:套接字连接是 中止。这可能是由于处理您的消息时出错或 接收超时被远程主机或底层 网络资源问题。本地套接字超时为“00:01:59.6870000”。 ---> System.Net.Sockets.SocketException: 现有连接被远程主机强行关闭

上层异常表明存在超时,但下层异常没有表明超时(从命令返回仅需几秒钟)。在无法访问服务器的情况下,最低级别的异常消息说明了很多,但是这个特定的措辞让我认为这里存在某种身份验证(或其他安全)问题。

2013 年 2 月 19 日更新: 当使用here 描述的模拟方法时,脚本会按预期运行。这让我觉得问题可能是 Windows 身份验证提供的 WindowsIdentity 对象可能不适合基本上对 AD 进行 RPC 调用的脚本。不幸的是,我真的不希望放弃 Windows 身份验证,因为我必须在我的应用程序代码中处理用户的密码(这不是我想要的责任)。

我还没有找到任何关于 windows auth 到底在做什么或使用它会导致什么样的模拟的文档。是否可以在使用 Windows 身份验证时执行此操作,还是我必须要求用户给我他们的密码?

【问题讨论】:

    标签: c# asp.net powershell active-directory impersonation


    【解决方案1】:

    原因

    此问题的根本原因是当您在 IIS 中使用 Windows 身份验证时,安全令牌仅对 Web 客户端计算机对 Web 服务器计算机的身份验证有效。相同的令牌对于向任何其他机器验证 Web 服务器机器无效,这就是我的应用程序试图做的事情:

    1. 客户端获取安全令牌并将其发送到 Web 服务器。
    2. IIS 要求 DC 验证令牌并验证令牌。此时,Web 客户端已通过 Web 服务器的身份验证。
    3. IIS 根据应用程序的授权规则检查经过身份验证的身份。
    4. Web 应用程序使用 IIS 接收到的令牌来模拟身份,并运行一个脚本,然后该脚本将继承相同的安全令牌。
    5. 脚本尝试使用相同的令牌对远程 RPC 服务进行身份验证。
    6. 域控制器将身份验证尝试识别为重放攻击(该令牌用于不同的服务)并关闭连接。

    将其称为 Kerberos 的“副作用”并不完全正确,但起初对我来说并不明显,尽管事后看来很明显。我希望有人能从这些信息中受益。

    解决方案

    解决方案是让应用程序生成它自己的安全令牌,然后通过对LogonUser() 进行 API 调用,该令牌可用于作为 Web 用户对其他机器上的服务进行身份验证。您的应用程序代码将需要访问用户的明文密码,这可以通过在 IIS 中仅启用 HTTP Basic 身份验证来实现。 Web 服务器仍将执行相同的身份验证和授权规则,但用户名和密码都将可用于您的应用程序代码。请记住,这些凭据以明文方式传输到 Web 服务器,因此您需要在生产环境中使用 SSL 之前。

    我根据here 描述的过程创建了一个小型帮助程序类,以促进这个过程。这是一个简短的演示:

    登录助手:

    public class IdentityHelper {
      [DllImport("advapi32.dll")]
      private static extern int LogonUserA( String lpszUserName, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken );
    
      [DllImport("advapi32.dll",
        CharSet = CharSet.Auto,
        SetLastError = true)]
      private static extern int DuplicateToken( IntPtr hToken, int impersonationLevel, ref IntPtr hNewToken );
    
      [DllImport("kernel32.dll",
        CharSet = CharSet.Auto)]
      private static extern bool CloseHandle( IntPtr handle );
    
      public const int LOGON32_LOGON_INTERACTIVE = 2;
      public const int LOGON32_PROVIDER_DEFAULT = 0;
      public const int IMPERSONATION_LEVEL_IMPERSONATE = 2;
    
      public static WindowsIdentity Logon( string username, string password, string domain = "" ) {
        IntPtr token = IntPtr.Zero;
        WindowsIdentity wid = null;
    
        if( domain == "" ) {
          split_username(username, ref username, ref domain);
        }
    
        if( LogonUserA(username, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, ref token) != 0 ) {
          wid = WIDFromToken(token);
        }
        if( token != IntPtr.Zero ) CloseHandle(token);
        return wid;
      }
    
      public static WindowsIdentity WIDFromToken( IntPtr src ) {
        WindowsIdentity wid = null;
        IntPtr token = IntPtr.Zero;
        if( DuplicateToken(src, IMPERSONATION_LEVEL_IMPERSONATE, ref token) != 0 ) {
          wid = new WindowsIdentity(token);
        }
        if( token != IntPtr.Zero ) CloseHandle(token);
        return wid;
      }
    
      private static void split_username( string username, ref string username_out, ref string domain_out ) {
        string[] composite_username = username.Split(new char[] { '\\' });
        if( composite_username.Length == 2 ) {
          domain_out = composite_username[0];
          username_out = composite_username[1];
        }
      }
    }
    

    Powershell 帮助类:

    public class PSHelper {
      public static Runspace new_runspace() {
        InitialSessionState init_state = InitialSessionState.CreateDefault();
        init_state.ThreadOptions = PSThreadOptions.UseCurrentThread;
        init_state.ImportPSModule(new[] { "ActiveDirectory" });
        return RunspaceFactory.CreateRunspace(init_state);
      }
    }
    

    ASP.NET 表单处理程序:

    protected void LookupButton_Click( object sender, EventArgs e ) {
      string outp = "";
      WindowsIdentity wid = IdentityHelper.Logon(Request["AUTH_USER"], Request["AUTH_PASSWORD"]);
      using( wid.Impersonate() ) {
        Runspace runspace;
        Pipeline pipe;
        runspace = PSHelper.new_runspace();
        runspace.Open();
        pipe = runspace.CreatePipeline();
        Command cmd = new Command("Get-ADUser");
        cmd.Parameters.Add(new CommandParameter("Identity", text_username.Text));
        pipe.Commands.Add(cmd);
        outp = pipe.Invoke().First().Properties["Name"].Value.ToString();
      }
      output.Text = outp;
    }
    

    【讨论】:

    • 我在使用示例管理 OData 服务时遇到了同样的基本问题。在我尝试使用 Get-ADUser 之前,我的所有测试都有效,然后我遇到了“错误详细信息:无法联系服务器。这可能是因为此服务器不存在、当前已关闭或没有运行 Active Directory Web 服务。在 ManagementODataService 操作日志中。一旦我在计算机帐户上启用了委派,添加了 DNS 名称,将其作为主机标头添加到站点并将其添加为 AppPool 域帐户的 SPN,它就可以工作了。
    【解决方案2】:

    在导入 ActiveDirectory 模块和尝试执行命令之间添加延迟是否有帮助?

    【讨论】:

    • 模块作为初始会话状态的一部分被导入。我看不出应该解决什么问题。
    • 这纯粹是轶事,但我遇到了另一种情况,即导入该模块并立即执行命令失败,但几秒钟后会起作用。我能想到的唯一原因是模块会加载,但允许脚本在建立与 DC 的连接并初始化 AD 提供程序之前继续执行下一个命令。
    • 我会在早上试试。关于我在此期间学到的知识,我还有更多细节需要稍后补充。
    • 谢谢。有备则无患! :)
    • 为了测试,我按照您的建议在打开运行空间之前和之后添加了一秒钟的睡眠。这应该为结束由导入 AD 模块引起的任何套接字播放提供足够的喘息空间。很遗憾地报告这样做并没有改变结果,但我已经确定了问题的原因并解决了这个问题。感谢您向我提供您的经验,您描述的情况会令人抓狂地进行故障排除。我将添加包含所有详细信息的问题的答案。
    猜你喜欢
    • 2023-01-19
    • 1970-01-01
    • 1970-01-01
    • 2020-05-08
    • 1970-01-01
    • 2022-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多