ASP.NET to host PowerShell scripts for Active Directory - DC hangs up

I am working on building an ASP.NET (C #) Application that is basically a gateway to run Powershell scripts for common admin tasks. Some of these scripts use the RSAT ActiveDirectory module, and I've found that some of these cmdlets won't work correctly when called through a gateway, and the trace appears to imply that the connection to the domain controller was successful, but then the DC disconnects.

The following code is an ASP.NET Web Form with one text input for specifying a username. Basically, it does the following:

  • Assumes web user id (verified by powershell inheritance)
  • Creates a powershell runtime and a pipeline inside this scope
  • Calls the Get-ADUser cmdlet and passes the username as the Identity parameter
  • Acknowledges success by reading the username into an output form element.

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

The interesting part is the specific wording of the error message found in the catch block (emphasis mine):

System.Management.Automation.CmdletInvocationException: Unable to execute contact server. This could be because this server does not exist, is currently unavailable, or does not have an Active Directory network. The service is running. ---> Microsoft.ActiveDirectory.Management.ADServerDownException: Unable to execute contact server. This may be because this server does not exist, is not currently available, or does not have an Active Directory network. The service is running. ---> System.ServiceModel.CommunicationException: The socket connection was interrupted. This could be caused by an error processing your message, or the remote host's receive timeout or a problem with a network resource. The local connector timeout was "00: 01: 59.6870000". ---> System.IO.IOException: Read operation failed, seeinner exception. ---> System.ServiceModel.CommunicationException: The socket connection was aborted. This could be caused by an error processing your message, or a receive timeout exceeded by the remote host, or a network resource issue. The local socket timeout was "00: 01: 59.6870000". ---> System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host

The top level exceptions assume there was a timeout, but there was no timeout indicated by the bottom exceptions (and it only took a few seconds to return from the command). In cases where the server is unavailable, the bottom-level exception message says the same, but this particular wording makes me think there is some sort of authentication (or other security) issue here.

UPDATE Feb 19, 2013: When using the impersonation method described here , the script works as expected. This leads me to think that the problem might be that the WindowsIdentity object provided by Windows Authentication is possibly unusable for a script that is essentially making RPC calls against AD. Unfortunately, I was reluctant to give up windows auth because I would have to handle user passwords in my application code (and this is not an obligation that I want).

I haven't been able to find any documentation on what exactly windows auth does, or which impersonation is allowed as a result of use. Is it possible to do this when using windows auth or do I have to require the user to give me their password?

+3


source to share


2 answers


Cause

The root cause of this problem is that when using Windows authentication in IIS, the security token is only valid for authenticating the web client machine to the web server machine. The same token is not valid to authenticate the web server machine to any other computer and this is what my application was trying to do:

  • The client receives a security token and sends it to the web server.
  • IIS asks the DC to validate the token and validate the token. The web client is currently authenticating to the web server.
  • IIS validates authenticated identity against application authorization rules.
  • The web application impersonates the identity using the token obtained by IIS and runs a script that then inherits the same security token.
  • The script tries to use the same token to authenticate to the remote RPC service.
  • The domain controller recognizes the authentication attempt as a re-attack (this token is for another service) and disconnects the connection.

It is not entirely correct to call this a "side effect" of Kerberos, but it was not obvious at first, although it seems obvious in retrospect. Hope someone can take advantage of this information.

Decision

The solution to this is an application to create its own security token, which can then be used to authenticate as a website user to services on other machines by calling the LogonUser () API . Your application code will need access to a cleared user password, and this can be made available by only enabling HTTP Basic authentication in IIS. The web server will still apply the same authorization and authorization rules, but both the username and password will be available to your application code. Keep in mind that these credentials are passed to the web server explicitly , so you will need to require SSL before using it in production.

I created a small helper class based on the procedure described here to make this process easier. Here's a short demo:



Login Assistant:

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 Helper Class:

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 Form Handler:

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

      

+4


source


Does this help add a delay between importing an ActiveDirectory module and trying to execute a command?



0


source







All Articles