c# - ASP.NET 托管 Active Directory RSAT powershell 脚本--DC 挂断

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

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

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

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

    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 block 中暴露的错误消息中的具体措辞(强调我的):

System.Management.Automation.CmdletInvocationException: Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running. ---> Microsoft.ActiveDirectory.Management.ADServerDownException: Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running. ---> System.ServiceModel.CommunicationException: The socket connection was aborted. This could be caused by an error processing your message or a receive timeout being exceeded by the remote host, or an underlying network resource issue. Local socket timeout was '00:01:59.6870000'. ---> System.IO.IOException: The read operation failed, see inner exception. ---> System.ServiceModel.CommunicationException: The socket connection was aborted. This could be caused by an error processing your message or a receive timeout being exceeded by the remote host, or an underlying network resource issue. Local socket timeout was '00:01:59.6870000'. ---> System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host

上层异常提示超时,但下层异常提示没有超时(命令返回只需要几秒)。在无法访问服务器的情况下,最低级别的异常消息说明了同样多的内容,但这种特定的措辞让我觉得这里存在某种身份验证(或其他安全)问题。

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

我无法找到任何关于 Windows 身份验证究竟在做什么或允许使用它进行何种模拟的文档。是否可以在使用 Windows 身份验证时执行此操作,还是我必须要求用户提供密码?

最佳答案

原因

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

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

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

解决方案

此问题的解决方案是让应用程序生成自己的安全 token ,然后通过对 LogonUser() 进行 API 调用,可以使用该 token 作为 Web 用户对其他机器上的服务进行身份验证。 .您的应用程序代码将需要访问用户的明文密码,这可以通过在 IIS 中仅启用 HTTP 基本身份验证来实现。 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;
}

关于c# - ASP.NET 托管 Active Directory RSAT powershell 脚本--DC 挂断,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14946575/

相关文章:

javascript - 从 ASP.NET 中的按钮播放声音

asp.net - 您何时会在 Web 自定义控件上使用 Web 用户控件?

c# - 通过C#将创建过程命令上传到mysql时忽略@symbol

c# - 如何在 C# 代码中创建 Razor Page Urls?

c# - gridview 中的标题文本更改为零

使用 SSL/TLS 安全通道的 Powershell Invoke-WebRequest 失败

powershell - 从主机 Powershell 脚本在 Docker 容器中运行 Powershell 脚本

.net - 如何从 PowerShell 访问 Web 服务?

c# - 无法访问对象初始值设定项内的属性

c# - 当一个项目可能为 null 时,如何正确地将其转换为 DataSet 中的项目?