diff options
author | Jordan Borean <jborean93@gmail.com> | 2024-11-12 00:16:22 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-11-12 00:16:22 +0100 |
commit | 3befdd3d151e66a7b17cbe49e31d158903191a76 (patch) | |
tree | c8a00cafd1d216f7480986431997eded3df5f13d | |
parent | ansible-test - align 2025 connection defaults to CI matrix (#84291) (diff) | |
download | ansible-3befdd3d151e66a7b17cbe49e31d158903191a76.tar.xz ansible-3befdd3d151e66a7b17cbe49e31d158903191a76.zip |
Fix runas become SYSTEM logic (#84280)
Fixes the logic when attempting to become the SYSTEM user using the
runas plugin. It was incorrectly assumed that calling LogonUser with the
SYSTEM username would produce a new token with all the privileges but
instead it creates a copy of the existing token. This reverts the logic
back to the original process and adds in new logic to avoid any tokens
that are restricted from creating new processes.
-rw-r--r-- | changelogs/fragments/become-runas-system-deux.yml | 3 | ||||
-rw-r--r-- | lib/ansible/module_utils/csharp/Ansible.AccessToken.cs | 51 | ||||
-rw-r--r-- | lib/ansible/module_utils/csharp/Ansible.Become.cs | 104 |
3 files changed, 123 insertions, 35 deletions
diff --git a/changelogs/fragments/become-runas-system-deux.yml b/changelogs/fragments/become-runas-system-deux.yml new file mode 100644 index 0000000000..e8b17f92a4 --- /dev/null +++ b/changelogs/fragments/become-runas-system-deux.yml @@ -0,0 +1,3 @@ +bugfixes: + - >- + runas become - Fix up become logic to still get the SYSTEM token with the most privileges when running as SYSTEM. diff --git a/lib/ansible/module_utils/csharp/Ansible.AccessToken.cs b/lib/ansible/module_utils/csharp/Ansible.AccessToken.cs index 49fba4e5e7..a7959efb30 100644 --- a/lib/ansible/module_utils/csharp/Ansible.AccessToken.cs +++ b/lib/ansible/module_utils/csharp/Ansible.AccessToken.cs @@ -339,19 +339,47 @@ namespace Ansible.AccessToken public static IEnumerable<SafeNativeHandle> EnumerateUserTokens(SecurityIdentifier sid, TokenAccessLevels access = TokenAccessLevels.Query) { + return EnumerateUserTokens(sid, access, (p, h) => true); + } + + public static IEnumerable<SafeNativeHandle> EnumerateUserTokens( + SecurityIdentifier sid, + TokenAccessLevels access, + Func<System.Diagnostics.Process, SafeNativeHandle, bool> processFilter) + { + // We always need the Query access level so we can query the TokenUser + access |= TokenAccessLevels.Query; + foreach (System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses()) { - // We always need the Query access level so we can query the TokenUser using (process) - using (SafeNativeHandle hToken = TryOpenAccessToken(process, access | TokenAccessLevels.Query)) + using (SafeNativeHandle processHandle = NativeMethods.OpenProcess(ProcessAccessFlags.QueryInformation, false, (UInt32)process.Id)) { - if (hToken == null) + if (processHandle.IsInvalid) + { continue; + } - if (!sid.Equals(GetTokenUser(hToken))) + if (!processFilter(process, processHandle)) + { continue; + } + + SafeNativeHandle accessToken; + if (!NativeMethods.OpenProcessToken(processHandle, access, out accessToken)) + { + continue; + } + + using (accessToken) + { + if (!sid.Equals(GetTokenUser(accessToken))) + { + continue; + } - yield return hToken; + yield return accessToken; + } } } } @@ -440,18 +468,5 @@ namespace Ansible.AccessToken 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 SafeNativeHandle TryOpenAccessToken(System.Diagnostics.Process process, TokenAccessLevels access) - { - try - { - using (SafeNativeHandle hProcess = OpenProcess(process.Id, ProcessAccessFlags.QueryInformation, false)) - return OpenProcessToken(hProcess, access); - } - catch (Win32Exception) - { - return null; - } - } } } diff --git a/lib/ansible/module_utils/csharp/Ansible.Become.cs b/lib/ansible/module_utils/csharp/Ansible.Become.cs index 68d4d11d7a..08b73d404b 100644 --- a/lib/ansible/module_utils/csharp/Ansible.Become.cs +++ b/lib/ansible/module_utils/csharp/Ansible.Become.cs @@ -93,10 +93,21 @@ namespace Ansible.Become CachedRemoteInteractive, CachedUnlock } + + [Flags] + public enum ProcessChildProcessPolicyFlags + { + None = 0x0, + NoChildProcessCreation = 0x1, + AuditNoChildProcessCreation = 0x2, + AllowSecureProcessCreation = 0x4, + } } internal class NativeMethods { + public const int ProcessChildProcessPolicy = 13; + [DllImport("advapi32.dll", SetLastError = true)] public static extern bool AllocateLocallyUniqueId( out Luid Luid); @@ -116,6 +127,13 @@ namespace Ansible.Become [DllImport("kernel32.dll")] public static extern UInt32 GetCurrentThreadId(); + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetProcessMitigationPolicy( + SafeNativeHandle hProcess, + int MitigationPolicy, + ref NativeHelpers.ProcessChildProcessPolicyFlags lpBuffer, + IntPtr dwLength); + [DllImport("user32.dll", SetLastError = true)] public static extern NoopSafeHandle GetProcessWindowStation(); @@ -217,6 +235,7 @@ namespace Ansible.Become }; private static int WINDOWS_STATION_ALL_ACCESS = 0x000F037F; private static int DESKTOP_RIGHTS_ALL_ACCESS = 0x000F01FF; + private static bool _getProcessMitigationPolicySupported = true; public static Result CreateProcessAsUser(string username, string password, string command) { @@ -333,12 +352,13 @@ namespace Ansible.Become // Grant access to the current Windows Station and Desktop to the become user GrantAccessToWindowStationAndDesktop(account); - // Try and impersonate a SYSTEM token. We need the SeTcbPrivilege for - // - LogonUser for a service SID - // - S4U logon - // - Token elevation + // Try and impersonate a SYSTEM token, we need a SYSTEM token to either become a well known service + // account or have administrative rights on the become access token. + // If we ultimately are becoming the SYSTEM account we want the token with the most privileges available. + // https://github.com/ansible/ansible/issues/71453 + bool usedForProcess = becomeSid == "S-1-5-18"; systemToken = GetPrimaryTokenForUser(new SecurityIdentifier("S-1-5-18"), - new List<string>() { "SeTcbPrivilege" }); + new List<string>() { "SeTcbPrivilege" }, usedForProcess); if (systemToken != null) { try @@ -356,9 +376,11 @@ namespace Ansible.Become try { + if (becomeSid == "S-1-5-18") + userTokens.Add(systemToken); // Cannot use String.IsEmptyOrNull() as an empty string is an account that doesn't have a pass. // We only use S4U if no password was defined or it was null - if (!SERVICE_SIDS.Contains(becomeSid) && password == null && logonType != LogonType.NewCredentials) + else if (!SERVICE_SIDS.Contains(becomeSid) && password == null && logonType != LogonType.NewCredentials) { // If no password was specified, try and duplicate an existing token for that user or use S4U to // generate one without network credentials @@ -381,11 +403,6 @@ namespace Ansible.Become string domain = null; switch (becomeSid) { - case "S-1-5-18": - logonType = LogonType.Service; - domain = "NT AUTHORITY"; - username = "SYSTEM"; - break; case "S-1-5-19": logonType = LogonType.Service; domain = "NT AUTHORITY"; @@ -427,8 +444,10 @@ namespace Ansible.Become return userTokens; } - private static SafeNativeHandle GetPrimaryTokenForUser(SecurityIdentifier sid, - List<string> requiredPrivileges = null) + private static SafeNativeHandle GetPrimaryTokenForUser( + SecurityIdentifier sid, + List<string> requiredPrivileges = null, + bool usedForProcess = false) { // According to CreateProcessWithTokenW we require a token with // TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY @@ -438,7 +457,19 @@ namespace Ansible.Become TokenAccessLevels.AssignPrimary | TokenAccessLevels.Impersonate; - foreach (SafeNativeHandle hToken in TokenUtil.EnumerateUserTokens(sid, dwAccess)) + SafeNativeHandle userToken = null; + int privilegeCount = 0; + + // If we are using this token for the process, we need to check the + // process mitigation policy allows child processes to be created. + var processFilter = usedForProcess + ? (Func<System.Diagnostics.Process, SafeNativeHandle, bool>)((p, t) => + { + return GetProcessChildProcessPolicyFlags(t) == NativeHelpers.ProcessChildProcessPolicyFlags.None; + }) + : ((p, t) => true); + + foreach (SafeNativeHandle hToken in TokenUtil.EnumerateUserTokens(sid, dwAccess, processFilter)) { // Filter out any Network logon tokens, using become with that is useless when S4U // can give us a Batch logon @@ -448,6 +479,10 @@ namespace Ansible.Become List<string> actualPrivileges = TokenUtil.GetTokenPrivileges(hToken).Select(x => x.Name).ToList(); + // If the token has less or the same number of privileges than the current token, skip it. + if (usedForProcess && privilegeCount >= actualPrivileges.Count) + continue; + // Check that the required privileges are on the token if (requiredPrivileges != null) { @@ -459,16 +494,22 @@ namespace Ansible.Become // Duplicate the token to convert it to a primary token with the access level required. try { - return TokenUtil.DuplicateToken(hToken, TokenAccessLevels.MaximumAllowed, + userToken = TokenUtil.DuplicateToken(hToken, TokenAccessLevels.MaximumAllowed, SecurityImpersonationLevel.Anonymous, TokenType.Primary); + privilegeCount = actualPrivileges.Count; } catch (Process.Win32Exception) { continue; } + + // If we don't care about getting the token with the most privileges, escape the loop as we already + // have a token. + if (!usedForProcess) + break; } - return null; + return userToken; } private static SafeNativeHandle GetS4UTokenForUser(SecurityIdentifier sid, LogonType logonType) @@ -581,6 +622,35 @@ namespace Ansible.Become return null; } + private static NativeHelpers.ProcessChildProcessPolicyFlags GetProcessChildProcessPolicyFlags(SafeNativeHandle processHandle) + { + // Because this is only used to check the policy, we ignore any + // errors and pretend that the policy is None. + NativeHelpers.ProcessChildProcessPolicyFlags policy = NativeHelpers.ProcessChildProcessPolicyFlags.None; + + if (_getProcessMitigationPolicySupported) + { + try + { + if (NativeMethods.GetProcessMitigationPolicy( + processHandle, + NativeMethods.ProcessChildProcessPolicy, + ref policy, + (IntPtr)4)) + { + return policy; + } + } + catch (EntryPointNotFoundException) + { + // If the function is not available, we won't try to call it again + _getProcessMitigationPolicySupported = false; + } + } + + return policy; + } + private static NativeHelpers.SECURITY_LOGON_TYPE GetTokenLogonType(SafeNativeHandle hToken) { TokenStatistics stats = TokenUtil.GetTokenStatistics(hToken); @@ -637,4 +707,4 @@ namespace Ansible.Become { } } } -} +}
\ No newline at end of file |