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:
parent
9cfd0a58b0
commit
15b492ca57
2 changed files with 387 additions and 113 deletions
|
@ -171,9 +171,12 @@ Set-StrictMode -Version 2
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$helper_def = @"
|
$helper_def = @"
|
||||||
|
using Microsoft.Win32.SafeHandles;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.AccessControl;
|
using System.Security.AccessControl;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
|
@ -212,9 +215,9 @@ namespace Ansible
|
||||||
public Int16 wShowWindow;
|
public Int16 wShowWindow;
|
||||||
public Int16 cbReserved2;
|
public Int16 cbReserved2;
|
||||||
public IntPtr lpReserved2;
|
public IntPtr lpReserved2;
|
||||||
public IntPtr hStdInput;
|
public SafeFileHandle hStdInput;
|
||||||
public IntPtr hStdOutput;
|
public SafeFileHandle hStdOutput;
|
||||||
public IntPtr hStdError;
|
public SafeFileHandle hStdError;
|
||||||
public STARTUPINFO()
|
public STARTUPINFO()
|
||||||
{
|
{
|
||||||
cb = Marshal.SizeOf(this);
|
cb = Marshal.SizeOf(this);
|
||||||
|
@ -254,6 +257,42 @@ namespace Ansible
|
||||||
public SID_AND_ATTRIBUTES User;
|
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]
|
[Flags]
|
||||||
public enum StartupInfoFlags : uint
|
public enum StartupInfoFlags : uint
|
||||||
{
|
{
|
||||||
|
@ -263,6 +302,7 @@ namespace Ansible
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum CreationFlags : uint
|
public enum CreationFlags : uint
|
||||||
{
|
{
|
||||||
|
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
|
||||||
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
|
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
|
||||||
CREATE_NEW_CONSOLE = 0x00000010,
|
CREATE_NEW_CONSOLE = 0x00000010,
|
||||||
CREATE_NEW_PROCESS_GROUP = 0x00000200,
|
CREATE_NEW_PROCESS_GROUP = 0x00000200,
|
||||||
|
@ -353,11 +393,35 @@ namespace Ansible
|
||||||
TokenImpersonation
|
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
|
class NativeWaitHandle : WaitHandle
|
||||||
{
|
{
|
||||||
public NativeWaitHandle(IntPtr handle)
|
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 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
|
public class BecomeUtil
|
||||||
{
|
{
|
||||||
[DllImport("advapi32.dll", SetLastError = true)]
|
[DllImport("advapi32.dll", SetLastError = true)]
|
||||||
|
@ -407,14 +534,14 @@ namespace Ansible
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
private static extern bool CreatePipe(
|
private static extern bool CreatePipe(
|
||||||
out IntPtr hReadPipe,
|
out SafeFileHandle hReadPipe,
|
||||||
out IntPtr hWritePipe,
|
out SafeFileHandle hWritePipe,
|
||||||
SECURITY_ATTRIBUTES lpPipeAttributes,
|
SECURITY_ATTRIBUTES lpPipeAttributes,
|
||||||
uint nSize);
|
uint nSize);
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern bool SetHandleInformation(
|
private static extern bool SetHandleInformation(
|
||||||
IntPtr hObject,
|
SafeFileHandle hObject,
|
||||||
HandleFlags dwMask,
|
HandleFlags dwMask,
|
||||||
int dwFlags);
|
int dwFlags);
|
||||||
|
|
||||||
|
@ -431,7 +558,8 @@ namespace Ansible
|
||||||
private static extern IntPtr GetProcessWindowStation();
|
private static extern IntPtr GetProcessWindowStation();
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true)]
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
private static extern IntPtr GetThreadDesktop(int dwThreadId);
|
private static extern IntPtr GetThreadDesktop(
|
||||||
|
int dwThreadId);
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern int GetCurrentThreadId();
|
private static extern int GetCurrentThreadId();
|
||||||
|
@ -480,17 +608,27 @@ namespace Ansible
|
||||||
out IntPtr phNewToken);
|
out IntPtr phNewToken);
|
||||||
|
|
||||||
[DllImport("advapi32.dll", SetLastError = true)]
|
[DllImport("advapi32.dll", SetLastError = true)]
|
||||||
public static extern bool ImpersonateLoggedOnUser(
|
private static extern bool ImpersonateLoggedOnUser(
|
||||||
IntPtr hToken);
|
IntPtr hToken);
|
||||||
|
|
||||||
[DllImport("advapi32.dll", SetLastError = true)]
|
[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)
|
public static CommandResult RunAsUser(string username, string password, string lpCommandLine, string lpCurrentDirectory, string stdinInput)
|
||||||
{
|
{
|
||||||
SecurityIdentifier account = GetBecomeSid(username);
|
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();
|
STARTUPINFOEX si = new STARTUPINFOEX();
|
||||||
si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
|
si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
|
||||||
|
@ -499,7 +637,7 @@ namespace Ansible
|
||||||
pipesec.bInheritHandle = true;
|
pipesec.bInheritHandle = true;
|
||||||
|
|
||||||
// Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo
|
// 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))
|
if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0))
|
||||||
throw new Win32Exception("STDOUT pipe setup failed");
|
throw new Win32Exception("STDOUT pipe setup failed");
|
||||||
if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0))
|
if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0))
|
||||||
|
@ -521,7 +659,7 @@ namespace Ansible
|
||||||
|
|
||||||
// Setup the stdin buffer
|
// Setup the stdin buffer
|
||||||
UTF8Encoding utf8_encoding = new UTF8Encoding(false);
|
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);
|
StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768);
|
||||||
|
|
||||||
// Create the environment block if set
|
// Create the environment block if set
|
||||||
|
@ -554,26 +692,44 @@ namespace Ansible
|
||||||
if (!launch_success)
|
if (!launch_success)
|
||||||
throw new Win32Exception("Failed to start become process");
|
throw new Win32Exception("Failed to start become process");
|
||||||
|
|
||||||
// Setup the output buffers and get stdout/stderr
|
// If 2012/8+ OS, create new job with JOB_OBJECT_LIMIT_BREAKAWAY_OK
|
||||||
FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, true, 4096);
|
// so that async can work
|
||||||
StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
|
Job job = null;
|
||||||
CloseHandle(stdout_write);
|
if (Environment.OSVersion.Version >= new Version("6.2"))
|
||||||
|
{
|
||||||
FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, true, 4096);
|
job = new Job();
|
||||||
StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096);
|
job.AssignProcess(pi.hProcess);
|
||||||
CloseHandle(stderr_write);
|
}
|
||||||
|
ResumeProcessById(pi.dwProcessId);
|
||||||
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);
|
|
||||||
|
|
||||||
CommandResult result = new CommandResult();
|
CommandResult result = new CommandResult();
|
||||||
result.StandardOut = stdout_str;
|
try
|
||||||
result.StandardError = stderr_str;
|
{
|
||||||
result.ExitCode = rc;
|
// 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;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -604,71 +760,64 @@ namespace Ansible
|
||||||
|
|
||||||
GrantAccessToWindowStationAndDesktop(account);
|
GrantAccessToWindowStationAndDesktop(account);
|
||||||
string account_sid = account.ToString();
|
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))
|
if (service_sids.Contains(account_sid))
|
||||||
{
|
{
|
||||||
// We are trying to become to a service account
|
logonType = LogonType.LOGON32_LOGON_SERVICE;
|
||||||
IntPtr hToken = GetUserHandle();
|
domain = "NT AUTHORITY";
|
||||||
if (hToken == IntPtr.Zero)
|
password = null;
|
||||||
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;
|
|
||||||
switch (account_sid)
|
switch (account_sid)
|
||||||
{
|
{
|
||||||
case "S-1-5-18":
|
case "S-1-5-18":
|
||||||
tokens.Add(hTokenDup);
|
tokens.Add(hSystemTokenDup);
|
||||||
return tokens;
|
return tokens;
|
||||||
case "S-1-5-19":
|
case "S-1-5-19":
|
||||||
lpszUsername = "LocalService";
|
username = "LocalService";
|
||||||
break;
|
break;
|
||||||
case "S-1-5-20":
|
case "S-1-5-20":
|
||||||
lpszUsername = "NetworkService";
|
username = "NetworkService";
|
||||||
break;
|
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
|
else
|
||||||
{
|
{
|
||||||
// We are trying to become a local or domain account
|
// We are trying to become a local or domain account
|
||||||
string domain = null;
|
logonType = LogonType.LOGON32_LOGON_INTERACTIVE;
|
||||||
if (username.Contains(@"\"))
|
if (username.Contains(@"\"))
|
||||||
{
|
{
|
||||||
var user_split = username.Split(Convert.ToChar(@"\"));
|
var user_split = username.Split(Convert.ToChar(@"\"));
|
||||||
|
@ -679,31 +828,35 @@ namespace Ansible
|
||||||
domain = null;
|
domain = null;
|
||||||
else
|
else
|
||||||
domain = ".";
|
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);
|
uint array_byte_size = 1024 * sizeof(uint);
|
||||||
IntPtr[] pids = new IntPtr[1024];
|
IntPtr[] pids = new IntPtr[1024];
|
||||||
|
@ -864,6 +1017,44 @@ namespace Ansible
|
||||||
security.Persist(safeHandle, AccessControlSections.Access);
|
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
|
private class GenericSecurity : NativeObjectSecurity
|
||||||
{
|
{
|
||||||
public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested)
|
public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested)
|
||||||
|
@ -969,8 +1160,7 @@ Function Run($payload) {
|
||||||
$username = $payload.become_user
|
$username = $payload.become_user
|
||||||
$password = $payload.become_password
|
$password = $payload.become_password
|
||||||
|
|
||||||
# FUTURE: convert to SafeHandle so we can stop ignoring warnings?
|
Add-Type -TypeDefinition $helper_def -Debug:$false
|
||||||
Add-Type -TypeDefinition $helper_def -Debug:$false -IgnoreWarnings
|
|
||||||
|
|
||||||
# NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must bootstrap via filesystem
|
# 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")
|
$temp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + ".ps1")
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
- set_fact:
|
- set_fact:
|
||||||
become_test_username: ansible_become_test
|
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') }}
|
gen_pw: password123! + {{ lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}
|
||||||
|
|
||||||
- name: create unprivileged user
|
- name: create unprivileged user
|
||||||
|
@ -9,16 +10,19 @@
|
||||||
update_password: always
|
update_password: always
|
||||||
groups: Users
|
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
|
- name: execute tests and ensure that test user is deleted regardless of success/failure
|
||||||
block:
|
block:
|
||||||
- name: ensure current user is not the become user
|
- name: ensure current user is not the become user
|
||||||
win_shell: whoami
|
win_shell: whoami
|
||||||
register: whoami_out
|
register: whoami_out
|
||||||
|
failed_when: whoami_out.stdout_lines[0].endswith(become_test_username) or whoami_out.stdout_lines[0].endswith(become_test_admin_username)
|
||||||
- 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
|
- name: get become user profile dir so we can clean it up later
|
||||||
vars: &become_vars
|
vars: &become_vars
|
||||||
|
@ -34,7 +38,21 @@
|
||||||
that:
|
that:
|
||||||
- become_test_username in profile_dir_out.stdout_lines[0]
|
- 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
|
vars: *become_vars
|
||||||
win_shell: whoami
|
win_shell: whoami
|
||||||
register: whoami_out
|
register: whoami_out
|
||||||
|
@ -44,6 +62,36 @@
|
||||||
that:
|
that:
|
||||||
- whoami_out.stdout_lines[0].endswith(become_test_username)
|
- 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
|
- name: test become runas via task keywords
|
||||||
vars:
|
vars:
|
||||||
ansible_become_password: "{{ gen_pw }}"
|
ansible_become_password: "{{ gen_pw }}"
|
||||||
|
@ -51,7 +99,6 @@
|
||||||
become_method: runas
|
become_method: runas
|
||||||
become_user: "{{ become_test_username }}"
|
become_user: "{{ become_test_username }}"
|
||||||
win_shell: whoami
|
win_shell: whoami
|
||||||
|
|
||||||
register: whoami_out
|
register: whoami_out
|
||||||
|
|
||||||
- name: verify output
|
- name: verify output
|
||||||
|
@ -111,17 +158,54 @@
|
||||||
that:
|
that:
|
||||||
- whoami_out.stdout_lines[0] == "nt authority\\local service"
|
- 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: 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
|
# FUTURE: add standalone playbook tests to include password prompting and play become keywords
|
||||||
|
|
||||||
always:
|
always:
|
||||||
- name: ensure test user is deleted
|
- name: ensure underprivileged test user is deleted
|
||||||
win_user:
|
win_user:
|
||||||
name: "{{ become_test_username }}"
|
name: "{{ become_test_username }}"
|
||||||
state: absent
|
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
|
# 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] }}
|
win_shell: rmdir /S /Q {{ profile_dir_out.stdout_lines[0] }}
|
||||||
args:
|
args:
|
||||||
executable: cmd.exe
|
executable: cmd.exe
|
||||||
when: become_test_username in profile_dir_out.stdout_lines[0]
|
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]
|
||||||
|
|
Loading…
Reference in a new issue