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

StackOverflow https://stackoverflow.com/questions/659013

  •  20-08-2019
  •  | 
  •  

Pregunta

Nos hemos encontrado con una situación interesante que debe resolverse, y mis búsquedas han aparecido casi nulas. Por lo tanto, pido ayuda a la comunidad SO.

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

Normalmente, uno resuelve este problema de una de dos maneras:

  1. Asigne el recurso compartido de archivos como 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 bajo la cual se ejecuta el programa esté duplicada (incluida la contraseña) en la máquina remota como usuario local. Básicamente aprovecha 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 el uso compartido remoto de archivos. Use FTP (o algún otro medio) para transferir el archivo, trabaje en él localmente y luego vuelva a transferirlo.

Por diversas y diversas razones, nuestros arquitectos de seguridad / redes han rechazado los dos primeros enfoques. El segundo enfoque es obviamente un agujero de seguridad; Si la computadora remota se ve comprometida, la computadora local ahora está en riesgo. El primer enfoque no es satisfactorio porque la unidad recién montada es un recurso compartido disponible para otros programas en la computadora local durante el acceso a los archivos por parte del programa. Aunque 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 la red remota insisten en SFTP en lugar de FTPS, y FtpWebRequest solo admite 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 puedo.

He buscado en MSDN un medio administrado o win32 para usar el intercambio remoto de archivos, pero no he podido encontrar nada útil.

Y entonces pregunto: ¿Hay otra forma? ¿Me perdí una función win32 súper secreta que hace lo que quiero? ¿O debo buscar alguna variante de la opción 3?

¿Fue útil?

Solución

La forma de resolver su problema es utilizar una API Win32 llamada WNetUseConnection .
Use esta función para conectarse a una ruta UNC con autenticación, NO para asignar 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 un código C # de muestra que utiliza WNetUseConnection .

Nota, para NetResource, debe pasar nulo para lpLocalName y lpProvider. El dwType debe ser RESOURCETYPE_DISK. El lpRemoteName debe ser \\ ComputerName.

Otros consejos

Para las personas que buscan una solución rápida, puede usar el NetworkShareAccesser que escribí recientemente (basado en esto 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 de que Desechar del NetworkShareAccesser se llama (¡incluso si su aplicación falla!), de lo contrario, se abrirá la conexión permanecerá en Windows. Puede ver todas las conexiones abiertas abriendo el indicador cmd e ingresando net use .

El Código:

/// <summary>
/// Provides access to a network share.
/// </summary>
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

    /// <summary>
    /// Creates a NetworkShareAccesser for the given computer name. The user will be promted to enter credentials
    /// </summary>
    /// <param name="remoteComputerName"></param>
    /// <returns></returns>
    public static NetworkShareAccesser Access(string remoteComputerName)
    {
        return new NetworkShareAccesser(remoteComputerName);
    }

    /// <summary>
    /// Creates a NetworkShareAccesser for the given computer name using the given domain/computer name, username and password
    /// </summary>
    /// <param name="remoteComputerName"></param>
    /// <param name="domainOrComuterName"></param>
    /// <param name="userName"></param>
    /// <param name="password"></param>
    public static NetworkShareAccesser Access(string remoteComputerName, string domainOrComuterName, string userName, string password)
    {
        return new NetworkShareAccesser(remoteComputerName,
                                        domainOrComuterName + @"\" + userName,
                                        password);
    }

    /// <summary>
    /// Creates a NetworkShareAccesser for the given computer name using the given username (format: domainOrComputername\Username) and password
    /// </summary>
    /// <param name="remoteComputerName"></param>
    /// <param name="userName"></param>
    /// <param name="password"></param>
    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);
        }
    }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    /// <filterpriority>2</filterpriority>
    public void Dispose()
    {
        this.DisconnectFromShare(this._remoteUncName);
    }
}

AFAIK, no necesita mapear la ruta UNC a una letra de unidad para establecer credenciales para un servidor. Usé regularmente scripts 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 programa que se ejecute en la misma cuenta que su programa aún podría acceder a todo lo que username: password tiene acceso. Una posible solución podría ser aislar su programa en su propia cuenta de usuario local (el acceso UNC es local a la cuenta que se llama NET USE ).

Nota: el uso de SMB entre dominios no es un buen uso de la tecnología, IMO. Si la seguridad es tan importante, el hecho de que SMB carece de cifrado es un poco amortiguador por sí solo.

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 que es visible en el Explorador de Windows. NetUseAdd es el equivalente a llamar al uso de la red en un indicador de DOS para autenticarse en una computadora remota.

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

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

Independientemente, ¿ha explorado la arquitectura de suplantación? Su 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 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 cuál se adapta mejor a sus necesidades.

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

La mayoría de los servidores SFTP también admiten SCP, lo que puede ser mucho más fácil de 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 podría llegar a escribir su propia implementación de cliente / servidor para manipular el archivo usando algo como .NET Remoting o web servicios.

He visto la opción 3 implementada con herramientas JScape de una manera bastante sencilla. Puedes intentarlo. No es gratis, pero hace su trabajo.

Aquí una clase mínima de POC con todo el cruft 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 despojarlo a la parte \\ server solamente de antemano.

Im adjunte mi código vb.net basado en referencia de brian

Imports System.ComponentModel

Imports System.Runtime.InteropServices

Public Class PinvokeWindowsNetworking

Const NO_ERROR As Integer = 0



Private Structure ErrorClass

    Public num As Integer

    Public message As String



    Public Sub New(ByVal num As Integer, ByVal message As String)

        Me.num = num

        Me.message = message

    End Sub

End Structure



Private Shared ERROR_LIST As ErrorClass() = New ErrorClass() {

    New ErrorClass(5, "Error: Access Denied"),

    New ErrorClass(85, "Error: Already Assigned"),

    New ErrorClass(1200, "Error: Bad Device"),

    New ErrorClass(67, "Error: Bad Net Name"),

    New ErrorClass(1204, "Error: Bad Provider"),

    New ErrorClass(1223, "Error: Cancelled"),

    New ErrorClass(1208, "Error: Extended Error"),

    New ErrorClass(487, "Error: Invalid Address"),

    New ErrorClass(87, "Error: Invalid Parameter"),

    New ErrorClass(1216, "Error: Invalid Password"),

    New ErrorClass(234, "Error: More Data"),

    New ErrorClass(259, "Error: No More Items"),

    New ErrorClass(1203, "Error: No Net Or Bad Path"),

    New ErrorClass(1222, "Error: No Network"),

    New ErrorClass(1206, "Error: Bad Profile"),

    New ErrorClass(1205, "Error: Cannot Open Profile"),

    New ErrorClass(2404, "Error: Device In Use"),

    New ErrorClass(2250, "Error: Not Connected"),

    New ErrorClass(2401, "Error: Open Files")}



Private Shared Function getErrorForNumber(ByVal errNum As Integer) As String

    For Each er As ErrorClass In ERROR_LIST

        If er.num = errNum Then Return er.message

    Next



    Try

        Throw New Win32Exception(errNum)

    Catch ex As Exception

        Return "Error: Unknown, " & errNum & " " & ex.Message

    End Try



    Return "Error: Unknown, " & errNum

End Function



<DllImport("Mpr.dll")>

Private Shared Function WNetUseConnection(ByVal hwndOwner As IntPtr, ByVal lpNetResource As NETRESOURCE, ByVal lpPassword As String, ByVal lpUserID As String, ByVal dwFlags As Integer, ByVal lpAccessName As String, ByVal lpBufferSize As String, ByVal lpResult As String) As Integer

End Function



<DllImport("Mpr.dll")>

Private Shared Function WNetCancelConnection2(ByVal lpName As String, ByVal dwFlags As Integer, ByVal fForce As Boolean) As Integer

End Function



<StructLayout(LayoutKind.Sequential)>

Private Class NETRESOURCE

    Public dwScope As Integer = 0

    Public dwType As Integer = 0

    Public dwDisplayType As Integer = 0

    Public dwUsage As Integer = 0

    Public lpLocalName As String = ""

    Public lpRemoteName As String = ""

    Public lpComment As String = ""

    Public lpProvider As String = ""

End Class



Public Shared Function connectToRemote(ByVal remoteUNC As String, ByVal username As String, ByVal password As String) As String

    Return connectToRemote(remoteUNC, username, password, False)

End Function



Public Shared Function connectToRemote(ByVal remoteUNC As String, ByVal username As String, ByVal password As String, ByVal promptUser As Boolean) As String

    Dim nr As NETRESOURCE = New NETRESOURCE()

    nr.dwType = ResourceTypes.Disk

    nr.lpRemoteName = remoteUNC

    Dim ret As Integer



    If promptUser Then

        ret = WNetUseConnection(IntPtr.Zero, nr, "", "", Connects.Interactive Or Connects.Prompt, Nothing, Nothing, Nothing)

    Else

        ret = WNetUseConnection(IntPtr.Zero, nr, password, username, 0, Nothing, Nothing, Nothing)

    End If



    If ret = NO_ERROR Then Return Nothing

    Return getErrorForNumber(ret)

End Function



Public Shared Function disconnectRemote(ByVal remoteUNC As String) As String

    Dim ret As Integer = WNetCancelConnection2(remoteUNC, Connects.UpdateProfile, False)

    If ret = NO_ERROR Then Return Nothing

    Return getErrorForNumber(ret)

End Function


Enum Resources As Integer

    Connected = &H1

    GlobalNet = &H2

    Remembered = &H3

End Enum


Enum ResourceTypes As Integer

    Any = &H0

    Disk = &H1

    Print = &H2

End Enum


Enum ResourceDisplayTypes As Integer

    Generic = &H0

    Domain = &H1

    Server = &H2

    Share = &H3

    File = &H4

    Group = &H5

End Enum


Enum ResourceUsages As Integer

    Connectable = &H1

    Container = &H2

End Enum


Enum Connects As Integer

    Interactive = &H8

    Prompt = &H10

    Redirect = &H80

    UpdateProfile = &H1

    CommandLine = &H800

    CmdSaveCred = &H1000

    LocalDrive = &H100

End Enum


End Class

cómo usarlo

Dim login = PinvokeWindowsNetworking.connectToRemote("\\ComputerName", "ComputerName\UserName", "Password")

    If IsNothing(login) Then



        'do your thing on the shared folder



       PinvokeWindowsNetworking.disconnectRemote("\\ComputerName")

    End If

Miré a MS para encontrar las respuestas. La primera solución supone 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 haga lo siguiente:

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

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

WebRequest req = FileWebRequest.Create(new Uri(@"\\<server Name>\Dir\test.txt"));

        req.Credentials = new NetworkCredential(@"<Domain>\<User>", "<Password>");
        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();
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top