3

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

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

  • 假设 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 日更新:使用此处描述的模拟方法时,脚本按预期运行。这让我认为问题可能是 Windows 身份验证提供的 WindowsIdentity 对象可能不适合基本上对 AD 进行 RPC 调用的脚本。不幸的是,我真的不希望放弃 Windows 身份验证,因为我必须在我的应用程序代码中处理用户的密码(这不是我想要的责任)。

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

4

2 回答 2

4

原因

此问题的根本原因是当您在 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。

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

登录助手:

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;
}
于 2013-02-20T22:06:44.407 回答
0

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

于 2013-02-19T02:42:29.947 回答