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
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; // 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... // 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; 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,44 +760,59 @@ namespace AnsibleBecome
private static IntPtr GetSystemUserHandle() private static IntPtr GetSystemUserHandle()
{ {
uint array_byte_size = 1024 * sizeof(uint); // According to CreateProcessWithTokenW we require a token with
IntPtr[] pids = new IntPtr[1024]; // TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY
uint bytes_copied; // Also add in TOKEN_IMPERSONATE so we can get an impersontated token
TokenAccessLevels desired_access = TokenAccessLevels.Query |
if (!EnumProcesses(pids, array_byte_size, out bytes_copied)) TokenAccessLevels.Duplicate |
TokenAccessLevels.AssignPrimary |
TokenAccessLevels.Impersonate;
foreach (System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses())
{ {
throw new Win32Exception("Failed to enumerate processes"); using (process)
}
// 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; IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_INFORMATION, false, (UInt32)process.Id);
// According to CreateProcessWithTokenW we require a token with if (hProcess == IntPtr.Zero)
// TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY continue;
// Also add in TOKEN_IMPERSONATE so we can get an impersontated token
TokenAccessLevels desired_access = TokenAccessLevels.Query | try
TokenAccessLevels.Duplicate |
TokenAccessLevels.AssignPrimary |
TokenAccessLevels.Impersonate;
if (OpenProcessToken(hProcess, desired_access, out hToken))
{ {
string sid = GetTokenUserSID(hToken); IntPtr hToken = IntPtr.Zero;
if (sid == "S-1-5-18") if (!OpenProcessToken(hProcess, desired_access, out hToken))
continue;
try
{ {
CloseHandle(hProcess); string sid = GetTokenUserSID(hToken);
return 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);
} }
} }
finally
CloseHandle(hToken); {
CloseHandle(hProcess);
}
} }
CloseHandle(hProcess);
} }
return IntPtr.Zero; return IntPtr.Zero;
@ -779,33 +820,12 @@ namespace AnsibleBecome
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)
{
using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, TokenInformationClass.TokenPrivileges))
{ {
if (!GetTokenInformation(hToken, TokenInformationClass.TokenElevationType, pTokenInfo, sizeof(int), out requestedLength)) TOKEN_PRIVILEGES tokenPrivileges = (TOKEN_PRIVILEGES)Marshal.PtrToStructure(
throw new Win32Exception("Unable to get TokenElevationType"); 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; }
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()));
GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, IntPtr.Zero, 0, out 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()));
IntPtr pLinkedToken = Marshal.AllocHGlobal((int)requestedLength); return tokenInfo;
}
if (!GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, pLinkedToken, requestedLength, out requestedLength)) private static string GetPrivilegeName(LUID luid)
throw new Win32Exception("Unable to get linked token"); {
UInt32 nameLen = 0;
LookupPrivilegeNameW(null, ref luid, null, ref nameLen);
IntPtr linkedToken = Marshal.ReadIntPtr(pLinkedToken); StringBuilder name = new StringBuilder((int)(nameLen + 1));
if (!LookupPrivilegeNameW(null, ref luid, name, ref nameLen))
throw new Win32Exception("LookupPrivilegeNameW() failed");
Marshal.FreeHGlobal(pLinkedToken); return name.ToString();
}
return linkedToken; private static void PtrToStructureArray<T>(T[] array, IntPtr ptr)
} {
finally IntPtr ptrOffset = ptr;
{ for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T))))
Marshal.FreeHGlobal(pTokenInfo); array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T));
}
} }
private static void GrantAccessToWindowStationAndDesktop(SecurityIdentifier account) private static void GrantAccessToWindowStationAndDesktop(SecurityIdentifier account)

Loading…
Cancel
Save