mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-11-23 21:30:40 +00:00
1752 lines
65 KiB
Python
1752 lines
65 KiB
Python
|
# Copyright (c) 2014, Chris Church <chris@ninemoreminutes.com>
|
||
|
# 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<IntPtr> 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<IntPtr> GetUserTokens(SecurityIdentifier account, string username, string password, LogonType logonType)
|
||
|
{
|
||
|
List<IntPtr> tokens = new List<IntPtr>();
|
||
|
List<String> service_sids = new List<String>()
|
||
|
{
|
||
|
"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<string> 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<string> 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>(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)
|