This message was discovered on microsoft.public.dotnet.framework.aspnet.webservices.
Responses highlighted in red are from those people who are likely to be able to contribute good, authoratitive information to this discussion. They include Microsoft employees, MVP's and others who IMHO contribute well to these kinds of discussions.
| charlie@nunya.com |
Hi,
I hope someone can help me with this - it's driving us all nuts.
We have an ASP.Net web application that must run an external executable to accomplish a document merge function. We got this to work on Windows XP using the following code (low level API calls). Before we call the RunIt method, we successfully impersonate with the same Domain/UserID/Password as is passed into the method. As I said, all is well in Win XP, however, when we try to execute the code in Server 2003, the executable just hangs. It doesn't get to any of the database processing (I know because I traced it) so I am assuming it is hanging up almost immediately.
We have set the following permissions on the Server 2003 box: SE_ASSIGNPRIMARYTOKEN_NAME by modifying the Local Security Policy ("Replace a process level token") - AND - SE_INCREASE_QUOTA_NAME by modifying the Local Security Policy ("Adjust memory quotas for a process")
The RunIt method that we are executing is below:
public string RunIt(string userID, string password, string domain, string appString, string argString) { string rc = "";
// Account to run as string _logonName = userID; // some user string _domain = domain; // domain string _password = password;
StringBuilder sb = new StringBuilder(); // command to execute string commandString = @appString + " " + @argString; sb.Append(@commandString);
PROCESS_INFORMATION processInfo; STARTUPINFO startInfo = new STARTUPINFO(); startInfo.cb = Marshal.SizeOf(startInfo); startInfo.lpTitle = null; startInfo.dwFlags = STARTF_USECOUNTCHARS; startInfo.dwYCountChars = 50; // create process similar as "runas" using the logon users profile bool ret = CreateProcessWithLogonW(_logonName, _domain, _password, LOGON_WITH_PROFILE, null, sb, NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT, IntPtr.Zero, "c:\\", ref startInfo, out processInfo); if(!ret) { // If failure ... rc = "Error: {0}" + Marshal.GetLastWin32Error() + commandString; } else { CloseHandle(processInfo.hThread); WaitForSingleObject(processInfo.hProcess, System.Threading.Timeout.Infinite); CloseHandle(processInfo.hProcess); }
return rc; }
ANY help would be appreciated!!! Thanks in advance. Charlie
|
|
| |
| |
| charlie@nunya.com |
Once again, I am answering my own query hoping that this might help someone else at some point (does anyone from microsoft ever answere questions that border on the hard to solve?).
Anyway - we solved this issue using WMI. It works great but there were a couple of quirks which I hope I have documented below. The code is taken out of context so I make no gaurantee that it will compile as presented. There were some good references for WMI and C# on the web. I didn't remember to save the url's but a quick search will turn them up. Ultimately, I believe this solution to be a little "cleaner" than the low level API call that we were using as it uses Framework classes to get the work done.
public void RunWIM(string domain, string userID, string pwd, string appString, string argString) { string rc = ""; ConnectionOptions options = new ConnectionOptions(); string serverName = Dns.GetHostName(); // because we are running against the local machine we can't validate // we are impersonating at this point so we have the correct security // level //options.Username = domain + @"\" + userID; //options.Password = pwd; //Create a scope to work in ManagementScope WmiScope = new ManagementScope(@"\\" + serverName, options); WmiScope.Connect(); ManagementClass processClass = new ManagementClass("Win32_Process"); processClass.Scope = WmiScope; //Get an input parameters object for this method ManagementBaseObject inParams = processClass.GetMethodParameters("Create"); //Fill in input parameter values inParams["CommandLine"] = appString + " " + argString; // this will execute our command but it will not wait for the job to complete... ManagementBaseObject outParams = processClass.InvokeMethod("Create", inParams, null); }
The calling method has this code in it - we are creating some files and writing them to disk so we just put some simple logic in a while loop to make sure we waited until the process finished: int x = 0; bool goOn = false; Impersonation i = new Impersonation(); string userID = "username"; string password = "password"; string domain = @ConfigurationSettings.AppSettings.Get("Default_Domain"); string app = @pAppPath + @"Tools\MergeEngine.exe"; string args = @iniFile; try { RunProcessAs rp = new RunProcessAs(); rc = rp.RunWIM(domain,userID,password,app,args); } catch (Exception e) { rc = e.ToString(); }
I hope this will help someone - we struggled with the API call for a couple of days but we got this going in less than an hour. Charlie
|
|
| |
| |
| Andrew Zimmer |
I have the same issue with using CreateProcessWithLogonW on a 2003 machine. The application does not start. It doesn't even generate an error message. I tried using the below example but it will not work when trying to login to the same machine.
Does anyone know how to deal with this Server 2003 security issue? I have granted about every local security setting policy setting to both the ID doing the impersonation and the ID it is trying to impersonate to.
I am trying to start an app under a specific ID from a windows service. It works great on Server 2000 but not 2003.
Click here to reveal e-mail address wrote in message news:<HUFLc.4611$Click here to reveal e-mail address>... [Original message clipped]
|
|
| |
| |
| Yu Chen [MS] (VIP) |
If your service is started under Local System account, this is a known issue in Windows Server 2003 and XPSP2 - the CreateProcessWithLogonW API is changed to better handle the new process' use of desktop by utilizing "Logon Sid" in the caller's token. However the local system token (under which your GINA is running) doesn't have a "Logon sid" so the API failed when caller is local system.
You can use LogonUser and CreateProcessAsUser to achieve the same thing.
This info will be included in next release of MSDN.
-- Yu Chen [MS] This posting is provided "AS IS" with no warranties, and confers no rights.
"Andrew Zimmer" <Click here to reveal e-mail address> wrote in message news:Click here to reveal e-mail address... [Original message clipped]
|
|
| |
| |
| Yu Chen [MS] (VIP) |
Please ignore the "GINA" part below - it's a cut & paste from an earlier reply to another thread.
> If your service is started under Local System account, this is a known issue [Original message clipped]
|
|
| |
| |
| Matthew Wieder |
Hi - You suggest to use LogonUser and CreateProcessAsUser to replace CreateProcessWithLogonW, but does that really replcae it exactly? It is my understanding there are major differences between the two such as LogonUser and CreateProcessAsUser doesn't load the user's registry hive. thanks, -Matthew
Yu Chen [MS] wrote: [Original message clipped]
|
|
| |
|
|
| |
| Andrew Zimmer |
Actually, I the service is logged on as an admin on the machine. It also has every local security policy setting imaginable.
"Yu Chen [MS]" <Click here to reveal e-mail address> wrote in message news:<Click here to reveal e-mail address>... [Original message clipped]
|
|
| |
| |
| Pavel Lebedinsky |
Click here to reveal e-mail address (Andrew Zimmer) wrote:
[Original message clipped]
What is the return value from CreateProcessWithLogonW? What are the exact parameters that you pass in?
If you enable "Detailed process tracking" in the auditing settings of your security policy, do you get any "process created" events in the security event log?
|
|
| |
|
|
|
| |
| charlie@nunya.com |
You should be able to do this on the same machine by first impersonating the user you want to authenticate as, then invking the process using WMI.
Hey Yu - where were you with all the good advice when I was beating my head against the wall last month?
|
|
| |
| |
| Matthew Wieder |
Is this addressing my question about the differences between CreateProcessWithLogonW and Yu's proscribed solution?
Click here to reveal e-mail address wrote: [Original message clipped]
|
|
| |
| |
| charlie@nunya.com |
After countless attempts at trying to get the CreateProcessWithLogonW to work consistantly, I would suggest going straight to the WMI approach. WMI has worked on every platform, every time.
Just my two cents but I have tried all that Yu has proposed and WMI works every time whereas the other methods are spotty.
|
|
| |
| |
| Matthew Wieder |
If you don't mind, since your suggestion seems very different to CreateProcessWithLogonW, could you please step me through the what calls I need to make in order to "first impersonating the user you want to authenticate as, then invking the process using WMI." What is the call to do the impersonation and what is the cell to run my process with the command line arguments? thanks for your time!
Click here to reveal e-mail address wrote: [Original message clipped]
|
|
| |
| |
| charlie@nunya.com |
OK - here is how I impersonate - the impersonation class is at the end of this message. I'll just do some code snippets to show the calling of the impersonation and the executing of the process in WMI. Since we are doing WMI on the same machine that we are executing on, there is no validation at that point. However, we need to impersonate first to create the correct security context in which to execute the external process.
.... some code... Impersonation i = new Impersonation(); // userid and password are stored in an encrypted registry entry string userID = i.GetUserNameFromRegistry(); string password = i.GetPasswordFromRegistry(); //domain is stored in a config file string domain = settings.DefaultDomain; string app = @pAppPath + @"bin\MergeEngine.exe"; string args = @"whatever arguments you need to pass"; WindowsImpersonationContext mWIC = i.ImpersonateUser(userID,password,domain); rc = RunWMI(domain,userID,password,app,args); if (mWIC != null) { impersonation.UndoImpersonate(mWIC); } .... some more code... public string RunWMI(string domain, string userID, string pwd, string appString, string argString) { string rc = ""; ConnectionOptions options = new ConnectionOptions(); string serverName = Dns.GetHostName(); // because we are impersonating and running against the local machine // we do not validate //options.Username = domain + @"\" + userID; //options.Password = pwd; //Create a scope to work in ManagementScope WmiScope = new ManagementScope(@"\\" + serverName, options); WmiScope.Connect(); ManagementClass processClass = new ManagementClass("Win32_Process"); processClass.Scope = WmiScope; //Get an input parameters object for this method ManagementBaseObject inParams = processClass.GetMethodParameters("Create"); //Fill in input parameter values inParams["CommandLine"] = appString + " " + argString; //' Or whatever application you want //Note: The return code of the method is provided in the "returnValue" property of the outParams object ManagementBaseObject outParams = processClass.InvokeMethod("Create", inParams, null); return rc; }
Impesonation Class follows:
using System; using System.Runtime.InteropServices; using System.Security.Principal; using System.Security.Permissions; using System.Text; using Microsoft.Win32;
[assembly:SecurityPermissionAttribute(SecurityAction.RequestMinimum, UnmanagedCode=true)] namespace Security.WebModules { /// <summary> /// This class has all the necessary methods to allow the application to impersonate another user /// when the standard user does not have the required permissions. /// </summary> public class Impersonation { public Impersonation() { }
/// <summary> /// This method returns the user name that has been stored in encrypted form in the registry. /// The user name should have been previously created and have the appropriate permissions. /// </summary> public string GetUserNameFromRegistry() { string userName = "";
Lookup l = new Lookup(); userName = l.GetUserNameFromRegistry();
return userName; }
/// <summary> /// This method returns the user name that has been stored in encrypted form in the registry. /// The user name should have been previously created and have the appropriate permissions. /// </summary> public string GetPasswordFromRegistry() { string password = "";
Lookup l = new Lookup(); password = l.GetPasswordFromRegistry();
return password; }
public WindowsImpersonationContext ImpersonateUser(string user, string password, string domain) { WindowsImpersonationContext mWIC = null;
ChangeUser cu = new ChangeUser();
mWIC = cu.Impersonate(user,password,domain);
return mWIC; }
public bool UndoImpersonate(WindowsImpersonationContext mWIC) { bool rc = true;
ChangeUser cu = new ChangeUser();
if (mWIC != null) { rc = cu.UndoImpersonate(mWIC); }
return rc; } }
public class ChangeUser { public ChangeUser() { }
[DllImport("advapi32.dll")] private static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, out int phToken);
[DllImport("Kernel32.dll")] private static extern int GetLastError();
public WindowsImpersonationContext Impersonate(string user, string password, string domain) { // The Windows user token. int token1; int ret = 0; WindowsImpersonationContext mWIC = null;
bool loggedOn = LogonUser(user,domain,password,8,0, out token1);
// Call GetLastError to try to determine why logon failed if it did not succeed. if (!loggedOn) { ret = GetLastError(); }
if (ret != 0) { return mWIC; }
//Starting impersonation here: WindowsIdentity mWI1 = WindowsIdentity.GetCurrent(); IntPtr token2 = new IntPtr(token1); WindowsIdentity mWI2 = new WindowsIdentity(token2); // Impersonate the user. mWIC = mWI2.Impersonate();
return mWIC; }
public bool UndoImpersonate(WindowsImpersonationContext mWIC) { bool rc = true;
// Revert to previous identity. mWIC.Undo();
return rc; } }
public class Lookup { private RegistryKey pRootKey; private RegistryKey pIdentityKey; private byte[] entropyBytes = null; private string description;
public RegistryKey rootKey { get { return pRootKey; } set { pRootKey = value; } }
public RegistryKey identityKey { get { return pIdentityKey; } set { pIdentityKey = value; } }
public Lookup() { pRootKey = Registry.LocalMachine.OpenSubKey("SOFTWARE",false); pIdentityKey = pRootKey.OpenSubKey(@"RQTitleCo\identity\ASPNET_SETREG"); }
// Wrapper for DPAPI CryptUnprotectData function. [DllImport( "crypt32.dll",SetLastError=true,CharSet=System.Runtime.InteropServices.CharSet.Auto)] private static extern bool CryptUnprotectData(ref DATA_BLOB pCipherText, ref string pszDescription, ref DATA_BLOB pEntropy, IntPtr pReserved, ref CRYPTPROTECT_PROMPTSTRUCT pPrompt, int dwFlags, ref DATA_BLOB pPlainText);
// BLOB structure used to pass data to DPAPI functions. [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] internal struct DATA_BLOB { public int cbData; public IntPtr pbData; }
// Prompt structure to be used for required parameters. [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] internal struct CRYPTPROTECT_PROMPTSTRUCT { public int cbSize; public int dwPromptFlags; public IntPtr hwndApp; public string szPrompt; }
// Wrapper for the NULL handle or pointer. static private IntPtr NullPtr = ((IntPtr)((int)(0)));
// DPAPI key initialization flags. private const int CRYPTPROTECT_UI_FORBIDDEN = 0x1; private const int CRYPTPROTECT_LOCAL_MACHINE = 0x4;
public string GetUserNameFromRegistry() { string userName = "";
byte[] userNameBytes;
userNameBytes = (byte[]) pIdentityKey.GetValue("userName"); userNameBytes = Decrypt(userNameBytes,entropyBytes,out description); userName = (new UnicodeEncoding()).GetString(userNameBytes);
return userName; }
public string GetPasswordFromRegistry() { string password = "";
byte[] passwordBytes;
passwordBytes = (byte[]) pIdentityKey.GetValue("password"); passwordBytes = Decrypt(passwordBytes,entropyBytes,out description); password = (new UnicodeEncoding()).GetString(passwordBytes);
return password; }
/// <SUMMARY> /// Initializes empty prompt structure. /// </SUMMARY> /// <PARAM name="ps"> /// Prompt parameter (which we do not actually need). /// </PARAM> private static void InitPrompt(ref CRYPTPROTECT_PROMPTSTRUCT ps) { ps.cbSize = Marshal.SizeOf( typeof(CRYPTPROTECT_PROMPTSTRUCT)); ps.dwPromptFlags= 0; ps.hwndApp = NullPtr; ps.szPrompt = null; }
/// <SUMMARY> /// Initializes a BLOB structure from a byte array. /// </SUMMARY> /// <PARAM name="data"> /// Original data in a byte array format. /// </PARAM> /// <PARAM name="blob"> /// Returned blob structure. /// </PARAM> private static void InitBLOB(byte[] data, ref DATA_BLOB blob) { // Allocate memory for the BLOB data. blob.pbData = Marshal.AllocHGlobal(data.Length);
// Make sure that memory allocation was successful. if (blob.pbData == IntPtr.Zero) throw new Exception( "Unable to allocate data buffer for BLOB structure.");
// Specify number of bytes in the BLOB. blob.cbData = data.Length;
// Copy data from original source to the BLOB structure. Marshal.Copy(data, 0, blob.pbData, data.Length); }
// Wrapper for Win32 message formatter. [DllImport("kernel32.dll", CharSet=System.Runtime.InteropServices.CharSet.Auto)] private unsafe static extern int FormatMessage( int dwFlags, ref IntPtr pMessageSource, int dwMessageID, int dwLanguageID, ref string lpBuffer, int nSize, IntPtr* pArguments);
public static byte[] Decrypt( byte[] cipherTextBytes, byte[] entropyBytes, out string description) { // Create BLOBs to hold data. DATA_BLOB plainTextBlob = new DATA_BLOB(); DATA_BLOB cipherTextBlob = new DATA_BLOB(); DATA_BLOB entropyBlob = new DATA_BLOB();
// We only need prompt structure because it is a required // parameter. CRYPTPROTECT_PROMPTSTRUCT prompt = new CRYPTPROTECT_PROMPTSTRUCT(); InitPrompt(ref prompt);
// Initialize description string. description = String.Empty;
try { // Convert ciphertext bytes into a BLOB structure. try { InitBLOB(cipherTextBytes, ref cipherTextBlob); } catch (Exception ex) { throw new Exception( "Cannot initialize ciphertext BLOB. " + ex.Message); }
// we don't use entropy bytes so this code is commented out... // // Convert entropy bytes into a BLOB structure. // try // { // InitBLOB(entropyBytes, ref entropyBlob); // } // catch (Exception ex) // { // throw new Exception( // "Cannot initialize entropy BLOB. " + // ex.Message); // }
// Disable any types of UI. CryptUnprotectData does not // mention CRYPTPROTECT_LOCAL_MACHINE flag in the list of // supported flags so we will not set it up. int flags = CRYPTPROTECT_UI_FORBIDDEN;
// Call DPAPI to decrypt data. bool success = CryptUnprotectData(ref cipherTextBlob, ref description, ref entropyBlob, IntPtr.Zero, ref prompt, flags, ref plainTextBlob);
// Check the result. if (!success) { // If operation failed, retrieve the last Win32 error. throw new Exception( "CryptUnprotectData failed. " + GetErrorMessage(Marshal.GetLastWin32Error())); }
// Allocate memory to hold plaintext. byte[] plainTextBytes = new byte[plainTextBlob.cbData];
// Copy ciphertext from the BLOB to a byte array. Marshal.Copy(plainTextBlob.pbData, plainTextBytes, 0, plainTextBlob.cbData);
// Return the result. return plainTextBytes; } catch (Exception ex) { throw new Exception("DPAPI was unable to decrypt data. " + ex.Message); } // Free all memory allocated for BLOBs. finally { if (plainTextBlob.pbData != IntPtr.Zero) Marshal.FreeHGlobal(plainTextBlob.pbData);
if (cipherTextBlob.pbData != IntPtr.Zero) Marshal.FreeHGlobal(cipherTextBlob.pbData);
if (entropyBlob.pbData != IntPtr.Zero) Marshal.FreeHGlobal(entropyBlob.pbData); } }
/// <SUMMARY> /// Formats error message from a Win32 error code. /// </SUMMARY> /// <PARAM name="errorCode"> /// Error code. /// </PARAM> /// <RETURNS> /// Formatted error message. /// </RETURNS> private unsafe static string GetErrorMessage(int errorCode) { // Define flags used to build message. int FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100; int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200; int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;
// Initialize data variables. int messageSize = 0; int minBufferSize = 0; string messageBuffer = String.Empty; string message = String.Empty;
// We need build a system message as opposed to a message from // a resource DLL. The operation will require auto memory // allocation. We will not use formatted arguments. int dwFlags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS;
// Set null pointers. IntPtr pMessageSource = new IntPtr(); IntPtr pArguments = new IntPtr();
// Use default language. int langID = 0;
// Get message. messageSize = FormatMessage( dwFlags, ref pMessageSource, errorCode, langID, ref messageBuffer, minBufferSize, &pArguments);
// Include error code in the message. if (messageSize == 0) message = String.Format("Error {0} occurred.", errorCode); else message = String.Format("Error {0}: {1}", errorCode, messageBuffer);
// Return formatted message. return message; } } }
|
|
| |
|
|
|
|
|
|
|
|
|
|
|