Skip to content

Commit e555f36

Browse files
Add named pipe (RPC/NP) transport support to SamServer
When SamConnectWithCreds is used, the RPC connection defaults to TCP, which blocks password change operations. Add a useNamedPipes parameter to SamServer that establishes an authenticated SMB session (IPC$) before connecting, forcing RPC over named pipes (ncacn_np). Restores WNet P/Invoke methods (WNetAddConnection2, WNetCancelConnection2) using CsWin32-generated types (NETRESOURCEW, NET_RESOURCE_SCOPE, etc.) instead of the previously deleted manual enum/struct files. Closes #218 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2f14e8f commit e555f36

File tree

6 files changed

+174
-6
lines changed

6 files changed

+174
-6
lines changed

Src/DSInternals.PowerShell/Commands/Base/SamCommandBase.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ namespace DSInternals.PowerShell.Commands;
77

88
public abstract class SamCommandBase : PSCmdletEx, IDisposable
99
{
10-
// TODO: Safe Critical everywhere?
1110
private const string DefaultServer = "localhost";
1211
private string server;
1312

@@ -61,13 +60,13 @@ protected override void BeginProcessing()
6160
WriteDebug($"Connecting to SAM server {this.Server}.");
6261
try
6362
{
64-
NetworkCredential netCred = this.Credential?.GetNetworkCredential();
65-
this.SamServer = new SamServer(this.Server, SamServerAccessMask.LookupDomain | SamServerAccessMask.EnumerateDomains, netCred);
63+
NetworkCredential? netCred = this.Credential?.GetNetworkCredential();
64+
this.SamServer = new(this.Server, SamServerAccessMask.LookupDomain | SamServerAccessMask.EnumerateDomains, netCred, useNamedPipes: true);
6665
}
6766
catch (Win32Exception ex)
6867
{
6968
ErrorCategory category = ((Win32ErrorCode)ex.NativeErrorCode).ToPSCategory();
70-
ErrorRecord error = new ErrorRecord(ex, "WinAPIErrorConnect", category, this.Server);
69+
ErrorRecord error = new(ex, "WinAPIErrorConnect", category, this.Server);
7170
// Terminate on this error:
7271
this.ThrowTerminatingError(error);
7372
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Windows.Win32.NetworkManagement.WNet;
2+
3+
namespace DSInternals.SAM.Interop;
4+
5+
/// <summary>
6+
/// Specifies the type of disconnection to perform when calling WNetCancelConnection2.
7+
/// </summary>
8+
/// <see>https://learn.microsoft.com/windows/win32/api/winnetwk/nf-winnetwk-wnetcancelconnection2w</see>
9+
internal enum NetCancelOptions : uint
10+
{
11+
/// <summary>
12+
/// The system does not update the user profile with information about the disconnection.
13+
/// </summary>
14+
NoUpdate = 0U,
15+
16+
/// <summary>
17+
/// The system updates the user profile with the information that the connection is no longer a persistent one.
18+
/// </summary>
19+
UpdateProfile = (uint)NET_CONNECT_FLAGS.CONNECT_UPDATE_PROFILE
20+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Net;
2+
using DSInternals.Common;
3+
using DSInternals.Common.Interop;
4+
using Windows.Win32.NetworkManagement.WNet;
5+
6+
namespace DSInternals.SAM.Interop;
7+
8+
/// <summary>
9+
/// Represents an authenticated SMB connection to a remote server's IPC$ share,
10+
/// to be used by SAM RPC over named pipes (ncacn_np).
11+
/// </summary>
12+
internal sealed class NamedPipeConnection : IDisposable
13+
{
14+
private readonly string _shareName;
15+
16+
internal NamedPipeConnection(string server, NetworkCredential? credential)
17+
{
18+
ArgumentException.ThrowIfNullOrWhiteSpace(server);
19+
20+
_shareName = $"\\\\{server}\\IPC$";
21+
22+
// Disconnect from the IPC share first in case of a preexisting connection. Ignore any errors.
23+
Disconnect();
24+
25+
// Connect using provided credentials
26+
Win32ErrorCode result = NativeMethods.WNetAddConnection2(
27+
_shareName,
28+
credential,
29+
NET_CONNECT_FLAGS.CONNECT_TEMPORARY
30+
);
31+
32+
Validator.AssertSuccess(result);
33+
}
34+
35+
private void Disconnect()
36+
{
37+
// Ignore errors during disconnect
38+
NativeMethods.WNetCancelConnection2(_shareName, NetCancelOptions.NoUpdate, force: true);
39+
}
40+
41+
public void Dispose()
42+
{
43+
Disconnect();
44+
GC.SuppressFinalize(this);
45+
}
46+
47+
~NamedPipeConnection()
48+
{
49+
Disconnect();
50+
}
51+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System.Net;
2+
using System.Runtime.InteropServices;
3+
using DSInternals.Common;
4+
using DSInternals.Common.Interop;
5+
using Windows.Win32.Foundation;
6+
using Windows.Win32.NetworkManagement.WNet;
7+
8+
namespace DSInternals.SAM.Interop;
9+
10+
/// <summary>
11+
/// Contains P/Invoke signatures for mpr.dll functions.
12+
/// </summary>
13+
internal static partial class NativeMethods
14+
{
15+
private const string Mpr = "mpr.dll";
16+
17+
/// <summary>
18+
/// Makes a connection to a network resource using the specified credentials.
19+
/// </summary>
20+
/// <param name="shareName">The remote network resource to connect to (e.g., \\server\IPC$).</param>
21+
/// <param name="credential">The credentials to use for the connection, or <c>null</c> to use the default credentials.</param>
22+
/// <param name="flags">A set of connection options.</param>
23+
internal static unsafe Win32ErrorCode WNetAddConnection2(string shareName, NetworkCredential? credential, NET_CONNECT_FLAGS flags)
24+
{
25+
fixed (char* remoteNamePtr = shareName)
26+
{
27+
NETRESOURCEW resource = new()
28+
{
29+
dwScope = NET_RESOURCE_SCOPE.RESOURCE_GLOBALNET,
30+
dwType = NET_RESOURCE_TYPE.RESOURCETYPE_ANY,
31+
lpRemoteName = new PWSTR(remoteNamePtr)
32+
};
33+
34+
string? userName = credential?.GetLogonName();
35+
IntPtr passwordPtr = credential != null
36+
? Marshal.SecureStringToGlobalAllocUnicode(credential.SecurePassword)
37+
: IntPtr.Zero;
38+
39+
try
40+
{
41+
return WNetAddConnection2(resource, passwordPtr, userName, flags);
42+
}
43+
finally
44+
{
45+
if (passwordPtr != IntPtr.Zero)
46+
{
47+
Marshal.ZeroFreeGlobalAllocUnicode(passwordPtr);
48+
}
49+
}
50+
}
51+
}
52+
53+
/// <see>https://learn.microsoft.com/windows/win32/api/winnetwk/nf-winnetwk-wnetaddconnection2w</see>
54+
[DllImport(Mpr, CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WNetAddConnection2W")]
55+
private static extern Win32ErrorCode WNetAddConnection2(in NETRESOURCEW netResource, IntPtr password, string? userName, NET_CONNECT_FLAGS flags);
56+
57+
/// <summary>
58+
/// The WNetCancelConnection2 function cancels an existing network connection. You can also call the function to remove remembered network connections that are not currently connected.
59+
/// </summary>
60+
/// <param name="name">The name of either the redirected local device or the remote network resource to disconnect from.</param>
61+
/// <param name="flags">Connection type.</param>
62+
/// <param name="force">Specifies whether the disconnection should occur if there are open files or jobs on the connection.</param>
63+
/// <see>https://learn.microsoft.com/windows/win32/api/winnetwk/nf-winnetwk-wnetcancelconnection2w</see>
64+
[DllImport(Mpr, CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WNetCancelConnection2W")]
65+
internal static extern Win32ErrorCode WNetCancelConnection2(string name, NetCancelOptions flags, [MarshalAs(UnmanagedType.Bool)] bool force);
66+
}

Src/DSInternals.SAM/NativeMethods.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ MAXIMUM_ALLOWED
1616
ACCESS_SYSTEM_SECURITY
1717
FILE_ACCESS_RIGHTS
1818
SID_NAME_USE
19+
NETRESOURCEW
20+
NET_RESOURCE_SCOPE
21+
NET_RESOURCE_TYPE
22+
NET_CONNECT_FLAGS

Src/DSInternals.SAM/Wrappers/SamServer.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public sealed class SamServer : SamObject
1818
private const uint PreferedMaximumBufferLength = 1000;
1919
private const uint InitialEnumerationContext = 0;
2020

21+
/// <summary>
22+
/// The authenticated SMB session used for named pipe transport, if any.
23+
/// </summary>
24+
private NamedPipeConnection? _namedPipeConnection;
25+
2126
/// <summary>
2227
/// The name of the server.
2328
/// </summary>
@@ -31,9 +36,14 @@ public string Name
3136
/// Initializes a new instance of the <see cref="SamServer"/> class and connects to the specified server with the specified credentials and access mask.
3237
/// </summary>
3338
/// <param name="serverName">The name of the server.</param>
34-
/// <param name="credential">The credentials to use for the connection.</param>
3539
/// <param name="accessMask">The access mask to use for the connection.</param>
36-
public SamServer(string serverName, SamServerAccessMask accessMask = SamServerAccessMask.MaximumAllowed, NetworkCredential credential = null) : base(null)
40+
/// <param name="credential">The credentials to use for the connection.</param>
41+
/// <param name="useNamedPipes">
42+
/// When <c>true</c> and <paramref name="credential"/> is provided,
43+
/// an authenticated SMB session is established first so that the RPC connection
44+
/// uses named pipes (ncacn_np) instead of TCP. This is required for password reset operations.
45+
/// </param>
46+
public SamServer(string serverName, SamServerAccessMask accessMask = SamServerAccessMask.MaximumAllowed, NetworkCredential? credential = null, bool useNamedPipes = true) : base(null)
3747
{
3848
if (string.IsNullOrEmpty(serverName))
3949
{
@@ -42,6 +52,12 @@ public SamServer(string serverName, SamServerAccessMask accessMask = SamServerAc
4252

4353
this.Name = serverName;
4454

55+
if (useNamedPipes && credential != null)
56+
{
57+
// Establish an authenticated SMB session to force RPC over named pipes
58+
_namedPipeConnection = new NamedPipeConnection(serverName, credential);
59+
}
60+
4561
NtStatus result = (credential != null) ?
4662
NativeMethods.SamConnectWithCreds(serverName, out SafeSamHandle serverHandle, accessMask, credential) :
4763
NativeMethods.SamConnect(serverName, out serverHandle, accessMask);
@@ -129,4 +145,16 @@ public SamDomain OpenDomain(SecurityIdentifier domainSid, SamDomainAccessMask ac
129145
Validator.AssertSuccess(result);
130146
return new SamDomain(domainHandle);
131147
}
148+
149+
/// <inheritdoc/>
150+
protected override void Dispose(bool disposing)
151+
{
152+
if (disposing)
153+
{
154+
_namedPipeConnection?.Dispose();
155+
_namedPipeConnection = null;
156+
}
157+
158+
base.Dispose(disposing);
159+
}
132160
}

0 commit comments

Comments
 (0)