# Copyright (c) 2014, Chris Church # Copyright (c) 2017 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' name: powershell plugin_type: shell version_added: "" short_description: Windows Powershell description: - The only option when using 'winrm' as a connection plugin options: remote_tmp: description: - Temporary directory to use on targets when copying files to the host. default: '%TEMP%' ini: - section: powershell key: remote_tmp vars: - name: ansible_remote_tmp set_module_language: description: - Controls if we set the locale for moduels when executing on the target. - Windows only supports C(no) as an option. type: bool default: 'no' choices: - 'no' environment: description: - Dictionary of environment variables and their values to use when executing commands. type: dict default: {} ''' # FIXME: admin_users and set_module_language don't belong here but must be set # so they don't failk when someone get_option('admin_users') on this plugin import base64 import os import re import shlex import pkgutil from ansible.errors import AnsibleError from ansible.module_utils._text import to_text from ansible.plugins.shell import ShellBase _common_args = ['PowerShell', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Unrestricted'] # Primarily for testing, allow explicitly specifying PowerShell version via # an environment variable. _powershell_version = os.environ.get('POWERSHELL_VERSION', None) if _powershell_version: _common_args = ['PowerShell', '-Version', _powershell_version] + _common_args[1:] exec_wrapper = br''' begin { $DebugPreference = "Continue" $ErrorActionPreference = "Stop" Set-StrictMode -Version 2 function ConvertTo-HashtableFromPsCustomObject ($myPsObject){ $output = @{}; $myPsObject | Get-Member -MemberType *Property | % { $val = $myPsObject.($_.name); If ($val -is [psobject]) { $val = ConvertTo-HashtableFromPsCustomObject $val } $output.($_.name) = $val } return $output; } # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives # exec runspace, capture output, cleanup, return module output # only init and stream in $json_raw if it wasn't set by the enclosing scope if (-not $(Get-Variable "json_raw" -ErrorAction SilentlyContinue)) { $json_raw = '' } } process { $input_as_string = [string]$input $json_raw += $input_as_string } end { If (-not $json_raw) { Write-Error "no input given" -Category InvalidArgument } $payload = ConvertTo-HashtableFromPsCustomObject (ConvertFrom-Json $json_raw) # TODO: handle binary modules # TODO: handle persistence $min_os_version = [version]$payload.min_os_version if ($min_os_version -ne $null) { $actual_os_version = [System.Environment]::OSVersion.Version if ($actual_os_version -lt $min_os_version) { $msg = "This module cannot run on this OS as it requires a minimum version of $min_os_version, actual was $actual_os_version" Write-Output (ConvertTo-Json @{failed=$true;msg=$msg}) exit 1 } } $min_ps_version = [version]$payload.min_ps_version if ($min_ps_version -ne $null) { $actual_ps_version = $PSVersionTable.PSVersion if ($actual_ps_version -lt $min_ps_version) { $msg = "This module cannot run as it requires a minimum PowerShell version of $min_ps_version, actual was $actual_ps_version" Write-Output (ConvertTo-Json @{failed=$true;msg=$msg}) exit 1 } } $actions = $payload.actions # pop 0th action as entrypoint $entrypoint = $payload.($actions[0]) $payload.actions = $payload.actions[1..99] $entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint)) # load the current action entrypoint as a module custom object with a Run method $entrypoint = New-Module -ScriptBlock ([scriptblock]::Create($entrypoint)) -AsCustomObject Set-Variable -Scope global -Name complex_args -Value $payload["module_args"] | Out-Null # dynamically create/load modules ForEach ($mod in $payload.powershell_modules.GetEnumerator()) { $decoded_module = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($mod.Value)) New-Module -ScriptBlock ([scriptblock]::Create($decoded_module)) -Name $mod.Key | Import-Module -WarningAction SilentlyContinue | Out-Null } $output = $entrypoint.Run($payload) Write-Output $output } ''' # end exec_wrapper leaf_exec = br''' Function Run($payload) { $entrypoint = $payload.module_entry $entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint)) $ps = [powershell]::Create() $ps.AddStatement().AddCommand("Set-Variable").AddParameters(@{Scope="global";Name="complex_args";Value=$payload.module_args}) | Out-Null $ps.AddCommand("Out-Null") | Out-Null # redefine Write-Host to dump to output instead of failing- lots of scripts use it $ps.AddStatement().AddScript("Function Write-Host(`$msg){ Write-Output `$msg }") | Out-Null ForEach ($env_kv in $payload.environment.GetEnumerator()) { # need to escape ' in both the key and value $env_key = $env_kv.Key.ToString().Replace("'", "''") $env_value = $env_kv.Value.ToString().Replace("'", "''") $escaped_env_set = "[System.Environment]::SetEnvironmentVariable('{0}', '{1}')" -f $env_key, $env_value $ps.AddStatement().AddScript($escaped_env_set) | Out-Null } # dynamically create/load modules ForEach ($mod in $payload.powershell_modules.GetEnumerator()) { $decoded_module = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($mod.Value)) $ps.AddStatement().AddCommand("New-Module").AddParameters(@{ScriptBlock=([scriptblock]::Create($decoded_module));Name=$mod.Key}) | Out-Null $ps.AddCommand("Import-Module").AddParameters(@{WarningAction="SilentlyContinue"}) | Out-Null $ps.AddCommand("Out-Null") | Out-Null } # force input encoding to preamble-free UTF8 so PS sub-processes (eg, # Start-Job) don't blow up. This is only required for WinRM, a PSRP # runspace doesn't have a host console and this will bomb out if ($host.Name -eq "ConsoleHost") { $ps.AddStatement().AddScript("[Console]::InputEncoding = New-Object Text.UTF8Encoding `$false") | Out-Null } $ps.AddStatement().AddScript($entrypoint) | Out-Null $output = $ps.Invoke() $output # PS3 doesn't properly set HadErrors in many cases, inspect the error stream as a fallback If ($ps.HadErrors -or ($PSVersionTable.PSVersion.Major -lt 4 -and $ps.Streams.Error.Count -gt 0)) { $host.UI.WriteErrorLine($($ps.Streams.Error | Out-String)) $exit_code = $ps.Runspace.SessionStateProxy.GetVariable("LASTEXITCODE") If(-not $exit_code) { $exit_code = 1 } # need to use this instead of Exit keyword to prevent runspace from crashing with dynamic modules $host.SetShouldExit($exit_code) } } ''' # end leaf_exec become_wrapper = br''' Set-StrictMode -Version 2 $ErrorActionPreference = "Stop" $helper_def = @" using Microsoft.Win32.SafeHandles; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using System.Text; using System.Threading; namespace AnsibleBecome { [StructLayout(LayoutKind.Sequential)] public class SECURITY_ATTRIBUTES { public int nLength; public IntPtr lpSecurityDescriptor; public bool bInheritHandle = false; public SECURITY_ATTRIBUTES() { nLength = Marshal.SizeOf(this); } } [StructLayout(LayoutKind.Sequential)] public class STARTUPINFO { public Int32 cb; public IntPtr lpReserved; public IntPtr lpDesktop; public IntPtr lpTitle; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)] public byte[] _data1; public Int32 dwFlags; public Int16 wShowWindow; public Int16 cbReserved2; public IntPtr lpReserved2; public SafeFileHandle hStdInput; public SafeFileHandle hStdOutput; public SafeFileHandle hStdError; public STARTUPINFO() { cb = Marshal.SizeOf(this); } } [StructLayout(LayoutKind.Sequential)] public class STARTUPINFOEX { public STARTUPINFO startupInfo; public IntPtr lpAttributeList; public STARTUPINFOEX() { startupInfo = new STARTUPINFO(); startupInfo.cb = Marshal.SizeOf(this); } } [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; } } [StructLayout(LayoutKind.Sequential)] public struct LUID_AND_ATTRIBUTES { public LUID Luid; public UInt32 Attributes; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } [StructLayout(LayoutKind.Sequential)] public struct SID_AND_ATTRIBUTES { public IntPtr Sid; public int Attributes; } [StructLayout(LayoutKind.Sequential)] public struct TOKEN_PRIVILEGES { public UInt32 PrivilegeCount; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public LUID_AND_ATTRIBUTES[] Privileges; } public struct TOKEN_USER { public SID_AND_ATTRIBUTES User; } [Flags] public enum StartupInfoFlags : uint { USESTDHANDLES = 0x00000100 } [Flags] public enum CreationFlags : uint { CREATE_BREAKAWAY_FROM_JOB = 0x01000000, CREATE_DEFAULT_ERROR_MODE = 0x04000000, CREATE_NEW_CONSOLE = 0x00000010, CREATE_SUSPENDED = 0x00000004, CREATE_UNICODE_ENVIRONMENT = 0x00000400, EXTENDED_STARTUPINFO_PRESENT = 0x00080000 } public enum HandleFlags : uint { None = 0, INHERIT = 1 } [Flags] public enum LogonFlags { LOGON_WITH_PROFILE = 0x00000001, LOGON_NETCREDENTIALS_ONLY = 0x00000002 } public enum LogonType { LOGON32_LOGON_INTERACTIVE = 2, LOGON32_LOGON_NETWORK = 3, LOGON32_LOGON_BATCH = 4, LOGON32_LOGON_SERVICE = 5, LOGON32_LOGON_UNLOCK = 7, LOGON32_LOGON_NETWORK_CLEARTEXT = 8, LOGON32_LOGON_NEW_CREDENTIALS = 9 } public enum LogonProvider { LOGON32_PROVIDER_DEFAULT = 0, } public enum TokenInformationClass { TokenUser = 1, TokenPrivileges = 3, TokenType = 8, TokenImpersonationLevel = 9, TokenElevationType = 18, TokenLinkedToken = 19, } public enum TokenElevationType { TokenElevationTypeDefault = 1, TokenElevationTypeFull, TokenElevationTypeLimited } [Flags] public enum ProcessAccessFlags : uint { PROCESS_QUERY_INFORMATION = 0x00000400, } public enum SECURITY_IMPERSONATION_LEVEL { SecurityImpersonation, } public enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation } class NativeWaitHandle : WaitHandle { public NativeWaitHandle(IntPtr handle) { this.SafeWaitHandle = new SafeWaitHandle(handle, false); } } 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() { Marshal.FreeHGlobal(handle); return true; } } 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); } } public class CommandResult { public string StandardOut { get; internal set; } public string StandardError { get; internal set; } public uint ExitCode { get; internal set; } } public class BecomeUtil { [DllImport("advapi32.dll", SetLastError = true)] private static extern bool LogonUser( string lpszUsername, string lpszDomain, string lpszPassword, LogonType dwLogonType, LogonProvider dwLogonProvider, out IntPtr phToken); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool CreateProcessWithTokenW( IntPtr hToken, LogonFlags dwLogonFlags, [MarshalAs(UnmanagedType.LPTStr)] string lpApplicationName, StringBuilder lpCommandLine, CreationFlags dwCreationFlags, IntPtr lpEnvironment, [MarshalAs(UnmanagedType.LPTStr)] string lpCurrentDirectory, STARTUPINFOEX lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll")] private static extern bool CreatePipe( out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, SECURITY_ATTRIBUTES lpPipeAttributes, uint nSize); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetHandleInformation( SafeFileHandle hObject, HandleFlags dwMask, int dwFlags); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GetExitCodeProcess( IntPtr hProcess, out uint lpExitCode); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle( IntPtr hObject); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr GetProcessWindowStation(); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr GetThreadDesktop( int dwThreadId); [DllImport("kernel32.dll", SetLastError = true)] private static extern int GetCurrentThreadId(); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool GetTokenInformation( IntPtr TokenHandle, TokenInformationClass TokenInformationClass, SafeMemoryBuffer TokenInformation, uint TokenInformationLength, out uint ReturnLength); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool LookupPrivilegeNameW( string lpSystemName, ref LUID lpLuid, StringBuilder lpName, ref UInt32 cchName); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr OpenProcess( ProcessAccessFlags processAccess, bool bInheritHandle, UInt32 processId); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool OpenProcessToken( IntPtr ProcessHandle, TokenAccessLevels DesiredAccess, out IntPtr TokenHandle); [DllImport("advapi32", SetLastError = true)] private static extern bool DuplicateTokenEx( IntPtr hExistingToken, TokenAccessLevels dwDesiredAccess, IntPtr lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, TOKEN_TYPE TokenType, out IntPtr phNewToken); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool ImpersonateLoggedOnUser( IntPtr hToken); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool RevertToSelf(); public static CommandResult RunAsUser(string username, string password, string lpCommandLine, string lpCurrentDirectory, string stdinInput, LogonFlags logonFlags, LogonType logonType) { SecurityIdentifier account = null; if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS) { account = GetBecomeSid(username); } STARTUPINFOEX si = new STARTUPINFOEX(); si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES; SECURITY_ATTRIBUTES pipesec = new SECURITY_ATTRIBUTES(); pipesec.bInheritHandle = true; // Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo SafeFileHandle stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write; if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0)) throw new Win32Exception("STDOUT pipe setup failed"); if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0)) throw new Win32Exception("STDOUT pipe handle setup failed"); if (!CreatePipe(out stderr_read, out stderr_write, pipesec, 0)) throw new Win32Exception("STDERR pipe setup failed"); if (!SetHandleInformation(stderr_read, HandleFlags.INHERIT, 0)) throw new Win32Exception("STDERR pipe handle setup failed"); if (!CreatePipe(out stdin_read, out stdin_write, pipesec, 0)) throw new Win32Exception("STDIN pipe setup failed"); if (!SetHandleInformation(stdin_write, HandleFlags.INHERIT, 0)) throw new Win32Exception("STDIN pipe handle setup failed"); si.startupInfo.hStdOutput = stdout_write; si.startupInfo.hStdError = stderr_write; si.startupInfo.hStdInput = stdin_read; // Setup the stdin buffer UTF8Encoding utf8_encoding = new UTF8Encoding(false); FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, 32768); StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768); // Create the environment block if set IntPtr lpEnvironment = IntPtr.Zero; CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT; PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); // Get the user tokens to try running processes with List tokens = GetUserTokens(account, username, password, logonType); bool launch_success = false; foreach (IntPtr token in tokens) { if (CreateProcessWithTokenW( token, logonFlags, null, new StringBuilder(lpCommandLine), startup_flags, lpEnvironment, lpCurrentDirectory, si, out pi)) { launch_success = true; break; } } if (!launch_success) throw new Win32Exception("Failed to start become process"); CommandResult result = new CommandResult(); // Setup the output buffers and get stdout/stderr FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096); StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096); stdout_write.Close(); FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096); StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096); stderr_write.Close(); stdin.WriteLine(stdinInput); stdin.Close(); string stdout_str, stderr_str = null; GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str); UInt32 rc = GetProcessExitCode(pi.hProcess); result.StandardOut = stdout_str; result.StandardError = stderr_str; result.ExitCode = rc; return result; } private static SecurityIdentifier GetBecomeSid(string username) { NTAccount account = new NTAccount(username); try { SecurityIdentifier security_identifier = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier)); return security_identifier; } catch (IdentityNotMappedException ex) { throw new Exception(String.Format("Unable to find become user {0}: {1}", username, ex.Message)); } } private static List GetUserTokens(SecurityIdentifier account, string username, string password, LogonType logonType) { List tokens = new List(); List service_sids = new List() { "S-1-5-18", // NT AUTHORITY\SYSTEM "S-1-5-19", // NT AUTHORITY\LocalService "S-1-5-20" // NT AUTHORITY\NetworkService }; IntPtr hSystemToken = IntPtr.Zero; string account_sid = ""; if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS) { GrantAccessToWindowStationAndDesktop(account); // Try to get SYSTEM token handle so we can impersonate to get full admin token hSystemToken = GetSystemUserHandle(); account_sid = account.ToString(); } bool impersonated = false; try { if (hSystemToken == IntPtr.Zero && service_sids.Contains(account_sid)) { // We need the SYSTEM token if we want to become one of those accounts, fail here throw new Win32Exception("Failed to get token for NT AUTHORITY\\SYSTEM"); } else if (hSystemToken != IntPtr.Zero) { // If SYSTEM impersonation failed but we're trying to become a regular user, just proceed; // might get a limited token in UAC-enabled cases, but better than nothing... if (ImpersonateLoggedOnUser(hSystemToken)) impersonated = true; else if (service_sids.Contains(account_sid)) throw new Win32Exception("Failed to impersonate as SYSTEM account"); } string domain = null; if (service_sids.Contains(account_sid)) { // We're using a well-known service account, do a service logon instead of the actual flag set logonType = LogonType.LOGON32_LOGON_SERVICE; domain = "NT AUTHORITY"; password = null; switch (account_sid) { case "S-1-5-18": tokens.Add(hSystemToken); return tokens; case "S-1-5-19": username = "LocalService"; break; case "S-1-5-20": username = "NetworkService"; break; } } else { // We are trying to become a local or domain account if (username.Contains(@"\")) { var user_split = username.Split(Convert.ToChar(@"\")); domain = user_split[0]; username = user_split[1]; } else if (username.Contains("@")) domain = null; else domain = "."; } IntPtr hToken = IntPtr.Zero; if (!LogonUser( username, domain, password, logonType, LogonProvider.LOGON32_PROVIDER_DEFAULT, out hToken)) { throw new Win32Exception("LogonUser failed"); } if (!service_sids.Contains(account_sid)) { // Try and get the elevated token for local/domain account IntPtr hTokenElevated = GetElevatedToken(hToken); tokens.Add(hTokenElevated); } // add the original token as a fallback tokens.Add(hToken); } finally { if (impersonated) RevertToSelf(); } return tokens; } private static IntPtr GetSystemUserHandle() { // According to CreateProcessWithTokenW we require a token with // TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY // Also add in TOKEN_IMPERSONATE so we can get an impersontated token TokenAccessLevels desired_access = TokenAccessLevels.Query | TokenAccessLevels.Duplicate | TokenAccessLevels.AssignPrimary | TokenAccessLevels.Impersonate; foreach (System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses()) { using (process) { IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_INFORMATION, false, (UInt32)process.Id); if (hProcess == IntPtr.Zero) continue; try { IntPtr hToken = IntPtr.Zero; if (!OpenProcessToken(hProcess, desired_access, out hToken)) continue; try { string sid = GetTokenUserSID(hToken); if (sid != "S-1-5-18") continue; // Make sure the SYSTEM token we are checking contains the SeTcbPrivilege required for // escalation. Some SYSTEM tokens have this privilege stripped out. List actualPrivileges = GetTokenPrivileges(hToken); if (!actualPrivileges.Contains("SeTcbPrivilege")) continue; IntPtr dupToken = IntPtr.Zero; if (!DuplicateTokenEx(hToken, TokenAccessLevels.MaximumAllowed, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out dupToken)) { continue; } return dupToken; } finally { CloseHandle(hToken); } } finally { CloseHandle(hProcess); } } } return IntPtr.Zero; } private static string GetTokenUserSID(IntPtr hToken) { using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, TokenInformationClass.TokenUser)) { TOKEN_USER tokenUser = (TOKEN_USER)Marshal.PtrToStructure(tokenInfo.DangerousGetHandle(), typeof(TOKEN_USER)); return new SecurityIdentifier(tokenUser.User.Sid).Value; } } private static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr) { var sowait = new EventWaitHandle(false, EventResetMode.ManualReset); var sewait = new EventWaitHandle(false, EventResetMode.ManualReset); string so = null, se = null; ThreadPool.QueueUserWorkItem((s) => { so = stdoutStream.ReadToEnd(); sowait.Set(); }); ThreadPool.QueueUserWorkItem((s) => { se = stderrStream.ReadToEnd(); sewait.Set(); }); foreach (var wh in new WaitHandle[] { sowait, sewait }) wh.WaitOne(); stdout = so; stderr = se; } private static uint GetProcessExitCode(IntPtr processHandle) { new NativeWaitHandle(processHandle).WaitOne(); uint exitCode; if (!GetExitCodeProcess(processHandle, out exitCode)) throw new Win32Exception("Error getting process exit code"); return exitCode; } private static IntPtr GetElevatedToken(IntPtr hToken) { // First determine if the current token is a limited token using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, TokenInformationClass.TokenElevationType)) { TokenElevationType tet = (TokenElevationType)Marshal.ReadInt32(tokenInfo.DangerousGetHandle()); // We already have the best token we can get, just use it if (tet != TokenElevationType.TokenElevationTypeLimited) return hToken; } // We have a limited token, get the linked elevated token using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken)) return Marshal.ReadIntPtr(tokenInfo.DangerousGetHandle()); } private static List GetTokenPrivileges(IntPtr hToken) { using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, TokenInformationClass.TokenPrivileges)) { TOKEN_PRIVILEGES tokenPrivileges = (TOKEN_PRIVILEGES)Marshal.PtrToStructure( tokenInfo.DangerousGetHandle(), typeof(TOKEN_PRIVILEGES)); LUID_AND_ATTRIBUTES[] luidAndAttributes = new LUID_AND_ATTRIBUTES[tokenPrivileges.PrivilegeCount]; PtrToStructureArray(luidAndAttributes, IntPtr.Add(tokenInfo.DangerousGetHandle(), Marshal.SizeOf(tokenPrivileges.PrivilegeCount))); return luidAndAttributes.Select(x => GetPrivilegeName(x.Luid)).ToList(); } } private static SafeMemoryBuffer GetTokenInformation(IntPtr hToken, TokenInformationClass tokenClass) { UInt32 tokenLength; bool res = GetTokenInformation(hToken, tokenClass, new SafeMemoryBuffer(IntPtr.Zero), 0, out tokenLength); if (!res && tokenLength == 0) // res will be false due to insufficient buffer size, we ignore if we got the buffer length throw new Win32Exception(String.Format("GetTokenInformation({0}) failed to get buffer length", tokenClass.ToString())); SafeMemoryBuffer tokenInfo = new SafeMemoryBuffer((int)tokenLength); if (!GetTokenInformation(hToken, tokenClass, tokenInfo, tokenLength, out tokenLength)) throw new Win32Exception(String.Format("GetTokenInformation({0}) failed", tokenClass.ToString())); return tokenInfo; } private static string GetPrivilegeName(LUID luid) { UInt32 nameLen = 0; LookupPrivilegeNameW(null, ref luid, null, ref nameLen); StringBuilder name = new StringBuilder((int)(nameLen + 1)); if (!LookupPrivilegeNameW(null, ref luid, name, ref nameLen)) throw new Win32Exception("LookupPrivilegeNameW() failed"); return name.ToString(); } private static void PtrToStructureArray(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)); } private static void GrantAccessToWindowStationAndDesktop(SecurityIdentifier account) { const int WindowStationAllAccess = 0x000f037f; GrantAccess(account, GetProcessWindowStation(), WindowStationAllAccess); const int DesktopRightsAllAccess = 0x000f01ff; GrantAccess(account, GetThreadDesktop(GetCurrentThreadId()), DesktopRightsAllAccess); } private static void GrantAccess(SecurityIdentifier account, IntPtr handle, int accessMask) { SafeHandle safeHandle = new NoopSafeHandle(handle); GenericSecurity security = new GenericSecurity(false, ResourceType.WindowObject, safeHandle, AccessControlSections.Access); security.AddAccessRule( new GenericAccessRule(account, accessMask, AccessControlType.Allow)); security.Persist(safeHandle, AccessControlSections.Access); } private class GenericSecurity : NativeObjectSecurity { public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested) : base(isContainer, resType, objectHandle, sectionsRequested) { } public new void Persist(SafeHandle handle, AccessControlSections includeSections) { base.Persist(handle, includeSections); } public new void AddAccessRule(AccessRule rule) { base.AddAccessRule(rule); } 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(); } 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(); } public override Type AuditRuleType { get { return typeof(AuditRule); } } } private class NoopSafeHandle : SafeHandle { public NoopSafeHandle(IntPtr handle) : base(handle, false) { } public override bool IsInvalid { get { return false; } } protected override bool ReleaseHandle() { return true; } } private class GenericAccessRule : AccessRule { public GenericAccessRule(IdentityReference identity, int accessMask, AccessControlType type) : base(identity, accessMask, false, InheritanceFlags.None, PropagationFlags.None, type) { } } } } "@ # due to the command line size limitations of CreateProcessWithTokenW, we # execute a simple PS script that executes our full exec_wrapper so no files # touch the disk $become_exec_wrapper = { chcp.com 65001 > $null $ProgressPreference = "SilentlyContinue" $raw = [System.Console]::In.ReadToEnd() $split_parts = $raw.Split(@("`0`0`0`0"), 0) If (-not $split_parts.Length -eq 2) { throw "invalid payload" } $json_raw = $split_parts[1] &([ScriptBlock]::Create($split_parts[0])) } $exec_wrapper = { &chcp.com 65001 > $null Set-StrictMode -Version 2 $DebugPreference = "Continue" $ErrorActionPreference = "Stop" Function ConvertTo-HashtableFromPsCustomObject($myPsObject) { $output = @{} $myPsObject | Get-Member -MemberType *Property | % { $val = $myPsObject.($_.name) if ($val -is [psobject]) { $val = ConvertTo-HashtableFromPsCustomObject -myPsObject $val } $output.($_.name) = $val } return $output } # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives # exec runspace, capture output, cleanup, return module output. Do not change this as it is set become before being passed to the # become process. if (-not $(Get-Variable "json_raw" -ErrorAction SilentlyContinue)) { Write-Error "no payload supplied" -Category InvalidArgument } $payload = ConvertTo-HashtableFromPsCustomObject -myPsObject (ConvertFrom-Json $json_raw) # TODO: handle binary modules # TODO: handle persistence $actions = $payload.actions # pop 0th action as entrypoint $entrypoint = $payload.($actions[0]) $payload.actions = $payload.actions[1..99] $entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint)) # load the current action entrypoint as a module custom object with a Run method $entrypoint = New-Module -ScriptBlock ([scriptblock]::Create($entrypoint)) -AsCustomObject Set-Variable -Scope global -Name complex_args -Value $payload["module_args"] | Out-Null # dynamically create/load modules ForEach ($mod in $payload.powershell_modules.GetEnumerator()) { $decoded_module = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($mod.Value)) New-Module -ScriptBlock ([scriptblock]::Create($decoded_module)) -Name $mod.Key | Import-Module -WarningAction SilentlyContinue | Out-Null } $output = $entrypoint.Run($payload) # base64 encode the output so the non-ascii characters are preserved Write-Output ([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((Write-Output $output)))) } # end exec_wrapper Function Dump-Error ($excep, $msg=$null) { $eo = @{failed=$true} $exception_message = $excep.Exception.Message if ($null -ne $msg) { $exception_message = "$($msg): $exception_message" } $eo.msg = $exception_message $eo.exception = $excep | Out-String $host.SetShouldExit(1) $eo | ConvertTo-Json -Depth 10 -Compress } Function Parse-EnumValue($enum, $flag_type, $value, $prefix) { $raw_enum_value = "$prefix$($value.ToUpper())" try { $enum_value = [Enum]::Parse($enum, $raw_enum_value) } catch [System.ArgumentException] { $valid_options = [Enum]::GetNames($enum) | ForEach-Object { $_.Substring($prefix.Length).ToLower() } throw "become_flags $flag_type value '$value' is not valid, valid values are: $($valid_options -join ", ")" } return $enum_value } Function Parse-BecomeFlags($flags) { $logon_type = [AnsibleBecome.LogonType]::LOGON32_LOGON_INTERACTIVE $logon_flags = [AnsibleBecome.LogonFlags]::LOGON_WITH_PROFILE if ($flags -eq $null -or $flags -eq "") { $flag_split = @() } elseif ($flags -is [string]) { $flag_split = $flags.Split(" ") } else { throw "become_flags must be a string, was $($flags.GetType())" } foreach ($flag in $flag_split) { $split = $flag.Split("=") if ($split.Count -ne 2) { throw "become_flags entry '$flag' is in an invalid format, must be a key=value pair" } $flag_key = $split[0] $flag_value = $split[1] if ($flag_key -eq "logon_type") { $enum_details = @{ enum = [AnsibleBecome.LogonType] flag_type = $flag_key value = $flag_value prefix = "LOGON32_LOGON_" } $logon_type = Parse-EnumValue @enum_details } elseif ($flag_key -eq "logon_flags") { $logon_flag_values = $flag_value.Split(",") $logon_flags = 0 -as [AnsibleBecome.LogonFlags] foreach ($logon_flag_value in $logon_flag_values) { if ($logon_flag_value -eq "") { continue } $enum_details = @{ enum = [AnsibleBecome.LogonFlags] flag_type = $flag_key value = $logon_flag_value prefix = "LOGON_" } $logon_flag = Parse-EnumValue @enum_details $logon_flags = $logon_flags -bor $logon_flag } } else { throw "become_flags key '$flag_key' is not a valid runas flag, must be 'logon_type' or 'logon_flags'" } } return $logon_type, [AnsibleBecome.LogonFlags]$logon_flags } Function Run($payload) { # NB: action popping handled inside subprocess wrapper $original_tmp = $env:TMP $remote_tmp = $payload["module_args"]["_ansible_remote_tmp"] $remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp) if ($null -eq $remote_tmp) { $remote_tmp = $original_tmp } # become process is run under a different console to the WinRM one so we # need to set the UTF-8 codepage again $env:TMP = $remote_tmp Add-Type -TypeDefinition $helper_def -Debug:$false $env:TMP = $original_tmp $username = $payload.become_user $password = $payload.become_password try { $logon_type, $logon_flags = Parse-BecomeFlags -flags $payload.become_flags } catch { Dump-Error -excep $_ -msg "Failed to parse become_flags '$($payload.become_flags)'" return $null } # NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must bootstrap via small # wrapper which calls our read wrapper passed through stdin. Cannot use 'powershell -' as # the $ErrorActionPreference is always set to Stop and cannot be changed $payload_string = $payload | ConvertTo-Json -Depth 99 -Compress $exec_wrapper = $exec_wrapper.ToString() + "`0`0`0`0" + $payload_string $rc = 0 $exec_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($become_exec_wrapper.ToString())) $lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $exec_command") $lp_current_directory = "$env:SystemRoot" Try { $result = [AnsibleBecome.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $exec_wrapper, $logon_flags, $logon_type) $stdout = $result.StandardOut $stdout = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($stdout.Trim())) $stderr = $result.StandardError $rc = $result.ExitCode $host.UI.WriteLine($stdout) $host.UI.WriteErrorLine($stderr.Trim()) } Catch { $excep = $_ Dump-Error -excep $excep -msg "Failed to become user $username" } $host.SetShouldExit($rc) } ''' async_wrapper = br''' Set-StrictMode -Version 2 $ErrorActionPreference = "Stop" # build exec_wrapper encoded command # start powershell with breakaway running exec_wrapper encodedcommand # stream payload to powershell with normal exec, but normal exec writes results to resultfile instead of stdout/stderr # return asyncresult to controller $exec_wrapper = { # help to debug any errors in the exec_wrapper or async_watchdog by generating # an error log in case of a terminating error trap { $log_path = "$($env:TEMP)\async-exec-wrapper-$(Get-Date -Format "yyyy-MM-ddTHH-mm-ss.ffffZ")-error.txt" $error_msg = "Error while running the async exec wrapper`r`n$_`r`n$($_.ScriptStackTrace)" Set-Content -Path $log_path -Value $error_msg throw $_ } &chcp.com 65001 > $null $DebugPreference = "Continue" $ErrorActionPreference = "Stop" Set-StrictMode -Version 2 function ConvertTo-HashtableFromPsCustomObject ($myPsObject){ $output = @{}; $myPsObject | Get-Member -MemberType *Property | % { $val = $myPsObject.($_.name); If ($val -is [psobject]) { $val = ConvertTo-HashtableFromPsCustomObject $val } $output.($_.name) = $val } return $output; } # store the pipe name and no. of bytes to read, these are populated by the # Run function before being run - do not remove or change $pipe_name = "" $bytes_length = 0 # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives # exec runspace, capture output, cleanup, return module output $input_bytes = New-Object -TypeName byte[] -ArgumentList $bytes_length $pipe = New-Object -TypeName System.IO.Pipes.NamedPipeClientStream -ArgumentList @( ".", # localhost $pipe_name, [System.IO.Pipes.PipeDirection]::In, [System.IO.Pipes.PipeOptions]::None, [System.Security.Principal.TokenImpersonationLevel]::Anonymous ) try { $pipe.Connect() $pipe.Read($input_bytes, 0, $bytes_length) > $null } finally { $pipe.Close() } $json_raw = [System.Text.Encoding]::UTF8.GetString($input_bytes) If (-not $json_raw) { Write-Error "no input given" -Category InvalidArgument } $payload = ConvertTo-HashtableFromPsCustomObject (ConvertFrom-Json $json_raw) # TODO: handle binary modules # TODO: handle persistence $actions = $payload.actions # pop 0th action as entrypoint $entrypoint = $payload.($actions[0]) $payload.actions = $payload.actions[1..99] $entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint)) # load the current action entrypoint as a module custom object with a Run method $entrypoint = New-Module -ScriptBlock ([scriptblock]::Create($entrypoint)) -AsCustomObject Set-Variable -Scope global -Name complex_args -Value $payload["module_args"] | Out-Null # dynamically create/load modules ForEach ($mod in $payload.powershell_modules.GetEnumerator()) { $decoded_module = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($mod.Value)) New-Module -ScriptBlock ([scriptblock]::Create($decoded_module)) -Name $mod.Key | Import-Module -WarningAction SilentlyContinue | Out-Null } $output = $entrypoint.Run($payload) Write-Output $output } # end exec_wrapper Function Run($payload) { $remote_tmp = $payload["module_args"]["_ansible_remote_tmp"] $remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp) # calculate the result path so we can include it in the worker payload $jid = $payload.async_jid $local_jid = $jid + "." + $pid $results_path = [System.IO.Path]::Combine($remote_tmp, ".ansible_async", $local_jid) $payload.async_results_path = $results_path [System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($results_path)) | Out-Null # can't use anonymous pipes as the spawned process will not be a child due to # the way WMI works, use a named pipe with a random name instead and set to # only allow current user to read from the pipe $pipe_name = "ansible-async-$jid-$([guid]::NewGuid())" $current_user = ([Security.Principal.WindowsIdentity]::GetCurrent()).User $payload_string = $payload | ConvertTo-Json -Depth 99 -Compress $payload_bytes = [System.Text.Encoding]::UTF8.GetBytes($payload_string) $pipe_sec = New-Object -TypeName System.IO.Pipes.PipeSecurity $pipe_ar = New-Object -TypeName System.IO.Pipes.PipeAccessRule -ArgumentList @( $current_user, [System.IO.Pipes.PipeAccessRights]::Read, [System.Security.AccessControl.AccessControlType]::Allow ) $pipe_sec.AddAccessRule($pipe_ar) $pipe = New-Object -TypeName System.IO.Pipes.NamedPipeServerStream -ArgumentList @( $pipe_name, [System.IO.Pipes.PipeDirection]::Out, 1, [System.IO.Pipes.PipeTransmissionMode]::Byte, [System.IO.Pipes.PipeOptions]::Asynchronous, 0, 0, $pipe_sec ) try { $exec_wrapper_str = $exec_wrapper.ToString() $exec_wrapper_str = $exec_wrapper_str.Replace('$pipe_name = ""', "`$pipe_name = `"$pipe_name`"") $exec_wrapper_str = $exec_wrapper_str.Replace('$bytes_length = 0', "`$bytes_length = $($payload_bytes.Count)") $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($exec_wrapper_str)) $exec_args = "powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command" # not all connection plugins support breakaway from job that is required # for async, Win32_Process.Create() is still able to escape so we use # that here $process = Invoke-CimMethod -ClassName Win32_Process -Name Create -Arguments @{CommandLine=$exec_args} $rc = $process.ReturnValue if ($rc -ne 0) { $error_msg = switch($rc) { 2 { "Access denied" } 3 { "Insufficient privilege" } 8 { "Unknown failure" } 9 { "Path not found" } 21 { "Invalid parameter" } default { "Other" } } throw "Failed to start async process: $rc ($error_msg)" } $watchdog_pid = $process.ProcessId # populate initial results before we send the async data to avoid result race $result = @{ started = 1; finished = 0; results_file = $results_path; ansible_job_id = $local_jid; _ansible_suppress_tmpdir_delete = $true; ansible_async_watchdog_pid = $watchdog_pid } $result_json = ConvertTo-Json $result Set-Content $results_path -Value $result_json # wait until the client connects, throw an error if the timeout is reached $wait_async = $pipe.BeginWaitForConnection($null, $null) $wait_async.AsyncWaitHandle.WaitOne(5000) > $null if (-not $wait_async.IsCompleted) { throw "timeout while waiting for child process to connect to named pipe" } $pipe.EndWaitForConnection($wait_async) # write the exec manifest to the child process $pipe.Write($payload_bytes, 0, $payload_bytes.Count) $pipe.Flush() $pipe.WaitForPipeDrain() } finally { $pipe.Close() } return $result_json } ''' # end async_wrapper async_watchdog = br''' Set-StrictMode -Version 2 $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.Web.Extensions Function Log { Param( [string]$msg ) If(Get-Variable -Name log_path -ErrorAction SilentlyContinue) { Add-Content $log_path $msg } } Function Deserialize-Json { Param( [Parameter(ValueFromPipeline=$true)] [string]$json ) # FUTURE: move this into module_utils/powershell.ps1 and use for everything (sidestep PSCustomObject issues) # FUTURE: won't work w/ Nano Server/.NET Core- fallback to DataContractJsonSerializer (which can't handle dicts on .NET 4.0) Log "Deserializing:`n$json" $jss = New-Object System.Web.Script.Serialization.JavaScriptSerializer return $jss.DeserializeObject($json) } Function Write-Result { Param( [hashtable]$result, [string]$resultfile_path ) $result | ConvertTo-Json | Set-Content -Path $resultfile_path } Function Run($payload) { $actions = $payload.actions # pop 0th action as entrypoint $entrypoint = $payload.($actions[0]) $entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint)) $payload.actions = $payload.actions[1..99] $resultfile_path = $payload.async_results_path $max_exec_time_sec = $payload.async_timeout_sec Log "deserializing existing resultfile args" # read in existing resultsfile to merge w/ module output (it should be written by the time we're unsuspended and running) $result = Get-Content $resultfile_path -Raw | Deserialize-Json Log "deserialized result is $($result | Out-String)" Log "creating runspace" $rs = [runspacefactory]::CreateRunspace() $rs.Open() Log "creating Powershell object" $job = [powershell]::Create() $job.Runspace = $rs $job.AddScript($entrypoint) | Out-Null $job.AddStatement().AddCommand("Run").AddArgument($payload) | Out-Null Log "job BeginInvoke()" $job_asyncresult = $job.BeginInvoke() Log "waiting $max_exec_time_sec seconds for job to complete" $signaled = $job_asyncresult.AsyncWaitHandle.WaitOne($max_exec_time_sec * 1000) $result["finished"] = 1 If($job_asyncresult.IsCompleted) { Log "job completed, calling EndInvoke()" $job_output = $job.EndInvoke($job_asyncresult) $job_error = $job.Streams.Error Log "raw module stdout: \r\n$job_output" If($job_error) { Log "raw module stderr: \r\n$job_error" } # write success/output/error to result object # TODO: cleanse leading/trailing junk Try { $module_result = Deserialize-Json $job_output # TODO: check for conflicting keys $result = $result + $module_result } Catch { $excep = $_ $result.failed = $true $result.msg = "failed to parse module output: $excep" # return the output back to Ansible to help with debugging errors $result.stdout = $job_output | Out-String $result.stderr = $job_error | Out-String } # TODO: determine success/fail, or always include stderr if nonempty? Write-Result $result $resultfile_path Log "wrote output to $resultfile_path" } Else { $job.BeginStop($null, $null) | Out-Null # best effort stop # write timeout to result object $result.failed = $true $result.msg = "timed out waiting for module completion" Write-Result $result $resultfile_path Log "wrote timeout to $resultfile_path" } # in the case of a hung pipeline, this will cause the process to stay alive until it's un-hung... #$rs.Close() | Out-Null } ''' # end async_watchdog from ansible.plugins import AnsiblePlugin class ShellModule(ShellBase): # Common shell filenames that this plugin handles # Powershell is handled differently. It's selected when winrm is the # connection COMPATIBLE_SHELLS = frozenset() # Family of shells this has. Must match the filename without extension SHELL_FAMILY = 'powershell' env = dict() # We're being overly cautious about which keys to accept (more so than # the Windows environment is capable of doing), since the powershell # env provider's limitations don't appear to be documented. safe_envkey = re.compile(r'^[\d\w_]{1,255}$') # TODO: add binary module support def assert_safe_env_key(self, key): if not self.safe_envkey.match(key): raise AnsibleError("Invalid PowerShell environment key: %s" % key) return key def safe_env_value(self, key, value): if len(value) > 32767: raise AnsibleError("PowerShell environment value for key '%s' exceeds 32767 characters in length" % key) # powershell single quoted literals need single-quote doubling as their only escaping value = value.replace("'", "''") return to_text(value, errors='surrogate_or_strict') def env_prefix(self, **kwargs): # powershell/winrm env handling is handled in the exec wrapper return "" def join_path(self, *args): parts = [] for arg in args: arg = self._unquote(arg).replace('/', '\\') parts.extend([a for a in arg.split('\\') if a]) path = '\\'.join(parts) if path.startswith('~'): return path return path def get_remote_filename(self, pathname): # powershell requires that script files end with .ps1 base_name = os.path.basename(pathname.strip()) name, ext = os.path.splitext(base_name.strip()) if ext.lower() not in ['.ps1', '.exe']: return name + '.ps1' return base_name.strip() def path_has_trailing_slash(self, path): # Allow Windows paths to be specified using either slash. path = self._unquote(path) return path.endswith('/') or path.endswith('\\') def chmod(self, paths, mode): raise NotImplementedError('chmod is not implemented for Powershell') def chown(self, paths, user): raise NotImplementedError('chown is not implemented for Powershell') def set_user_facl(self, paths, user, mode): raise NotImplementedError('set_user_facl is not implemented for Powershell') def remove(self, path, recurse=False): path = self._escape(self._unquote(path)) if recurse: return self._encode_script('''Remove-Item "%s" -Force -Recurse;''' % path) else: return self._encode_script('''Remove-Item "%s" -Force;''' % path) def mkdtemp(self, basefile=None, system=False, mode=None, tmpdir=None): # Windows does not have an equivalent for the system temp files, so # the param is ignored basefile = self._escape(self._unquote(basefile)) basetmpdir = tmpdir if tmpdir else self.get_option('remote_tmp') script = ''' $tmp_path = [System.Environment]::ExpandEnvironmentVariables('%s') $tmp = New-Item -Type Directory -Path $tmp_path -Name '%s' Write-Output -InputObject $tmp.FullName ''' % (basetmpdir, basefile) return self._encode_script(script.strip()) def expand_user(self, user_home_path, username=''): # PowerShell only supports "~" (not "~username"). Resolve-Path ~ does # not seem to work remotely, though by default we are always starting # in the user's home directory. user_home_path = self._unquote(user_home_path) if user_home_path == '~': script = 'Write-Output (Get-Location).Path' elif user_home_path.startswith('~\\'): script = 'Write-Output ((Get-Location).Path + "%s")' % self._escape(user_home_path[1:]) else: script = 'Write-Output "%s"' % self._escape(user_home_path) return self._encode_script(script) def exists(self, path): path = self._escape(self._unquote(path)) script = ''' If (Test-Path "%s") { $res = 0; } Else { $res = 1; } Write-Output "$res"; Exit $res; ''' % path return self._encode_script(script) def checksum(self, path, *args, **kwargs): path = self._escape(self._unquote(path)) script = ''' If (Test-Path -PathType Leaf "%(path)s") { $sp = new-object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider; $fp = [System.IO.File]::Open("%(path)s", [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read); [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower(); $fp.Dispose(); } ElseIf (Test-Path -PathType Container "%(path)s") { Write-Output "3"; } Else { Write-Output "1"; } ''' % dict(path=path) return self._encode_script(script) def build_module_command(self, env_string, shebang, cmd, arg_path=None): bootstrap_wrapper = pkgutil.get_data("ansible.executor.powershell", "bootstrap_wrapper.ps1") # pipelining bypass if cmd == '': return self._encode_script(script=bootstrap_wrapper, strict_mode=False, preserve_rc=False) # non-pipelining cmd_parts = shlex.split(cmd, posix=False) cmd_parts = list(map(to_text, cmd_parts)) if shebang and shebang.lower() == '#!powershell': if not self._unquote(cmd_parts[0]).lower().endswith('.ps1'): # we're running a module via the bootstrap wrapper cmd_parts[0] = '"%s.ps1"' % self._unquote(cmd_parts[0]) wrapper_cmd = "type " + cmd_parts[0] + " | " + self._encode_script(script=bootstrap_wrapper, strict_mode=False, preserve_rc=False) return wrapper_cmd elif shebang and shebang.startswith('#!'): cmd_parts.insert(0, shebang[2:]) elif not shebang: # The module is assumed to be a binary cmd_parts[0] = self._unquote(cmd_parts[0]) cmd_parts.append(arg_path) script = ''' Try { %s %s } Catch { $_obj = @{ failed = $true } If ($_.Exception.GetType) { $_obj.Add('msg', $_.Exception.Message) } Else { $_obj.Add('msg', $_.ToString()) } If ($_.InvocationInfo.PositionMessage) { $_obj.Add('exception', $_.InvocationInfo.PositionMessage) } ElseIf ($_.ScriptStackTrace) { $_obj.Add('exception', $_.ScriptStackTrace) } Try { $_obj.Add('error_record', ($_ | ConvertTo-Json | ConvertFrom-Json)) } Catch { } Echo $_obj | ConvertTo-Json -Compress -Depth 99 Exit 1 } ''' % (env_string, ' '.join(cmd_parts)) return self._encode_script(script, preserve_rc=False) def wrap_for_exec(self, cmd): return '& %s; exit $LASTEXITCODE' % cmd def _unquote(self, value): '''Remove any matching quotes that wrap the given value.''' value = to_text(value or '') m = re.match(r'^\s*?\'(.*?)\'\s*?$', value) if m: return m.group(1) m = re.match(r'^\s*?"(.*?)"\s*?$', value) if m: return m.group(1) return value def _escape(self, value, include_vars=False): '''Return value escaped for use in PowerShell command.''' # http://www.techotopia.com/index.php/Windows_PowerShell_1.0_String_Quoting_and_Escape_Sequences # http://stackoverflow.com/questions/764360/a-list-of-string-replacements-in-python subs = [('\n', '`n'), ('\r', '`r'), ('\t', '`t'), ('\a', '`a'), ('\b', '`b'), ('\f', '`f'), ('\v', '`v'), ('"', '`"'), ('\'', '`\''), ('`', '``'), ('\x00', '`0')] if include_vars: subs.append(('$', '`$')) pattern = '|'.join('(%s)' % re.escape(p) for p, s in subs) substs = [s for p, s in subs] def replace(m): return substs[m.lastindex - 1] return re.sub(pattern, replace, value) def _encode_script(self, script, as_list=False, strict_mode=True, preserve_rc=True): '''Convert a PowerShell script to a single base64-encoded command.''' script = to_text(script) if script == u'-': cmd_parts = _common_args + ['-'] else: if strict_mode: script = u'Set-StrictMode -Version Latest\r\n%s' % script # try to propagate exit code if present- won't work with begin/process/end-style scripts (ala put_file) # NB: the exit code returned may be incorrect in the case of a successful command followed by an invalid command if preserve_rc: script = u'%s\r\nIf (-not $?) { If (Get-Variable LASTEXITCODE -ErrorAction SilentlyContinue) { exit $LASTEXITCODE } Else { exit 1 } }\r\n'\ % script script = '\n'.join([x.strip() for x in script.splitlines() if x.strip()]) encoded_script = to_text(base64.b64encode(script.encode('utf-16-le')), 'utf-8') cmd_parts = _common_args + ['-EncodedCommand', encoded_script] if as_list: return cmd_parts return ' '.join(cmd_parts)