Accès à un fichier partagé (UNC) à partir d'un domaine distant non approuvé avec des informations d'identification
-
20-08-2019 - |
Question
Nous avons rencontré une situation intéressante qui doit être résolue et mes recherches se sont avérées vaines. Je lance donc un appel à la communauté SO pour obtenir de l'aide.
Le problème est le suivant: nous avons besoin d'accéder par programme à un fichier partagé qui ne se trouve pas dans notre domaine et qui ne fait pas partie d'un domaine externe approuvé via le partage de fichiers à distance / UNC. Naturellement, nous devons fournir les informations d'identification à la machine distante.
Généralement, on résout ce problème de deux manières:
- Mappez le partage de fichiers en tant que lecteur et fournissez les informations d'identification à ce moment-là. Cela s'effectue généralement à l'aide de la commande
NET USE
ou des fonctions Win32 qui dupliquentNET USE
. - Accédez au fichier avec un chemin UNC comme si l’ordinateur distant se trouvait sur le domaine et assurez-vous que le compte sous lequel le programme est exécuté est dupliqué (y compris le mot de passe) sur la machine distante en tant qu’utilisateur local. Exploitez essentiellement le fait que Windows fournira automatiquement les informations d'identification de l'utilisateur actuel lorsque celui-ci tente d'accéder à un fichier partagé.
- N'utilisez pas le partage de fichiers à distance. Utilisez FTP (ou un autre moyen) pour transférer le fichier, travaillez-le localement, puis transférez-le à nouveau.
Pour diverses raisons, nos architectes de la sécurité et des réseaux ont rejeté les deux premières approches. La deuxième approche est évidemment une faille de sécurité; si l'ordinateur distant est compromis, l'ordinateur local est maintenant exposé. La première approche n'est pas satisfaisante car le lecteur nouvellement monté est une ressource partagée disponible pour les autres programmes de l'ordinateur local lors de l'accès au fichier par le programme. Même s'il est tout à fait possible de faire ce travail temporaire, cela reste un trou dans leur opinion.
Ils sont ouverts à la troisième option, mais les administrateurs de réseau distant insistent sur le protocole SFTP plutôt que sur FTPS, et FtpWebRequest ne prend en charge que FTPS. SFTP est l'option la plus compatible avec le pare-feu. Il existe quelques bibliothèques que je pourrais utiliser pour cette approche, mais je préférerais réduire mes dépendances si je le peux.
J'ai cherché dans MSDN un moyen géré ou un moyen win32 d'utiliser le partage de fichiers à distance, mais je n'ai rien trouvé d'utile.
Et alors je demande: y a-t-il un autre moyen? Ai-je oublié une fonction Win32 super-secrète qui fait ce que je veux? Ou dois-je rechercher une variante de l'option 3?
La solution
Pour résoudre votre problème, utilisez une API Win32 appelée WNetUseConnection .
Utilisez cette fonction pour vous connecter à un chemin UNC avec authentification, mais PAS pour mapper un lecteur .
Cela vous permettra de vous connecter à une machine distante, même si celle-ci ne se trouve pas sur le même domaine, et même si elle possède un nom d'utilisateur et un mot de passe différents.
Une fois que vous avez utilisé WNetUseConnection, vous pourrez accéder au fichier via un chemin UNC comme si vous étiez sur le même domaine. La meilleure solution consiste probablement à utiliser des actions intégrées administratives.
Exemple: \\ nom_ordinateur \ c $ \ fichiers programme \ dossier \ fichier.txt
Voici un exemple de code C # utilisant WNetUseConnection .
Remarque: pour NetResource, vous devez passer la valeur null pour lpLocalName et lpProvider. Le type de fichier doit être RESOURCETYPE_DISK. LpRemoteName doit être \\ ComputerName.
Autres conseils
Pour ceux qui recherchent une solution rapide, vous pouvez utiliser le NetworkShareAccesser
que j'ai écrit récemment (à partir de this répondre (merci beaucoup!)):
Utilisation:
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");
}
AVERTISSEMENT: veuillez vous assurer que Dispose
de NetworkShareAccesser
est appelé (même si votre application se bloque!), sinon la connexion restera sous Windows. Vous pouvez voir toutes les connexions ouvertes en ouvrant l'invite cmd
et en entrant net use
.
Le code:
/// <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);
}
}
Comme je l'ai dit, il n'est pas nécessaire de mapper le chemin d'accès UNC à une lettre de lecteur pour établir les informations d'identification d'un serveur. J'ai régulièrement utilisé des scripts batch tels que:
net use \\myserver /user:username password
:: do something with \\myserver\the\file\i\want.xml
net use /delete \\my.server.com
Toutefois, tout programme exécuté sur le même compte que votre programme pourra toujours accéder à tout ce à quoi nom d'utilisateur: mot de passe
a accès. Une solution possible pourrait être d’isoler votre programme dans son propre compte utilisateur local (l’accès UNC est local au compte appelé NET USE
).
Remarque: L'utilisation de SMB sur plusieurs domaines ne constitue pas une utilisation judicieuse de la technologie IMO. Si la sécurité est aussi importante que cela, le manque de cryptage pour les PME est un problème en soi.
Plutôt que WNetUseConnection, je recommanderais NetUseAdd . WNetUseConnection est une fonction héritée remplacée par WNetUseConnection2 et WNetUseConnection3, mais toutes ces fonctions créent un périphérique réseau visible dans l'Explorateur Windows. NetUseAdd revient à appeler Net use dans une invite DOS pour s’authentifier sur un ordinateur distant.
Si vous appelez NetUseAdd, les tentatives suivantes pour accéder au répertoire doivent aboutir.
Bien que je ne me connaisse pas moi-même, j'espère certainement que le n ° 2 est incorrect ... J'aimerais penser que Windows ne va pas automatiquement donner mes informations de connexion (le moindre de tous mes mots de passe!) à n'importe quelle machine, sans parler de celle qui ne fait pas partie de ma confiance.
Quoi qu’il en soit, avez-vous exploré l’architecture d’emprunt d’identité? Votre code va ressembler à ceci:
using (System.Security.Principal.WindowsImpersonationContext context = System.Security.Principal.WindowsIdentity.Impersonate(token))
{
// Do network operations here
context.Undo();
}
Dans ce cas, la variable token
est un IntPtr. Pour obtenir une valeur pour cette variable, vous devez appeler la fonction API Windows non gérée de LogonUser. Un rapide voyage à pinvoke.net nous donne la signature suivante:
[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
);
Le nom d'utilisateur, le domaine et le mot de passe devraient sembler assez évidents. Consultez les différentes valeurs pouvant être transmises à dwLogonType et dwLogonProvider afin de déterminer celle qui répond le mieux à vos besoins.
Ce code n'a pas été testé, car je n'ai pas de deuxième domaine ici que je puisse vérifier, mais cela devrait, espérons-le, vous mettre sur la bonne voie.
La plupart des serveurs SFTP supportent également SCP, ce qui peut être beaucoup plus facile à trouver pour les bibliothèques. Vous pouvez même appeler un client existant à partir de votre code, tel que pscp, inclus dans PuTTY .
Si le type de fichier sur lequel vous travaillez est simple, par exemple un fichier texte ou XML, vous pouvez même aller jusqu'à écrire votre propre implémentation client / serveur pour manipuler le fichier à l'aide de quelque chose comme .NET Remoting ou Web. services.
J'ai déjà vu l'option 3 implémentée avec les outils JScape de manière assez simple. Vous pourriez essayer. Ce n'est pas gratuit, mais ça fait son travail.
Ici, une classe POC minimale avec toute la croupe enlevée
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;
}
}
}
Vous pouvez utiliser directement \\ serveur \ partage \ dossier
w / WNetUseConnection
, inutile de le supprimer à la partie \\ serveur
au préalable.
j'attache mon code vb.net basé sur la référence 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
comment l'utiliser
Dim login = PinvokeWindowsNetworking.connectToRemote("\\ComputerName", "ComputerName\UserName", "Password")
If IsNothing(login) Then
'do your thing on the shared folder
PinvokeWindowsNetworking.disconnectRemote("\\ComputerName")
End If
J'ai regardé vers MS pour trouver les réponses. La première solution suppose que le compte d'utilisateur exécutant le processus d'application a accès au dossier ou au lecteur partagé (même domaine). Assurez-vous que votre DNS est résolu ou essayez d'utiliser une adresse IP. Faites simplement ce qui suit:
DirectoryInfo di = new DirectoryInfo(PATH);
var files = di.EnumerateFiles("*.*", SearchOption.AllDirectories);
Si vous souhaitez utiliser différents domaines, .NET 2.0 avec des informations d'identification est conforme à ce modèle:
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();