Acceso a un archivo compartido (UNC) desde un dominio remoto no confiable con credenciales

Nos encontramos con una situación interesante que necesita solución, y mis búsquedas han aparecido hasta ahora. Por lo tanto, hago un llamamiento a la comunidad SO para obtener ayuda.

El problema es este: tenemos la necesidad de acceder mediante progtwigción a un archivo compartido que no está en nuestro dominio, y no está dentro de un dominio externo de confianza a través de compartir archivos remotos / UNC. Naturalmente, necesitamos proporcionar credenciales a la máquina remota.

Típicamente, uno resuelve este problema de una de dos maneras:

  1. Asigne el recurso compartido de archivos como una unidad y proporcione las credenciales en ese momento. Esto normalmente se hace usando el comando NET USE o las funciones Win32 que duplican NET USE .
  2. Acceda al archivo con una ruta UNC como si la computadora remota estuviera en el dominio y asegúrese de que la cuenta en la que se ejecuta el progtwig esté duplicada (incluida la contraseña) en la máquina remota como un usuario local. Básicamente aprovechar el hecho de que Windows proporcionará automáticamente las credenciales del usuario actual cuando el usuario intente acceder a un archivo compartido.
  3. No use archivos compartidos remotos. Use FTP (u otro medio) para transferir el archivo, trabaje en él localmente y luego transfiéralo nuevamente.

Por diversos motivos, nuestros arquitectos de seguridad / red han rechazado los dos primeros enfoques. El segundo enfoque es obviamente un agujero de seguridad; si la computadora remota está en peligro, la computadora local ahora está en riesgo. El primer enfoque es insatisfactorio porque la unidad recientemente montada es un recurso compartido disponible para otros progtwigs en la computadora local durante el acceso al archivo por parte del progtwig. A pesar de que es bastante posible hacer que esto sea temporal, sigue siendo un agujero en su opinión.

Están abiertos a la tercera opción, pero los administradores de red remota insisten en SFTP en lugar de FTPS, y FtpWebRequest solo es compatible con FTPS. SFTP es la opción más amigable con el firewall y hay un par de bibliotecas que podría usar para ese enfoque, pero preferiría reducir mis dependencias si pudiera.

He buscado en MSDN un medio administrado o win32 de uso compartido de archivos remoto, pero no he podido encontrar nada útil.

Y entonces pregunto: ¿Hay otra manera? ¿Extrañé una función supersecreta de win32 que hace lo que quiero? ¿O debo buscar alguna variante de la opción 3?

La forma de resolver su problema es usar una API de Win32 llamada WNetUseConnection .
Utilice esta función para conectarse a una ruta UNC con autenticación, NO para mapear una unidad .

Esto le permitirá conectarse a una máquina remota, incluso si no está en el mismo dominio, e incluso si tiene un nombre de usuario y contraseña diferentes.

Una vez que haya utilizado WNetUseConnection, podrá acceder al archivo a través de una ruta UNC como si estuviera en el mismo dominio. La mejor manera es probablemente a través de las acciones administrativas integradas.
Ejemplo: \\ computername \ c $ \ program files \ Folder \ file.txt

Aquí hay algunos ejemplos de código C # que usa WNetUseConnection .

Tenga en cuenta que para NetResource, debe pasar null para lpLocalName y lpProvider. El dwType debe ser RESOURCETYPE_DISK. LpRemoteName debe ser \\ ComputerName.

Para las personas que buscan una solución rápida, pueden usar el NetworkShareAccesser que escribí recientemente (basado en esta respuesta (¡muchas gracias!)):

Uso:

 using (NetworkShareAccesser.Access(REMOTE_COMPUTER_NAME, DOMAIN, USER_NAME, PASSWORD)) { File.Copy(@"C:\Some\File\To\copy.txt", @"\\REMOTE-COMPUTER\My\Shared\Target\file.txt"); } 

ADVERTENCIA: asegúrese por NetworkShareAccesser se llame a NetworkShareAccesser del NetworkShareAccesser (¡incluso si la aplicación falla!); De lo contrario, la conexión permanecerá en Windows. Puede ver todas las conexiones abiertas abriendo el indicador de cmd e ingrese net use .

El código:

 ///  /// Provides access to a network share. ///  public class NetworkShareAccesser : IDisposable { private string _remoteUncName; private string _remoteComputerName; public string RemoteComputerName { get { return this._remoteComputerName; } set { this._remoteComputerName = value; this._remoteUncName = @"\\" + this._remoteComputerName; } } public string UserName { get; set; } public string Password { get; set; } #region Consts private const int RESOURCE_CONNECTED = 0x00000001; private const int RESOURCE_GLOBALNET = 0x00000002; private const int RESOURCE_REMEMBERED = 0x00000003; private const int RESOURCETYPE_ANY = 0x00000000; private const int RESOURCETYPE_DISK = 0x00000001; private const int RESOURCETYPE_PRINT = 0x00000002; private const int RESOURCEDISPLAYTYPE_GENERIC = 0x00000000; private const int RESOURCEDISPLAYTYPE_DOMAIN = 0x00000001; private const int RESOURCEDISPLAYTYPE_SERVER = 0x00000002; private const int RESOURCEDISPLAYTYPE_SHARE = 0x00000003; private const int RESOURCEDISPLAYTYPE_FILE = 0x00000004; private const int RESOURCEDISPLAYTYPE_GROUP = 0x00000005; private const int RESOURCEUSAGE_CONNECTABLE = 0x00000001; private const int RESOURCEUSAGE_CONTAINER = 0x00000002; private const int CONNECT_INTERACTIVE = 0x00000008; private const int CONNECT_PROMPT = 0x00000010; private const int CONNECT_REDIRECT = 0x00000080; private const int CONNECT_UPDATE_PROFILE = 0x00000001; private const int CONNECT_COMMANDLINE = 0x00000800; private const int CONNECT_CMD_SAVECRED = 0x00001000; private const int CONNECT_LOCALDRIVE = 0x00000100; #endregion #region Errors private const int NO_ERROR = 0; private const int ERROR_ACCESS_DENIED = 5; private const int ERROR_ALREADY_ASSIGNED = 85; private const int ERROR_BAD_DEVICE = 1200; private const int ERROR_BAD_NET_NAME = 67; private const int ERROR_BAD_PROVIDER = 1204; private const int ERROR_CANCELLED = 1223; private const int ERROR_EXTENDED_ERROR = 1208; private const int ERROR_INVALID_ADDRESS = 487; private const int ERROR_INVALID_PARAMETER = 87; private const int ERROR_INVALID_PASSWORD = 1216; private const int ERROR_MORE_DATA = 234; private const int ERROR_NO_MORE_ITEMS = 259; private const int ERROR_NO_NET_OR_BAD_PATH = 1203; private const int ERROR_NO_NETWORK = 1222; private const int ERROR_BAD_PROFILE = 1206; private const int ERROR_CANNOT_OPEN_PROFILE = 1205; private const int ERROR_DEVICE_IN_USE = 2404; private const int ERROR_NOT_CONNECTED = 2250; private const int ERROR_OPEN_FILES = 2401; #endregion #region PInvoke Signatures [DllImport("Mpr.dll")] private static extern int WNetUseConnection( IntPtr hwndOwner, NETRESOURCE lpNetResource, string lpPassword, string lpUserID, int dwFlags, string lpAccessName, string lpBufferSize, string lpResult ); [DllImport("Mpr.dll")] private static extern int WNetCancelConnection2( string lpName, int dwFlags, bool fForce ); [StructLayout(LayoutKind.Sequential)] private class NETRESOURCE { public int dwScope = 0; public int dwType = 0; public int dwDisplayType = 0; public int dwUsage = 0; public string lpLocalName = ""; public string lpRemoteName = ""; public string lpComment = ""; public string lpProvider = ""; } #endregion ///  /// Creates a NetworkShareAccesser for the given computer name. The user will be promted to enter credentials ///  ///  ///  public static NetworkShareAccesser Access(string remoteComputerName) { return new NetworkShareAccesser(remoteComputerName); } ///  /// Creates a NetworkShareAccesser for the given computer name using the given domain/computer name, username and password ///  ///  ///  ///  ///  public static NetworkShareAccesser Access(string remoteComputerName, string domainOrComuterName, string userName, string password) { return new NetworkShareAccesser(remoteComputerName, domainOrComuterName + @"\" + userName, password); } ///  /// Creates a NetworkShareAccesser for the given computer name using the given username (format: domainOrComputername\Username) and password ///  ///  ///  ///  public static NetworkShareAccesser Access(string remoteComputerName, string userName, string password) { return new NetworkShareAccesser(remoteComputerName, userName, password); } private NetworkShareAccesser(string remoteComputerName) { RemoteComputerName = remoteComputerName; this.ConnectToShare(this._remoteUncName, null, null, true); } private NetworkShareAccesser(string remoteComputerName, string userName, string password) { RemoteComputerName = remoteComputerName; UserName = userName; Password = password; this.ConnectToShare(this._remoteUncName, this.UserName, this.Password, false); } private void ConnectToShare(string remoteUnc, string username, string password, bool promptUser) { NETRESOURCE nr = new NETRESOURCE { dwType = RESOURCETYPE_DISK, lpRemoteName = remoteUnc }; int result; if (promptUser) { result = WNetUseConnection(IntPtr.Zero, nr, "", "", CONNECT_INTERACTIVE | CONNECT_PROMPT, null, null, null); } else { result = WNetUseConnection(IntPtr.Zero, nr, password, username, 0, null, null, null); } if (result != NO_ERROR) { throw new Win32Exception(result); } } private void DisconnectFromShare(string remoteUnc) { int result = WNetCancelConnection2(remoteUnc, CONNECT_UPDATE_PROFILE, false); if (result != NO_ERROR) { throw new Win32Exception(result); } } ///  /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. ///  /// 2 public void Dispose() { this.DisconnectFromShare(this._remoteUncName); } } 

AFAIK, no necesita asignar la ruta UNC a una letra de unidad para establecer credenciales para un servidor. Solía ​​utilizar secuencias de comandos por lotes como:

 net use \\myserver /user:username password :: do something with \\myserver\the\file\i\want.xml net use /delete \\my.server.com 

Sin embargo, cualquier progtwig que se ejecute en la misma cuenta que su progtwig aún podría acceder a todo ese username:password tiene acceso. Una posible solución podría ser aislar su progtwig en su propia cuenta de usuario local (el acceso de UNC es local a la cuenta que llamó NET USE ).

Nota: Usar SMB en varios dominios no es un buen uso de la tecnología, IMO. Si la seguridad es tan importante, el hecho de que SMB carece de encriptación es un poco difícil.

Si bien no me conozco a mí mismo, ciertamente espero que el # 2 sea incorrecto … Me gustaría pensar que Windows no va a AUTOMÁTICAMENTE dar mi información de inicio de sesión (¡menos que mi contraseña!) A cualquier máquina , y mucho menos uno que no es parte de mi confianza.

De todos modos, ¿has explorado la architecture de la suplantación? Tu código se verá similar a esto:

 using (System.Security.Principal.WindowsImpersonationContext context = System.Security.Principal.WindowsIdentity.Impersonate(token)) { // Do network operations here context.Undo(); } 

En este caso, la variable token es un IntPtr. Para obtener un valor para esta variable, deberá llamar a la función de API de Windows LogonUser no administrada. Un viaje rápido a pinvoke.net nos da la siguiente firma:

 [System.Runtime.InteropServices.DllImport("advapi32.dll", SetLastError = true)] public static extern bool LogonUser( string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken ); 

El nombre de usuario, el dominio y la contraseña deberían parecer bastante obvios. Eche un vistazo a los diversos valores que se pueden pasar a dwLogonType y dwLogonProvider para determinar el que mejor se adapte a sus necesidades.

Este código no ha sido probado, ya que no tengo un segundo dominio aquí donde pueda verificarlo, pero espero que esto te ponga en el camino correcto.

En lugar de WNetUseConnection, recomendaría NetUseAdd . WNetUseConnection es una función heredada que ha sido reemplazada por WNetUseConnection2 y WNetUseConnection3, pero todas esas funciones crean un dispositivo de red visible en el Explorador de Windows. NetUseAdd es el equivalente de llamar al uso de red en un indicador de DOS para autenticarse en una computadora remota.

Si llama a NetUseAdd, los bashs subsiguientes de acceder al directorio deberían tener éxito.

La mayoría de los servidores SFTP también son compatibles con SCP, por lo que puede ser mucho más fácil encontrar bibliotecas. Incluso podría llamar a un cliente existente desde su código como pscp incluido con PuTTY .

Si el tipo de archivo con el que está trabajando es algo simple como un archivo de texto o XML, incluso puede escribir su propia implementación cliente / servidor para manipular el archivo usando algo como .NET Remoting o servicios web.

He visto la opción 3 implementada con herramientas JScape de una manera bastante directa. Puede intentarlo. No es gratis, pero cumple su función.

Aquí una clase POC mínima con todo el material eliminado

 using System; using System.ComponentModel; using System.Runtime.InteropServices; public class UncShareWithCredentials : IDisposable { private string _uncShare; public UncShareWithCredentials(string uncShare, string userName, string password) { var nr = new Native.NETRESOURCE { dwType = Native.RESOURCETYPE_DISK, lpRemoteName = uncShare }; int result = Native.WNetUseConnection(IntPtr.Zero, nr, password, userName, 0, null, null, null); if (result != Native.NO_ERROR) { throw new Win32Exception(result); } _uncShare = uncShare; } public void Dispose() { if (!string.IsNullOrEmpty(_uncShare)) { Native.WNetCancelConnection2(_uncShare, Native.CONNECT_UPDATE_PROFILE, false); _uncShare = null; } } private class Native { public const int RESOURCETYPE_DISK = 0x00000001; public const int CONNECT_UPDATE_PROFILE = 0x00000001; public const int NO_ERROR = 0; [DllImport("mpr.dll")] public static extern int WNetUseConnection(IntPtr hwndOwner, NETRESOURCE lpNetResource, string lpPassword, string lpUserID, int dwFlags, string lpAccessName, string lpBufferSize, string lpResult); [DllImport("mpr.dll")] public static extern int WNetCancelConnection2(string lpName, int dwFlags, bool fForce); [StructLayout(LayoutKind.Sequential)] public class NETRESOURCE { public int dwScope; public int dwType; public int dwDisplayType; public int dwUsage; public string lpLocalName; public string lpRemoteName; public string lpComment; public string lpProvider; } } } 

Puede usar directamente \\server\share\folder w / WNetUseConnection , no es necesario WNetUseConnection en \\server parte del \\server solo de antemano.

Miré a MS para encontrar las respuestas. La primera solución asume que la cuenta de usuario que ejecuta el proceso de la aplicación tiene acceso a la carpeta o unidad compartida (Mismo dominio). Asegúrese de que su DNS esté resuelto o intente usar la dirección IP. Simplemente haz lo siguiente:

  DirectoryInfo di = new DirectoryInfo(PATH); var files = di.EnumerateFiles("*.*", SearchOption.AllDirectories); 

Si desea acceder a diferentes dominios .NET 2.0 con credenciales, siga este modelo:

 WebRequest req = FileWebRequest.Create(new Uri(@"\\\Dir\test.txt")); req.Credentials = new NetworkCredential(@"\", ""); req.PreAuthenticate = true; WebResponse d = req.GetResponse(); FileStream fs = File.Create("test.txt"); // here you can check that the cast was successful if you want. fs = d.GetResponseStream() as FileStream; fs.Close();