win become - fix token elevation issues

This is an implementation of 8bffcf8e50
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.
pull/51919/head
Jordan Borean 7 years ago committed by Toshio Kuratomi
parent 7f33c7def5
commit cc5088c9e1

@ -0,0 +1,2 @@
bugfixes:
- win become - Fix some scenarios where become failed to create an elevated process

@ -211,6 +211,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.Security.Principal; using System.Security.Principal;
@ -265,6 +266,25 @@ namespace AnsibleBecome
} }
} }
[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)] [StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION public struct PROCESS_INFORMATION
{ {
@ -281,6 +301,14 @@ namespace AnsibleBecome
public int Attributes; 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 struct TOKEN_USER
{ {
public SID_AND_ATTRIBUTES User; public SID_AND_ATTRIBUTES User;
@ -335,6 +363,7 @@ namespace AnsibleBecome
public enum TokenInformationClass public enum TokenInformationClass
{ {
TokenUser = 1, TokenUser = 1,
TokenPrivileges = 3,
TokenType = 8, TokenType = 8,
TokenImpersonationLevel = 9, TokenImpersonationLevel = 9,
TokenElevationType = 18, TokenElevationType = 18,
@ -373,6 +402,26 @@ namespace AnsibleBecome
} }
} }
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 public class Win32Exception : System.ComponentModel.Win32Exception
{ {
private string _msg; private string _msg;
@ -453,23 +502,22 @@ namespace AnsibleBecome
private static extern bool GetTokenInformation( private static extern bool GetTokenInformation(
IntPtr TokenHandle, IntPtr TokenHandle,
TokenInformationClass TokenInformationClass, TokenInformationClass TokenInformationClass,
IntPtr TokenInformation, SafeMemoryBuffer TokenInformation,
uint TokenInformationLength, uint TokenInformationLength,
out uint ReturnLength); out uint ReturnLength);
[DllImport("psapi.dll", SetLastError = true)] [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool EnumProcesses( public static extern bool LookupPrivilegeNameW(
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U4)] string lpSystemName,
[In][Out] IntPtr[] processIds, ref LUID lpLuid,
uint cb, StringBuilder lpName,
[MarshalAs(UnmanagedType.U4)] ref UInt32 cchName);
out uint pBytesReturned);
[DllImport("kernel32.dll", SetLastError = true)] [DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess( private static extern IntPtr OpenProcess(
ProcessAccessFlags processAccess, ProcessAccessFlags processAccess,
bool bInheritHandle, bool bInheritHandle,
IntPtr processId); UInt32 processId);
[DllImport("advapi32.dll", SetLastError = true)] [DllImport("advapi32.dll", SetLastError = true)]
private static extern bool OpenProcessToken( private static extern bool OpenProcessToken(
@ -477,12 +525,6 @@ namespace AnsibleBecome
TokenAccessLevels DesiredAccess, TokenAccessLevels DesiredAccess,
out IntPtr TokenHandle); 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)] [DllImport("advapi32", SetLastError = true)]
private static extern bool DuplicateTokenEx( private static extern bool DuplicateTokenEx(
IntPtr hExistingToken, IntPtr hExistingToken,
@ -633,7 +675,6 @@ namespace AnsibleBecome
try try
{ {
IntPtr hSystemTokenDup = IntPtr.Zero;
if (hSystemToken == IntPtr.Zero && service_sids.Contains(account_sid)) 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 // We need the SYSTEM token if we want to become one of those accounts, fail here
@ -641,28 +682,13 @@ namespace AnsibleBecome
} }
else if (hSystemToken != IntPtr.Zero) else if (hSystemToken != IntPtr.Zero)
{ {
// We have the token, need to duplicate and impersonate // If SYSTEM impersonation failed but we're trying to become a regular user, just proceed;
bool dupResult = DuplicateTokenEx( // might get a limited token in UAC-enabled cases, but better than nothing...
hSystemToken, if (ImpersonateLoggedOnUser(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; impersonated = true;
else if (service_sids.Contains(account_sid)) else if (service_sids.Contains(account_sid))
throw new Win32Exception("Failed to impersonate as SYSTEM account"); 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...
} }
string domain = null; string domain = null;
@ -676,7 +702,7 @@ namespace AnsibleBecome
switch (account_sid) switch (account_sid)
{ {
case "S-1-5-18": case "S-1-5-18":
tokens.Add(hSystemTokenDup); tokens.Add(hSystemToken);
return tokens; return tokens;
case "S-1-5-19": case "S-1-5-19":
username = "LocalService"; username = "LocalService";
@ -734,23 +760,6 @@ namespace AnsibleBecome
private static IntPtr GetSystemUserHandle() 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))
{
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)
{
IntPtr hToken = IntPtr.Zero;
// According to CreateProcessWithTokenW we require a token with // According to CreateProcessWithTokenW we require a token with
// TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY // TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY
// Also add in TOKEN_IMPERSONATE so we can get an impersontated token // Also add in TOKEN_IMPERSONATE so we can get an impersontated token
@ -759,53 +768,64 @@ namespace AnsibleBecome
TokenAccessLevels.AssignPrimary | TokenAccessLevels.AssignPrimary |
TokenAccessLevels.Impersonate; TokenAccessLevels.Impersonate;
if (OpenProcessToken(hProcess, desired_access, out hToken)) foreach (System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses())
{
using (process)
{
IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_INFORMATION, false, (UInt32)process.Id);
if (hProcess == IntPtr.Zero)
continue;
try
{
IntPtr hToken = IntPtr.Zero;
if (!OpenProcessToken(hProcess, desired_access, out hToken))
continue;
try
{ {
string sid = GetTokenUserSID(hToken); string sid = GetTokenUserSID(hToken);
if (sid == "S-1-5-18") 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))
{ {
CloseHandle(hProcess); continue;
return hToken;
} }
return dupToken;
} }
finally
{
CloseHandle(hToken); CloseHandle(hToken);
} }
}
finally
{
CloseHandle(hProcess); CloseHandle(hProcess);
} }
}
}
return IntPtr.Zero; return IntPtr.Zero;
} }
private static string GetTokenUserSID(IntPtr hToken) private static string GetTokenUserSID(IntPtr hToken)
{ {
uint token_length; using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, TokenInformationClass.TokenUser))
string sid;
if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, IntPtr.Zero, 0, out token_length))
{ {
int last_err = Marshal.GetLastWin32Error(); TOKEN_USER tokenUser = (TOKEN_USER)Marshal.PtrToStructure(tokenInfo.DangerousGetHandle(),
if (last_err != 122) // ERROR_INSUFFICIENT_BUFFER typeof(TOKEN_USER));
throw new Win32Exception(last_err, "Failed to get TokenUser length"); 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) private static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
@ -840,38 +860,65 @@ namespace AnsibleBecome
private static IntPtr GetElevatedToken(IntPtr hToken) 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)
{ {
if (!GetTokenInformation(hToken, TokenInformationClass.TokenElevationType, pTokenInfo, sizeof(int), out requestedLength)) using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, TokenInformationClass.TokenPrivileges))
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 return luidAndAttributes.Select(x => GetPrivilegeName(x.Luid)).ToList();
if (tet != TokenElevationType.TokenElevationTypeLimited) }
return hToken; }
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)) return tokenInfo;
throw new Win32Exception("Unable to get linked token"); }
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; return name.ToString();
} }
finally
private static void PtrToStructureArray<T>(T[] array, IntPtr ptr)
{ {
Marshal.FreeHGlobal(pTokenInfo); 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) private static void GrantAccessToWindowStationAndDesktop(SecurityIdentifier account)

Loading…
Cancel
Save