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;
+ }
+ }
}