Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions Src/DSInternals.PowerShell/Commands/Base/SamCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
Expand Down
22 changes: 22 additions & 0 deletions Src/DSInternals.SAM/Interop/Enums/NetCancelOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using Windows.Win32.NetworkManagement.WNet;

namespace DSInternals.SAM.Interop;

/// <summary>
/// Specifies the type of disconnection to perform when calling WNetCancelConnection2.
/// </summary>
/// <see>https://learn.microsoft.com/windows/win32/api/winnetwk/nf-winnetwk-wnetcancelconnection2w</see>
[Flags]
internal enum NetCancelOptions : uint
{
/// <summary>
/// The system does not update the user profile with information about the disconnection.
/// </summary>
NoUpdate = 0U,

/// <summary>
/// The system updates the user profile with the information that the connection is no longer a persistent one.
/// </summary>
UpdateProfile = (uint)NET_CONNECT_FLAGS.CONNECT_UPDATE_PROFILE
}
51 changes: 51 additions & 0 deletions Src/DSInternals.SAM/Interop/NamedPipeConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Net;
using DSInternals.Common;
using DSInternals.Common.Interop;
using Windows.Win32.NetworkManagement.WNet;

namespace DSInternals.SAM.Interop;

/// <summary>
/// Represents an authenticated SMB connection to a remote server's IPC$ share,
/// to be used by SAM RPC over named pipes (ncacn_np).
/// </summary>
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();
}
}
66 changes: 66 additions & 0 deletions Src/DSInternals.SAM/Interop/NativeMethods.Mpr.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Contains P/Invoke signatures for mpr.dll functions.
/// </summary>
internal static partial class NativeMethods
{
private const string Mpr = "mpr.dll";

/// <summary>
/// Makes a connection to a network resource using the specified credentials.
/// </summary>
/// <param name="shareName">The remote network resource to connect to (e.g., \\server\IPC$).</param>
/// <param name="credential">The credentials to use for the connection, or <c>null</c> to use the default credentials.</param>
/// <param name="flags">A set of connection options.</param>
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);
}
}
}
}

/// <see>https://learn.microsoft.com/windows/win32/api/winnetwk/nf-winnetwk-wnetaddconnection2w</see>
[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);

/// <summary>
/// The WNetCancelConnection2 function cancels an existing network connection. You can also call the function to remove remembered network connections that are not currently connected.
/// </summary>
/// <param name="name">The name of either the redirected local device or the remote network resource to disconnect from.</param>
/// <param name="flags">Connection type.</param>
/// <param name="force">Specifies whether the disconnection should occur if there are open files or jobs on the connection.</param>
/// <see>https://learn.microsoft.com/windows/win32/api/winnetwk/nf-winnetwk-wnetcancelconnection2w</see>
[DllImport(Mpr, CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WNetCancelConnection2W")]
internal static extern Win32ErrorCode WNetCancelConnection2(string name, NetCancelOptions flags, [MarshalAs(UnmanagedType.Bool)] bool force);
}
4 changes: 4 additions & 0 deletions Src/DSInternals.SAM/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 30 additions & 2 deletions Src/DSInternals.SAM/Wrappers/SamServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public sealed class SamServer : SamObject
private const uint PreferedMaximumBufferLength = 1000;
private const uint InitialEnumerationContext = 0;

/// <summary>
/// The authenticated SMB session used for named pipe transport, if any.
/// </summary>
private NamedPipeConnection? _namedPipeConnection;

/// <summary>
/// The name of the server.
/// </summary>
Expand All @@ -31,9 +36,14 @@ public string Name
/// Initializes a new instance of the <see cref="SamServer"/> class and connects to the specified server with the specified credentials and access mask.
/// </summary>
/// <param name="serverName">The name of the server.</param>
/// <param name="credential">The credentials to use for the connection.</param>
/// <param name="accessMask">The access mask to use for the connection.</param>
public SamServer(string serverName, SamServerAccessMask accessMask = SamServerAccessMask.MaximumAllowed, NetworkCredential credential = null) : base(null)
/// <param name="credential">The credentials to use for the connection.</param>
/// <param name="useNamedPipes">
/// When <c>true</c> and <paramref name="credential"/> 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.
/// </param>
public SamServer(string serverName, SamServerAccessMask accessMask = SamServerAccessMask.MaximumAllowed, NetworkCredential? credential = null, bool useNamedPipes = true) : base(null)
{
if (string.IsNullOrEmpty(serverName))
{
Expand All @@ -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);
Expand Down Expand Up @@ -129,4 +145,16 @@ public SamDomain OpenDomain(SecurityIdentifier domainSid, SamDomainAccessMask ac
Validator.AssertSuccess(result);
return new SamDomain(domainHandle);
}

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

if (disposing)
{
_namedPipeConnection?.Dispose();
_namedPipeConnection = null;
}
}
}
Loading