Skip to content

Instantly share code, notes, and snippets.

@jborean93
Last active December 23, 2019 05:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jborean93/05db2707db1a57811ec1f17aa7c6f0b1 to your computer and use it in GitHub Desktop.
Save jborean93/05db2707db1a57811ec1f17aa7c6f0b1 to your computer and use it in GitHub Desktop.
Quick and dirty PowerShell module that implements Start-Process using CreateProcessWithToken
# Copyright: (c) 2019, Jordan Borean (@jborean93) <jborean93@gmail.com>
# MIT License (see LICENSE or https://opensource.org/licenses/MIT)
$pinvokeParams = @{
IgnoreWarnings = $true
WarningAction = 'Ignore'
TypeDefinition = @'
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Management.Automation;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Threading;
namespace Win32
{
internal class NativeHelpers
{
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public UInt32 dwProcessId;
public UInt32 dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
public class SECURITY_ATTRIBUTES
{
public UInt32 nLength;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle = false;
public SECURITY_ATTRIBUTES()
{
nLength = (UInt32)Marshal.SizeOf(this);
}
}
[StructLayout(LayoutKind.Sequential)]
public struct SID_AND_ATTRIBUTES
{
public IntPtr Sid;
public TokenGroupAttributes Attributes;
}
[StructLayout(LayoutKind.Sequential)]
public struct TOKEN_GROUPS
{
public UInt32 GroupCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public SID_AND_ATTRIBUTES[] Groups;
}
}
public class GenericSecurity : NativeObjectSecurity
{
public static int WINSTA_ALL_ACCESS = 0x000F037F;
public static int DESKTOP_ALL_ACCESS = 0x000F01FF;
public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle,
AccessControlSections sectionsRequested) : base(isContainer, resType, objectHandle, sectionsRequested) { }
public override Type AccessRightType { get { throw new NotImplementedException(); } }
public override Type AccessRuleType { get { return typeof(AccessRule); } }
public override Type AuditRuleType { get { return typeof(AuditRule); } }
public new void Persist(SafeHandle handle, AccessControlSections includeSections)
{
base.Persist(handle, includeSections);
}
public new void AddAccessRule(AccessRule rule)
{
base.AddAccessRule(rule);
}
public override AccessRule AccessRuleFactory(System.Security.Principal.IdentityReference identityReference,
int accessMask, bool isInherited, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags,
AccessControlType type)
{
throw new NotImplementedException();
}
public override AuditRule AuditRuleFactory(System.Security.Principal.IdentityReference identityReference,
int accessMask, bool isInherited, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags,
AuditFlags flags)
{
throw new NotImplementedException();
}
}
public class GenericAccessRule : AccessRule
{
public GenericAccessRule(IdentityReference identity, int accessMask, AccessControlType type) :
base(identity, accessMask, false, InheritanceFlags.None, PropagationFlags.None, type)
{ }
}
[StructLayout(LayoutKind.Sequential)]
public struct Luid
{
public UInt32 LowPart;
public Int32 HighPart;
public static explicit operator UInt64(Luid l)
{
return (UInt64)((UInt64)l.HighPart << 32) | (UInt64)l.LowPart;
}
}
public class NativeMethods
{
[DllImport("Kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true)]
internal static extern bool CloseHandleNative(
IntPtr hObject);
[DllImport("Kernel32.dll", SetLastError = true)]
internal static extern bool CreatePipe(
out SafeFileHandle hReadPipe,
out SafeFileHandle hWritePipe,
NativeHelpers.SECURITY_ATTRIBUTES lpPipeAttributes,
UInt32 nSize);
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool CreateProcessWithTokenW(
SafeHandle hToken,
LogonFlags dwLogonFlags,
[MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName,
StringBuilder lpCommandLine,
CreationFlags dwCreationFlags,
SafeMemoryBuffer lpEnvironment,
[MarshalAs(UnmanagedType.LPWStr)] string lpCurrentDirectory,
StartupInfoEx lpStartupInfo,
out NativeHelpers.PROCESS_INFORMATION lpProcessInformation);
[DllImport("Kernel32.dll")]
public static extern UInt32 GetCurrentThreadId();
[DllImport("User32.dll", SetLastError = true)]
public static extern NoopSafeHandle GetProcessWindowStation();
[DllImport("User32.dll", SetLastError = true)]
public static extern NoopSafeHandle GetThreadDesktop(
UInt32 dwThreadId);
[DllImport("Advapi32.dll", SetLastError = true)]
internal static extern bool GetTokenInformation(
SafeHandle TokenHandle,
TokenInformationClass TokenInformationClass,
SafeMemoryBuffer TokenInformation,
UInt32 TokenInformationLength,
out UInt32 ReturnLength);
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool LogonUserW(
[MarshalAs(UnmanagedType.LPWStr)] string lpszUsername,
[MarshalAs(UnmanagedType.LPWStr)] string lpszDomain,
IntPtr lpszPassword,
LogonType dwLogonType,
LogonProvider dwLogonProvider,
out SafeNativeHandle phToken);
[DllImport("Kernel32.dll", EntryPoint = "ResumeThread", SetLastError = true)]
internal static extern UInt32 ResumeThreadNative(
SafeHandle hThread);
[DllImport("Kernel32.dll")]
public static extern UInt32 WaitForSingleObject(
SafeHandle hHandle,
UInt32 dwMilliseconds);
public static void CloseHandle(IntPtr handle)
{
if (!CloseHandleNative(handle))
throw new Win32Exception(Marshal.GetLastWin32Error());
}
public static Tuple<SafeFileHandle, SafeFileHandle> CreatePipe(UInt32 bufferSize = 0, bool inheritHandle = false)
{
SafeFileHandle readPipe = null;
SafeFileHandle writePipe = null;
NativeHelpers.SECURITY_ATTRIBUTES secAttrs = new NativeHelpers.SECURITY_ATTRIBUTES();
secAttrs.bInheritHandle = inheritHandle;
if (!CreatePipe(out readPipe, out writePipe, secAttrs, bufferSize))
throw new Win32Exception(Marshal.GetLastWin32Error());
return Tuple.Create(readPipe, writePipe);
}
public static ProcessInformation CreateProcessWithToken(SafeHandle token, LogonFlags logonFlags,
string applicationName, string commandLine, CreationFlags creationFlags, IDictionary environment,
string currentDirectory, StartupInfoEx startupInfo)
{
if (String.IsNullOrEmpty(applicationName))
applicationName = null;
StringBuilder cmdText = new StringBuilder(commandLine);
IntPtr lpEnvironment = IntPtr.Zero;
if (environment != null && environment.Count > 0)
{
StringBuilder environmentString = new StringBuilder();
foreach (DictionaryEntry kv in environment)
environmentString.AppendFormat("{0}={1}\0", kv.Key, kv.Value);
environmentString.Append('\0');
lpEnvironment = Marshal.StringToHGlobalUni(environmentString.ToString());
}
SafeMemoryBuffer safeEnvironment = new SafeMemoryBuffer(lpEnvironment);
if (String.IsNullOrEmpty(currentDirectory))
currentDirectory = null;
NativeHelpers.PROCESS_INFORMATION processInfo = new NativeHelpers.PROCESS_INFORMATION();
if (!CreateProcessWithTokenW(token, logonFlags, applicationName, cmdText, creationFlags, safeEnvironment,
currentDirectory, startupInfo, out processInfo))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
return new ProcessInformation()
{
Process = new SafeNativeHandle(processInfo.hProcess),
Thread = new SafeNativeHandle(processInfo.hThread),
ProcessId = processInfo.dwProcessId,
ThreadId = processInfo.dwThreadId,
};
}
public static List<TokenGroup> GetTokenGroups(SafeHandle handle, TokenInformationClass tokenClass)
{
using (SafeMemoryBuffer buffer = GetTokenInformation(handle, tokenClass))
{
NativeHelpers.TOKEN_GROUPS raw = (NativeHelpers.TOKEN_GROUPS)Marshal.PtrToStructure(
buffer.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_GROUPS));
NativeHelpers.SID_AND_ATTRIBUTES[] sids = new NativeHelpers.SID_AND_ATTRIBUTES[raw.GroupCount];
PtrToStructureArray(sids, IntPtr.Add(buffer.DangerousGetHandle(), IntPtr.Size));
List<TokenGroup> groups = new List<TokenGroup>();
foreach (NativeHelpers.SID_AND_ATTRIBUTES sid in sids)
{
groups.Add(new TokenGroup()
{
Group = new SecurityIdentifier(sid.Sid),
Attributes = sid.Attributes,
});
}
return groups;
}
}
public static SafeMemoryBuffer GetTokenInformation(SafeHandle handle, TokenInformationClass tokenClass)
{
UInt32 returnSize = 0;
if (!GetTokenInformation(handle, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out returnSize))
{
int err = Marshal.GetLastWin32Error();
if (err != 24 && err != 122) // ERROR_INSUFFICIENT_BUFFER, ERROR_BAD_LENGTH
throw new Win32Exception(err);
}
SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)returnSize);
if (!GetTokenInformation(handle, tokenClass, buffer, returnSize, out returnSize))
throw new Win32Exception(Marshal.GetLastWin32Error());
return buffer;
}
public static SafeNativeHandle LogonUser(PSCredential credential, LogonType logonType,
LogonProvider logonProvider = LogonProvider.Default)
{
string username = credential.UserName;
string domain = null;
if (username.Contains("\\"))
{
string[] userSplit = username.Split(new char[] { '\\' }, 2);
domain = userSplit[0];
username = userSplit[1];
}
IntPtr pPassword = Marshal.SecureStringToGlobalAllocUnicode(credential.Password);
try
{
SafeNativeHandle hToken = null;
if (!LogonUserW(username, domain, pPassword, logonType, logonProvider, out hToken))
throw new Win32Exception(Marshal.GetLastWin32Error());
return hToken;
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(pPassword);
}
}
public static UInt32 ResumeThread(SafeHandle thread)
{
UInt32 res = ResumeThreadNative(thread);
if (res == 0xFFFFFFFF)
throw new Win32Exception(Marshal.GetLastWin32Error());
return res;
}
private static void PtrToStructureArray<T>(T[] array, IntPtr ptr)
{
IntPtr ptrOffset = ptr;
for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T))))
array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T));
}
}
public class NoopSafeHandle : SafeHandle
{
public NoopSafeHandle() : base(IntPtr.Zero, false) { }
public override bool IsInvalid { get { return false; } }
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle() { return true; }
}
public class ProcessInformation : IDisposable
{
public SafeNativeHandle Process;
public SafeNativeHandle Thread;
public UInt32 ProcessId;
public UInt32 ThreadId;
public static WaitHandle[] GetProcessOutput(SafeFileHandle stdout, Stream stdoutWriter,
SafeFileHandle stderr, Stream stderrWriter)
{
WaitHandle[] handles = new WaitHandle[2]
{
CreateStdioWorker(stdout, stdoutWriter), CreateStdioWorker(stderr, stderrWriter)
};
return handles;
}
private static EventWaitHandle CreateStdioWorker(SafeFileHandle pipe, Stream output)
{
if (pipe == null)
return null;
EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
int bufferSize = 4096;
ThreadPool.QueueUserWorkItem((s) =>
{
using (FileStream fs = new FileStream(pipe, FileAccess.Read, bufferSize))
{
fs.CopyTo(output);
waitHandle.Set();
}
});
return waitHandle;
}
public void Dispose()
{
Process.Dispose();
Thread.Dispose();
GC.SuppressFinalize(this);
}
~ProcessInformation() { Dispose(); }
}
public class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeMemoryBuffer() : base(true) { }
public SafeMemoryBuffer(int cb) : base(true)
{
base.SetHandle(Marshal.AllocHGlobal(cb));
}
public SafeMemoryBuffer(IntPtr handle) : base(true)
{
base.SetHandle(handle);
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
if (handle != IntPtr.Zero)
Marshal.FreeHGlobal(handle);
return true;
}
}
public class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeNativeHandle() : base(true) { }
public SafeNativeHandle(IntPtr handle) : base(true)
{
base.SetHandle(handle);
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
NativeMethods.CloseHandle(handle);
return true;
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public class StartupInfo
{
public UInt32 Size;
[MarshalAs(UnmanagedType.LPWStr)] public string Reserved;
[MarshalAs(UnmanagedType.LPWStr)] public string Desktop;
[MarshalAs(UnmanagedType.LPWStr)] public string Title;
public UInt32 X;
public UInt32 Y;
public UInt32 XSize;
public UInt32 YSize;
public UInt32 XCountChars;
public UInt32 YCoundChars;
public UInt32 FillAttributes;
public StartupInfoFlags Flags;
public ShowWindow ShowWindow;
public UInt16 Reserved2Size;
public IntPtr Reserved2;
public SafeFileHandle StdInput;
public SafeFileHandle StdOutput;
public SafeFileHandle StdError;
public StartupInfo()
{
Size = (UInt32)Marshal.SizeOf(this);
StdInput = new SafeFileHandle(IntPtr.Zero, false);
StdOutput = new SafeFileHandle(IntPtr.Zero, false);
StdError = new SafeFileHandle(IntPtr.Zero, false);
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public class StartupInfoEx
{
public StartupInfo StartupInfo;
public IntPtr AttributeList;
public StartupInfoEx()
{
StartupInfo = new StartupInfo();
StartupInfo.Size = (UInt32)Marshal.SizeOf(this);
}
}
public class TokenGroup
{
public SecurityIdentifier Group;
public TokenGroupAttributes Attributes;
}
[Flags]
public enum CreationFlags : uint
{
DebugProcess = 0x00000001,
DebugOnlyThisProcess = 0x00000002,
CreateSuspended = 0x00000004,
DetachedProcess = 0x00000008,
CreateNewConsole = 0x00000010,
NormalPriorityClass = 0x00000020,
IdlePriorityClass = 0x00000040,
HighPriorityClass = 0x00000080,
RealtimePriorityClass = 0x00000100,
CreateNewProcessGroup = 0x00000200,
CreateUnicodeEnvironment = 0x00000400,
CreateSeparateWowVdm = 0x00000800,
CreateSharedWowVdm = 0x00001000,
CreateForceDos = 0x00002000,
BelowNormalPriorityClass = 0x00004000,
AboveNormalPriorityClass = 0x00008000,
InheritParentAffinity = 0x00010000,
InheritCallerPriority = 0x00020000,
CreateProtectedProcess = 0x00040000,
ExtendedStartupInfoPresent = 0x00080000,
ProcessModeBackgroundBegin = 0x00100000,
ProcessModeBackgroundEnd = 0x00200000,
CreateSecureProcess = 0x00400000,
CreateBreakawayFromJob = 0x01000000,
CreatePreserveCodeAuthzLevel = 0x02000000,
CreateDefaultErrorMode = 0x04000000,
CreateNoWindow = 0x08000000,
ProfileUser = 0x10000000,
ProfileKernel = 0x20000000,
ProfileServer = 0x40000000,
CreateIgnoreSystemDefault = 0x80000000
}
[Flags]
public enum LogonFlags : uint
{
None = 0x00000000,
WithProfile = 0x00000001,
NetCredentialsOnly = 0x00000002,
ZeroPasswordBuffer = 0x80000000,
}
public enum LogonProvider
{
Default,
WinNT35,
WinNT40,
WinNT50,
Virtual,
}
public enum LogonType
{
Interactive = 2,
Network = 3,
Batch = 4,
Service = 5,
Unlock = 7,
NetworkCleartext = 8,
NewCredentials = 9,
}
public enum ShowWindow : ushort
{
Hide = 0,
ShowNormal = 1,
ShowMinimized = 2,
ShowMaximized = 3,
ShowNoActivate = 4,
Show = 5,
Minimize = 6,
ShowMinNoActive = 7,
ShowNA = 8,
Restore = 9,
ForceMinimize = 11,
}
[Flags]
public enum StartupInfoFlags : uint
{
UseShowWindow = 0x00000001,
UseSize = 0x00000002,
UsePosition = 0x00000004,
UseCountChars = 0x00000008,
UseFillAttribute = 0x00000010,
RunFullScreen = 0x00000020,
ForceOnFeedback = 0x00000040,
ForceOffFeedback = 0x00000080,
UseStdHandles = 0x00000100,
UseHotkey = 0x00000200,
TitleIsLinkName = 0x00000800,
TitleIsAppId = 0x00001000,
PreventPinning = 0x00002000,
UntrustedSource = 0x00008000,
}
[Flags]
public enum TokenGroupAttributes : uint
{
Mandatory = 0x00000001,
EnabledByDefault = 0x00000002,
Enabled = 0x00000004,
Owner = 0x00000008,
UseForDenyOnly = 0x00000010,
Integrity = 0x00000020,
IntegrityEnabled = 0x00000040,
Resource = 0x20000000,
LogonId = 0xC0000000,
}
public enum TokenInformationClass : uint
{
User = 1,
Groups = 2,
Privileges = 3,
Owner = 4,
PrimaryGroup = 5,
DefaultDacl = 6,
Source = 7,
Type = 8,
ImpersonationLevel = 9,
Statistics = 10,
RestrictedSids = 11,
SessionId = 12,
GroupsAndPrivileges = 13,
SessionReference = 14,
SandBoxInert = 15,
AuditPolicy = 16,
Origin = 17,
ElevationType = 18,
LinkedToken = 19,
Elevation = 20,
HasRestrictions = 21,
AccessInformation = 22,
VirtualizationAllowed = 23,
VirtualizationEnabled = 24,
IntegrityLevel = 25,
UIAccess = 26,
MandatoryPolicy = 27,
LogonSid = 28,
}
}
'@
}
if ((Get-Variable -Name IsCoreCLR -ErrorAction Ignore) -and $IsCoreCLR) {
$psAssembly = [PSObject].Assembly.Location
$refPath = Join-Path -Path (Split-Path -Path $psAssembly -Parent)-ChildPath 'ref'
$pinvokeParams.ReferencedAssemblies = @(
(Join-Path -Path $refPath -ChildPath 'System.Collections.dll'), # System.Collections.Generic
[System.ComponentModel.Win32Exception].Assembly.Location, # System.ComponentModel
$psAssembly, # System.Management.Automation
[System.Security.AccessControl.NativeObjectSecurity].Assembly.Location, # System.Security.AccessControl
[System.Security.Principal.IdentityReference].Assembly.Location, # System.Security.Principal
(Join-Path -Path $refPath -ChildPath 'System.Threading.dll'), # System.Threading
(Join-Path -Path $refPath -ChildPath 'System.Threading.ThreadPool.dll') # System.Threading.ThreadPool
)
}
Add-Type @pinvokeParams
Function Grant-WindowAccessRight {
<#
.SYNOPSIS
Adds an ACE to a Windows object Security Descriptor.
.DESCRIPTION
Used to add an ACE to the specified Windows object Security Descriptor.
.PARAMETER Handle
The handle to the Windows object to add the ACE to.
.PARAMETER Mask
The access right to add to the allow list on the ACE.
.PARAMETER IdentityReference
The Identity to add the allow ACE for.
.EXAMPLE
$accessParams = @{
IdentityReference = $logonSid
Handle = [Win32.NativeMethods]::GetProcessWindowStation()
Mask = [Win32.GenericSecurity]::WINSTA_ALL_ACCESS
}
Grant-WindowAccessRight @accessParams
.NOTES
This can be used to add an ACE to the Windows station and desktop required to execute a process as another user
on the current station/desktop.
#>
[CmdletBinding(SupportsShouldProcess=$true)]
Param (
[Parameter(Mandatory=$true)]
[System.Runtime.InteropServices.SafeHandle]
$Handle,
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[Int32]
$Mask,
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[System.Security.Principal.IdentityReference]
$IdentityReference
)
Begin {
Try {
$sd = New-Object -TypeName Win32.GenericSecurity -ArgumentList @(
$false,
[System.Security.AccessControl.ResourceType]::WindowObject,
$Handle,
[System.Security.AccessControl.AccessControlSections]::Access
)
} Catch {
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
Process {
Try {
$ace = New-Object -TypeName Win32.GenericAccessRule -ArgumentList @(
$IdentityReference,
$Mask,
[System.Security.AccessControl.AccessControlType]::Allow
)
$sd.AddAccessRule($ace)
} Catch {
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
End {
Try {
if ($PSCmdlet.ShouldProcess("Grant ACE(s) to Window Object")) {
$sd.Persist($Handle, [System.Security.AccessControl.AccessControlSections]::Access)
}
} Catch {
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Function New-UserToken {
<#
.SYNOPSIS
Creates a new user token with the credentials specified.
.DESCRIPTION
Creates a new user token with the credentials specified by calling LogonUser.
.PARAMETER Credential
The PSCredential object to log on with.
.PARAMETER LogonType
The logon type, defaults to Interactive, valid values;
Interactive: For interactive users, replicated a standard console Window.
Network: Like an SMB, WinRM logon, credentials are not cached for further network auth.
Batch: List a scheduled task logon
Service: Like a service logon.
NetworkCleartext: Like Network but the credentials are cached for further network auth.
NewCredentials: Clone current access token but network auth uses new Credentials set like runas.exe /netonly.
The WinNT50 LogonProvider must be used with this.
.PARAMETER LogonProvider
The logon provider to use, typically this shouldn't be changed unless NewCredentials is being used. Valid values;
Default: Standard provider for the system. The default is WinNT50 unless the UserName in the credential is not
in the UPN or Netlogon form. In this case the provider is WinNT40.
WinNT40: - NTLM logon provider
WinNT50: - Negotiate logon provider
.EXAMPLE
$hToken = New-UserToken -Credential (Get-Credential)
$hToken.Dispose()
.NOTES
The token should be disposed with .Dispose() as soon as it is no longer required.
#>
[CmdletBinding(SupportsShouldProcess=$true)]
Param (
[Parameter(Mandatory=$true)]
[System.Management.Automation.PSCredential]
$Credential,
[Win32.LogonType]
$LogonType = [Win32.LogonType]::Interactive,
[Win32.LogonProvider]
$LogonProvider = [Win32.LogonProvider]::Default
)
Process {
Try {
If ($PSCmdlet.ShouldProcess($Credential.UserName, "Logon")) {
[Win32.NativeMethods]::LogonUser($Credential, $LogonType, $LogonProvider)
}
} Catch {
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Function Start-ProcessWithToken {
<#
.SYNOPSIS
Starts a process as another user.
.DESCRIPTION
Starts a new process as another user. This cmdlet is designed to mimic the Start-Process cmdlet builtin to Windows
but offer more control over how to execute the executable as another user. Internally it calls
CreateProcessWithToken and requires the caller to have Administrative rights.
.PARAMETER Credential
The Windows credentials to logon on and run as. Alternatively the New-UserToken cmdlet can be used to create your
own access token and passed in with TokenHandle instead of this parameter.
.PARAMETER FilePath
The path to the file to execute. If not an absolute path, it is first resolved by the WorkingDirectory if set, if
still not found then it is looked up in the PATH like a normal shell.
.PARAMETER ArgumentList
A list of arguments to run with the executable, the values need to be escaped manually with a double quote if it
contains a space.
.PARAMETER Environment
A hashtable or dictionary to set as the new environment for the user.
.PARAMETER LoadUserProfile
Whether to load the user profile for the UserName supplied in the Credential parameter.
.PARAMETER NetCredentialsOnly
Used to log on the user specified by Credentials with the NewCredentials logon type. When set, the new process
will use the same token as the caller but any network connections will use the specified Credential for outbound
authentication.
.PARAMETER PassThru
Returns a System.Diagnostics.Process object for each process that the cmdlet started. By default, this cmdlet does
not generate any output.
.PARAMETER RedirectStandardError
Can be 1 of 3 things to redirect the new process' standard error stream.
String: The path to a file to redirect the standard error to.
Stream: A writable System.IO.Stream to redirect the standard error to.
StringBuilder: A System.Text.StringBuilder object to redirect the standard error to.
If the value is a Stream or StringBuilder, Wait must be specified.
If omitted, errors are displayed in the new console.
.PARAMETER RedirectStandardInput
Can be 1 of 3 things to redirect the new process' standard input stream.
String: The path to a file to redirect the standard input from.
Stream: A readable System.IO.Stream to redirect the standard input from.
Byte[]: Raw byte array to write to the process' standard input stream.
If Stream or Byte[] is set, the input is read into the process when it is first started.
If omitted, the process get its input from the keyboard.
.PARAMETER RedirectStandardOutput
Can be 1 of 3 things to redirect the new process' standard output stream.
String: The path to a file to redirect the standard output to.
Stream: A writable System.IO.Stream to redirect the standard output to.
StringBuilder: A System.Text.StringBuilder object to redirect the standard output to.
If the value is a Stream or StringBuilder, Wait must be specified.
If omitted, the output is displayed in the new console.
.PARAMETER TokenHandle
Instead of specifying a Credential object, a raw access token can be specified to start a new process.
.PARAMETER Wait
Indicates that this cmdlet waits for the specified process and its descendants to complete before accepting more
input. This parameter suppresses the command prompt or retains the window until the processes finish. This
parameter is also redirected if RedirectStandardError or RedirectStandardOutput is not set to a file path.
.PARAMETER WindowStyle
Specifies the state of the windows that is created for the new process. The acceptable values for this parameter
are: Normal, Hidden, Minimized, and Maximized. The default value is Normal.
.PARAMETER WorkingDirectory
Specifies the working directory of the new process.
.EXAMPLE Start a process in the background as another user
Start-ProcessWithToken -FilePath powershell.exe -Credential (Get-Credential)
.EXAMPLE Start a process like runas.exe /netonly
Start-ProcessWithToken -FilePath powershell.exe -Credential (Get-Credential) -NetCredentialsOnly
.EXAMPLE Start a process and wait for it to finish
Start-ProcessWithToken -FilePath cmd.exe -ArgumentList @("/c", "echo hi") -Wait
.EXAMPLE Start a process and redirect stdout, stderr to a string
$stdout = [System.Text.StringBuilder]::new()
$stderr = [System.Text.StringBuilder]::new()
Start-ProcessWithToken -FilePath whoami.exe -RedirectStandardOutput $stdout `
-RedirectStandardError $stderr -Wait -Credential (Get-Credential)
$stdout.ToString()
$stderr.ToString()
.EXAMPLE Start a process with redirect stdout, stderr to a file, read stdin from a file
Start-ProcessWithToken -FilePath cmd.exe -RedirectStandardOutput out.txt `
-RedirectStandardError err.txt `
-RedirectStandardIn in.txt `
-Credential (Get-Credential)
.EXAMPLE Start a process and send raw stdin bytes
$stdinBytes = [System.Text.Encoding]::UTF8.GetBytes("echo hi`r`nexit 0")
Start-ProcessWithToken -FilePath cmd.exe -RedirectStandardInput $stdinBytes `
-Credential (Get-Credential)
.EXAMPLE Start a process with a batch logon type
$hToken = New-UserToken -Credential (Get-Credential) -LogonType Batch
Try {
Start-ProcessWithToken -FilePath cmd.exe -TokenHandle $hToken
} Finally {
$hToken.Dispose()
}
.NOTES
If Credential is specified and UAC is enabled, the new process will not be elevated. This process can be further
elevated using UAC inside that new process or a raw access token with a Batch LogonType can be used instead.
#>
[CmdletBinding(DefaultParameterSetName="Credential")]
Param (
[Parameter(Mandatory=$true, ParameterSetName="Credential")]
[System.Management.Automation.PSCredential]
$Credential,
[Parameter(Mandatory=$true)]
[String]
$FilePath,
[String[]]
$ArgumentList,
[System.Collections.IDictionary]
$Environment,
[Switch]
$LoadUserProfile,
[Switch]
$NetCredentialsOnly,
[Switch]
$PassThru,
[Object]
$RedirectStandardError,
[Object]
$RedirectStandardInput,
[Object]
$RedirectStandardOutput,
[Parameter(Mandatory=$true, ParameterSetName="Raw")]
[System.Runtime.InteropServices.SafeHandle]
$TokenHandle,
[Switch]
$Wait,
[System.Diagnostics.ProcessWindowStyle]
$WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Normal,
[String]
$WorkingDirectory
)
Process {
$validateRedirection = {
Param (
[Object]
$Redirection,
[String]
$Name,
[Switch]
$Read
)
# stdin can be a byte[] array while the stdout/stderr can be a StringBuilder.
if ($Read) {
$extraType = [Byte[]]
} Else {
$extraType = [System.Text.StringBuilder]
}
If ($Redirection -and
$Redirection -isnot [System.IO.Stream] -and
$Redirection -isnot [String] -and
$Redirection -isnot $extraType) {
$msg = "$Name is not an expected input type, redirection parameters must be either a String, Stream, "
$msg += "or $($extraType.Name)."
throw $msg
} ElseIf ($Redirection -and $Redirection -is [System.IO.Stream]) {
If ($Read) {
$prop = "CanRead"
} Else {
$prop = "CanWrite"
}
If (-not $Redirection."$prop") {
$msg = "$Name as a Stream.$prop is `$false, expected `$true"
throw $msg
}
}
If ($Redirection -and $Redirection -isnot [String] -and -not $Wait) {
throw "$Name can only be set to a file path is -Wait is not set"
}
}
.$validateRedirection -Redirection $RedirectStandardError -Name 'RedirectStandardError'
.$validateRedirection -Redirection $RedirectStandardOutput -Name 'RedirectStandardOutput'
.$validateRedirection -Redirection $RedirectStandardInput -Name 'RedirectStandardInput' -Read
If (-not [System.IO.Path]::IsPathRooted($FilePath)) {
$exePath = $null
# Try and resolve the path based on the working directory if set.
If ($WorkingDirectory) {
$absPath = [System.IO.Path]::GetFullName((Join-Path -Path $WorkingDirectory -ChildPath $FilePath))
If (Test-Path -LiteralPath $absPath -PathType Leaf) {
$exePath = $absPath
}
}
# Otherwise try and resolve the path through the normal rules
If (-not $exePath) {
$command = Get-Command -Name $FilePath -CommandType Application -ErrorAction SilentlyContinue
If (-not $command) {
Write-Error -Message "Failed to find absolute path to '$FilePath'" -Category ObjectNotFound
return
}
$exePath = $command.Source
}
$FilePath = $exePath
}
$arguments = $FilePath
If ($ArgumentList) {
# Escaping the arguments is still up to the end user to do.
$arguments += " " + ($ArgumentList -join " ")
}
If ($PSCmdlet.ParameterSetName -eq "Credential") {
$newUserParams = @{
Credential = $Credential
}
If ($NetCredentialsOnly) {
$newUserParams.LogonType = [Win32.LogonType]::NewCredentials
$newUserParams.LogonProvider = [Win32.LogonProvider]::WinNT50
} Else {
$newUserParams.LogonType = [Win32.LogonType]::Interactive
}
$TokenHandle = New-UserToken @newUserParams
}
Try {
# Need to grant that logon sid the right to access the current station and desktop.
$logonSid = [Win32.NativeMethods]::GetTokenGroups($TokenHandle,
[Win32.TokenInformationClass]::LogonSid)[0].Group
$accessParams = @{
IdentityReference = $logonSid
Handle = [Win32.NativeMethods]::GetProcessWindowStation()
Mask = [Win32.GenericSecurity]::WINSTA_ALL_ACCESS
}
Grant-WindowAccessRight @accessParams
$tid = [Win32.NativeMethods]::GetCurrentThreadId()
$accessParams.Handle = [Win32.NativeMethods]::GetThreadDesktop($tid)
$accessParams.Mask = [Win32.GenericSecurity]::DESKTOP_ALL_ACCESS
Grant-WindowAccessRight @accessParams
$logonFlags = [Win32.LogonFlags]::None
If ($LoadUserProfile) {
$logonFlags = $logonFlags -bor [Win32.LogonFlags]::WithProfile
}
If ($NetCredentialsOnly) {
$logonFlags = $logonFlags -bor [Win32.LogonFlags]::NetCredentialsOnly
}
$siFlags = 0
$showWindow = 0
switch ($WindowStyle) {
'Hidden' {
$siFlags = $siFlags -bor [Win32.StartupInfoFlags]::UseShowWindow
$showWindow = [Win32.ShowWindow]::Hide
}
'Maximized' {
$siFlags = $siFlags -bor [Win32.StartupInfoFlags]::UseShowWindow
$showWindow = [Win32.ShowWindow]::ShowMaximized
}
'Minimized' {
$siFlags = $siFlags -bor [Win32.StartupInfoFlags]::UseShowWindow
$showWindow = [Win32.ShowWindow]::ShowMinimized
}
}
$startupInfo = New-Object -TypeName Win32.StartupInfoEx
$startupInfo.StartupInfo.ShowWindow = $showWindow
$disposals = [System.Collections.Generic.List[Object]]@()
Try {
$redir = {
Param (
[Object]
$Redirection,
[String]
$PipeType,
[Switch]
$InputPipe
)
if (-not $Redirection) {
$null, $null # Output empty pipes for the caller
return
}
$siFlags = $siFlags -bor [Win32.StartupInfoFlags]::UseStdHandles
$readPipe = $null
$writePipe = $null
$siPipe = $null
If ($Redirection -is [String]) {
# We are redirecting the pipe to a file, first check if the path is an absolute path or not.
If (-not ([System.IO.Path]::IsPathRooted($RedirectStandardError))) {
$Redirection = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
$Redirection
)
}
If ($InputPipe) {
$fsStream = [System.IO.File]::Open(
$Redirection,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read
)
} Else {
$fsStream = [System.IO.File]::Create($Redirection)
}
$disposals.Add($fsStream)
$siPipe = $fsStream.SafeFileHandle
If ($InputPipe) {
$readPipe = $fsStream.SafeFileHandle
} Else {
$writePipe = $fsStream.SafeFileHandle
}
} Else {
$pipes = [Win32.NativeMethods]::CreatePipe(0, $true)
$readPipe = $pipes.Item1
$writePipe = $pipes.Item2
$disposals.Add($readPipe)
$disposals.Add($writePipe)
$siPipe = If ($InputPipe) { $readPipe } Else { $writePipe }
}
$startupInfo.StartupInfo."$PipeType" = $siPipe
$readPipe, $writePipe
}
$stdoutRead, $stdoutWrite = .$redir -Redirection $RedirectStandardOutput -PipeType StdOutput
$stderrRead, $stderrWrite = .$redir -Redirection $RedirectStandardError -PipeType StdError
$stdinRead, $stdinWrite = .$redir -Redirection $RedirectStandardInput -PipeType StdInput -InputPipe
# To avoid a race condition where the process has ended before we get the .NET Process object, we make
# sure the process is suspended and resume it once we have the .NET object.
$creationFlags = [Win32.CreationFlags]'CreateSuspended, CreateUnicodeEnvironment'
$startupInfo.StartupInfo.Flags = $siFlags
$procInfo = [Win32.NativeMethods]::CreateProcessWithToken(
$TokenHandle,
$logonFlags,
$FilePath,
$arguments,
$creationFlags,
$Environment,
$WorkingDirectory,
$startupInfo
)
Try {
# Only start the process once we have the .NET Process object.
$dotnetProcess = [System.Diagnostics.Process]::GetProcessById([Int32]$procInfo.ProcessId)
[Win32.NativeMethods]::ResumeThread($procInfo.Thread) > $null
# Send the stdin and close the handles
If ($RedirectStandardInput) {
If ($stdinWrite) { # RedirectStandardInput was not a string/path.
$stdinFS = New-Object -TypeName System.IO.FileStream -ArgumentList @(
$stdinWrite,
[System.IO.FileAccess]::Write
)
Try {
If ($RedirectStandardInput -is [System.IO.Stream]) {
$RedirectStandardInput.CopyTo($stdinFS)
} Else {
$stdinFS.Write($RedirectStandardInput, 0, $RedirectStandardInput.Length)
}
} Finally {
$stdinFS.Dispose()
}
$stdinWrite.Dispose()
$disposals.Remove($stdinWrite) > $null
}
$stdinRead.Dispose()
$disposals.Remove($stdinRead) > $null
}
If ($Wait) {
$setupStream = {
Param (
[Object]
$Redirection,
[Microsoft.Win32.SafeHandles.SafeFileHandle]
$Pipe
)
$stream = $null
If ($Redirection) {
$Pipe.Dispose()
$disposals.Remove($Pipe) > $null
$stream = $Redirection
If ($RedirectStandardOutput -isnot [System.IO.Stream]) {
$stream = New-Object -TypeName System.IO.MemoryStream
$disposals.Add($stream)
}
}
$stream
}
# Start separate threads to copy the output to our memory streams.
$stdoutStream = .$setupStream -Redirection $RedirectStandardOutput -Pipe $stdoutWrite
$stderrStream = .$setupStream -Redirection $RedirectStandardError -Pipe $stderrWrite
$readWaits = [Win32.ProcessInformation]::GetProcessOutput(
$stdoutRead, $stdoutStream,
$stderrRead, $stderrStream
)
# Wait for the process to finish.
[Win32.NativeMethods]::WaitForSingleObject($procInfo.Process, [UInt32]"0xFFFFFFFF") > $null
# Now the process has exited, dispose our side of the pipes and wait for it to close.
If ($stdoutWrite) {
$stdoutWrite.Dispose()
$disposals.Remove($stdoutWrite) > $null
}
If ($stderrWrite) {
$stderrWrite.Dispose()
$disposals.Remove($stderrWrite) > $null
}
foreach ($waitHandle in $readWaits) {
If ($waitHandle) {
$waitHandle.WaitOne() > $null
}
}
$processStream = {
Param (
[Object]
$Redirection,
[System.IO.Stream]
$Stream
)
If ($Stream) {
$Stream.Seek(0, 'Begin') > $null
If ($Redirection -is [System.Text.StringBuilder]) {
$sr = New-Object -TypeName System.IO.StreamReader -ArgumentList @(
$Stream,
[System.Text.Encoding]::UTF8
)
Try {
$Redirection.Append($sr.ReadToEnd()) > $null
} Finally {
$sr.Dispose()
}
}
}
}
.$processStream -Redirection $RedirectStandardOutput -Stream $stdoutStream
.$processStream -Redirection $RedirectStandardError -Stream $stderrStream
}
} Finally {
$procInfo.Dispose()
}
If ($PassThru) {
$dotnetProcess
}
} Finally {
foreach ($dispose in $disposals) {
$dispose.Dispose()
}
}
} Finally {
If ($PSCmdlet.ParameterSetName -eq "Credential") {
$TokenHandle.Dispose()
}
}
}
}
Export-ModuleMember -Function New-UserToken, Start-ProcessWithToken
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment