converted become runas to explicit CreateProcessWithLogonW (#26378)
* fixes become_method: runas for unprivileged users * sets permissions on tempdir appropriately * allows automatic system environment generation for new token (old Process.Start way prevents this) * add basic become runas tests
This commit is contained in:
parent
1b727c372e
commit
0ee46cb0df
3 changed files with 409 additions and 49 deletions
|
@ -156,6 +156,7 @@ $helper_def = @"
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using System.Security.AccessControl;
|
using System.Security.AccessControl;
|
||||||
|
@ -164,7 +165,7 @@ using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ansible.Shell
|
namespace Ansible.Shell
|
||||||
{
|
{
|
||||||
public class ProcessUtil
|
public class NativeProcessUtil
|
||||||
{
|
{
|
||||||
public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
|
public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
|
||||||
{
|
{
|
||||||
|
@ -201,6 +202,26 @@ namespace Ansible.Shell
|
||||||
GrantAccess(username, GetThreadDesktop(GetCurrentThreadId()), DesktopRightsAllAccess);
|
GrantAccess(username, GetThreadDesktop(GetCurrentThreadId()), DesktopRightsAllAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string SearchPath(string findThis)
|
||||||
|
{
|
||||||
|
StringBuilder sbOut = new StringBuilder(1024);
|
||||||
|
IntPtr filePartOut;
|
||||||
|
|
||||||
|
if(SearchPath(null, findThis, null, sbOut.Capacity, sbOut, out filePartOut) == 0)
|
||||||
|
throw new FileNotFoundException("Couldn't locate " + findThis + " on path");
|
||||||
|
|
||||||
|
return sbOut.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint GetProcessExitCode(IntPtr processHandle) {
|
||||||
|
new NativeWaitHandle(processHandle).WaitOne();
|
||||||
|
uint exitCode;
|
||||||
|
if(!GetExitCodeProcess(processHandle, out exitCode)) {
|
||||||
|
throw new Exception("Error getting process exit code: " + Marshal.GetLastWin32Error());
|
||||||
|
}
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
private static void GrantAccess(string username, IntPtr handle, int accessMask)
|
private static void GrantAccess(string username, IntPtr handle, int accessMask)
|
||||||
{
|
{
|
||||||
SafeHandle safeHandle = new NoopSafeHandle(handle);
|
SafeHandle safeHandle = new NoopSafeHandle(handle);
|
||||||
|
@ -212,6 +233,74 @@ namespace Ansible.Shell
|
||||||
security.Persist(safeHandle, AccessControlSections.Access);
|
security.Persist(safeHandle, AccessControlSections.Access);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
||||||
|
public static extern uint SearchPath (
|
||||||
|
string lpPath,
|
||||||
|
string lpFileName,
|
||||||
|
string lpExtension,
|
||||||
|
int nBufferLength,
|
||||||
|
[MarshalAs (UnmanagedType.LPTStr)]
|
||||||
|
StringBuilder lpBuffer,
|
||||||
|
out IntPtr lpFilePart);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError=true)]
|
||||||
|
private static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe, SECURITY_ATTRIBUTES lpPipeAttributes, uint nSize);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError=true)]
|
||||||
|
public static extern IntPtr GetStdHandle(StandardHandleValues nStdHandle);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError=true)]
|
||||||
|
public static extern bool SetHandleInformation(IntPtr hObject, HandleFlags dwMask, int dwFlags);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError=true)]
|
||||||
|
public static extern bool CloseHandle(IntPtr hObject);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError=true)]
|
||||||
|
public static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref int lpSize);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError=true)]
|
||||||
|
public static extern bool UpdateProcThreadAttribute(
|
||||||
|
IntPtr lpAttributeList,
|
||||||
|
uint dwFlags,
|
||||||
|
IntPtr Attribute,
|
||||||
|
IntPtr lpValue,
|
||||||
|
IntPtr cbSize,
|
||||||
|
IntPtr lpPreviousValue,
|
||||||
|
IntPtr lpReturnSize);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode, BestFitMapping=false)]
|
||||||
|
public static extern bool CreateProcess(
|
||||||
|
[MarshalAs(UnmanagedType.LPTStr)]
|
||||||
|
string lpApplicationName,
|
||||||
|
StringBuilder lpCommandLine,
|
||||||
|
IntPtr lpProcessAttributes,
|
||||||
|
IntPtr lpThreadAttributes,
|
||||||
|
bool bInheritHandles,
|
||||||
|
uint dwCreationFlags,
|
||||||
|
IntPtr lpEnvironment,
|
||||||
|
[MarshalAs(UnmanagedType.LPTStr)]
|
||||||
|
string lpCurrentDirectory,
|
||||||
|
STARTUPINFO lpStartupInfo,
|
||||||
|
out PROCESS_INFORMATION lpProcessInformation);
|
||||||
|
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
||||||
|
public static extern bool CreateProcessWithLogonW(
|
||||||
|
string userName,
|
||||||
|
string domain,
|
||||||
|
string password,
|
||||||
|
LOGON_FLAGS logonFlags,
|
||||||
|
string applicationName,
|
||||||
|
string commandLine,
|
||||||
|
uint creationFlags,
|
||||||
|
IntPtr environment,
|
||||||
|
string currentDirectory,
|
||||||
|
STARTUPINFOEX startupInfo,
|
||||||
|
out PROCESS_INFORMATION processInformation);
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true)]
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
private static extern IntPtr GetProcessWindowStation();
|
private static extern IntPtr GetProcessWindowStation();
|
||||||
|
|
||||||
|
@ -256,7 +345,117 @@ namespace Ansible.Shell
|
||||||
protected override bool ReleaseHandle() { return true; }
|
protected override bool ReleaseHandle() { return true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NativeWaitHandle : WaitHandle {
|
||||||
|
public NativeWaitHandle(IntPtr handle) {
|
||||||
|
this.Handle = handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum LOGON_FLAGS
|
||||||
|
{
|
||||||
|
LOGON_WITH_PROFILE = 0x00000001,
|
||||||
|
LOGON_NETCREDENTIALS_ONLY = 0x00000002
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum CREATION_FLAGS
|
||||||
|
{
|
||||||
|
CREATE_SUSPENDED = 0x00000004,
|
||||||
|
CREATE_NEW_CONSOLE = 0x00000010,
|
||||||
|
CREATE_UNICODE_ENVIRONMENT = 0x000000400,
|
||||||
|
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
|
||||||
|
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum StartupInfoFlags : uint
|
||||||
|
{
|
||||||
|
USESTDHANDLES = 0x00000100
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StandardHandleValues : int
|
||||||
|
{
|
||||||
|
STD_INPUT_HANDLE = -10,
|
||||||
|
STD_OUTPUT_HANDLE = -11,
|
||||||
|
STD_ERROR_HANDLE = -12
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum HandleFlags : uint
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
INHERIT = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[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;
|
||||||
|
public Int32 dwX;
|
||||||
|
public Int32 dwY;
|
||||||
|
public Int32 dwXSize;
|
||||||
|
public Int32 dwYSize;
|
||||||
|
public Int32 dwXCountChars;
|
||||||
|
public Int32 dwYCountChars;
|
||||||
|
public Int32 dwFillAttribute;
|
||||||
|
public Int32 dwFlags;
|
||||||
|
public Int16 wShowWindow;
|
||||||
|
public Int16 cbReserved2;
|
||||||
|
public IntPtr lpReserved2;
|
||||||
|
public IntPtr hStdInput;
|
||||||
|
public IntPtr hStdOutput;
|
||||||
|
public IntPtr 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 PROCESS_INFORMATION
|
||||||
|
{
|
||||||
|
public IntPtr hProcess;
|
||||||
|
public IntPtr hThread;
|
||||||
|
public int dwProcessId;
|
||||||
|
public int dwThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
"@
|
"@
|
||||||
|
|
||||||
|
@ -297,7 +496,6 @@ $actions = $payload.actions
|
||||||
$entrypoint = $payload.($actions[0])
|
$entrypoint = $payload.($actions[0])
|
||||||
$payload.actions = $payload.actions[1..99]
|
$payload.actions = $payload.actions[1..99]
|
||||||
|
|
||||||
|
|
||||||
$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint))
|
$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint))
|
||||||
|
|
||||||
# load the current action entrypoint as a module custom object with a Run method
|
# load the current action entrypoint as a module custom object with a Run method
|
||||||
|
@ -333,7 +531,8 @@ Function Run($payload) {
|
||||||
$username = $payload.become_user
|
$username = $payload.become_user
|
||||||
$password = $payload.become_password
|
$password = $payload.become_password
|
||||||
|
|
||||||
Add-Type -TypeDefinition $helper_def -Debug:$false
|
# FUTURE: convert to SafeHandle so we can stop ignoring warnings?
|
||||||
|
Add-Type -TypeDefinition $helper_def -Debug:$false -IgnoreWarnings
|
||||||
|
|
||||||
$exec_args = $null
|
$exec_args = $null
|
||||||
|
|
||||||
|
@ -342,27 +541,105 @@ Function Run($payload) {
|
||||||
# NB: CreateProcessWithLogonW commandline maxes out at 1024 chars, must bootstrap via filesystem
|
# NB: CreateProcessWithLogonW commandline maxes out at 1024 chars, must bootstrap via filesystem
|
||||||
$temp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + ".ps1")
|
$temp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + ".ps1")
|
||||||
$exec_wrapper.ToString() | Set-Content -Path $temp
|
$exec_wrapper.ToString() | Set-Content -Path $temp
|
||||||
|
# allow (potentially unprivileged) target user access to the tempfile (NB: this likely won't work if traverse checking is enabled)
|
||||||
|
$acl = Get-Acl $temp
|
||||||
|
$acl.AddAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule($username, "FullControl", "Allow")))
|
||||||
|
Set-Acl $temp $acl | Out-Null
|
||||||
|
|
||||||
# TODO: grant target user permissions on tempfile/tempdir
|
# TODO: grant target user permissions on tempfile/tempdir
|
||||||
|
|
||||||
Try {
|
Try {
|
||||||
|
|
||||||
# Base64 encode the command so we don't have to worry about the various levels of escaping
|
|
||||||
# $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($exec_wrapper.ToString()))
|
|
||||||
|
|
||||||
# force the input encoding to preamble-free UTF8 before we create the new process
|
|
||||||
[System.Console]::InputEncoding = $(New-Object System.Text.UTF8Encoding @($false))
|
|
||||||
|
|
||||||
$exec_args = @("-noninteractive", $temp)
|
$exec_args = @("-noninteractive", $temp)
|
||||||
|
|
||||||
$proc = New-Object System.Diagnostics.Process
|
# FUTURE: move these flags into C# enum?
|
||||||
$psi = $proc.StartInfo
|
# start process suspended + breakaway so we can record the watchdog pid without worrying about a completion race
|
||||||
$psi.FileName = $exec_application
|
Set-Variable CREATE_BREAKAWAY_FROM_JOB -Value ([uint32]0x01000000) -Option Constant
|
||||||
$psi.Arguments = $exec_args
|
Set-Variable CREATE_SUSPENDED -Value ([uint32]0x00000004) -Option Constant
|
||||||
$psi.RedirectStandardInput = $true
|
Set-Variable CREATE_UNICODE_ENVIRONMENT -Value ([uint32]0x000000400) -Option Constant
|
||||||
$psi.RedirectStandardOutput = $true
|
Set-Variable CREATE_NEW_CONSOLE -Value ([uint32]0x00000010) -Option Constant
|
||||||
$psi.RedirectStandardError = $true
|
Set-Variable EXTENDED_STARTUPINFO_PRESENT -Value ([uint32]0x00080000) -Option Constant
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
|
$pstartup_flags = $CREATE_BREAKAWAY_FROM_JOB -bor $CREATE_UNICODE_ENVIRONMENT -bor $CREATE_NEW_CONSOLE # -bor $EXTENDED_STARTUPINFO_PRESENT
|
||||||
|
|
||||||
|
$si = New-Object Ansible.Shell.STARTUPINFOEX
|
||||||
|
|
||||||
|
$pipesec = New-Object Ansible.Shell.SECURITY_ATTRIBUTES
|
||||||
|
$pipesec.bInheritHandle = $true
|
||||||
|
$stdout_read = $stdout_write = $stderr_read = $stderr_write = 0
|
||||||
|
|
||||||
|
If(-not [Ansible.Shell.NativeProcessUtil]::CreatePipe([ref]$stdout_read, [ref]$stdout_write, $pipesec, 0)) {
|
||||||
|
throw "Stdout pipe setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
||||||
|
}
|
||||||
|
If(-not [Ansible.Shell.NativeProcessUtil]::SetHandleInformation($stdout_read, [Ansible.Shell.HandleFlags]::INHERIT, 0)) {
|
||||||
|
throw "Stdout handle setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
||||||
|
}
|
||||||
|
|
||||||
|
If(-not [Ansible.Shell.NativeProcessUtil]::CreatePipe([ref]$stderr_read, [ref]$stderr_write, $pipesec, 0)) {
|
||||||
|
throw "Stderr pipe setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
||||||
|
}
|
||||||
|
If(-not [Ansible.Shell.NativeProcessUtil]::SetHandleInformation($stderr_read, [Ansible.Shell.HandleFlags]::INHERIT, 0)) {
|
||||||
|
throw "Stderr handle setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
||||||
|
}
|
||||||
|
|
||||||
|
# setup stdin redirection, we'll leave stdout/stderr as normal
|
||||||
|
$si.startupInfo.dwFlags = [Ansible.Shell.StartupInfoFlags]::USESTDHANDLES
|
||||||
|
$si.startupInfo.hStdOutput = $stdout_write #[Ansible.Shell.NativeProcessUtil]::GetStdHandle([Ansible.Shell.StandardHandleValues]::STD_OUTPUT_HANDLE)
|
||||||
|
$si.startupInfo.hStdError = $stderr_write #[Ansible.Shell.NativeProcessUtil]::GetStdHandle([Ansible.Shell.StandardHandleValues]::STD_ERROR_HANDLE)
|
||||||
|
|
||||||
|
$stdin_read = $stdin_write = 0
|
||||||
|
|
||||||
|
$pipesec = New-Object Ansible.Shell.SECURITY_ATTRIBUTES
|
||||||
|
$pipesec.bInheritHandle = $true
|
||||||
|
|
||||||
|
If(-not [Ansible.Shell.NativeProcessUtil]::CreatePipe([ref]$stdin_read, [ref]$stdin_write, $pipesec, 0)) {
|
||||||
|
throw "Stdin pipe setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
||||||
|
}
|
||||||
|
If(-not [Ansible.Shell.NativeProcessUtil]::SetHandleInformation($stdin_write, [Ansible.Shell.HandleFlags]::INHERIT, 0)) {
|
||||||
|
throw "Stdin handle setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
||||||
|
}
|
||||||
|
$si.startupInfo.hStdInput = $stdin_read
|
||||||
|
|
||||||
|
|
||||||
|
# create an attribute list with our explicit handle inheritance list to pass to CreateProcess
|
||||||
|
[int]$buf_sz = 0
|
||||||
|
|
||||||
|
# determine the buffer size necessary for our attribute list
|
||||||
|
If(-not [Ansible.Shell.NativeProcessUtil]::InitializeProcThreadAttributeList([IntPtr]::Zero, 1, 0, [ref]$buf_sz)) {
|
||||||
|
$last_err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
||||||
|
If($last_err -ne 122) { # ERROR_INSUFFICIENT_BUFFER
|
||||||
|
throw "Attribute list size query failed, Win32Error: $last_err"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$si.lpAttributeList = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buf_sz)
|
||||||
|
|
||||||
|
# initialize the attribute list
|
||||||
|
If(-not [Ansible.Shell.NativeProcessUtil]::InitializeProcThreadAttributeList($si.lpAttributeList, 1, 0, [ref]$buf_sz)) {
|
||||||
|
throw "Attribute list init failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
||||||
|
}
|
||||||
|
|
||||||
|
$handles_to_inherit = [IntPtr[]]@($stdin_read,$stdout_write,$stderr_write)
|
||||||
|
$pinned_handles = [System.Runtime.InteropServices.GCHandle]::Alloc($handles_to_inherit, [System.Runtime.InteropServices.GCHandleType]::Pinned)
|
||||||
|
|
||||||
|
# update the attribute list with the handles we want to inherit
|
||||||
|
If(-not [Ansible.Shell.NativeProcessUtil]::UpdateProcThreadAttribute($si.lpAttributeList, 0, 0x20002, `
|
||||||
|
$pinned_handles.AddrOfPinnedObject(), [System.Runtime.InteropServices.Marshal]::SizeOf([type][IntPtr]) * $handles_to_inherit.Length, `
|
||||||
|
[System.IntPtr]::Zero, [System.IntPtr]::Zero)) {
|
||||||
|
throw "Attribute list update failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
||||||
|
}
|
||||||
|
|
||||||
|
# need to use a preamble-free version of UTF8Encoding
|
||||||
|
$utf8_encoding = New-Object System.Text.UTF8Encoding @($false)
|
||||||
|
$stdin_fs = New-Object System.IO.FileStream @($stdin_write, [System.IO.FileAccess]::Write, $true, 32768)
|
||||||
|
$stdin = New-Object System.IO.StreamWriter @($stdin_fs, $utf8_encoding, 32768)
|
||||||
|
|
||||||
|
$pi = New-Object Ansible.Shell.PROCESS_INFORMATION
|
||||||
|
|
||||||
|
# FUTURE: direct cmdline CreateProcess path lookup fails- this works but is sub-optimal
|
||||||
|
$exec_cmd = [Ansible.Shell.NativeProcessUtil]::SearchPath("powershell.exe")
|
||||||
|
$exec_args = New-Object System.Text.StringBuilder @("-NonInteractive -NoProfile -ExecutionPolicy Bypass $temp")
|
||||||
|
|
||||||
|
[Ansible.Shell.NativeProcessUtil]::GrantAccessToWindowStationAndDesktop($username)
|
||||||
|
|
||||||
If($username.Contains("\")) {
|
If($username.Contains("\")) {
|
||||||
$sp = $username.Split(@([char]"\"), 2)
|
$sp = $username.Split(@([char]"\"), 2)
|
||||||
|
@ -376,54 +653,49 @@ Function Run($payload) {
|
||||||
$domain = "."
|
$domain = "."
|
||||||
}
|
}
|
||||||
|
|
||||||
$psi.Domain = $domain
|
# TODO: use proper Win32Exception + error
|
||||||
$psi.Username = $username
|
If(-not [Ansible.Shell.NativeProcessUtil]::CreateProcessWithLogonW($username, $domain, $password, [Ansible.Shell.LOGON_FLAGS]::LOGON_WITH_PROFILE,
|
||||||
$psi.Password = $($password | ConvertTo-SecureString -AsPlainText -Force)
|
$exec_cmd, $exec_args,
|
||||||
|
$pstartup_flags, [IntPtr]::Zero, $env:windir, $si, [ref]$pi)) {
|
||||||
Try {
|
#throw New-Object System.ComponentModel.Win32Exception
|
||||||
[Ansible.Shell.ProcessUtil]::GrantAccessToWindowStationAndDesktop($username)
|
throw "Worker creation failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
||||||
}
|
|
||||||
Catch {
|
|
||||||
$excep = $_
|
|
||||||
throw "Error granting windowstation/desktop access to '$username' (is the username valid?): $excep"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Try {
|
$stdout_fs = New-Object System.IO.FileStream @($stdout_read, [System.IO.FileAccess]::Read, $true, 4096)
|
||||||
$proc.Start() | Out-Null # will always return $true for non shell-exec cases
|
$stdout = New-Object System.IO.StreamReader @($stdout_fs, $utf8_encoding, $true, 4096)
|
||||||
}
|
$stderr_fs = New-Object System.IO.FileStream @($stderr_read, [System.IO.FileAccess]::Read, $true, 4096)
|
||||||
Catch {
|
$stderr = New-Object System.IO.StreamReader @($stderr_fs, $utf8_encoding, $true, 4096)
|
||||||
$excep = $_
|
|
||||||
if ($excep.Exception.InnerException -and `
|
# close local write ends of stdout/stderr pipes so the open handles won't prevent EOF
|
||||||
$excep.Exception.InnerException -is [System.ComponentModel.Win32Exception] -and `
|
[Ansible.Shell.NativeProcessUtil]::CloseHandle($stdout_write)
|
||||||
$excep.Exception.InnerException.NativeErrorCode -eq 5) {
|
[Ansible.Shell.NativeProcessUtil]::CloseHandle($stderr_write)
|
||||||
throw "Become method 'runas' become is not currently supported with the NTLM or Kerberos auth types"
|
|
||||||
}
|
|
||||||
throw "Error launching under identity '$username': $excep"
|
|
||||||
}
|
|
||||||
|
|
||||||
$payload_string = $payload | ConvertTo-Json -Depth 99 -Compress
|
$payload_string = $payload | ConvertTo-Json -Depth 99 -Compress
|
||||||
|
|
||||||
# push the execution payload over stdin
|
# push the execution payload over stdin
|
||||||
$proc.StandardInput.WriteLine($payload_string)
|
$stdin.WriteLine($payload_string)
|
||||||
$proc.StandardInput.Close()
|
$stdin.Close()
|
||||||
|
|
||||||
$stdout = $stderr = [string] $null
|
$str_stdout = $str_stderr = ""
|
||||||
|
|
||||||
[Ansible.Shell.ProcessUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) | Out-Null
|
[Ansible.Shell.NativeProcessUtil]::GetProcessOutput($stdout, $stderr, [ref] $str_stdout, [ref] $str_stderr)
|
||||||
|
|
||||||
# TODO: decode CLIXML stderr output (and other streams?)
|
# FUTURE: decode CLIXML stderr output (and other streams?)
|
||||||
|
|
||||||
$proc.WaitForExit() | Out-Null
|
#$proc.WaitForExit() | Out-Null
|
||||||
|
|
||||||
$rc = $proc.ExitCode
|
|
||||||
|
# TODO: wait on process handle for exit, get process exit code
|
||||||
|
$rc = [Ansible.Shell.NativeProcessUtil]::GetProcessExitCode($pi.hProcess)
|
||||||
|
|
||||||
If ($rc -eq 0) {
|
If ($rc -eq 0) {
|
||||||
$stdout
|
$str_stdout
|
||||||
$stderr
|
$str_stderr
|
||||||
}
|
}
|
||||||
Else {
|
Else {
|
||||||
Throw "failed, rc was $rc, stderr was $stderr, stdout was $stdout"
|
Throw "failed, rc was $rc, stderr was $stderr, stdout was $stdout"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Catch {
|
Catch {
|
||||||
$excep = $_
|
$excep = $_
|
||||||
|
|
1
test/integration/targets/win_become/aliases
Normal file
1
test/integration/targets/win_become/aliases
Normal file
|
@ -0,0 +1 @@
|
||||||
|
windows/ci/group1
|
87
test/integration/targets/win_become/tasks/main.yml
Normal file
87
test/integration/targets/win_become/tasks/main.yml
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
- set_fact:
|
||||||
|
become_test_username: ansible_become_test
|
||||||
|
gen_pw: password123! + {{ lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}
|
||||||
|
|
||||||
|
- name: create unprivileged user
|
||||||
|
win_user:
|
||||||
|
name: "{{ become_test_username }}"
|
||||||
|
password: "{{ gen_pw }}"
|
||||||
|
update_password: always
|
||||||
|
groups: Users
|
||||||
|
|
||||||
|
- name: execute tests and ensure that test user is deleted regardless of success/failure
|
||||||
|
block:
|
||||||
|
- name: ensure current user is not the become user
|
||||||
|
win_shell: whoami
|
||||||
|
register: whoami_out
|
||||||
|
|
||||||
|
- name: verify output
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not whoami_out.stdout_lines[0].endswith(become_test_username)
|
||||||
|
|
||||||
|
- name: get become user profile dir so we can clean it up later
|
||||||
|
vars: &become_vars
|
||||||
|
ansible_become_user: "{{ become_test_username }}"
|
||||||
|
ansible_become_password: "{{ gen_pw }}"
|
||||||
|
ansible_become_method: runas
|
||||||
|
ansible_become: yes
|
||||||
|
win_shell: $env:USERPROFILE
|
||||||
|
register: profile_dir_out
|
||||||
|
|
||||||
|
- name: ensure profile dir contains test username (eg, if become fails silently, prevent deletion of real user profile)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- become_test_username in profile_dir_out.stdout_lines[0]
|
||||||
|
|
||||||
|
- name: test become runas via task vars
|
||||||
|
vars: *become_vars
|
||||||
|
win_shell: whoami
|
||||||
|
register: whoami_out
|
||||||
|
|
||||||
|
- name: verify output
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- whoami_out.stdout_lines[0].endswith(become_test_username)
|
||||||
|
|
||||||
|
- name: test become runas via task keywords
|
||||||
|
vars:
|
||||||
|
ansible_become_password: "{{ gen_pw }}"
|
||||||
|
become: yes
|
||||||
|
become_method: runas
|
||||||
|
become_user: "{{ become_test_username }}"
|
||||||
|
win_shell: whoami
|
||||||
|
|
||||||
|
register: whoami_out
|
||||||
|
|
||||||
|
- name: verify output
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- whoami_out.stdout_lines[0].endswith(become_test_username)
|
||||||
|
|
||||||
|
- name: test become via block vars
|
||||||
|
vars: *become_vars
|
||||||
|
block:
|
||||||
|
- name: ask who the current user is
|
||||||
|
win_shell: whoami
|
||||||
|
register: whoami_out
|
||||||
|
|
||||||
|
- name: verify output
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- whoami_out.stdout_lines[0].endswith(become_test_username)
|
||||||
|
|
||||||
|
# FUTURE: test raw + script become behavior once they're running under the exec wrapper again
|
||||||
|
# FUTURE: add standalone playbook tests to include password prompting and play become keywords
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: ensure test user is deleted
|
||||||
|
win_user:
|
||||||
|
name: "{{ become_test_username }}"
|
||||||
|
state: absent
|
||||||
|
- name: ensure test user profile is deleted
|
||||||
|
# NB: have to work around powershell limitation of long filenames until win_file fixes it
|
||||||
|
win_shell: rmdir /S /Q {{ profile_dir_out.stdout_lines[0] }}
|
||||||
|
args:
|
||||||
|
executable: cmd.exe
|
||||||
|
when: become_test_username in profile_dir_out.stdout_lines[0]
|
Loading…
Reference in a new issue