switch become/runas to LogonUser/CreateProcessWithTokenW (#28253)

* non-uac works

* switch become/runas to LogonUser/CreateProcessWithTokenW

* fixes #22218
* provides consistent behavior across authtypes
* auto-elevates on UAC if target user has SE_TCB_NAME ("Act as part of the operating system") privilege
* sets us up for much more granular capabilities later (eg, network/service/batch logons)
This commit is contained in:
Matt Davis 2017-08-15 18:55:17 -07:00 committed by GitHub
parent 1b6b74f655
commit 9b383403ce
2 changed files with 183 additions and 72 deletions

View file

@ -34,6 +34,7 @@ Ansible Changes By Release
- TODO: build upon this to add many features detailed in ansible-config proposal https://github.com/ansible/proposals/issues/35
* Windows modules now support the use of multiple shared module_utils files in the form of Powershell modules (.psm1), via `#Requires -Module Ansible.ModuleUtils.Whatever.psm1`
* Python module argument_spec now supports custom validation logic by accepting a callable as the `type` argument.
* Windows become_method: runas now works across all authtypes and will auto-elevate under UAC if WinRM user has "Act as part of the operating system" privilege
### Deprecations
* The behaviour when specifying `--tags` (or `--skip-tags`) multiple times on the command line

View file

@ -147,7 +147,6 @@ Function Run($payload) {
}
''' # end leaf_exec
become_wrapper = br'''
Set-StrictMode -Version 2
$ErrorActionPreference = "Stop"
@ -174,7 +173,7 @@ namespace Ansible.Shell
string so = null, se = null;
ThreadPool.QueueUserWorkItem((s)=>
ThreadPool.QueueUserWorkItem((s) =>
{
so = stdoutStream.ReadToEnd();
sowait.Set();
@ -186,13 +185,50 @@ namespace Ansible.Shell
sewait.Set();
});
foreach(var wh in new WaitHandle[] { sowait, sewait })
foreach (var wh in new WaitHandle[] { sowait, sewait })
wh.WaitOne();
stdout = so;
stderr = se;
}
public static IntPtr GetElevatedToken(IntPtr hToken)
{
uint requestedLength;
IntPtr pTokenInfo = Marshal.AllocHGlobal(sizeof(int));
try
{
if(!GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenElevationType, pTokenInfo, sizeof(int), out requestedLength))
throw new Win32Exception("Unable to get TokenElevationType");
var tet = (TOKEN_ELEVATION_TYPE)Marshal.ReadInt32(pTokenInfo);
// we already have the best token we can get, just use it
if(tet != TOKEN_ELEVATION_TYPE.TokenElevationTypeLimited)
return hToken;
GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenLinkedToken, IntPtr.Zero, 0, out requestedLength);
IntPtr pLinkedToken = Marshal.AllocHGlobal((int)requestedLength);
if(!GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenLinkedToken, pLinkedToken, requestedLength, out requestedLength))
throw new Win32Exception("Unable to get linked token");
IntPtr linkedToken = Marshal.ReadIntPtr(pLinkedToken);
Marshal.FreeHGlobal(pLinkedToken);
return linkedToken;
}
finally
{
Marshal.FreeHGlobal(pTokenInfo);
}
}
// http://stackoverflow.com/a/30687230/139652
public static void GrantAccessToWindowStationAndDesktop(string username)
{
@ -207,18 +243,19 @@ namespace Ansible.Shell
StringBuilder sbOut = new StringBuilder(1024);
IntPtr filePartOut;
if(SearchPath(null, findThis, null, sbOut.Capacity, sbOut, out filePartOut) == 0)
if (SearchPath(null, findThis, null, sbOut.Capacity, sbOut, out filePartOut) == 0)
throw new FileNotFoundException("Couldn't locate " + findThis + " on path");
return sbOut.ToString();
}
public static uint GetProcessExitCode(IntPtr processHandle) {
public static uint GetProcessExitCode(IntPtr processHandle)
{
new NativeWaitHandle(processHandle).WaitOne();
uint exitCode;
if(!GetExitCodeProcess(processHandle, out exitCode)) {
throw new Exception("Error getting process exit code: " + Marshal.GetLastWin32Error());
}
if (!GetExitCodeProcess(processHandle, out exitCode))
throw new Win32Exception("Error getting process exit code");
return exitCode;
}
@ -233,8 +270,8 @@ namespace Ansible.Shell
security.Persist(safeHandle, AccessControlSections.Access);
}
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
public static extern uint SearchPath (
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern uint SearchPath(
string lpPath,
string lpFileName,
string lpExtension,
@ -243,25 +280,25 @@ namespace Ansible.Shell
StringBuilder lpBuffer,
out IntPtr lpFilePart);
[DllImport("kernel32.dll", SetLastError=true)]
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
[DllImport("kernel32.dll")]
public static extern bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe, SECURITY_ATTRIBUTES lpPipeAttributes, uint nSize);
[DllImport("kernel32.dll", SetLastError=true)]
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(StandardHandleValues nStdHandle);
[DllImport("kernel32.dll", SetLastError=true)]
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetHandleInformation(IntPtr hObject, HandleFlags dwMask, int dwFlags);
[DllImport("kernel32.dll", SetLastError=true)]
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError=true)]
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref int lpSize);
[DllImport("kernel32.dll", SetLastError=true)]
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool UpdateProcThreadAttribute(
IntPtr lpAttributeList,
uint dwFlags,
@ -271,7 +308,16 @@ namespace Ansible.Shell
IntPtr lpPreviousValue,
IntPtr lpReturnSize);
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode, BestFitMapping=false)]
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(
string lpszUsername,
string lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out IntPtr phToken);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
public static extern bool CreateProcess(
[MarshalAs(UnmanagedType.LPTStr)]
string lpApplicationName,
@ -286,20 +332,25 @@ namespace Ansible.Shell
STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateProcessWithTokenW(
IntPtr hToken,
LOGON_FLAGS dwLogonFlags,
string lpApplicationName,
string lpCommandLine,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
public static extern bool CreateProcessWithLogonW(
string userName,
string domain,
string password,
LOGON_FLAGS logonFlags,
string applicationName,
string commandLine,
uint creationFlags,
IntPtr environment,
string currentDirectory,
STARTUPINFOEX startupInfo,
out PROCESS_INFORMATION processInformation);
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool GetTokenInformation(
IntPtr TokenHandle,
TOKEN_INFORMATION_CLASS TokenInformationClass,
IntPtr TokenInformation,
uint TokenInformationLength,
out uint ReturnLength);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr GetProcessWindowStation();
@ -313,7 +364,8 @@ namespace Ansible.Shell
private class GenericAccessRule : AccessRule
{
public GenericAccessRule(IdentityReference identity, int accessMask, AccessControlType type) :
base(identity, accessMask, false, InheritanceFlags.None, PropagationFlags.None, type) { }
base(identity, accessMask, false, InheritanceFlags.None, PropagationFlags.None, type)
{ }
}
private class GenericSecurity : NativeObjectSecurity
@ -328,12 +380,14 @@ namespace Ansible.Shell
public override Type AccessRightType { get { throw new NotImplementedException(); } }
public override AccessRule AccessRuleFactory(System.Security.Principal.IdentityReference identityReference, int accessMask, bool isInherited,
InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, AccessControlType type) { throw new NotImplementedException(); }
InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, AccessControlType type)
{ throw new NotImplementedException(); }
public override Type AccessRuleType { get { return typeof(AccessRule); } }
public override AuditRule AuditRuleFactory(System.Security.Principal.IdentityReference identityReference, int accessMask, bool isInherited,
InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, AuditFlags flags) { throw new NotImplementedException(); }
InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, AuditFlags flags)
{ throw new NotImplementedException(); }
public override Type AuditRuleType { get { return typeof(AuditRule); } }
}
@ -348,8 +402,25 @@ namespace Ansible.Shell
}
class NativeWaitHandle : WaitHandle {
public NativeWaitHandle(IntPtr handle) {
public class Win32Exception : System.ComponentModel.Win32Exception
{
private string _msg;
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
public Win32Exception(int errorCode, string message) : base(errorCode)
{
_msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
}
public override string Message { get { return _msg; } }
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
}
class NativeWaitHandle : WaitHandle
{
public NativeWaitHandle(IntPtr handle)
{
this.Handle = handle;
}
}
@ -357,8 +428,8 @@ namespace Ansible.Shell
[Flags]
public enum LOGON_FLAGS
{
LOGON_WITH_PROFILE = 0x00000001,
LOGON_NETCREDENTIALS_ONLY = 0x00000002
LOGON_WITH_PROFILE = 0x00000001,
LOGON_NETCREDENTIALS_ONLY = 0x00000002
}
[Flags]
@ -371,8 +442,6 @@ namespace Ansible.Shell
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
}
[Flags]
public enum StartupInfoFlags : uint
{
@ -393,6 +462,42 @@ namespace Ansible.Shell
INHERIT = 1
}
public enum TOKEN_INFORMATION_CLASS
{
TokenType = 8,
TokenImpersonationLevel = 9,
TokenElevationType = 18,
TokenLinkedToken = 19,
}
public enum TOKEN_ELEVATION_TYPE
{
TokenElevationTypeDefault = 1,
TokenElevationTypeFull,
TokenElevationTypeLimited
}
[StructLayout(LayoutKind.Sequential)]
public class PROFILEINFO {
public int dwSize;
public int dwFlags;
[MarshalAs(UnmanagedType.LPTStr)]
public String lpUserName;
[MarshalAs(UnmanagedType.LPTStr)]
public String lpProfilePath;
[MarshalAs(UnmanagedType.LPTStr)]
public String lpDefaultPath;
[MarshalAs(UnmanagedType.LPTStr)]
public String lpServerName;
[MarshalAs(UnmanagedType.LPTStr)]
public String lpPolicyPath;
public IntPtr hProfile;
public PROFILEINFO()
{
dwSize = Marshal.SizeOf(this);
}
}
[StructLayout(LayoutKind.Sequential)]
public class SECURITY_ATTRIBUTES
@ -401,7 +506,8 @@ namespace Ansible.Shell
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle = false;
public SECURITY_ATTRIBUTES() {
public SECURITY_ATTRIBUTES()
{
nLength = Marshal.SizeOf(this);
}
}
@ -429,17 +535,20 @@ namespace Ansible.Shell
public IntPtr hStdOutput;
public IntPtr hStdError;
public STARTUPINFO() {
public STARTUPINFO()
{
cb = Marshal.SizeOf(this);
}
}
[StructLayout(LayoutKind.Sequential)]
public class STARTUPINFOEX {
public class STARTUPINFOEX
{
public STARTUPINFO startupInfo;
public IntPtr lpAttributeList;
public STARTUPINFOEX() {
public STARTUPINFOEX()
{
startupInfo = new STARTUPINFO();
startupInfo.cb = Marshal.SizeOf(this);
}
@ -453,9 +562,6 @@ namespace Ansible.Shell
public int dwProcessId;
public int dwThreadId;
}
}
"@
@ -546,8 +652,6 @@ Function Run($payload) {
$acl.AddAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule($username, "FullControl", "Allow")))
Set-Acl $temp $acl | Out-Null
# TODO: grant target user permissions on tempfile/tempdir
Try {
$exec_args = @("-noninteractive", $temp)
@ -568,17 +672,18 @@ Function Run($payload) {
$stdout_read = $stdout_write = $stderr_read = $stderr_write = 0
If(-not [Ansible.Shell.NativeProcessUtil]::CreatePipe([ref]$stdout_read, [ref]$stdout_write, $pipesec, 0)) {
throw "Stdout pipe setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
throw [Ansible.Shell.Win32Exception] "Stdout pipe setup failed"
}
If(-not [Ansible.Shell.NativeProcessUtil]::SetHandleInformation($stdout_read, [Ansible.Shell.HandleFlags]::INHERIT, 0)) {
throw "Stdout handle setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
throw [Ansible.Shell.Win32Exception] "Stdout handle setup failed"
}
If(-not [Ansible.Shell.NativeProcessUtil]::CreatePipe([ref]$stderr_read, [ref]$stderr_write, $pipesec, 0)) {
throw "Stderr pipe setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
throw [Ansible.Shell.Win32Exception] "Stderr pipe setup failed"
}
If(-not [Ansible.Shell.NativeProcessUtil]::SetHandleInformation($stderr_read, [Ansible.Shell.HandleFlags]::INHERIT, 0)) {
throw "Stderr handle setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
throw [Ansible.Shell.Win32Exception] "Stderr handle setup failed"
}
# setup stdin redirection, we'll leave stdout/stderr as normal
@ -592,14 +697,13 @@ Function Run($payload) {
$pipesec.bInheritHandle = $true
If(-not [Ansible.Shell.NativeProcessUtil]::CreatePipe([ref]$stdin_read, [ref]$stdin_write, $pipesec, 0)) {
throw "Stdin pipe setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
throw [Ansible.Shell.Win32Exception] "Stdin pipe setup failed"
}
If(-not [Ansible.Shell.NativeProcessUtil]::SetHandleInformation($stdin_write, [Ansible.Shell.HandleFlags]::INHERIT, 0)) {
throw "Stdin handle setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
throw [Ansible.Shell.Win32Exception] "Stdin handle setup failed"
}
$si.startupInfo.hStdInput = $stdin_read
# create an attribute list with our explicit handle inheritance list to pass to CreateProcess
[int]$buf_sz = 0
@ -607,7 +711,7 @@ Function Run($payload) {
If(-not [Ansible.Shell.NativeProcessUtil]::InitializeProcThreadAttributeList([IntPtr]::Zero, 1, 0, [ref]$buf_sz)) {
$last_err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
If($last_err -ne 122) { # ERROR_INSUFFICIENT_BUFFER
throw "Attribute list size query failed, Win32Error: $last_err"
throw New-Object Ansible.Shell.Win32Exception $last_err, "Attribute list size query failed"
}
}
@ -615,7 +719,7 @@ Function Run($payload) {
# initialize the attribute list
If(-not [Ansible.Shell.NativeProcessUtil]::InitializeProcThreadAttributeList($si.lpAttributeList, 1, 0, [ref]$buf_sz)) {
throw "Attribute list init failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
throw [Ansible.Shell.Win32Exception] "Attribute list init failed"
}
$handles_to_inherit = [IntPtr[]]@($stdin_read,$stdout_write,$stderr_write)
@ -625,7 +729,7 @@ Function Run($payload) {
If(-not [Ansible.Shell.NativeProcessUtil]::UpdateProcThreadAttribute($si.lpAttributeList, 0, 0x20002, `
$pinned_handles.AddrOfPinnedObject(), [System.Runtime.InteropServices.Marshal]::SizeOf([type][IntPtr]) * $handles_to_inherit.Length, `
[System.IntPtr]::Zero, [System.IntPtr]::Zero)) {
throw "Attribute list update failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
throw [Ansible.Shell.Win32Exception] "Attribute list update failed"
}
# need to use a preamble-free version of UTF8Encoding
@ -653,12 +757,26 @@ Function Run($payload) {
$domain = "."
}
# TODO: use proper Win32Exception + error
If(-not [Ansible.Shell.NativeProcessUtil]::CreateProcessWithLogonW($username, $domain, $password, [Ansible.Shell.LOGON_FLAGS]::LOGON_WITH_PROFILE,
$exec_cmd, $exec_args,
$pstartup_flags, [IntPtr]::Zero, $env:windir, $si, [ref]$pi)) {
#throw New-Object System.ComponentModel.Win32Exception
throw "Worker creation failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
[System.IntPtr]$hToken = [System.IntPtr]::Zero
If(-not [Ansible.Shell.NativeProcessUtil]::LogonUser($username, $domain, $password, 2, 0, [ref]$hToken)) {
throw [Ansible.Shell.Win32Exception] "LogonUser failed"
}
$hTokenElevated = [Ansible.Shell.NativeProcessUtil]::GetElevatedToken($hToken);
$launch_success = $false
foreach($ht in @($hTokenElevated, $hToken)) {
If([Ansible.Shell.NativeProcessUtil]::CreateProcessWithTokenW($ht, [Ansible.Shell.LOGON_FLAGS]::LOGON_WITH_PROFILE,
$exec_cmd, $exec_args, $pstartup_flags, [System.IntPtr]::Zero, $env:windir, $si, [ref]$pi)) {
$launch_success = $true
break
}
}
If(-not $launch_success) {
throw [Ansible.Shell.Win32Exception] "Failed to create process with new token"
}
$stdout_fs = New-Object System.IO.FileStream @($stdout_read, [System.IO.FileAccess]::Read, $true, 4096)
@ -682,10 +800,6 @@ Function Run($payload) {
# FUTURE: decode CLIXML stderr output (and other streams?)
#$proc.WaitForExit() | Out-Null
# TODO: wait on process handle for exit, get process exit code
$rc = [Ansible.Shell.NativeProcessUtil]::GetProcessExitCode($pi.hProcess)
If ($rc -eq 0) {
@ -709,7 +823,6 @@ Function Run($payload) {
''' # end become_wrapper
async_wrapper = br'''
Set-StrictMode -Version 2
$ErrorActionPreference = "Stop"
@ -1256,9 +1369,6 @@ class ShellModule(object):
# env provider's limitations don't appear to be documented.
safe_envkey = re.compile(r'^[\d\w_]{1,255}$')
# TODO: implement module transfer
# TODO: implement #Requires -Modules parser/locator
# TODO: add KEEP_REMOTE_FILES support + debug wrapper dump
# TODO: add binary module support
def assert_safe_env_key(self, key):