diff options
author | Jordan Borean <jborean93@gmail.com> | 2019-02-12 11:12:39 +1000 |
---|---|---|
committer | Toshio Kuratomi <a.badger@gmail.com> | 2019-02-12 11:30:42 -0800 |
commit | 19d20304dec6c37e28fea2b3f8c697a79fe808b7 (patch) | |
tree | ed46a3c35d394f16e16e888cf6bd262a97e25e82 | |
parent | adbe9d5d9e696aa2c26c8cfbf4c478b7a30077cf (diff) | |
download | ansible-19d20304dec6c37e28fea2b3f8c697a79fe808b7.tar.gz |
win become - fix token elevation issues
This is an implementation of https://github.com/ansible/ansible/pull/48082/commits/8bffcf8e50e6493c332bfcaec9e1abe61b92a416
that was done in the PR https://github.com/ansible/ansible/pull/48082 to devel.
The changes have been manually brought across to the the stable-2.7 branch as it
cannot be cleanly cherry picked due to the substantial differences in become
between these versions.
Currently we impersonate the `SYSTEM` token in order to elevate our become
process with the highest privileges it has available but there are some edge
cases where the first `SYSTEM` token we come across doesn't have the
`SeTcbPrivilege` which is required for the above. This PR adds a further check
in the search for a `SYSTEM` token to make sure it has the `SeTcbPrivilege`
before continuing.
(cherry picked from commit cc5088c9e197996f95d2b7cc04cd767157d76ded)
-rw-r--r-- | changelogs/fragments/win-become-elevation.yaml | 2 | ||||
-rw-r--r-- | lib/ansible/plugins/shell/powershell.py | 275 |
2 files changed, 163 insertions, 114 deletions
diff --git a/changelogs/fragments/win-become-elevation.yaml b/changelogs/fragments/win-become-elevation.yaml new file mode 100644 index 0000000000..4c5d10f778 --- /dev/null +++ b/changelogs/fragments/win-become-elevation.yaml @@ -0,0 +1,2 @@ +bugfixes: +- win become - Fix some scenarios where become failed to create an elevated process diff --git a/lib/ansible/plugins/shell/powershell.py b/lib/ansible/plugins/shell/powershell.py index 9f6ca65d27..4a2332232e 100644 --- a/lib/ansible/plugins/shell/powershell.py +++ b/lib/ansible/plugins/shell/powershell.py @@ -207,6 +207,7 @@ 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; @@ -262,6 +263,25 @@ namespace Ansible } [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; @@ -277,6 +297,14 @@ namespace Ansible 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; @@ -355,6 +383,7 @@ namespace Ansible public enum TokenInformationClass { TokenUser = 1, + TokenPrivileges = 3, TokenType = 8, TokenImpersonationLevel = 9, TokenElevationType = 18, @@ -411,6 +440,26 @@ namespace Ansible } } + 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; @@ -548,23 +597,22 @@ namespace Ansible private static extern bool GetTokenInformation( IntPtr TokenHandle, TokenInformationClass TokenInformationClass, - IntPtr TokenInformation, + SafeMemoryBuffer TokenInformation, uint TokenInformationLength, out uint ReturnLength); - [DllImport("psapi.dll", SetLastError = true)] - private static extern bool EnumProcesses( - [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U4)] - [In][Out] IntPtr[] processIds, - uint cb, - [MarshalAs(UnmanagedType.U4)] - out uint pBytesReturned); + [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, - IntPtr processId); + UInt32 processId); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool OpenProcessToken( @@ -572,12 +620,6 @@ namespace Ansible TokenAccessLevels DesiredAccess, out IntPtr TokenHandle); - [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool ConvertSidToStringSidW( - IntPtr pSID, - [MarshalAs(UnmanagedType.LPTStr)] - out string StringSid); - [DllImport("advapi32", SetLastError = true)] private static extern bool DuplicateTokenEx( IntPtr hExistingToken, @@ -757,7 +799,6 @@ namespace Ansible try { - IntPtr hSystemTokenDup = IntPtr.Zero; 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 @@ -765,28 +806,13 @@ namespace Ansible } 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"); - } // 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; @@ -800,7 +826,7 @@ namespace Ansible switch (account_sid) { case "S-1-5-18": - tokens.Add(hSystemTokenDup); + tokens.Add(hSystemToken); return tokens; case "S-1-5-19": username = "LocalService"; @@ -858,44 +884,59 @@ namespace Ansible private static IntPtr GetSystemUserHandle() { - uint array_byte_size = 1024 * sizeof(uint); - IntPtr[] pids = new IntPtr[1024]; - uint bytes_copied; - - if (!EnumProcesses(pids, array_byte_size, out bytes_copied)) + // 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()) { - throw new Win32Exception("Failed to enumerate processes"); - } - // TODO: Handle if bytes_copied is larger than the array size and rerun EnumProcesses with larger array - uint num_processes = bytes_copied / sizeof(uint); - - for (uint i = 0; i < num_processes; i++) - { - IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_INFORMATION, false, pids[i]); - if (hProcess != IntPtr.Zero) + using (process) { - IntPtr hToken = IntPtr.Zero; - // 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; - - if (OpenProcessToken(hProcess, desired_access, out hToken)) + IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_INFORMATION, false, (UInt32)process.Id); + if (hProcess == IntPtr.Zero) + continue; + + try { - string sid = GetTokenUserSID(hToken); - if (sid == "S-1-5-18") + IntPtr hToken = IntPtr.Zero; + if (!OpenProcessToken(hProcess, desired_access, out hToken)) + continue; + + try { - CloseHandle(hProcess); - return hToken; + 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); } } - - CloseHandle(hToken); + finally + { + CloseHandle(hProcess); + } } - CloseHandle(hProcess); } return IntPtr.Zero; @@ -903,33 +944,12 @@ namespace Ansible private static string GetTokenUserSID(IntPtr hToken) { - uint token_length; - string sid; - - if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, IntPtr.Zero, 0, out token_length)) + using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, TokenInformationClass.TokenUser)) { - int last_err = Marshal.GetLastWin32Error(); - if (last_err != 122) // ERROR_INSUFFICIENT_BUFFER - throw new Win32Exception(last_err, "Failed to get TokenUser length"); + TOKEN_USER tokenUser = (TOKEN_USER)Marshal.PtrToStructure(tokenInfo.DangerousGetHandle(), + typeof(TOKEN_USER)); + return new SecurityIdentifier(tokenUser.User.Sid).Value; } - - IntPtr token_information = Marshal.AllocHGlobal((int)token_length); - try - { - if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, token_information, token_length, out token_length)) - throw new Win32Exception("Failed to get TokenUser information"); - - TOKEN_USER token_user = (TOKEN_USER)Marshal.PtrToStructure(token_information, typeof(TOKEN_USER)); - - if (!ConvertSidToStringSidW(token_user.User.Sid, out sid)) - throw new Win32Exception("Failed to get user SID"); - } - finally - { - Marshal.FreeHGlobal(token_information); - } - - return sid; } private static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr) @@ -964,38 +984,65 @@ namespace Ansible private static IntPtr GetElevatedToken(IntPtr hToken) { - uint requestedLength; + // 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; + } - IntPtr pTokenInfo = Marshal.AllocHGlobal(sizeof(int)); + // We have a limited token, get the linked elevated token + using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken)) + return Marshal.ReadIntPtr(tokenInfo.DangerousGetHandle()); + } - try + private static List<string> GetTokenPrivileges(IntPtr hToken) + { + using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, TokenInformationClass.TokenPrivileges)) { - if (!GetTokenInformation(hToken, TokenInformationClass.TokenElevationType, pTokenInfo, sizeof(int), out requestedLength)) - throw new Win32Exception("Unable to get TokenElevationType"); + TOKEN_PRIVILEGES tokenPrivileges = (TOKEN_PRIVILEGES)Marshal.PtrToStructure( + tokenInfo.DangerousGetHandle(), typeof(TOKEN_PRIVILEGES)); - var tet = (TokenElevationType)Marshal.ReadInt32(pTokenInfo); + LUID_AND_ATTRIBUTES[] luidAndAttributes = new LUID_AND_ATTRIBUTES[tokenPrivileges.PrivilegeCount]; + PtrToStructureArray(luidAndAttributes, IntPtr.Add(tokenInfo.DangerousGetHandle(), Marshal.SizeOf(tokenPrivileges.PrivilegeCount))); - // we already have the best token we can get, just use it - if (tet != TokenElevationType.TokenElevationTypeLimited) - return hToken; + return luidAndAttributes.Select(x => GetPrivilegeName(x.Luid)).ToList(); + } + } - GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, IntPtr.Zero, 0, out requestedLength); + 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())); - IntPtr pLinkedToken = Marshal.AllocHGlobal((int)requestedLength); + SafeMemoryBuffer tokenInfo = new SafeMemoryBuffer((int)tokenLength); + if (!GetTokenInformation(hToken, tokenClass, tokenInfo, tokenLength, out tokenLength)) + throw new Win32Exception(String.Format("GetTokenInformation({0}) failed", tokenClass.ToString())); - if (!GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, pLinkedToken, requestedLength, out requestedLength)) - throw new Win32Exception("Unable to get linked token"); + return tokenInfo; + } - IntPtr linkedToken = Marshal.ReadIntPtr(pLinkedToken); + private static string GetPrivilegeName(LUID luid) + { + UInt32 nameLen = 0; + LookupPrivilegeNameW(null, ref luid, null, ref nameLen); - Marshal.FreeHGlobal(pLinkedToken); + StringBuilder name = new StringBuilder((int)(nameLen + 1)); + if (!LookupPrivilegeNameW(null, ref luid, name, ref nameLen)) + throw new Win32Exception("LookupPrivilegeNameW() failed"); - return linkedToken; - } - finally - { - Marshal.FreeHGlobal(pTokenInfo); - } + 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) |