Last active
September 3, 2019 22:49
-
-
Save marcogrcr/f4e5916c735c71992b1b1a25f7454c05 to your computer and use it in GitHub Desktop.
How to run a non-interactive process as another user in a Windows Service using C#? [proof-of-concept]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This application converts a text message to uppercase by spawning a child process as another user and saves it to a file. | |
* | |
* Usage: app.exe MESSAGE FILE_PATH | |
* or | |
* Usage: app.exe DOMAIN USERNAME PASSWORD MESSAGE FILE_PATH | |
* | |
* | |
* Setup instructions: | |
* | |
* 1. Open secpol.msc | |
* 2. Navigate to 'Local Policies' > 'User Rights Assignment' > 'Log on as a service'. | |
* 3. Add your account and click the 'OK' button. | |
* 4. Compile this file as a console application. | |
* 5. Open a PowerShell console as administrator. | |
* 6. PS C:\> New-Service ServiceTest "CONSOLE_EXE_PATH MESSAGE FILE_PATH" -Credential YOUR_DOMAIN\YOUR_ACCOUNT | |
* 7. PS C:\> Start-Service ServiceTest | |
* 8. Verify that the file is created with the message in uppercase. | |
* 10. Delete the file. | |
* 11. PS C:\> Stop-Service ServiceTest | |
* 12. PS C:\> sc.exe delete ServiceTest | |
* 13. PS C:\> New-Service ServiceTest "CONSOLE_EXE_PATH YOUR_DOMAIN ANOTHER_ACCOUNT_USERNAME PASSWORD MESSAGE FILE_PATH" -Credential YOUR_DOMAIN\YOUR_ACCOUNT | |
* 14. Ensure the YOUR_DOMAIN\ANOTHER_ACCOUNT_USERNAME has access to the folder where CONSOLE_EXE_PATH is located. | |
* 15. PS C:\> Start-Service ServiceTest | |
* 16. Verify that the file is not created. | |
* 17. Open eventvwr | |
* 18. Verify that a { "Log Name": "System", "Source": "Application Popup", "EventId": 26, "Data": ["CONSOLE_EXE - Application Error", "The application was unable to start correctly (0xc0000142). Click OK to close the application."] } event is registered. | |
* 19. PS C:\> Stop-Service ServiceTest | |
* 20. PS C:\> sc.exe delete ServiceTest | |
*/ | |
namespace ServiceTest | |
{ | |
using System; | |
using System.Diagnostics; | |
using System.IO; | |
using System.Net; | |
using System.ServiceProcess; | |
using System.Text; | |
using System.Threading; | |
internal sealed class Program : ServiceBase | |
{ | |
private const string ChildIndicator = "079a9ab7-e4b8-4192-9dc7-dc7b93ec9376"; | |
private readonly string[] args; | |
private readonly ManualResetEventSlim processDone = new ManualResetEventSlim(); | |
private Program(string[] args) | |
{ | |
this.args = args; | |
} | |
public static void Main(string[] args) | |
{ | |
if (Environment.UserInteractive || Program.IsChildProcess(args)) | |
{ | |
var p = new Program(args); | |
p.OnStart(null); | |
p.OnStop(); | |
} | |
else | |
{ | |
ServiceBase.Run(new Program(args)); | |
} | |
} | |
protected override void OnStart(string[] notUsed) | |
{ | |
if (Program.IsChildProcess(this.args)) | |
{ | |
Console.WriteLine("handshake"); | |
var input = Console.In.ReadToEnd(); | |
if (input != null) | |
{ | |
Console.WriteLine(input.ToUpper()); | |
} | |
this.processDone.Set(); | |
} | |
else if (this.args.Length == 2 || this.args.Length == 5) | |
{ | |
var credentials = this.args.Length == 5 ? new NetworkCredential(this.args[1], this.args[2], this.args[0]) : null; | |
var message = this.args.Length == 5 ? this.args[3] : this.args[0]; | |
var outputFile = this.args.Length == 5 ? this.args[4] : this.args[1]; | |
// Create process. | |
var process = new Process | |
{ | |
EnableRaisingEvents = true, | |
StartInfo = | |
{ | |
FileName = Process.GetCurrentProcess().MainModule.FileName, | |
Arguments = Program.ChildIndicator, | |
CreateNoWindow = true, | |
RedirectStandardError = true, | |
RedirectStandardInput = true, | |
RedirectStandardOutput = true, | |
UseShellExecute = false, | |
WorkingDirectory = @"C:\" | |
} | |
}; | |
if (credentials != null) | |
{ | |
process.StartInfo.Domain = credentials.Domain; | |
process.StartInfo.UserName = credentials.UserName; | |
process.StartInfo.Password = credentials.SecurePassword; | |
} | |
var error = new StringBuilder(); | |
var errorWaitHandle = new ManualResetEventSlim(); | |
process.ErrorDataReceived += (s, e) => | |
{ | |
if (e.Data != null) | |
{ | |
error.AppendLine(e.Data); | |
} | |
else | |
{ | |
errorWaitHandle.Set(); | |
} | |
}; | |
var output = new StringBuilder(); | |
var outputWaitHandle = new ManualResetEventSlim(); | |
process.OutputDataReceived += (s, e) => | |
{ | |
if (e.Data == "handshake") | |
{ | |
process.StandardInput.Write(message); | |
process.StandardInput.Close(); | |
} | |
else if (e.Data != null) | |
{ | |
output.AppendLine(e.Data); | |
} | |
else | |
{ | |
outputWaitHandle.Set(); | |
} | |
}; | |
process.Exited += (s, e) => | |
{ | |
try | |
{ | |
errorWaitHandle.Wait(); | |
outputWaitHandle.Wait(); | |
if (error.Length > 0) | |
{ | |
File.WriteAllText(outputFile, string.Format("Child process completed with an error: {0}", error)); | |
} | |
else if (output.Length > 0) | |
{ | |
File.WriteAllText(outputFile, output.ToString()); | |
} | |
} | |
finally | |
{ | |
process.Dispose(); | |
this.processDone.Set(); | |
} | |
}; | |
process.Start(); | |
process.BeginErrorReadLine(); | |
process.BeginOutputReadLine(); | |
} | |
else | |
{ | |
using (var currentProcess = Process.GetCurrentProcess()) | |
{ | |
Console.WriteLine("Usage:"); | |
Console.WriteLine("{0} <msg> <path_to_output_file>", currentProcess.MainModule.ModuleName); | |
Console.WriteLine("{0} <domain> <username> <password> <msg> <path_to_output_file>", currentProcess.MainModule.ModuleName); | |
this.processDone.Set(); | |
} | |
} | |
} | |
protected override void OnStop() | |
{ | |
this.processDone.Wait(); | |
} | |
private static bool IsChildProcess(string[] args) | |
{ | |
return args.Length == 1 && args[0] == Program.ChildIndicator; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment