using Microsoft.Win32.SafeHandles; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using System.Text; using Ansible.AccessToken; using Ansible.Process; namespace Ansible.Become { internal class NativeHelpers { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct KERB_S4U_LOGON { public UInt32 MessageType; public UInt32 Flags; public LSA_UNICODE_STRING ClientUpn; public LSA_UNICODE_STRING ClientRealm; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct LSA_STRING { public UInt16 Length; public UInt16 MaximumLength; [MarshalAs(UnmanagedType.LPStr)] public string Buffer; public static implicit operator string(LSA_STRING s) { return s.Buffer; } public static implicit operator LSA_STRING(string s) { if (s == null) s = ""; LSA_STRING lsaStr = new LSA_STRING { Buffer = s, Length = (UInt16)s.Length, MaximumLength = (UInt16)(s.Length + 1), }; return lsaStr; } } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct LSA_UNICODE_STRING { public UInt16 Length; public UInt16 MaximumLength; public IntPtr Buffer; } [StructLayout(LayoutKind.Sequential)] public struct SECURITY_LOGON_SESSION_DATA { public UInt32 Size; public Luid LogonId; public LSA_UNICODE_STRING UserName; public LSA_UNICODE_STRING LogonDomain; public LSA_UNICODE_STRING AuthenticationPackage; public SECURITY_LOGON_TYPE LogonType; } [StructLayout(LayoutKind.Sequential)] public struct TOKEN_SOURCE { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public char[] SourceName; public Luid SourceIdentifier; } public enum SECURITY_LOGON_TYPE { System = 0, // Used only by the System account Interactive = 2, Network, Batch, Service, Proxy, Unlock, NetworkCleartext, NewCredentials, RemoteInteractive, CachedInteractive, CachedRemoteInteractive, CachedUnlock } } internal class NativeMethods { [DllImport("advapi32.dll", SetLastError = true)] public static extern bool AllocateLocallyUniqueId( out Luid Luid); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool CreateProcessWithTokenW( SafeNativeHandle hToken, LogonFlags dwLogonFlags, [MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName, StringBuilder lpCommandLine, Process.NativeHelpers.ProcessCreationFlags dwCreationFlags, Process.SafeMemoryBuffer lpEnvironment, [MarshalAs(UnmanagedType.LPWStr)] string lpCurrentDirectory, Process.NativeHelpers.STARTUPINFOEX lpStartupInfo, out Process.NativeHelpers.PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll")] public static extern UInt32 GetCurrentThreadId(); [DllImport("user32.dll", SetLastError = true)] public static extern NoopSafeHandle GetProcessWindowStation(); [DllImport("user32.dll", SetLastError = true)] public static extern NoopSafeHandle GetThreadDesktop( UInt32 dwThreadId); [DllImport("secur32.dll", SetLastError = true)] public static extern UInt32 LsaDeregisterLogonProcess( IntPtr LsaHandle); [DllImport("secur32.dll", SetLastError = true)] public static extern UInt32 LsaFreeReturnBuffer( IntPtr Buffer); [DllImport("secur32.dll", SetLastError = true)] public static extern UInt32 LsaGetLogonSessionData( ref Luid LogonId, out SafeLsaMemoryBuffer ppLogonSessionData); [DllImport("secur32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern UInt32 LsaLogonUser( SafeLsaHandle LsaHandle, NativeHelpers.LSA_STRING OriginName, LogonType LogonType, UInt32 AuthenticationPackage, IntPtr AuthenticationInformation, UInt32 AuthenticationInformationLength, IntPtr LocalGroups, NativeHelpers.TOKEN_SOURCE SourceContext, out SafeLsaMemoryBuffer ProfileBuffer, out UInt32 ProfileBufferLength, out Luid LogonId, out SafeNativeHandle Token, out IntPtr Quotas, out UInt32 SubStatus); [DllImport("secur32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern UInt32 LsaLookupAuthenticationPackage( SafeLsaHandle LsaHandle, NativeHelpers.LSA_STRING PackageName, out UInt32 AuthenticationPackage); [DllImport("advapi32.dll")] public static extern UInt32 LsaNtStatusToWinError( UInt32 Status); [DllImport("secur32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern UInt32 LsaRegisterLogonProcess( NativeHelpers.LSA_STRING LogonProcessName, out SafeLsaHandle LsaHandle, out IntPtr SecurityMode); } internal class SafeLsaHandle : SafeHandleZeroOrMinusOneIsInvalid { public SafeLsaHandle() : base(true) { } [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] protected override bool ReleaseHandle() { UInt32 res = NativeMethods.LsaDeregisterLogonProcess(handle); return res == 0; } } internal class SafeLsaMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid { public SafeLsaMemoryBuffer() : base(true) { } [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] protected override bool ReleaseHandle() { UInt32 res = NativeMethods.LsaFreeReturnBuffer(handle); return res == 0; } } internal class NoopSafeHandle : SafeHandle { public NoopSafeHandle() : base(IntPtr.Zero, false) { } public override bool IsInvalid { get { return false; } } [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] protected override bool ReleaseHandle() { return true; } } [Flags] public enum LogonFlags { WithProfile = 0x00000001, NetcredentialsOnly = 0x00000002 } public class BecomeUtil { private static List SERVICE_SIDS = new List() { "S-1-5-18", // NT AUTHORITY\SYSTEM "S-1-5-19", // NT AUTHORITY\LocalService "S-1-5-20" // NT AUTHORITY\NetworkService }; private static int WINDOWS_STATION_ALL_ACCESS = 0x000F037F; private static int DESKTOP_RIGHTS_ALL_ACCESS = 0x000F01FF; public static Result CreateProcessAsUser(string username, string password, string command) { return CreateProcessAsUser(username, password, LogonFlags.WithProfile, LogonType.Interactive, null, command, null, null, ""); } public static Result CreateProcessAsUser(string username, string password, LogonFlags logonFlags, LogonType logonType, string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, IDictionary environment, string stdin) { byte[] stdinBytes; if (String.IsNullOrEmpty(stdin)) stdinBytes = new byte[0]; else { if (!stdin.EndsWith(Environment.NewLine)) stdin += Environment.NewLine; stdinBytes = new UTF8Encoding(false).GetBytes(stdin); } return CreateProcessAsUser(username, password, logonFlags, logonType, lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdinBytes); } /// /// Creates a process as another user account. This method will attempt to run as another user with the /// highest possible permissions available. The main privilege required is the SeDebugPrivilege, without /// this privilege you can only run as a local or domain user if the username and password is specified. /// /// The username of the runas user /// The password of the runas user /// LogonFlags to control how to logon a user when the password is specified /// Controls what type of logon is used, this only applies when the password is specified /// The name of the executable or batch file to executable /// The command line to execute, typically this includes lpApplication as the first argument /// The full path to the current directory for the process, null will have the same cwd as the calling process /// A dictionary of key/value pairs to define the new process environment /// Bytes sent to the stdin pipe /// Ansible.Process.Result object that contains the command output and return code public static Result CreateProcessAsUser(string username, string password, LogonFlags logonFlags, LogonType logonType, string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, IDictionary environment, byte[] stdin) { // While we use STARTUPINFOEX having EXTENDED_STARTUPINFO_PRESENT causes a parameter validation error Process.NativeHelpers.ProcessCreationFlags creationFlags = Process.NativeHelpers.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT; Process.NativeHelpers.PROCESS_INFORMATION pi = new Process.NativeHelpers.PROCESS_INFORMATION(); Process.NativeHelpers.STARTUPINFOEX si = new Process.NativeHelpers.STARTUPINFOEX(); si.startupInfo.dwFlags = Process.NativeHelpers.StartupInfoFlags.USESTDHANDLES; SafeFileHandle stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinRead, stdinWrite; ProcessUtil.CreateStdioPipes(si, out stdoutRead, out stdoutWrite, out stderrRead, out stderrWrite, out stdinRead, out stdinWrite); FileStream stdinStream = new FileStream(stdinWrite, FileAccess.Write); // $null from PowerShell ends up as an empty string, we need to convert back as an empty string doesn't // make sense for these parameters if (lpApplicationName == "") lpApplicationName = null; if (lpCurrentDirectory == "") lpCurrentDirectory = null; // A user may have 2 tokens, 1 limited and 1 elevated. GetUserTokens will return both token to ensure // we don't close one of the pairs while the process is still running. If the process tries to retrieve // one of the pairs and the token handle is closed then it will fail with ERROR_NO_SUCH_LOGON_SESSION. List userTokens = GetUserTokens(username, password, logonType); try { using (Process.SafeMemoryBuffer lpEnvironment = ProcessUtil.CreateEnvironmentPointer(environment)) { bool launchSuccess = false; StringBuilder commandLine = new StringBuilder(lpCommandLine); foreach (SafeNativeHandle token in userTokens) { // GetUserTokens could return null if an elevated token could not be retrieved. if (token == null) continue; if (NativeMethods.CreateProcessWithTokenW(token, logonFlags, lpApplicationName, commandLine, creationFlags, lpEnvironment, lpCurrentDirectory, si, out pi)) { launchSuccess = true; break; } } if (!launchSuccess) throw new Process.Win32Exception("CreateProcessWithTokenW() failed"); } return ProcessUtil.WaitProcess(stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinStream, stdin, pi.hProcess); } finally { userTokens.Where(t => t != null).ToList().ForEach(t => t.Dispose()); } } private static List GetUserTokens(string username, string password, LogonType logonType) { List userTokens = new List(); SafeNativeHandle systemToken = null; bool impersonated = false; string becomeSid = username; if (logonType != LogonType.NewCredentials) { // If prefixed with .\, we are becoming a local account, strip the prefix if (username.StartsWith(".\\")) username = username.Substring(2); NTAccount account = new NTAccount(username); becomeSid = ((SecurityIdentifier)account.Translate(typeof(SecurityIdentifier))).Value; // Grant access to the current Windows Station and Desktop to the become user GrantAccessToWindowStationAndDesktop(account); // 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 mostPrivileges = becomeSid == "S-1-5-18"; systemToken = GetPrimaryTokenForUser(new SecurityIdentifier("S-1-5-18"), new List() { "SeTcbPrivilege" }, mostPrivileges); if (systemToken != null) { try { TokenUtil.ImpersonateToken(systemToken); impersonated = true; } catch (Process.Win32Exception) { } // We tried, just rely on current user's permissions. } } // We require impersonation if becoming a service sid or becoming a user without a password if (!impersonated && (SERVICE_SIDS.Contains(becomeSid) || String.IsNullOrEmpty(password))) throw new Exception("Failed to get token for NT AUTHORITY\\SYSTEM required for become as a service account or an account without a password"); 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 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 SecurityIdentifier sid = new SecurityIdentifier(becomeSid); SafeNativeHandle becomeToken = GetPrimaryTokenForUser(sid); if (becomeToken != null) { userTokens.Add(GetElevatedToken(becomeToken)); userTokens.Add(becomeToken); } else { becomeToken = GetS4UTokenForUser(sid, logonType); userTokens.Add(null); userTokens.Add(becomeToken); } } else { string domain = null; switch (becomeSid) { case "S-1-5-19": logonType = LogonType.Service; domain = "NT AUTHORITY"; username = "LocalService"; break; case "S-1-5-20": logonType = LogonType.Service; domain = "NT AUTHORITY"; username = "NetworkService"; break; default: // Trying to become a local or domain account if (username.Contains(@"\")) { string[] userSplit = username.Split(new char[1] { '\\' }, 2); domain = userSplit[0]; username = userSplit[1]; } else if (!username.Contains("@")) domain = "."; break; } SafeNativeHandle hToken = TokenUtil.LogonUser(username, domain, password, logonType, LogonProvider.Default); // Get the elevated token for a local/domain accounts only if (!SERVICE_SIDS.Contains(becomeSid)) userTokens.Add(GetElevatedToken(hToken)); userTokens.Add(hToken); } } finally { if (impersonated) TokenUtil.RevertToSelf(); } return userTokens; } private static SafeNativeHandle GetPrimaryTokenForUser(SecurityIdentifier sid, List requiredPrivileges = null, bool mostPrivileges = false) { // 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 impersonated token TokenAccessLevels dwAccess = TokenAccessLevels.Query | TokenAccessLevels.Duplicate | TokenAccessLevels.AssignPrimary | TokenAccessLevels.Impersonate; SafeNativeHandle userToken = null; int privilegeCount = 0; foreach (SafeNativeHandle hToken in TokenUtil.EnumerateUserTokens(sid, dwAccess)) { // Filter out any Network logon tokens, using become with that is useless when S4U // can give us a Batch logon NativeHelpers.SECURITY_LOGON_TYPE tokenLogonType = GetTokenLogonType(hToken); if (tokenLogonType == NativeHelpers.SECURITY_LOGON_TYPE.Network) continue; List 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 (mostPrivileges && privilegeCount >= actualPrivileges.Count) continue; // Check that the required privileges are on the token if (requiredPrivileges != null) { int missing = requiredPrivileges.Where(x => !actualPrivileges.Contains(x)).Count(); if (missing > 0) continue; } // Duplicate the token to convert it to a primary token with the access level required. try { 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 (!mostPrivileges) break; } return userToken; } private static SafeNativeHandle GetS4UTokenForUser(SecurityIdentifier sid, LogonType logonType) { NTAccount becomeAccount = (NTAccount)sid.Translate(typeof(NTAccount)); string[] userSplit = becomeAccount.Value.Split(new char[1] { '\\' }, 2); string domainName = userSplit[0]; string username = userSplit[1]; bool domainUser = domainName.ToLowerInvariant() != Environment.MachineName.ToLowerInvariant(); NativeHelpers.LSA_STRING logonProcessName = "ansible"; SafeLsaHandle lsaHandle; IntPtr securityMode; UInt32 res = NativeMethods.LsaRegisterLogonProcess(logonProcessName, out lsaHandle, out securityMode); if (res != 0) throw new Process.Win32Exception((int)NativeMethods.LsaNtStatusToWinError(res), "LsaRegisterLogonProcess() failed"); using (lsaHandle) { NativeHelpers.LSA_STRING packageName = domainUser ? "Kerberos" : "MICROSOFT_AUTHENTICATION_PACKAGE_V1_0"; UInt32 authPackage; res = NativeMethods.LsaLookupAuthenticationPackage(lsaHandle, packageName, out authPackage); if (res != 0) throw new Process.Win32Exception((int)NativeMethods.LsaNtStatusToWinError(res), String.Format("LsaLookupAuthenticationPackage({0}) failed", (string)packageName)); int usernameLength = username.Length * sizeof(char); int domainLength = domainName.Length * sizeof(char); int authInfoLength = (Marshal.SizeOf(typeof(NativeHelpers.KERB_S4U_LOGON)) + usernameLength + domainLength); IntPtr authInfo = Marshal.AllocHGlobal((int)authInfoLength); try { IntPtr usernamePtr = IntPtr.Add(authInfo, Marshal.SizeOf(typeof(NativeHelpers.KERB_S4U_LOGON))); IntPtr domainPtr = IntPtr.Add(usernamePtr, usernameLength); // KERB_S4U_LOGON has the same structure as MSV1_0_S4U_LOGON (local accounts) NativeHelpers.KERB_S4U_LOGON s4uLogon = new NativeHelpers.KERB_S4U_LOGON { MessageType = 12, // KerbS4ULogon Flags = 0, ClientUpn = new NativeHelpers.LSA_UNICODE_STRING { Length = (UInt16)usernameLength, MaximumLength = (UInt16)usernameLength, Buffer = usernamePtr, }, ClientRealm = new NativeHelpers.LSA_UNICODE_STRING { Length = (UInt16)domainLength, MaximumLength = (UInt16)domainLength, Buffer = domainPtr, }, }; Marshal.StructureToPtr(s4uLogon, authInfo, false); Marshal.Copy(username.ToCharArray(), 0, usernamePtr, username.Length); Marshal.Copy(domainName.ToCharArray(), 0, domainPtr, domainName.Length); Luid sourceLuid; if (!NativeMethods.AllocateLocallyUniqueId(out sourceLuid)) throw new Process.Win32Exception("AllocateLocallyUniqueId() failed"); NativeHelpers.TOKEN_SOURCE tokenSource = new NativeHelpers.TOKEN_SOURCE { SourceName = "ansible\0".ToCharArray(), SourceIdentifier = sourceLuid, }; // Only Batch or Network will work with S4U, prefer Batch but use Network if asked LogonType lsaLogonType = logonType == LogonType.Network ? LogonType.Network : LogonType.Batch; SafeLsaMemoryBuffer profileBuffer; UInt32 profileBufferLength; Luid logonId; SafeNativeHandle hToken; IntPtr quotas; UInt32 subStatus; res = NativeMethods.LsaLogonUser(lsaHandle, logonProcessName, lsaLogonType, authPackage, authInfo, (UInt32)authInfoLength, IntPtr.Zero, tokenSource, out profileBuffer, out profileBufferLength, out logonId, out hToken, out quotas, out subStatus); if (res != 0) throw new Process.Win32Exception((int)NativeMethods.LsaNtStatusToWinError(res), String.Format("LsaLogonUser() failed with substatus {0}", subStatus)); profileBuffer.Dispose(); return hToken; } finally { Marshal.FreeHGlobal(authInfo); } } } private static SafeNativeHandle GetElevatedToken(SafeNativeHandle hToken) { TokenElevationType tet = TokenUtil.GetTokenElevationType(hToken); // We already have the best token we can get, no linked token is really available. if (tet != TokenElevationType.Limited) return null; SafeNativeHandle linkedToken = TokenUtil.GetTokenLinkedToken(hToken); TokenStatistics tokenStats = TokenUtil.GetTokenStatistics(linkedToken); // We can only use a token if it's a primary one (we had the SeTcbPrivilege set) if (tokenStats.TokenType == TokenType.Primary) return linkedToken; else return null; } private static NativeHelpers.SECURITY_LOGON_TYPE GetTokenLogonType(SafeNativeHandle hToken) { TokenStatistics stats = TokenUtil.GetTokenStatistics(hToken); SafeLsaMemoryBuffer sessionDataPtr; UInt32 res = NativeMethods.LsaGetLogonSessionData(ref stats.AuthenticationId, out sessionDataPtr); if (res != 0) // Default to Network, if we weren't able to get the actual type treat it as an error and assume // we don't want to run a process with the token return NativeHelpers.SECURITY_LOGON_TYPE.Network; using (sessionDataPtr) { NativeHelpers.SECURITY_LOGON_SESSION_DATA sessionData = (NativeHelpers.SECURITY_LOGON_SESSION_DATA)Marshal.PtrToStructure( sessionDataPtr.DangerousGetHandle(), typeof(NativeHelpers.SECURITY_LOGON_SESSION_DATA)); return sessionData.LogonType; } } private static void GrantAccessToWindowStationAndDesktop(IdentityReference account) { GrantAccess(account, NativeMethods.GetProcessWindowStation(), WINDOWS_STATION_ALL_ACCESS); GrantAccess(account, NativeMethods.GetThreadDesktop(NativeMethods.GetCurrentThreadId()), DESKTOP_RIGHTS_ALL_ACCESS); } private static void GrantAccess(IdentityReference account, NoopSafeHandle handle, int accessMask) { GenericSecurity security = new GenericSecurity(false, ResourceType.WindowObject, handle, AccessControlSections.Access); security.AddAccessRule(new GenericAccessRule(account, accessMask, AccessControlType.Allow)); security.Persist(handle, AccessControlSections.Access); } private class GenericSecurity : NativeObjectSecurity { public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested) : base(isContainer, resType, objectHandle, sectionsRequested) { } public new void Persist(SafeHandle handle, AccessControlSections includeSections) { base.Persist(handle, includeSections); } public new void AddAccessRule(AccessRule rule) { base.AddAccessRule(rule); } public override Type AccessRightType { get { throw new NotImplementedException(); } } public override AccessRule AccessRuleFactory(System.Security.Principal.IdentityReference identityReference, int accessMask, bool isInherited, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, AccessControlType type) { throw new NotImplementedException(); } public override Type AccessRuleType { get { return typeof(AccessRule); } } public override AuditRule AuditRuleFactory(System.Security.Principal.IdentityReference identityReference, int accessMask, bool isInherited, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, AuditFlags flags) { throw new NotImplementedException(); } public override Type AuditRuleType { get { return typeof(AuditRule); } } } private class GenericAccessRule : AccessRule { public GenericAccessRule(IdentityReference identity, int accessMask, AccessControlType type) : base(identity, accessMask, false, InheritanceFlags.None, PropagationFlags.None, type) { } } } }