diff --git a/Src/DSInternals.PowerShell/Commands/Base/SamCommandBase.cs b/Src/DSInternals.PowerShell/Commands/Base/SamCommandBase.cs index 60890e39..f769143c 100644 --- a/Src/DSInternals.PowerShell/Commands/Base/SamCommandBase.cs +++ b/Src/DSInternals.PowerShell/Commands/Base/SamCommandBase.cs @@ -7,7 +7,6 @@ namespace DSInternals.PowerShell.Commands; public abstract class SamCommandBase : PSCmdletEx, IDisposable { - // TODO: Safe Critical everywhere? private const string DefaultServer = "localhost"; private string server; @@ -61,13 +60,13 @@ protected override void BeginProcessing() WriteDebug($"Connecting to SAM server {this.Server}."); try { - NetworkCredential netCred = this.Credential?.GetNetworkCredential(); - this.SamServer = new SamServer(this.Server, SamServerAccessMask.LookupDomain | SamServerAccessMask.EnumerateDomains, netCred); + NetworkCredential? netCred = this.Credential?.GetNetworkCredential(); + this.SamServer = new(this.Server, SamServerAccessMask.LookupDomain | SamServerAccessMask.EnumerateDomains, netCred, useNamedPipes: true); } catch (Win32Exception ex) { ErrorCategory category = ((Win32ErrorCode)ex.NativeErrorCode).ToPSCategory(); - ErrorRecord error = new ErrorRecord(ex, "WinAPIErrorConnect", category, this.Server); + ErrorRecord error = new(ex, "WinAPIErrorConnect", category, this.Server); // Terminate on this error: this.ThrowTerminatingError(error); } diff --git a/Src/DSInternals.SAM/Interop/Enums/NetCancelOptions.cs b/Src/DSInternals.SAM/Interop/Enums/NetCancelOptions.cs new file mode 100644 index 00000000..4bb1eb2e --- /dev/null +++ b/Src/DSInternals.SAM/Interop/Enums/NetCancelOptions.cs @@ -0,0 +1,22 @@ +using System; +using Windows.Win32.NetworkManagement.WNet; + + namespace DSInternals.SAM.Interop; + + /// + /// Specifies the type of disconnection to perform when calling WNetCancelConnection2. + /// + /// https://learn.microsoft.com/windows/win32/api/winnetwk/nf-winnetwk-wnetcancelconnection2w + [Flags] + internal enum NetCancelOptions : uint + { + /// + /// The system does not update the user profile with information about the disconnection. + /// + NoUpdate = 0U, + + /// + /// The system updates the user profile with the information that the connection is no longer a persistent one. + /// + UpdateProfile = (uint)NET_CONNECT_FLAGS.CONNECT_UPDATE_PROFILE + } diff --git a/Src/DSInternals.SAM/Interop/NamedPipeConnection.cs b/Src/DSInternals.SAM/Interop/NamedPipeConnection.cs new file mode 100644 index 00000000..4e410c3e --- /dev/null +++ b/Src/DSInternals.SAM/Interop/NamedPipeConnection.cs @@ -0,0 +1,51 @@ +using System.Net; +using DSInternals.Common; +using DSInternals.Common.Interop; +using Windows.Win32.NetworkManagement.WNet; + +namespace DSInternals.SAM.Interop; + +/// +/// Represents an authenticated SMB connection to a remote server's IPC$ share, +/// to be used by SAM RPC over named pipes (ncacn_np). +/// +internal sealed class NamedPipeConnection : IDisposable +{ + private readonly string _shareName; + + internal NamedPipeConnection(string server, NetworkCredential? credential) + { + ArgumentException.ThrowIfNullOrWhiteSpace(server); + + _shareName = $"\\\\{server}\\IPC$"; + + // Disconnect from the IPC share first in case of a preexisting connection. Ignore any errors. + Disconnect(); + + // Connect using provided credentials + Win32ErrorCode result = NativeMethods.WNetAddConnection2( + _shareName, + credential, + NET_CONNECT_FLAGS.CONNECT_TEMPORARY + ); + + Validator.AssertSuccess(result); + } + + private void Disconnect() + { + // Ignore errors during disconnect + NativeMethods.WNetCancelConnection2(_shareName, NetCancelOptions.NoUpdate, force: true); + } + + public void Dispose() + { + Disconnect(); + GC.SuppressFinalize(this); + } + + ~NamedPipeConnection() + { + Disconnect(); + } +} diff --git a/Src/DSInternals.SAM/Interop/NativeMethods.Mpr.cs b/Src/DSInternals.SAM/Interop/NativeMethods.Mpr.cs new file mode 100644 index 00000000..1cc97805 --- /dev/null +++ b/Src/DSInternals.SAM/Interop/NativeMethods.Mpr.cs @@ -0,0 +1,66 @@ +using System.Net; +using System.Runtime.InteropServices; +using DSInternals.Common; +using DSInternals.Common.Interop; +using Windows.Win32.Foundation; +using Windows.Win32.NetworkManagement.WNet; + +namespace DSInternals.SAM.Interop; + +/// +/// Contains P/Invoke signatures for mpr.dll functions. +/// +internal static partial class NativeMethods +{ + private const string Mpr = "mpr.dll"; + + /// + /// Makes a connection to a network resource using the specified credentials. + /// + /// The remote network resource to connect to (e.g., \\server\IPC$). + /// The credentials to use for the connection, or null to use the default credentials. + /// A set of connection options. + internal static unsafe Win32ErrorCode WNetAddConnection2(string shareName, NetworkCredential? credential, NET_CONNECT_FLAGS flags) + { + fixed (char* remoteNamePtr = shareName) + { + NETRESOURCEW resource = new() + { + dwScope = NET_RESOURCE_SCOPE.RESOURCE_GLOBALNET, + dwType = NET_RESOURCE_TYPE.RESOURCETYPE_ANY, + lpRemoteName = new PWSTR(remoteNamePtr) + }; + + string? userName = credential?.GetLogonName(); + IntPtr passwordPtr = credential != null + ? Marshal.SecureStringToGlobalAllocUnicode(credential.SecurePassword) + : IntPtr.Zero; + + try + { + return WNetAddConnection2(resource, passwordPtr, userName, flags); + } + finally + { + if (passwordPtr != IntPtr.Zero) + { + Marshal.ZeroFreeGlobalAllocUnicode(passwordPtr); + } + } + } + } + + /// https://learn.microsoft.com/windows/win32/api/winnetwk/nf-winnetwk-wnetaddconnection2w + [DllImport(Mpr, CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WNetAddConnection2W")] + private static extern Win32ErrorCode WNetAddConnection2(in NETRESOURCEW netResource, IntPtr password, string? userName, NET_CONNECT_FLAGS flags); + + /// + /// The WNetCancelConnection2 function cancels an existing network connection. You can also call the function to remove remembered network connections that are not currently connected. + /// + /// The name of either the redirected local device or the remote network resource to disconnect from. + /// Connection type. + /// Specifies whether the disconnection should occur if there are open files or jobs on the connection. + /// https://learn.microsoft.com/windows/win32/api/winnetwk/nf-winnetwk-wnetcancelconnection2w + [DllImport(Mpr, CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WNetCancelConnection2W")] + internal static extern Win32ErrorCode WNetCancelConnection2(string name, NetCancelOptions flags, [MarshalAs(UnmanagedType.Bool)] bool force); +} diff --git a/Src/DSInternals.SAM/NativeMethods.txt b/Src/DSInternals.SAM/NativeMethods.txt index 730d69d8..e328d57c 100644 --- a/Src/DSInternals.SAM/NativeMethods.txt +++ b/Src/DSInternals.SAM/NativeMethods.txt @@ -16,3 +16,7 @@ MAXIMUM_ALLOWED ACCESS_SYSTEM_SECURITY FILE_ACCESS_RIGHTS SID_NAME_USE +NETRESOURCEW +NET_RESOURCE_SCOPE +NET_RESOURCE_TYPE +NET_CONNECT_FLAGS diff --git a/Src/DSInternals.SAM/Wrappers/SamServer.cs b/Src/DSInternals.SAM/Wrappers/SamServer.cs index f4da261e..f2193caa 100644 --- a/Src/DSInternals.SAM/Wrappers/SamServer.cs +++ b/Src/DSInternals.SAM/Wrappers/SamServer.cs @@ -18,6 +18,11 @@ public sealed class SamServer : SamObject private const uint PreferedMaximumBufferLength = 1000; private const uint InitialEnumerationContext = 0; + /// + /// The authenticated SMB session used for named pipe transport, if any. + /// + private NamedPipeConnection? _namedPipeConnection; + /// /// The name of the server. /// @@ -31,9 +36,14 @@ public string Name /// Initializes a new instance of the class and connects to the specified server with the specified credentials and access mask. /// /// The name of the server. - /// The credentials to use for the connection. /// The access mask to use for the connection. - public SamServer(string serverName, SamServerAccessMask accessMask = SamServerAccessMask.MaximumAllowed, NetworkCredential credential = null) : base(null) + /// The credentials to use for the connection. + /// + /// When true and is provided, + /// an authenticated SMB session is established first so that the RPC connection + /// uses named pipes (ncacn_np) instead of TCP. This is required for password reset operations. + /// + public SamServer(string serverName, SamServerAccessMask accessMask = SamServerAccessMask.MaximumAllowed, NetworkCredential? credential = null, bool useNamedPipes = true) : base(null) { if (string.IsNullOrEmpty(serverName)) { @@ -42,6 +52,12 @@ public SamServer(string serverName, SamServerAccessMask accessMask = SamServerAc this.Name = serverName; + if (useNamedPipes && credential != null) + { + // Establish an authenticated SMB session to force RPC over named pipes + _namedPipeConnection = new NamedPipeConnection(serverName, credential); + } + NtStatus result = (credential != null) ? NativeMethods.SamConnectWithCreds(serverName, out SafeSamHandle serverHandle, accessMask, credential) : NativeMethods.SamConnect(serverName, out serverHandle, accessMask); @@ -129,4 +145,16 @@ public SamDomain OpenDomain(SecurityIdentifier domainSid, SamDomainAccessMask ac Validator.AssertSuccess(result); return new SamDomain(domainHandle); } + + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _namedPipeConnection?.Dispose(); + _namedPipeConnection = null; + } + } }