1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

win_become: get admin token and fix async (#32485)

* win_become: make it easier to become with an admin token

* Fixed up pep8 whitespace

* fix for Server 2008

* Added support for async and become on newer hosts and fix warnings
This commit is contained in:
Jordan Borean 2017-11-04 09:14:48 +10:00 committed by Matt Davis
parent 9cfd0a58b0
commit 15b492ca57
2 changed files with 387 additions and 113 deletions

View file

@ -171,9 +171,12 @@ 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.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
@ -212,9 +215,9 @@ namespace Ansible
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
public SafeFileHandle hStdInput;
public SafeFileHandle hStdOutput;
public SafeFileHandle hStdError;
public STARTUPINFO()
{
cb = Marshal.SizeOf(this);
@ -254,6 +257,42 @@ namespace Ansible
public SID_AND_ATTRIBUTES User;
}
[StructLayout(LayoutKind.Sequential)]
public struct IO_COUNTERS
{
public UInt64 ReadOperationCount;
public UInt64 WriteOperationCount;
public UInt64 OtherOperationCount;
public UInt64 ReadTransferCount;
public UInt64 WriteTransferCount;
public UInt64 OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public UInt64 PerProcessUserTimeLimit;
public UInt64 PerJobUserTimeLimit;
public LimitFlags LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public UInt32 ActiveProcessLimit;
public UIntPtr Affinity;
public UInt32 PriorityClass;
public UInt32 SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public UIntPtr ProcessMemoryLimit;
public UIntPtr JobMemoryLimit;
public UIntPtr PeakProcessMemoryUsed;
public UIntPtr PeakJobMemoryUsed;
}
[Flags]
public enum StartupInfoFlags : uint
{
@ -263,6 +302,7 @@ namespace Ansible
[Flags]
public enum CreationFlags : uint
{
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
CREATE_NEW_CONSOLE = 0x00000010,
CREATE_NEW_PROCESS_GROUP = 0x00000200,
@ -353,11 +393,35 @@ namespace Ansible
TokenImpersonation
}
enum JobObjectInfoType
{
AssociateCompletionPortInformation = 7,
BasicLimitInformation = 2,
BasicUIRestrictions = 4,
EndOfJobTimeInformation = 6,
ExtendedLimitInformation = 9,
SecurityLimitInformation = 5,
GroupInformation = 11
}
[Flags]
enum ThreadAccessRights : uint
{
SUSPEND_RESUME = 0x0002
}
[Flags]
public enum LimitFlags : uint
{
JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
}
class NativeWaitHandle : WaitHandle
{
public NativeWaitHandle(IntPtr handle)
{
this.Handle = handle;
this.SafeWaitHandle = new SafeWaitHandle(handle, false);
}
}
@ -380,6 +444,69 @@ namespace Ansible
public uint ExitCode { get; internal set; }
}
public class Job : IDisposable
{
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr CreateJobObject(
IntPtr lpJobAttributes,
string lpName);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetInformationJobObject(
IntPtr hJob,
JobObjectInfoType JobObjectInfoClass,
IntPtr lpJobObjectInfo,
UInt32 cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AssignProcessToJobObject(
IntPtr hJob,
IntPtr hProcess);
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(
IntPtr hObject);
private IntPtr handle;
public Job()
{
handle = CreateJobObject(IntPtr.Zero, null);
if (handle == IntPtr.Zero)
throw new Win32Exception("CreateJobObject() failed");
JOBOBJECT_BASIC_LIMIT_INFORMATION jobInfo = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
jobInfo.LimitFlags = LimitFlags.JOB_OBJECT_LIMIT_BREAKAWAY_OK | LimitFlags.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedJobInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
extendedJobInfo.BasicLimitInformation = jobInfo;
int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
IntPtr pExtendedJobInfo = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(extendedJobInfo, pExtendedJobInfo, false);
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, pExtendedJobInfo, (UInt32)length))
throw new Win32Exception("SetInformationJobObject() failed");
}
public void AssignProcess(IntPtr processHandle)
{
if (!AssignProcessToJobObject(handle, processHandle))
throw new Win32Exception("AssignProcessToJobObject() failed");
}
public void Dispose()
{
if (handle != IntPtr.Zero)
{
CloseHandle(handle);
handle = IntPtr.Zero;
}
GC.SuppressFinalize(this);
}
}
public class BecomeUtil
{
[DllImport("advapi32.dll", SetLastError = true)]
@ -407,14 +534,14 @@ namespace Ansible
[DllImport("kernel32.dll")]
private static extern bool CreatePipe(
out IntPtr hReadPipe,
out IntPtr hWritePipe,
out SafeFileHandle hReadPipe,
out SafeFileHandle hWritePipe,
SECURITY_ATTRIBUTES lpPipeAttributes,
uint nSize);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetHandleInformation(
IntPtr hObject,
SafeFileHandle hObject,
HandleFlags dwMask,
int dwFlags);
@ -431,7 +558,8 @@ namespace Ansible
private static extern IntPtr GetProcessWindowStation();
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr GetThreadDesktop(int dwThreadId);
private static extern IntPtr GetThreadDesktop(
int dwThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int GetCurrentThreadId();
@ -480,17 +608,27 @@ namespace Ansible
out IntPtr phNewToken);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool ImpersonateLoggedOnUser(
private static extern bool ImpersonateLoggedOnUser(
IntPtr hToken);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool RevertToSelf();
private static extern bool RevertToSelf();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern SafeFileHandle OpenThread(
ThreadAccessRights dwDesiredAccess,
bool bInheritHandle,
int dwThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int ResumeThread(
SafeHandle hThread);
public static CommandResult RunAsUser(string username, string password, string lpCommandLine, string lpCurrentDirectory, string stdinInput)
{
SecurityIdentifier account = GetBecomeSid(username);
CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT;
CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT | CreationFlags.CREATE_BREAKAWAY_FROM_JOB | CreationFlags.CREATE_SUSPENDED;
STARTUPINFOEX si = new STARTUPINFOEX();
si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
@ -499,7 +637,7 @@ namespace Ansible
pipesec.bInheritHandle = true;
// Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo
IntPtr stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write = IntPtr.Zero;
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))
@ -521,7 +659,7 @@ namespace Ansible
// Setup the stdin buffer
UTF8Encoding utf8_encoding = new UTF8Encoding(false);
FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, true, 32768);
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
@ -554,26 +692,44 @@ namespace Ansible
if (!launch_success)
throw new Win32Exception("Failed to start become process");
// Setup the output buffers and get stdout/stderr
FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, true, 4096);
StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
CloseHandle(stdout_write);
FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, true, 4096);
StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096);
CloseHandle(stderr_write);
stdin.WriteLine(stdinInput);
stdin.Close();
string stdout_str, stderr_str = null;
GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str);
uint rc = GetProcessExitCode(pi.hProcess);
// If 2012/8+ OS, create new job with JOB_OBJECT_LIMIT_BREAKAWAY_OK
// so that async can work
Job job = null;
if (Environment.OSVersion.Version >= new Version("6.2"))
{
job = new Job();
job.AssignProcess(pi.hProcess);
}
ResumeProcessById(pi.dwProcessId);
CommandResult result = new CommandResult();
result.StandardOut = stdout_str;
result.StandardError = stderr_str;
result.ExitCode = rc;
try
{
// 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;
}
finally
{
if (job != null)
job.Dispose();
}
return result;
}
@ -604,71 +760,64 @@ namespace Ansible
GrantAccessToWindowStationAndDesktop(account);
string account_sid = account.ToString();
bool impersonated = false;
IntPtr hSystemTokenDup = IntPtr.Zero;
// Try to get SYSTEM token handle so we can impersonate to get full admin token
IntPtr hSystemToken = GetSystemUserHandle();
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)
{
// We have the token, need to duplicate and impersonate
bool dupResult = DuplicateTokenEx(
hSystemToken,
TokenAccessLevels.MaximumAllowed,
IntPtr.Zero,
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
TOKEN_TYPE.TokenPrimary,
out hSystemTokenDup);
int lastError = Marshal.GetLastWin32Error();
CloseHandle(hSystemToken);
if (!dupResult && service_sids.Contains(account_sid))
throw new Win32Exception(lastError, "Failed to duplicate token for NT AUTHORITY\\SYSTEM");
else if (dupResult && account_sid != "S-1-5-18")
{
if (ImpersonateLoggedOnUser(hSystemTokenDup))
impersonated = true;
else if (service_sids.Contains(account_sid))
throw new Win32Exception("Failed to impersonate as SYSTEM account");
}
}
LogonType logonType;
string domain = null;
if (service_sids.Contains(account_sid))
{
// We are trying to become to a service account
IntPtr hToken = GetUserHandle();
if (hToken == IntPtr.Zero)
throw new Exception("Failed to get token for NT AUTHORITY\\SYSTEM");
IntPtr hTokenDup = IntPtr.Zero;
try
{
if (!DuplicateTokenEx(
hToken,
TokenAccessLevels.MaximumAllowed,
IntPtr.Zero,
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
TOKEN_TYPE.TokenPrimary,
out hTokenDup))
{
throw new Win32Exception("Failed to duplicate the SYSTEM account token");
}
}
finally
{
CloseHandle(hToken);
}
string lpszDomain = "NT AUTHORITY";
string lpszUsername = null;
logonType = LogonType.LOGON32_LOGON_SERVICE;
domain = "NT AUTHORITY";
password = null;
switch (account_sid)
{
case "S-1-5-18":
tokens.Add(hTokenDup);
tokens.Add(hSystemTokenDup);
return tokens;
case "S-1-5-19":
lpszUsername = "LocalService";
username = "LocalService";
break;
case "S-1-5-20":
lpszUsername = "NetworkService";
username = "NetworkService";
break;
}
if (!ImpersonateLoggedOnUser(hTokenDup))
throw new Win32Exception("Failed to impersonate as SYSTEM account");
IntPtr newToken = IntPtr.Zero;
if (!LogonUser(
lpszUsername,
lpszDomain,
null,
LogonType.LOGON32_LOGON_SERVICE,
LogonProvider.LOGON32_PROVIDER_DEFAULT,
out newToken))
{
throw new Win32Exception("LogonUser failed");
}
RevertToSelf();
tokens.Add(newToken);
return tokens;
}
else
{
// We are trying to become a local or domain account
string domain = null;
logonType = LogonType.LOGON32_LOGON_INTERACTIVE;
if (username.Contains(@"\"))
{
var user_split = username.Split(Convert.ToChar(@"\"));
@ -679,31 +828,35 @@ namespace Ansible
domain = null;
else
domain = ".";
// Logon and get the token
IntPtr hToken = IntPtr.Zero;
if (!LogonUser(
username,
domain,
password,
LogonType.LOGON32_LOGON_INTERACTIVE,
LogonProvider.LOGON32_PROVIDER_DEFAULT,
out hToken))
{
throw new Win32Exception("LogonUser failed");
}
// Get the elevate token
IntPtr hTokenElevated = GetElevatedToken(hToken);
tokens.Add(hTokenElevated);
tokens.Add(hToken);
return tokens;
}
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);
}
tokens.Add(hToken);
if (impersonated)
RevertToSelf();
return tokens;
}
private static IntPtr GetUserHandle()
private static IntPtr GetSystemUserHandle()
{
uint array_byte_size = 1024 * sizeof(uint);
IntPtr[] pids = new IntPtr[1024];
@ -864,6 +1017,44 @@ namespace Ansible
security.Persist(safeHandle, AccessControlSections.Access);
}
private static void ResumeThreadById(int threadId)
{
var threadHandle = OpenThread(ThreadAccessRights.SUSPEND_RESUME, false, threadId);
if (threadHandle.IsInvalid)
throw new Win32Exception(String.Format("Thread ID {0} is invalid", threadId));
try
{
if (ResumeThread(threadHandle) == -1)
throw new Win32Exception(String.Format("Thread ID {0} cannot be resumed", threadId));
}
finally
{
threadHandle.Dispose();
}
}
private static void ResumeProcessById(int pid)
{
var proc = Process.GetProcessById(pid);
// wait for at least one suspended thread in the process (this handles possible slow startup race where
// primary thread of created-suspended process has not yet become runnable)
var retryCount = 0;
while (!proc.Threads.OfType<ProcessThread>().Any(t => t.ThreadState == System.Diagnostics.ThreadState.Wait &&
t.WaitReason == ThreadWaitReason.Suspended))
{
proc.Refresh();
Thread.Sleep(50);
if (retryCount > 100)
throw new InvalidOperationException(String.Format("No threads were suspended in target PID {0} after 5s", pid));
}
foreach (var thread in proc.Threads.OfType<ProcessThread>().Where(t => t.ThreadState == System.Diagnostics.ThreadState.Wait &&
t.WaitReason == ThreadWaitReason.Suspended))
ResumeThreadById(thread.Id);
}
private class GenericSecurity : NativeObjectSecurity
{
public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested)
@ -969,8 +1160,7 @@ Function Run($payload) {
$username = $payload.become_user
$password = $payload.become_password
# FUTURE: convert to SafeHandle so we can stop ignoring warnings?
Add-Type -TypeDefinition $helper_def -Debug:$false -IgnoreWarnings
Add-Type -TypeDefinition $helper_def -Debug:$false
# NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must bootstrap via filesystem
$temp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + ".ps1")

View file

@ -1,5 +1,6 @@
- set_fact:
become_test_username: ansible_become_test
become_test_admin_username: ansible_become_admin
gen_pw: password123! + {{ lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}
- name: create unprivileged user
@ -9,16 +10,19 @@
update_password: always
groups: Users
- name: create a privileged user
win_user:
name: "{{ become_test_admin_username }}"
password: "{{ gen_pw }}"
update_password: always
groups: Administrators
- 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)
failed_when: whoami_out.stdout_lines[0].endswith(become_test_username) or whoami_out.stdout_lines[0].endswith(become_test_admin_username)
- name: get become user profile dir so we can clean it up later
vars: &become_vars
@ -34,7 +38,21 @@
that:
- become_test_username in profile_dir_out.stdout_lines[0]
- name: test become runas via task vars
- name: get become admin user profile dir so we can clean it up later
vars: &admin_become_vars
ansible_become_user: "{{ become_test_admin_username }}"
ansible_become_password: "{{ gen_pw }}"
ansible_become_method: runas
ansible_become: yes
win_shell: $env:USERPROFILE
register: admin_profile_dir_out
- name: ensure profile dir contains admin test username
assert:
that:
- become_test_admin_username in admin_profile_dir_out.stdout_lines[0]
- name: test become runas via task vars (underprivileged user)
vars: *become_vars
win_shell: whoami
register: whoami_out
@ -44,6 +62,36 @@
that:
- whoami_out.stdout_lines[0].endswith(become_test_username)
- name: test become runas to ensure underprivileged user has medium integrity level
vars: *become_vars
win_shell: whoami /groups
register: whoami_out
- name: verify output
assert:
that:
- '"Mandatory Label\Medium Mandatory Level" in whoami_out.stdout'
- name: test become runas via task vars (privileged user)
vars: *admin_become_vars
win_shell: whoami
register: whoami_out
- name: verify output
assert:
that:
- whoami_out.stdout_lines[0].endswith(become_test_admin_username)
- name: test become runas to ensure privileged user has high integrity level
vars: *admin_become_vars
win_shell: whoami /groups
register: whoami_out
- name: verify output
assert:
that:
- '"Mandatory Label\High Mandatory Level" in whoami_out.stdout'
- name: test become runas via task keywords
vars:
ansible_become_password: "{{ gen_pw }}"
@ -51,7 +99,6 @@
become_method: runas
become_user: "{{ become_test_username }}"
win_shell: whoami
register: whoami_out
- name: verify output
@ -111,17 +158,54 @@
that:
- whoami_out.stdout_lines[0] == "nt authority\\local service"
# Test out Async on Windows Server 2012+
- name: get OS version
win_shell: if ([System.Environment]::OSVersion.Version -ge [Version]"6.2") { $true } else { $false }
register: os_version
- name: test become + async on older hosts
vars: *become_vars
win_command: whoami
async: 10
register: whoami_out
ignore_errors: yes
- name: verify older hosts failed with become + async
assert:
that:
- whoami_out|failed
when: os_version.stdout_lines[0] == "False"
- name: verify newer hosts worked with become + async
assert:
that:
- whoami_out|success
when: os_version.stdout_lines[0] == "True"
# 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
- name: ensure underprivileged test user is deleted
win_user:
name: "{{ become_test_username }}"
state: absent
- name: ensure test user profile is deleted
- name: ensure privileged test user is deleted
win_user:
name: "{{ become_test_admin_username }}"
state: absent
- name: ensure underprivileged 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]
- name: ensure privileged 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 {{ admin_profile_dir_out.stdout_lines[0] }}
args:
executable: cmd.exe
when: become_test_admin_username in admin_profile_dir_out.stdout_lines[0]