diff --git a/api/Impersonate.Impersonator.html b/api/Impersonate.Impersonator.html index 3aed469c..5e3f4cda 100644 --- a/api/Impersonate.Impersonator.html +++ b/api/Impersonate.Impersonator.html @@ -161,10 +161,10 @@
43
}
585
}
8
}
9
}
102
}
102
}
7
}
9
}
47
}
205
}
16
}
460
}
460
}
17
}
29
}
47
}
13
}
16
90
}
52
}
158
}
785
}
357
}
52
}
9
}
159
}
52
}
87
}
14
}
106
}
10
}
460
}
9
}
407
}
627
}
115
}
171
/// <param name="target">Raw service principal name</param>
172
/// <returns>Stripped service principal name with (hopefully) just the hostname</returns>
173
public static string StripServicePrincipalName(string target)
174
{
175
return SPNRegex.IsMatch(target) ? target.Split('/')[1].Split(':')[0] : target;
176
}
174
{
175
return SPNRegex.IsMatch(target) ? target.Split('/')[1].Split(':')[0] : target;
176
}
177
178
/// <summary>
179
/// Converts a string to its base64 representation
337
}
199
}
7
}
21
}
272
}
785
}
19
}
14
18
using SharpHoundCommonLib.LDAPQueries;
19
using SharpHoundCommonLib.OutputTypes;
20
using SharpHoundCommonLib.Processors;
21
using SharpHoundRPC;
22
using SharpHoundRPC.NetAPINative;
23
using SharpHoundRPC.Wrappers;
24
using Domain = System.DirectoryServices.ActiveDirectory.Domain;
25
using SearchScope = System.DirectoryServices.Protocols.SearchScope;
26
using SecurityMasks = System.DirectoryServices.Protocols.SecurityMasks;
27
28
namespace SharpHoundCommonLib
29
{
30
public class LDAPUtils : ILDAPUtils
31
{
32
private const string NullCacheKey = "UNIQUENULL";
33
34
// The following byte stream contains the necessary message to request a NetBios name from a machine
35
// http://web.archive.org/web/20100409111218/http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.a
36
private static readonly byte[] NameRequest =
37
{
38
0x80, 0x94, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
39
0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4b, 0x41,
21
using SharpHoundRPC.NetAPINative;
22
using Domain = System.DirectoryServices.ActiveDirectory.Domain;
23
using SearchScope = System.DirectoryServices.Protocols.SearchScope;
24
using SecurityMasks = System.DirectoryServices.Protocols.SecurityMasks;
25
26
namespace SharpHoundCommonLib
27
{
28
public class LDAPUtils : ILDAPUtils
29
{
30
private const string NullCacheKey = "UNIQUENULL";
31
32
// The following byte stream contains the necessary message to request a NetBios name from a machine
33
// http://web.archive.org/web/20100409111218/http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.a
34
private static readonly byte[] NameRequest =
35
{
36
0x80, 0x94, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
37
0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4b, 0x41,
38
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
39
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
40
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
41
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
42
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
43
0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21,
44
0x00, 0x01
45
};
46
47
48
private static readonly ConcurrentDictionary<string, ResolvedWellKnownPrincipal>
49
SeenWellKnownPrincipals = new();
41
0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21,
42
0x00, 0x01
43
};
44
45
46
private static readonly ConcurrentDictionary<string, ResolvedWellKnownPrincipal>
47
SeenWellKnownPrincipals = new();
48
49
private static readonly ConcurrentDictionary<string, byte> DomainControllers = new();
50
51
private static readonly ConcurrentDictionary<string, byte> DomainControllers = new();
52
53
private readonly ConcurrentDictionary<string, Domain> _domainCache = new();
54
private readonly ConcurrentDictionary<string, string> _domainControllerCache = new();
55
private static readonly TimeSpan MinBackoffDelay = TimeSpan.FromSeconds(2);
56
private static readonly TimeSpan MaxBackoffDelay = TimeSpan.FromSeconds(10);
57
private static readonly TimeSpan BackoffDelayMultiplier = TimeSpan.FromSeconds(2);
58
private const int MaxRetries = 3;
59
60
private readonly ConcurrentDictionary<string, LdapConnection> _globalCatalogConnections = new();
61
private readonly ConcurrentDictionary<string, string> _hostResolutionMap = new();
62
private readonly ConcurrentDictionary<string, LdapConnection> _ldapConnections = new();
63
private readonly ConcurrentDictionary<string, int> _ldapRangeSizeCache = new();
64
private readonly ILogger _log;
65
private readonly NativeMethods _nativeMethods;
66
private readonly ConcurrentDictionary<string, string> _netbiosCache = new();
67
private readonly PortScanner _portScanner;
68
private LDAPConfig _ldapConfig = new();
51
private readonly ConcurrentDictionary<string, Domain> _domainCache = new();
52
private readonly ConcurrentDictionary<string, string> _domainControllerCache = new();
53
private static readonly TimeSpan MinBackoffDelay = TimeSpan.FromSeconds(2);
54
private static readonly TimeSpan MaxBackoffDelay = TimeSpan.FromSeconds(20);
55
private const int BackoffDelayMultiplier = 2;
56
private const int MaxRetries = 3;
57
58
private readonly ConcurrentDictionary<string, LdapConnection> _globalCatalogConnections = new();
59
private readonly ConcurrentDictionary<string, string> _hostResolutionMap = new();
60
private readonly ConcurrentDictionary<string, LdapConnection> _ldapConnections = new();
61
private readonly ConcurrentDictionary<string, int> _ldapRangeSizeCache = new();
62
private readonly ILogger _log;
63
private readonly NativeMethods _nativeMethods;
64
private readonly ConcurrentDictionary<string, string> _netbiosCache = new();
65
private readonly PortScanner _portScanner;
66
private LDAPConfig _ldapConfig = new();
67
private readonly ManualResetEvent _connectionResetEvent = new(false);
68
private readonly object _lockObj = new();
69
70
/// <summary>
71
/// Creates a new instance of LDAP Utils with defaults
72
/// </summary>
73
public LDAPUtils()
74
{
75
_nativeMethods = new NativeMethods();
76
_portScanner = new PortScanner();
77
_log = Logging.LogProvider.CreateLogger("LDAPUtils");
78
}
79
80
/// <summary>
81
/// Creates a new instance of LDAP utils and allows overriding implementations
82
/// </summary>
83
/// <param name="nativeMethods"></param>
84
/// <param name="scanner"></param>
85
/// <param name="log"></param>
86
public LDAPUtils(NativeMethods nativeMethods = null, PortScanner scanner = null, ILogger log = null)
87
{
88
_nativeMethods = nativeMethods ?? new NativeMethods();
89
_portScanner = scanner ?? new PortScanner();
90
_log = log ?? Logging.LogProvider.CreateLogger("LDAPUtils");
91
}
92
93
/// <summary>
94
/// Sets the configuration for LDAP queries
95
/// </summary>
96
/// <param name="config"></param>
97
/// <exception cref="Exception"></exception>
98
public void SetLDAPConfig(LDAPConfig config)
99
{
100
_ldapConfig = config ?? throw new Exception("LDAP Configuration can not be null");
101
_domainControllerCache.Clear();
102
foreach (var kv in _globalCatalogConnections)
103
{
104
kv.Value.Dispose();
105
}
106
_globalCatalogConnections.Clear();
107
foreach (var kv in _ldapConnections)
108
{
109
kv.Value.Dispose();
110
}
111
_ldapConnections.Clear();
112
}
70
71
/// <summary>
72
/// Creates a new instance of LDAP Utils with defaults
73
/// </summary>
74
public LDAPUtils()
75
{
76
_nativeMethods = new NativeMethods();
77
_portScanner = new PortScanner();
78
_log = Logging.LogProvider.CreateLogger("LDAPUtils");
79
}
80
81
/// <summary>
82
/// Creates a new instance of LDAP utils and allows overriding implementations
83
/// </summary>
84
/// <param name="nativeMethods"></param>
85
/// <param name="scanner"></param>
86
/// <param name="log"></param>
87
public LDAPUtils(NativeMethods nativeMethods = null, PortScanner scanner = null, ILogger log = null)
88
{
89
_nativeMethods = nativeMethods ?? new NativeMethods();
90
_portScanner = scanner ?? new PortScanner();
91
_log = log ?? Logging.LogProvider.CreateLogger("LDAPUtils");
92
}
93
94
/// <summary>
95
/// Sets the configuration for LDAP queries
96
/// </summary>
97
/// <param name="config"></param>
98
/// <exception cref="Exception"></exception>
99
public void SetLDAPConfig(LDAPConfig config)
100
{
101
_ldapConfig = config ?? throw new Exception("LDAP Configuration can not be null");
102
_domainControllerCache.Clear();
103
foreach (var kv in _globalCatalogConnections)
104
{
105
kv.Value.Dispose();
106
}
107
108
_globalCatalogConnections.Clear();
109
foreach (var kv in _ldapConnections)
110
{
111
kv.Value.Dispose();
112
}
113
114
/// <summary>
115
/// Turns a sid into a well known principal ID.
116
/// </summary>
117
/// <param name="sid"></param>
118
/// <param name="domain"></param>
119
/// <param name="commonPrincipal"></param>
120
/// <returns>True if a well known principal was identified, false if not</returns>
121
public bool GetWellKnownPrincipal(string sid, string domain, out TypedPrincipal commonPrincipal)
122
{
123
if (!WellKnownPrincipal.GetWellKnownPrincipal(sid, out commonPrincipal)) return false;
124
var tempDomain = domain ?? GetDomain()?.Name ?? "UNKNOWN";
125
commonPrincipal.ObjectIdentifier = ConvertWellKnownPrincipal(sid, tempDomain);
126
SeenWellKnownPrincipals.TryAdd(commonPrincipal.ObjectIdentifier, new ResolvedWellKnownPrincipal
127
{
128
DomainName = domain,
129
WkpId = sid
130
});
131
return true;
132
}
133
134
/// <summary>
135
/// Adds a SID to an internal list of domain controllers
136
/// </summary>
137
/// <param name="domainControllerSID"></param>
138
public void AddDomainController(string domainControllerSID)
139
{
140
DomainControllers.TryAdd(domainControllerSID, new byte());
141
}
142
143
/// <summary>
144
/// Gets output objects for currently observed well known principals
145
/// </summary>
146
/// <returns></returns>
147
/// <exception cref="ArgumentOutOfRangeException"></exception>
148
public IEnumerable<OutputBase> GetWellKnownPrincipalOutput(string domain)
149
{
150
foreach (var wkp in SeenWellKnownPrincipals)
151
{
152
WellKnownPrincipal.GetWellKnownPrincipal(wkp.Value.WkpId, out var principal);
153
OutputBase output = principal.ObjectType switch
154
{
155
Label.User => new User(),
156
Label.Computer => new Computer(),
157
Label.Group => new Group(),
158
Label.GPO => new GPO(),
159
Label.Domain => new OutputTypes.Domain(),
160
Label.OU => new OU(),
161
Label.Container => new Container(),
162
Label.Configuration => new Container(),
163
_ => throw new ArgumentOutOfRangeException()
164
};
165
166
output.Properties.Add("name", $"{principal.ObjectIdentifier}@{wkp.Value.DomainName}".ToUpper());
167
var domainSid = GetSidFromDomainName(wkp.Value.DomainName);
168
output.Properties.Add("domainsid", domainSid);
169
output.Properties.Add("domain", wkp.Value.DomainName.ToUpper());
170
output.ObjectIdentifier = wkp.Key;
171
yield return output;
172
}
173
174
var entdc = GetBaseEnterpriseDC(domain);
175
entdc.Members = DomainControllers.Select(x => new TypedPrincipal(x.Key, Label.Computer)).ToArray();
176
yield return entdc;
177
}
178
179
/// <summary>
180
/// Converts a
181
/// </summary>
182
/// <param name="sid"></param>
183
/// <param name="domain"></param>
184
/// <returns></returns>
185
public string ConvertWellKnownPrincipal(string sid, string domain)
186
{
187
if (!WellKnownPrincipal.GetWellKnownPrincipal(sid, out _)) return sid;
188
189
if (sid != "S-1-5-9") return $"{domain}-{sid}".ToUpper();
190
191
var forest = GetForest(domain)?.Name;
192
if (forest == null) _log.LogWarning("Error getting forest, ENTDC sid is likely incorrect");
193
return $"{forest ?? "UNKNOWN"}-{sid}".ToUpper();
194
}
195
196
/// <summary>
197
/// Queries the global catalog to get potential SID matches for a username in the forest
198
/// </summary>
199
/// <param name="name"></param>
200
/// <returns></returns>
201
public string[] GetUserGlobalCatalogMatches(string name)
202
{
203
var tempName = name.ToLower();
204
if (Cache.GetGCCache(tempName, out var sids))
205
return sids;
206
207
var query = new LDAPFilter().AddUsers($"samaccountname={tempName}").GetFilter();
208
var results = QueryLDAP(query, SearchScope.Subtree, new[] { "objectsid" }, globalCatalog: true)
209
.Select(x => x.GetSid()).Where(x => x != null).ToArray();
210
Cache.AddGCCache(tempName, results);
211
return results;
212
}
213
214
/// <summary>
215
/// Uses an LDAP lookup to attempt to find the Label for a given SID
216
/// Will also convert to a well known principal ID if needed
217
/// </summary>
218
/// <param name="id"></param>
219
/// <param name="fallbackDomain"></param>
220
/// <returns></returns>
221
public TypedPrincipal ResolveIDAndType(string id, string fallbackDomain)
222
{
223
//This is a duplicated SID object which is weird and makes things unhappy. Throw it out
224
if (id.Contains("0ACNF"))
225
return null;
226
227
if (GetWellKnownPrincipal(id, fallbackDomain, out var principal))
228
return principal;
114
_ldapConnections.Clear();
115
}
116
117
/// <summary>
118
/// Turns a sid into a well known principal ID.
119
/// </summary>
120
/// <param name="sid"></param>
121
/// <param name="domain"></param>
122
/// <param name="commonPrincipal"></param>
123
/// <returns>True if a well known principal was identified, false if not</returns>
124
public bool GetWellKnownPrincipal(string sid, string domain, out TypedPrincipal commonPrincipal)
125
{
126
if (!WellKnownPrincipal.GetWellKnownPrincipal(sid, out commonPrincipal)) return false;
127
var tempDomain = domain ?? GetDomain()?.Name ?? "UNKNOWN";
128
commonPrincipal.ObjectIdentifier = ConvertWellKnownPrincipal(sid, tempDomain);
129
SeenWellKnownPrincipals.TryAdd(commonPrincipal.ObjectIdentifier, new ResolvedWellKnownPrincipal
130
{
131
DomainName = domain,
132
WkpId = sid
133
});
134
return true;
135
}
136
137
/// <summary>
138
/// Adds a SID to an internal list of domain controllers
139
/// </summary>
140
/// <param name="domainControllerSID"></param>
141
public void AddDomainController(string domainControllerSID)
142
{
143
DomainControllers.TryAdd(domainControllerSID, new byte());
144
}
145
146
/// <summary>
147
/// Gets output objects for currently observed well known principals
148
/// </summary>
149
/// <returns></returns>
150
/// <exception cref="ArgumentOutOfRangeException"></exception>
151
public IEnumerable<OutputBase> GetWellKnownPrincipalOutput(string domain)
152
{
153
foreach (var wkp in SeenWellKnownPrincipals)
154
{
155
WellKnownPrincipal.GetWellKnownPrincipal(wkp.Value.WkpId, out var principal);
156
OutputBase output = principal.ObjectType switch
157
{
158
Label.User => new User(),
159
Label.Computer => new Computer(),
160
Label.Group => new Group(),
161
Label.GPO => new GPO(),
162
Label.Domain => new OutputTypes.Domain(),
163
Label.OU => new OU(),
164
Label.Container => new Container(),
165
Label.Configuration => new Container(),
166
_ => throw new ArgumentOutOfRangeException()
167
};
168
169
output.Properties.Add("name", $"{principal.ObjectIdentifier}@{wkp.Value.DomainName}".ToUpper());
170
var domainSid = GetSidFromDomainName(wkp.Value.DomainName);
171
output.Properties.Add("domainsid", domainSid);
172
output.Properties.Add("domain", wkp.Value.DomainName.ToUpper());
173
output.ObjectIdentifier = wkp.Key;
174
yield return output;
175
}
176
177
var entdc = GetBaseEnterpriseDC(domain);
178
entdc.Members = DomainControllers.Select(x => new TypedPrincipal(x.Key, Label.Computer)).ToArray();
179
yield return entdc;
180
}
181
182
/// <summary>
183
/// Converts a
184
/// </summary>
185
/// <param name="sid"></param>
186
/// <param name="domain"></param>
187
/// <returns></returns>
188
public string ConvertWellKnownPrincipal(string sid, string domain)
189
{
190
if (!WellKnownPrincipal.GetWellKnownPrincipal(sid, out _)) return sid;
191
192
if (sid != "S-1-5-9") return $"{domain}-{sid}".ToUpper();
193
194
var forest = GetForest(domain)?.Name;
195
if (forest == null) _log.LogWarning("Error getting forest, ENTDC sid is likely incorrect");
196
return $"{forest ?? "UNKNOWN"}-{sid}".ToUpper();
197
}
198
199
/// <summary>
200
/// Queries the global catalog to get potential SID matches for a username in the forest
201
/// </summary>
202
/// <param name="name"></param>
203
/// <returns></returns>
204
public string[] GetUserGlobalCatalogMatches(string name)
205
{
206
var tempName = name.ToLower();
207
if (Cache.GetGCCache(tempName, out var sids))
208
return sids;
209
210
var query = new LDAPFilter().AddUsers($"samaccountname={tempName}").GetFilter();
211
var results = QueryLDAP(query, SearchScope.Subtree, new[] { "objectsid" }, globalCatalog: true)
212
.Select(x => x.GetSid()).Where(x => x != null).ToArray();
213
Cache.AddGCCache(tempName, results);
214
return results;
215
}
216
217
/// <summary>
218
/// Uses an LDAP lookup to attempt to find the Label for a given SID
219
/// Will also convert to a well known principal ID if needed
220
/// </summary>
221
/// <param name="id"></param>
222
/// <param name="fallbackDomain"></param>
223
/// <returns></returns>
224
public TypedPrincipal ResolveIDAndType(string id, string fallbackDomain)
225
{
226
//This is a duplicated SID object which is weird and makes things unhappy. Throw it out
227
if (id.Contains("0ACNF"))
228
return null;
229
230
var type = id.StartsWith("S-") ? LookupSidType(id, fallbackDomain) : LookupGuidType(id, fallbackDomain);
231
return new TypedPrincipal(id, type);
232
}
233
234
public TypedPrincipal ResolveCertTemplateByProperty(string propValue, string propertyName, string containerDN, s
235
{
236
var filter = new LDAPFilter().AddCertificateTemplates().AddFilter(propertyName + "=" + propValue, true);
237
var res = QueryLDAP(filter.GetFilter(), SearchScope.OneLevel,
238
CommonProperties.TypeResolutionProps, adsPath: containerDN, domainName: domainName);
239
240
if (res == null)
241
{
242
_log.LogWarning("Could not find certificate template with '{propertyName}:{propValue}' under {containerD
243
return null;
244
}
245
246
List<ISearchResultEntry> resList = new List<ISearchResultEntry>(res);
247
if (resList.Count == 0)
248
{
249
_log.LogWarning("Could not find certificate template with '{propertyName}:{propValue}' under {containerD
250
return null;
251
}
252
253
if (resList.Count > 1)
230
if (GetWellKnownPrincipal(id, fallbackDomain, out var principal))
231
return principal;
232
233
var type = id.StartsWith("S-") ? LookupSidType(id, fallbackDomain) : LookupGuidType(id, fallbackDomain);
234
return new TypedPrincipal(id, type);
235
}
236
237
public TypedPrincipal ResolveCertTemplateByProperty(string propValue, string propertyName, string containerDN,
238
string domainName)
239
{
240
var filter = new LDAPFilter().AddCertificateTemplates().AddFilter(propertyName + "=" + propValue, true);
241
var res = QueryLDAP(filter.GetFilter(), SearchScope.OneLevel,
242
CommonProperties.TypeResolutionProps, adsPath: containerDN, domainName: domainName);
243
244
if (res == null)
245
{
246
_log.LogWarning(
247
"Could not find certificate template with '{propertyName}:{propValue}' under {containerDN}; null res
248
propertyName, propValue, containerDN);
249
return null;
250
}
251
252
List<ISearchResultEntry> resList = new List<ISearchResultEntry>(res);
253
if (resList.Count == 0)
254
{
255
_log.LogWarning("Found more than one certificate template with '{propertyName}:{propValue}' under {conta
256
return null;
257
}
258
259
ISearchResultEntry searchResultEntry = resList.FirstOrDefault();
260
return new TypedPrincipal(searchResultEntry.GetGuid(), Label.CertTemplate);
261
}
262
263
/// <summary>
264
/// Attempts to lookup the Label for a sid
265
/// </summary>
266
/// <param name="sid"></param>
267
/// <param name="domain"></param>
268
/// <returns></returns>
269
public Label LookupSidType(string sid, string domain)
270
{
271
if (Cache.GetIDType(sid, out var type))
272
return type;
273
274
var rDomain = GetDomainNameFromSid(sid) ?? domain;
275
276
var result =
277
QueryLDAP(CommonFilters.SpecificSID(sid), SearchScope.Subtree, CommonProperties.TypeResolutionProps,
278
rDomain)
279
.DefaultIfEmpty(null).FirstOrDefault();
280
281
type = result?.GetLabel() ?? Label.Base;
282
Cache.AddType(sid, type);
283
return type;
284
}
255
_log.LogWarning(
256
"Could not find certificate template with '{propertyName}:{propValue}' under {containerDN}; empty li
257
propertyName, propValue, containerDN);
258
return null;
259
}
260
261
if (resList.Count > 1)
262
{
263
_log.LogWarning(
264
"Found more than one certificate template with '{propertyName}:{propValue}' under {containerDN}",
265
propertyName, propValue, containerDN);
266
return null;
267
}
268
269
ISearchResultEntry searchResultEntry = resList.FirstOrDefault();
270
return new TypedPrincipal(searchResultEntry.GetGuid(), Label.CertTemplate);
271
}
272
273
/// <summary>
274
/// Attempts to lookup the Label for a sid
275
/// </summary>
276
/// <param name="sid"></param>
277
/// <param name="domain"></param>
278
/// <returns></returns>
279
public Label LookupSidType(string sid, string domain)
280
{
281
if (Cache.GetIDType(sid, out var type))
282
return type;
283
284
var rDomain = GetDomainNameFromSid(sid) ?? domain;
285
286
/// <summary>
287
/// Attempts to lookup the Label for a GUID
288
/// </summary>
289
/// <param name="guid"></param>
290
/// <param name="domain"></param>
291
/// <returns></returns>
292
public Label LookupGuidType(string guid, string domain)
293
{
294
if (Cache.GetIDType(guid, out var type))
295
return type;
296
297
var hex = Helpers.ConvertGuidToHexGuid(guid);
298
if (hex == null)
299
return Label.Base;
300
301
var result =
302
QueryLDAP($"(objectguid={hex})", SearchScope.Subtree, CommonProperties.TypeResolutionProps, domain)
303
.DefaultIfEmpty(null).FirstOrDefault();
304
305
type = result?.GetLabel() ?? Label.Base;
306
Cache.AddType(guid, type);
307
return type;
308
}
309
310
/// <summary>
311
/// Attempts to find the domain associated with a SID
312
/// </summary>
313
/// <param name="sid"></param>
314
/// <returns></returns>
315
public string GetDomainNameFromSid(string sid)
316
{
317
try
318
{
319
var parsedSid = new SecurityIdentifier(sid);
320
var domainSid = parsedSid.AccountDomainSid?.Value.ToUpper();
321
if (domainSid == null)
322
return null;
323
324
_log.LogDebug("Resolving sid {DomainSid}", domainSid);
325
326
if (Cache.GetDomainSidMapping(domainSid, out var domain))
327
return domain;
328
329
_log.LogDebug("No cache hit for {DomainSid}", domainSid);
330
domain = GetDomainNameFromSidLdap(domainSid);
331
_log.LogDebug("Resolved to {Domain}", domain);
332
333
//Cache both to and from so we can use this later
334
if (domain != null)
335
{
336
Cache.AddSidToDomain(domainSid, domain);
337
Cache.AddSidToDomain(domain, domainSid);
338
}
339
340
return domain;
341
}
342
catch
343
{
344
return null;
345
}
346
}
347
348
/// <summary>
349
/// Attempts to get the SID associated with a domain name
350
/// </summary>
351
/// <param name="domainName"></param>
352
/// <returns></returns>
353
public string GetSidFromDomainName(string domainName)
354
{
355
var tempDomainName = NormalizeDomainName(domainName);
356
if (tempDomainName == null)
357
return null;
358
if (Cache.GetDomainSidMapping(tempDomainName, out var sid)) return sid;
359
360
var domainObj = GetDomain(tempDomainName);
361
362
if (domainObj != null)
363
sid = domainObj.GetDirectoryEntry().GetSid();
364
else
365
sid = null;
366
367
if (sid != null)
368
{
369
Cache.AddSidToDomain(sid, tempDomainName);
370
Cache.AddSidToDomain(tempDomainName, sid);
371
}
372
373
return sid;
374
}
375
376
// Saving this code for an eventual async implementation
377
// public async IAsyncEnumerable<string> DoRangedRetrievalAsync(string distinguishedName, string attributeName)
378
// {
379
// var domainName = Helpers.DistinguishedNameToDomain(distinguishedName);
380
// LdapConnection conn;
381
// try
382
// {
383
// conn = await CreateLDAPConnection(domainName, authType: _ldapConfig.AuthType);
384
// }
385
// catch
386
// {
387
// yield break;
388
// }
389
//
390
// if (conn == null)
391
// yield break;
392
//
393
// var index = 0;
394
// var step = 0;
395
// var currentRange = $"{attributeName};range={index}-*";
396
// var complete = false;
397
//
398
// var searchRequest = CreateSearchRequest($"{attributeName}=*", SearchScope.Base, new[] {currentRange},
399
// domainName, distinguishedName);
400
//
401
// var backoffDelay = MinBackoffDelay;
402
// var retryCount = 0;
403
//
404
// while (true)
405
// {
406
// DirectoryResponse searchResult;
407
// try
408
// {
409
// searchResult = await Task.Factory.FromAsync(conn.BeginSendRequest, conn.EndSendRequest,
410
// searchRequest,
411
// PartialResultProcessing.NoPartialResultSupport, null);
412
// }
413
// catch (LdapException le) when (le.ErrorCode == 51 && retryCount < MaxRetries)
414
// {
415
// //Allow three retries with a backoff on each one if we get a "Server is Busy" error
416
// retryCount++;
417
// await Task.Delay(backoffDelay);
418
// backoffDelay = TimeSpan.FromSeconds(Math.Min(
419
// backoffDelay.TotalSeconds * BackoffDelayMultiplier.TotalSeconds, MaxBackoffDelay.TotalSeconds
420
// continue;
421
// }
422
// catch (Exception e)
423
// {
424
// _log.LogWarning(e,"Caught exception during ranged retrieval for {DN}", distinguishedName);
425
// yield break;
426
// }
427
//
428
// if (searchResult is SearchResponse response && response.Entries.Count == 1)
429
// {
430
// var entry = response.Entries[0];
431
// var attributeNames = entry?.Attributes?.AttributeNames;
432
// if (attributeNames != null)
433
// {
434
// foreach (string attr in attributeNames)
435
// {
436
// //Set our current range to the name of the attribute, which will tell us how far we are i
437
// currentRange = attr;
438
// //Check if the string has the * character in it. If it does, we've reached the end of thi
439
// complete = currentRange.IndexOf("*", 0, StringComparison.Ordinal) > 0;
440
// //Set our step to the number of attributes that came back.
441
// step = entry.Attributes[currentRange].Count;
442
// }
443
// }
444
//
445
//
446
// foreach (string val in entry.Attributes[currentRange].GetValues(typeof(string)))
447
// {
448
// yield return val;
449
// index++;
450
// }
451
//
452
// if (complete) yield break;
453
//
454
// currentRange = $"{attributeName};range={index}-{index + step}";
455
// searchRequest.Attributes.Clear();
456
// searchRequest.Attributes.Add(currentRange);
457
// }
458
// else
459
// {
460
// yield break;
461
// }
462
// }
463
// }
464
465
/// <summary>
466
/// Performs Attribute Ranged Retrieval
467
/// https://docs.microsoft.com/en-us/windows/win32/adsi/attribute-range-retrieval
468
/// The function self-determines the range and internally handles the maximum step allowed by the server
469
/// </summary>
470
/// <param name="distinguishedName"></param>
471
/// <param name="attributeName"></param>
472
/// <returns></returns>
473
public IEnumerable<string> DoRangedRetrieval(string distinguishedName, string attributeName)
474
{
475
var domainName = Helpers.DistinguishedNameToDomain(distinguishedName);
476
var task = Task.Run(() => CreateLDAPConnection(domainName, authType: _ldapConfig.AuthType));
477
478
LdapConnection conn;
479
480
try
481
{
482
conn = task.ConfigureAwait(false).GetAwaiter().GetResult();
483
}
484
catch
485
{
486
yield break;
487
}
488
489
if (conn == null)
490
yield break;
491
492
var index = 0;
493
var step = 0;
494
var baseString = $"{attributeName}";
495
//Example search string: member;range=0-1000
496
var currentRange = $"{baseString};range={index}-*";
497
var complete = false;
286
var result =
287
QueryLDAP(CommonFilters.SpecificSID(sid), SearchScope.Subtree, CommonProperties.TypeResolutionProps,
288
rDomain)
289
.DefaultIfEmpty(null).FirstOrDefault();
290
291
type = result?.GetLabel() ?? Label.Base;
292
Cache.AddType(sid, type);
293
return type;
294
}
295
296
/// <summary>
297
/// Attempts to lookup the Label for a GUID
298
/// </summary>
299
/// <param name="guid"></param>
300
/// <param name="domain"></param>
301
/// <returns></returns>
302
public Label LookupGuidType(string guid, string domain)
303
{
304
if (Cache.GetIDType(guid, out var type))
305
return type;
306
307
var hex = Helpers.ConvertGuidToHexGuid(guid);
308
if (hex == null)
309
return Label.Base;
310
311
var result =
312
QueryLDAP($"(objectguid={hex})", SearchScope.Subtree, CommonProperties.TypeResolutionProps, domain)
313
.DefaultIfEmpty(null).FirstOrDefault();
314
315
type = result?.GetLabel() ?? Label.Base;
316
Cache.AddType(guid, type);
317
return type;
318
}
319
320
/// <summary>
321
/// Attempts to find the domain associated with a SID
322
/// </summary>
323
/// <param name="sid"></param>
324
/// <returns></returns>
325
public string GetDomainNameFromSid(string sid)
326
{
327
try
328
{
329
var parsedSid = new SecurityIdentifier(sid);
330
var domainSid = parsedSid.AccountDomainSid?.Value.ToUpper();
331
if (domainSid == null)
332
return null;
333
334
_log.LogDebug("Resolving sid {DomainSid}", domainSid);
335
336
if (Cache.GetDomainSidMapping(domainSid, out var domain))
337
return domain;
338
339
_log.LogDebug("No cache hit for {DomainSid}", domainSid);
340
domain = GetDomainNameFromSidLdap(domainSid);
341
_log.LogDebug("Resolved to {Domain}", domain);
342
343
//Cache both to and from so we can use this later
344
if (domain != null)
345
{
346
Cache.AddSidToDomain(domainSid, domain);
347
Cache.AddSidToDomain(domain, domainSid);
348
}
349
350
return domain;
351
}
352
catch
353
{
354
return null;
355
}
356
}
357
358
/// <summary>
359
/// Attempts to get the SID associated with a domain name
360
/// </summary>
361
/// <param name="domainName"></param>
362
/// <returns></returns>
363
public string GetSidFromDomainName(string domainName)
364
{
365
var tempDomainName = NormalizeDomainName(domainName);
366
if (tempDomainName == null)
367
return null;
368
if (Cache.GetDomainSidMapping(tempDomainName, out var sid)) return sid;
369
370
var domainObj = GetDomain(tempDomainName);
371
372
if (domainObj != null)
373
sid = domainObj.GetDirectoryEntry().GetSid();
374
else
375
sid = null;
376
377
if (sid != null)
378
{
379
Cache.AddSidToDomain(sid, tempDomainName);
380
Cache.AddSidToDomain(tempDomainName, sid);
381
}
382
383
return sid;
384
}
385
386
// Saving this code for an eventual async implementation
387
// public async IAsyncEnumerable<string> DoRangedRetrievalAsync(string distinguishedName, string attributeName)
388
// {
389
// var domainName = Helpers.DistinguishedNameToDomain(distinguishedName);
390
// LdapConnection conn;
391
// try
392
// {
393
// conn = await CreateLDAPConnection(domainName, authType: _ldapConfig.AuthType);
394
// }
395
// catch
396
// {
397
// yield break;
398
// }
399
//
400
// if (conn == null)
401
// yield break;
402
//
403
// var index = 0;
404
// var step = 0;
405
// var currentRange = $"{attributeName};range={index}-*";
406
// var complete = false;
407
//
408
// var searchRequest = CreateSearchRequest($"{attributeName}=*", SearchScope.Base, new[] {currentRange},
409
// domainName, distinguishedName);
410
//
411
// var backoffDelay = MinBackoffDelay;
412
// var retryCount = 0;
413
//
414
// while (true)
415
// {
416
// DirectoryResponse searchResult;
417
// try
418
// {
419
// searchResult = await Task.Factory.FromAsync(conn.BeginSendRequest, conn.EndSendRequest,
420
// searchRequest,
421
// PartialResultProcessing.NoPartialResultSupport, null);
422
// }
423
// catch (LdapException le) when (le.ErrorCode == 51 && retryCount < MaxRetries)
424
// {
425
// //Allow three retries with a backoff on each one if we get a "Server is Busy" error
426
// retryCount++;
427
// await Task.Delay(backoffDelay);
428
// backoffDelay = TimeSpan.FromSeconds(Math.Min(
429
// backoffDelay.TotalSeconds * BackoffDelayMultiplier.TotalSeconds, MaxBackoffDelay.TotalSeconds
430
// continue;
431
// }
432
// catch (Exception e)
433
// {
434
// _log.LogWarning(e,"Caught exception during ranged retrieval for {DN}", distinguishedName);
435
// yield break;
436
// }
437
//
438
// if (searchResult is SearchResponse response && response.Entries.Count == 1)
439
// {
440
// var entry = response.Entries[0];
441
// var attributeNames = entry?.Attributes?.AttributeNames;
442
// if (attributeNames != null)
443
// {
444
// foreach (string attr in attributeNames)
445
// {
446
// //Set our current range to the name of the attribute, which will tell us how far we are i
447
// currentRange = attr;
448
// //Check if the string has the * character in it. If it does, we've reached the end of thi
449
// complete = currentRange.IndexOf("*", 0, StringComparison.Ordinal) > 0;
450
// //Set our step to the number of attributes that came back.
451
// step = entry.Attributes[currentRange].Count;
452
// }
453
// }
454
//
455
//
456
// foreach (string val in entry.Attributes[currentRange].GetValues(typeof(string)))
457
// {
458
// yield return val;
459
// index++;
460
// }
461
//
462
// if (complete) yield break;
463
//
464
// currentRange = $"{attributeName};range={index}-{index + step}";
465
// searchRequest.Attributes.Clear();
466
// searchRequest.Attributes.Add(currentRange);
467
// }
468
// else
469
// {
470
// yield break;
471
// }
472
// }
473
// }
474
475
/// <summary>
476
/// Performs Attribute Ranged Retrieval
477
/// https://docs.microsoft.com/en-us/windows/win32/adsi/attribute-range-retrieval
478
/// The function self-determines the range and internally handles the maximum step allowed by the server
479
/// </summary>
480
/// <param name="distinguishedName"></param>
481
/// <param name="attributeName"></param>
482
/// <returns></returns>
483
public IEnumerable<string> DoRangedRetrieval(string distinguishedName, string attributeName)
484
{
485
var domainName = Helpers.DistinguishedNameToDomain(distinguishedName);
486
var task = Task.Run(() => CreateLDAPConnection(domainName, authType: _ldapConfig.AuthType));
487
488
LdapConnection conn;
489
490
try
491
{
492
conn = task.ConfigureAwait(false).GetAwaiter().GetResult();
493
}
494
catch
495
{
496
yield break;
497
}
498
499
var searchRequest = CreateSearchRequest($"{attributeName}=*", SearchScope.Base, new[] { currentRange },
500
domainName, distinguishedName);
499
if (conn == null)
500
yield break;
501
502
if (searchRequest == null)
503
yield break;
504
505
var backoffDelay = MinBackoffDelay;
506
var retryCount = 0;
507
508
while (true)
509
{
510
SearchResponse response;
511
try
512
{
513
response = (SearchResponse)conn.SendRequest(searchRequest);
514
}
515
catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.Busy && retryCount < MaxRetries)
516
{
517
//Allow three retries with a backoff on each one if we get a "Server is Busy" error
518
retryCount++;
519
Thread.Sleep(backoffDelay);
520
backoffDelay = TimeSpan.FromSeconds(Math.Min(
521
backoffDelay.TotalSeconds * BackoffDelayMultiplier.TotalSeconds, MaxBackoffDelay.TotalSeconds));
522
continue;
523
}
524
catch (Exception e)
525
{
526
_log.LogError(e, "Error doing ranged retrieval for {Attribute} on {Dn}", attributeName, distinguishe
527
yield break;
528
}
529
530
//If we ever get more than one response from here, something is horribly wrong
531
if (response?.Entries.Count == 1)
532
{
533
var entry = response.Entries[0];
534
//Process the attribute we get back to determine a few things
535
foreach (string attr in entry.Attributes.AttributeNames)
536
{
537
//Set our current range to the name of the attribute, which will tell us how far we are in "pagi
538
currentRange = attr;
539
//Check if the string has the * character in it. If it does, we've reached the end of this searc
540
complete = currentRange.IndexOf("*", 0, StringComparison.Ordinal) > 0;
541
//Set our step to the number of attributes that came back.
542
step = entry.Attributes[currentRange].Count;
543
}
544
545
foreach (string val in entry.Attributes[currentRange].GetValues(typeof(string)))
502
var index = 0;
503
var step = 0;
504
var baseString = $"{attributeName}";
505
//Example search string: member;range=0-1000
506
var currentRange = $"{baseString};range={index}-*";
507
var complete = false;
508
509
var searchRequest = CreateSearchRequest($"{attributeName}=*", SearchScope.Base, new[] { currentRange },
510
domainName, distinguishedName);
511
512
if (searchRequest == null)
513
yield break;
514
515
var backoffDelay = MinBackoffDelay;
516
var retryCount = 0;
517
518
while (true)
519
{
520
SearchResponse response;
521
try
522
{
523
response = (SearchResponse)conn.SendRequest(searchRequest);
524
}
525
catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.Busy && retryCount < MaxRetries)
526
{
527
//Allow three retries with a backoff on each one if we get a "Server is Busy" error
528
retryCount++;
529
Thread.Sleep(backoffDelay);
530
backoffDelay = GetNextBackoff(retryCount);
531
continue;
532
}
533
catch (Exception e)
534
{
535
_log.LogError(e, "Error doing ranged retrieval for {Attribute} on {Dn}", attributeName,
536
distinguishedName);
537
yield break;
538
}
539
540
//If we ever get more than one response from here, something is horribly wrong
541
if (response?.Entries.Count == 1)
542
{
543
var entry = response.Entries[0];
544
//Process the attribute we get back to determine a few things
545
foreach (string attr in entry.Attributes.AttributeNames)
546
{
547
yield return val;
548
index++;
549
}
550
551
if (complete) yield break;
552
553
currentRange = $"{baseString};range={index}-{index + step}";
554
searchRequest.Attributes.Clear();
555
searchRequest.Attributes.Add(currentRange);
556
}
557
else
558
{
559
//Something went wrong here.
560
yield break;
561
}
562
}
563
}
564
565
/// <summary>
566
/// Takes a host in most applicable forms from AD and attempts to resolve it into a SID.
567
/// </summary>
568
/// <param name="hostname"></param>
569
/// <param name="domain"></param>
570
/// <returns></returns>
571
public async Task<string> ResolveHostToSid(string hostname, string domain)
572
{
573
var strippedHost = Helpers.StripServicePrincipalName(hostname).ToUpper().TrimEnd('$');
547
//Set our current range to the name of the attribute, which will tell us how far we are in "pagi
548
currentRange = attr;
549
//Check if the string has the * character in it. If it does, we've reached the end of this searc
550
complete = currentRange.IndexOf("*", 0, StringComparison.Ordinal) > 0;
551
//Set our step to the number of attributes that came back.
552
step = entry.Attributes[currentRange].Count;
553
}
554
555
foreach (string val in entry.Attributes[currentRange].GetValues(typeof(string)))
556
{
557
yield return val;
558
index++;
559
}
560
561
if (complete) yield break;
562
563
currentRange = $"{baseString};range={index}-{index + step}";
564
searchRequest.Attributes.Clear();
565
searchRequest.Attributes.Add(currentRange);
566
}
567
else
568
{
569
//Something went wrong here.
570
yield break;
571
}
572
}
573
}
574
575
if (_hostResolutionMap.TryGetValue(strippedHost, out var sid)) return sid;
576
577
var normalDomain = NormalizeDomainName(domain);
578
579
string tempName;
580
string tempDomain = null;
581
582
//Step 1: Handle non-IP address values
583
if (!IPAddress.TryParse(strippedHost, out _))
584
{
585
// Format: ABC.TESTLAB.LOCAL
586
if (strippedHost.Contains("."))
587
{
588
var split = strippedHost.Split('.');
589
tempName = split[0];
590
tempDomain = string.Join(".", split.Skip(1).ToArray());
591
}
592
// Format: WINDOWS
593
else
594
{
595
tempName = strippedHost;
596
tempDomain = normalDomain;
597
}
598
599
// Add $ to the end of the name to match how computers are stored in AD
600
tempName = $"{tempName}$".ToUpper();
601
var principal = ResolveAccountName(tempName, tempDomain);
602
sid = principal?.ObjectIdentifier;
603
if (sid != null)
604
{
605
_hostResolutionMap.TryAdd(strippedHost, sid);
606
return sid;
607
}
608
}
609
610
//Step 2: Try NetWkstaGetInfo
611
//Next we'll try calling NetWkstaGetInfo in hopes of getting the NETBIOS name directly from the computer
612
//We'll use the hostname that we started with instead of the one from our previous step
613
var workstationInfo = await GetWorkstationInfo(strippedHost);
614
if (workstationInfo.HasValue)
615
{
616
tempName = workstationInfo.Value.ComputerName;
617
tempDomain = workstationInfo.Value.LanGroup;
618
619
if (string.IsNullOrEmpty(tempDomain))
620
tempDomain = normalDomain;
621
622
if (!string.IsNullOrEmpty(tempName))
623
{
624
//Append the $ to indicate this is a computer
625
tempName = $"{tempName}$".ToUpper();
626
var principal = ResolveAccountName(tempName, tempDomain);
627
sid = principal?.ObjectIdentifier;
628
if (sid != null)
629
{
630
_hostResolutionMap.TryAdd(strippedHost, sid);
631
return sid;
632
}
633
}
634
}
575
/// <summary>
576
/// Takes a host in most applicable forms from AD and attempts to resolve it into a SID.
577
/// </summary>
578
/// <param name="hostname"></param>
579
/// <param name="domain"></param>
580
/// <returns></returns>
581
public async Task<string> ResolveHostToSid(string hostname, string domain)
582
{
583
var strippedHost = Helpers.StripServicePrincipalName(hostname).ToUpper().TrimEnd('$');
584
if (string.IsNullOrEmpty(strippedHost))
585
{
586
return null;
587
}
588
589
if (_hostResolutionMap.TryGetValue(strippedHost, out var sid)) return sid;
590
591
var normalDomain = NormalizeDomainName(domain);
592
593
string tempName;
594
string tempDomain = null;
595
596
//Step 1: Handle non-IP address values
597
if (!IPAddress.TryParse(strippedHost, out _))
598
{
599
// Format: ABC.TESTLAB.LOCAL
600
if (strippedHost.Contains("."))
601
{
602
var split = strippedHost.Split('.');
603
tempName = split[0];
604
tempDomain = string.Join(".", split.Skip(1).ToArray());
605
}
606
// Format: WINDOWS
607
else
608
{
609
tempName = strippedHost;
610
tempDomain = normalDomain;
611
}
612
613
// Add $ to the end of the name to match how computers are stored in AD
614
tempName = $"{tempName}$".ToUpper();
615
var principal = ResolveAccountName(tempName, tempDomain);
616
sid = principal?.ObjectIdentifier;
617
if (sid != null)
618
{
619
_hostResolutionMap.TryAdd(strippedHost, sid);
620
return sid;
621
}
622
}
623
624
//Step 2: Try NetWkstaGetInfo
625
//Next we'll try calling NetWkstaGetInfo in hopes of getting the NETBIOS name directly from the computer
626
//We'll use the hostname that we started with instead of the one from our previous step
627
var workstationInfo = await GetWorkstationInfo(strippedHost);
628
if (workstationInfo.HasValue)
629
{
630
tempName = workstationInfo.Value.ComputerName;
631
tempDomain = workstationInfo.Value.LanGroup;
632
633
if (string.IsNullOrEmpty(tempDomain))
634
tempDomain = normalDomain;
635
636
//Step 3: Socket magic
637
// Attempt to request the NETBIOS name of the computer directly
638
if (RequestNETBIOSNameFromComputer(strippedHost, normalDomain, out tempName))
639
{
640
tempDomain ??= normalDomain;
641
tempName = $"{tempName}$".ToUpper();
642
643
var principal = ResolveAccountName(tempName, tempDomain);
644
sid = principal?.ObjectIdentifier;
645
if (sid != null)
646
{
647
_hostResolutionMap.TryAdd(strippedHost, sid);
648
return sid;
649
}
650
}
651
652
//Try DNS resolution next
653
string resolvedHostname;
654
try
655
{
656
resolvedHostname = (await Dns.GetHostEntryAsync(strippedHost)).HostName;
657
}
658
catch
659
{
660
resolvedHostname = null;
661
}
662
663
if (resolvedHostname != null)
664
{
665
var splitName = resolvedHostname.Split('.');
666
tempName = $"{splitName[0]}$".ToUpper();
667
tempDomain = string.Join(".", splitName.Skip(1));
668
669
var principal = ResolveAccountName(tempName, tempDomain);
670
sid = principal?.ObjectIdentifier;
671
if (sid != null)
672
{
673
_hostResolutionMap.TryAdd(strippedHost, sid);
674
return sid;
675
}
676
}
677
678
//If we get here, everything has failed, and life is very sad.
679
tempName = strippedHost;
680
tempDomain = normalDomain;
681
682
if (tempName.Contains("."))
683
{
684
_hostResolutionMap.TryAdd(strippedHost, tempName);
685
return tempName;
686
}
687
688
tempName = $"{tempName}.{tempDomain}";
689
_hostResolutionMap.TryAdd(strippedHost, tempName);
690
return tempName;
691
}
692
693
/// <summary>
694
/// Attempts to convert a bare account name (usually from session enumeration) to its corresponding ID and o
695
/// </summary>
696
/// <param name="name"></param>
697
/// <param name="domain"></param>
698
/// <returns></returns>
699
public TypedPrincipal ResolveAccountName(string name, string domain)
700
{
701
if (string.IsNullOrWhiteSpace(name))
702
return null;
703
704
if (Cache.GetPrefixedValue(name, domain, out var id) && Cache.GetIDType(id, out var type))
705
return new TypedPrincipal
706
{
707
ObjectIdentifier = id,
708
ObjectType = type
709
};
710
711
var d = NormalizeDomainName(domain);
712
var result = QueryLDAP($"(samaccountname={name})", SearchScope.Subtree,
713
CommonProperties.TypeResolutionProps,
714
d).DefaultIfEmpty(null).FirstOrDefault();
715
716
if (result == null)
717
{
718
_log.LogDebug("ResolveAccountName - unable to get result for {Name}", name);
719
return null;
720
}
721
722
type = result.GetLabel();
723
id = result.GetObjectIdentifier();
636
if (!string.IsNullOrEmpty(tempName))
637
{
638
//Append the $ to indicate this is a computer
639
tempName = $"{tempName}$".ToUpper();
640
var principal = ResolveAccountName(tempName, tempDomain);
641
sid = principal?.ObjectIdentifier;
642
if (sid != null)
643
{
644
_hostResolutionMap.TryAdd(strippedHost, sid);
645
return sid;
646
}
647
}
648
}
649
650
//Step 3: Socket magic
651
// Attempt to request the NETBIOS name of the computer directly
652
if (RequestNETBIOSNameFromComputer(strippedHost, normalDomain, out tempName))
653
{
654
tempDomain ??= normalDomain;
655
tempName = $"{tempName}$".ToUpper();
656
657
var principal = ResolveAccountName(tempName, tempDomain);
658
sid = principal?.ObjectIdentifier;
659
if (sid != null)
660
{
661
_hostResolutionMap.TryAdd(strippedHost, sid);
662
return sid;
663
}
664
}
665
666
//Try DNS resolution next
667
string resolvedHostname;
668
try
669
{
670
resolvedHostname = (await Dns.GetHostEntryAsync(strippedHost)).HostName;
671
}
672
catch
673
{
674
resolvedHostname = null;
675
}
676
677
if (resolvedHostname != null)
678
{
679
var splitName = resolvedHostname.Split('.');
680
tempName = $"{splitName[0]}$".ToUpper();
681
tempDomain = string.Join(".", splitName.Skip(1));
682
683
var principal = ResolveAccountName(tempName, tempDomain);
684
sid = principal?.ObjectIdentifier;
685
if (sid != null)
686
{
687
_hostResolutionMap.TryAdd(strippedHost, sid);
688
return sid;
689
}
690
}
691
692
//If we get here, everything has failed, and life is very sad.
693
tempName = strippedHost;
694
tempDomain = normalDomain;
695
696
if (tempName.Contains("."))
697
{
698
_hostResolutionMap.TryAdd(strippedHost, tempName);
699
return tempName;
700
}
701
702
tempName = $"{tempName}.{tempDomain}";
703
_hostResolutionMap.TryAdd(strippedHost, tempName);
704
return tempName;
705
}
706
707
/// <summary>
708
/// Attempts to convert a bare account name (usually from session enumeration) to its corresponding ID and o
709
/// </summary>
710
/// <param name="name"></param>
711
/// <param name="domain"></param>
712
/// <returns></returns>
713
public TypedPrincipal ResolveAccountName(string name, string domain)
714
{
715
if (string.IsNullOrWhiteSpace(name))
716
return null;
717
718
if (Cache.GetPrefixedValue(name, domain, out var id) && Cache.GetIDType(id, out var type))
719
return new TypedPrincipal
720
{
721
ObjectIdentifier = id,
722
ObjectType = type
723
};
724
725
if (id == null)
726
{
727
_log.LogDebug("ResolveAccountName - could not retrieve ID on {DN} for {Name}", result.DistinguishedName,
728
name);
729
return null;
730
}
731
732
Cache.AddPrefixedValue(name, domain, id);
733
Cache.AddType(id, type);
734
735
id = ConvertWellKnownPrincipal(id, domain);
736
737
return new TypedPrincipal
738
{
739
ObjectIdentifier = id,
740
ObjectType = type
741
};
742
}
743
744
/// <summary>
745
/// Attempts to convert a distinguishedname to its corresponding ID and object type.
746
/// </summary>
747
/// <param name="dn">DistinguishedName</param>
748
/// <returns>A <c>TypedPrincipal</c> object with the SID and Label</returns>
749
public TypedPrincipal ResolveDistinguishedName(string dn)
750
{
751
if (Cache.GetConvertedValue(dn, out var id) && Cache.GetIDType(id, out var type))
752
return new TypedPrincipal
753
{
754
ObjectIdentifier = id,
755
ObjectType = type
756
};
725
var d = NormalizeDomainName(domain);
726
var result = QueryLDAP($"(samaccountname={name})", SearchScope.Subtree,
727
CommonProperties.TypeResolutionProps,
728
d).DefaultIfEmpty(null).FirstOrDefault();
729
730
if (result == null)
731
{
732
_log.LogDebug("ResolveAccountName - unable to get result for {Name}", name);
733
return null;
734
}
735
736
type = result.GetLabel();
737
id = result.GetObjectIdentifier();
738
739
if (id == null)
740
{
741
_log.LogDebug("ResolveAccountName - could not retrieve ID on {DN} for {Name}", result.DistinguishedName,
742
name);
743
return null;
744
}
745
746
Cache.AddPrefixedValue(name, domain, id);
747
Cache.AddType(id, type);
748
749
id = ConvertWellKnownPrincipal(id, domain);
750
751
return new TypedPrincipal
752
{
753
ObjectIdentifier = id,
754
ObjectType = type
755
};
756
}
757
758
var domain = Helpers.DistinguishedNameToDomain(dn);
759
var result = QueryLDAP("(objectclass=*)", SearchScope.Base, CommonProperties.TypeResolutionProps, domain,
760
adsPath: dn)
761
.DefaultIfEmpty(null).FirstOrDefault();
762
763
if (result == null)
764
{
765
_log.LogDebug("ResolveDistinguishedName - No result for {DN}", dn);
766
return null;
767
}
768
769
id = result.GetObjectIdentifier();
770
if (id == null)
771
{
772
_log.LogDebug("ResolveDistinguishedName - could not retrieve object identifier from {DN}", dn);
773
return null;
774
}
775
776
if (GetWellKnownPrincipal(id, domain, out var principal)) return principal;
777
778
type = result.GetLabel();
779
780
Cache.AddConvertedValue(dn, id);
781
Cache.AddType(id, type);
758
/// <summary>
759
/// Attempts to convert a distinguishedname to its corresponding ID and object type.
760
/// </summary>
761
/// <param name="dn">DistinguishedName</param>
762
/// <returns>A <c>TypedPrincipal</c> object with the SID and Label</returns>
763
public TypedPrincipal ResolveDistinguishedName(string dn)
764
{
765
if (Cache.GetConvertedValue(dn, out var id) && Cache.GetIDType(id, out var type))
766
return new TypedPrincipal
767
{
768
ObjectIdentifier = id,
769
ObjectType = type
770
};
771
772
var domain = Helpers.DistinguishedNameToDomain(dn);
773
var result = QueryLDAP("(objectclass=*)", SearchScope.Base, CommonProperties.TypeResolutionProps, domain,
774
adsPath: dn)
775
.DefaultIfEmpty(null).FirstOrDefault();
776
777
if (result == null)
778
{
779
_log.LogDebug("ResolveDistinguishedName - No result for {DN}", dn);
780
return null;
781
}
782
783
id = ConvertWellKnownPrincipal(id, domain);
784
785
return new TypedPrincipal
786
{
787
ObjectIdentifier = id,
788
ObjectType = type
789
};
790
}
783
id = result.GetObjectIdentifier();
784
if (id == null)
785
{
786
_log.LogDebug("ResolveDistinguishedName - could not retrieve object identifier from {DN}", dn);
787
return null;
788
}
789
790
if (GetWellKnownPrincipal(id, domain, out var principal)) return principal;
791
792
/// <summary>
793
/// Queries LDAP using LDAPQueryOptions
794
/// </summary>
795
/// <param name="options"></param>
796
/// <returns></returns>
797
public IEnumerable<ISearchResultEntry> QueryLDAP(LDAPQueryOptions options)
798
{
799
return QueryLDAP(
800
options.Filter,
801
options.Scope,
802
options.Properties,
803
options.CancellationToken,
804
options.DomainName,
805
options.IncludeAcl,
806
options.ShowDeleted,
807
options.AdsPath,
808
options.GlobalCatalog,
809
options.SkipCache,
810
options.ThrowException
811
);
812
}
813
814
/// <summary>
815
/// Performs an LDAP query using the parameters specified by the user.
816
/// </summary>
817
/// <param name="ldapFilter">LDAP filter</param>
818
/// <param name="scope">SearchScope to query</param>
819
/// <param name="props">LDAP properties to fetch for each object</param>
820
/// <param name="cancellationToken">Cancellation Token</param>
821
/// <param name="includeAcl">Include the DACL and Owner values in the NTSecurityDescriptor</param>
822
/// <param name="showDeleted">Include deleted objects</param>
823
/// <param name="domainName">Domain to query</param>
824
/// <param name="adsPath">ADS path to limit the query too</param>
825
/// <param name="globalCatalog">Use the global catalog instead of the regular LDAP server</param>
826
/// <param name="skipCache">
827
/// Skip the connection cache and force a new connection. You must dispose of this connection
828
/// yourself.
829
/// </param>
830
/// <param name="throwException">Throw exceptions rather than logging the errors directly</param>
831
/// <returns>All LDAP search results matching the specified parameters</returns>
832
/// <exception cref="LDAPQueryException">
833
/// Thrown when an error occurs during LDAP query (only when throwException = true)
834
/// </exception>
835
public IEnumerable<ISearchResultEntry> QueryLDAP(string ldapFilter, SearchScope scope,
836
string[] props, CancellationToken cancellationToken, string domainName = null, bool includeAcl = false,
837
bool showDeleted = false, string adsPath = null, bool globalCatalog = false, bool skipCache = false,
838
bool throwException = false)
839
{
840
var queryParams = SetupLDAPQueryFilter(
841
ldapFilter, scope, props, includeAcl, domainName, includeAcl, adsPath, globalCatalog, skipCache);
842
843
if (queryParams.Exception != null)
844
{
845
_log.LogWarning("Failed to setup LDAP Query Filter: {Message}", queryParams.Exception.Message);
846
if (throwException) throw new LDAPQueryException("Failed to setup LDAP Query Filter", queryParams.Except
847
yield break;
848
}
849
850
var conn = queryParams.Connection;
851
var request = queryParams.SearchRequest;
852
var pageControl = queryParams.PageControl;
853
854
PageResultResponseControl pageResponse = null;
855
var backoffDelay = MinBackoffDelay;
856
var retryCount = 0;
857
while (true)
858
{
859
if (cancellationToken.IsCancellationRequested)
860
yield break;
861
862
SearchResponse response;
863
try
864
{
865
_log.LogTrace("Sending LDAP request for {Filter}", ldapFilter);
866
response = (SearchResponse)conn.SendRequest(request);
867
if (response != null)
868
pageResponse = (PageResultResponseControl)response.Controls
869
.Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault();
870
}catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown &&
871
retryCount < MaxRetries)
872
{
873
retryCount++;
874
Thread.Sleep(backoffDelay);
875
backoffDelay = TimeSpan.FromSeconds(Math.Min(
876
backoffDelay.TotalSeconds * BackoffDelayMultiplier.TotalSeconds, MaxBackoffDelay.TotalSeconds));
877
conn = CreateNewConnection(domainName, globalCatalog, skipCache);
878
if (conn == null)
879
{
880
_log.LogError("Unable to create replacement ldap connection for ServerDown exception. Breaking l
881
yield break;
882
}
883
884
_log.LogInformation("Created new LDAP connection after receiving ServerDown from server");
885
continue;
886
}catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.Busy && retryCount < MaxRetries) {
887
retryCount++;
888
Thread.Sleep(backoffDelay);
889
backoffDelay = TimeSpan.FromSeconds(Math.Min(
890
backoffDelay.TotalSeconds * BackoffDelayMultiplier.TotalSeconds, MaxBackoffDelay.TotalSeconds));
891
continue;
892
}
893
catch (LdapException le)
894
{
895
if (le.ErrorCode != 82)
896
if (throwException)
897
throw new LDAPQueryException(
898
$"LDAP Exception in Loop: {le.ErrorCode}. {le.ServerErrorMessage}. {le.Message}. Filter:
899
le);
900
else
901
_log.LogWarning(le,
902
"LDAP Exception in Loop: {ErrorCode}. {ServerErrorMessage}. {Message}. Filter: {Filter}.
903
le.ErrorCode, le.ServerErrorMessage, le.Message, ldapFilter, domainName);
904
905
yield break;
906
}
907
catch (Exception e)
908
{
909
_log.LogWarning(e, "Exception in LDAP loop for {Filter} and {Domain}", ldapFilter, domainName);
910
if (throwException)
911
throw new LDAPQueryException($"Exception in LDAP loop for {ldapFilter} and {domainName}", e);
912
913
yield break;
914
}
915
916
if (cancellationToken.IsCancellationRequested)
917
yield break;
918
919
if (response == null || pageResponse == null)
920
continue;
921
922
foreach (SearchResultEntry entry in response.Entries)
923
{
924
if (cancellationToken.IsCancellationRequested)
925
yield break;
926
927
yield return new SearchResultEntryWrapper(entry, this);
928
}
929
930
if (pageResponse.Cookie.Length == 0 || response.Entries.Count == 0 ||
931
cancellationToken.IsCancellationRequested)
932
yield break;
933
934
pageControl.Cookie = pageResponse.Cookie;
935
}
936
}
937
938
private LdapConnection CreateNewConnection(string domainName = null, bool globalCatalog = false, bool skipCache
939
{
940
var task = globalCatalog
941
? Task.Run(() => CreateGlobalCatalogConnection(domainName, _ldapConfig.AuthType))
942
: Task.Run(() => CreateLDAPConnection(domainName, skipCache, _ldapConfig.AuthType));
943
944
try
945
{
946
return task.ConfigureAwait(false).GetAwaiter().GetResult();
947
}
948
catch
949
{
950
return null;
951
}
952
}
953
954
/// <summary>
955
/// Performs an LDAP query using the parameters specified by the user.
956
/// </summary>
957
/// <param name="ldapFilter">LDAP filter</param>
958
/// <param name="scope">SearchScope to query</param>
959
/// <param name="props">LDAP properties to fetch for each object</param>
960
/// <param name="includeAcl">Include the DACL and Owner values in the NTSecurityDescriptor</param>
961
/// <param name="showDeleted">Include deleted objects</param>
962
/// <param name="domainName">Domain to query</param>
963
/// <param name="adsPath">ADS path to limit the query too</param>
964
/// <param name="globalCatalog">Use the global catalog instead of the regular LDAP server</param>
965
/// <param name="skipCache">
966
/// Skip the connection cache and force a new connection. You must dispose of this connection
967
/// yourself.
968
/// </param>
969
/// <param name="throwException">Throw exceptions rather than logging the errors directly</param>
970
/// <returns>All LDAP search results matching the specified parameters</returns>
971
/// <exception cref="LDAPQueryException">
972
/// Thrown when an error occurs during LDAP query (only when throwException = true)
973
/// </exception>
974
public virtual IEnumerable<ISearchResultEntry> QueryLDAP(string ldapFilter, SearchScope scope,
975
string[] props, string domainName = null, bool includeAcl = false, bool showDeleted = false,
976
string adsPath = null, bool globalCatalog = false, bool skipCache = false, bool throwException = false)
977
{
978
var queryParams = SetupLDAPQueryFilter(
979
ldapFilter, scope, props, includeAcl, domainName, includeAcl, adsPath, globalCatalog, skipCache);
980
981
if (queryParams.Exception != null)
982
{
983
if (throwException) throw queryParams.Exception;
984
985
_log.LogWarning(queryParams.Exception, "Failed to setup LDAP Query Filter");
986
yield break;
987
}
988
var conn = queryParams.Connection;
989
var request = queryParams.SearchRequest;
990
var pageControl = queryParams.PageControl;
991
992
PageResultResponseControl pageResponse = null;
792
type = result.GetLabel();
793
794
Cache.AddConvertedValue(dn, id);
795
Cache.AddType(id, type);
796
797
id = ConvertWellKnownPrincipal(id, domain);
798
799
return new TypedPrincipal
800
{
801
ObjectIdentifier = id,
802
ObjectType = type
803
};
804
}
805
806
/// <summary>
807
/// Queries LDAP using LDAPQueryOptions
808
/// </summary>
809
/// <param name="options"></param>
810
/// <returns></returns>
811
public IEnumerable<ISearchResultEntry> QueryLDAP(LDAPQueryOptions options)
812
{
813
return QueryLDAP(
814
options.Filter,
815
options.Scope,
816
options.Properties,
817
options.CancellationToken,
818
options.DomainName,
819
options.IncludeAcl,
820
options.ShowDeleted,
821
options.AdsPath,
822
options.GlobalCatalog,
823
options.SkipCache,
824
options.ThrowException
825
);
826
}
827
828
/// <summary>
829
/// Performs an LDAP query using the parameters specified by the user.
830
/// </summary>
831
/// <param name="ldapFilter">LDAP filter</param>
832
/// <param name="scope">SearchScope to query</param>
833
/// <param name="props">LDAP properties to fetch for each object</param>
834
/// <param name="cancellationToken">Cancellation Token</param>
835
/// <param name="includeAcl">Include the DACL and Owner values in the NTSecurityDescriptor</param>
836
/// <param name="showDeleted">Include deleted objects</param>
837
/// <param name="domainName">Domain to query</param>
838
/// <param name="adsPath">ADS path to limit the query too</param>
839
/// <param name="globalCatalog">Use the global catalog instead of the regular LDAP server</param>
840
/// <param name="skipCache">
841
/// Skip the connection cache and force a new connection. You must dispose of this connection
842
/// yourself.
843
/// </param>
844
/// <param name="throwException">Throw exceptions rather than logging the errors directly</param>
845
/// <returns>All LDAP search results matching the specified parameters</returns>
846
/// <exception cref="LDAPQueryException">
847
/// Thrown when an error occurs during LDAP query (only when throwException = true)
848
/// </exception>
849
public IEnumerable<ISearchResultEntry> QueryLDAP(string ldapFilter, SearchScope scope,
850
string[] props, CancellationToken cancellationToken, string domainName = null, bool includeAcl = false,
851
bool showDeleted = false, string adsPath = null, bool globalCatalog = false, bool skipCache = false,
852
bool throwException = false)
853
{
854
var queryParams = SetupLDAPQueryFilter(
855
ldapFilter, scope, props, includeAcl, domainName, includeAcl, adsPath, globalCatalog, skipCache);
856
857
if (queryParams.Exception != null)
858
{
859
_log.LogWarning("Failed to setup LDAP Query Filter: {Message}", queryParams.Exception.Message);
860
if (throwException)
861
throw new LDAPQueryException("Failed to setup LDAP Query Filter", queryParams.Exception);
862
yield break;
863
}
864
865
var conn = queryParams.Connection;
866
var request = queryParams.SearchRequest;
867
var pageControl = queryParams.PageControl;
868
869
PageResultResponseControl pageResponse = null;
870
var backoffDelay = MinBackoffDelay;
871
var retryCount = 0;
872
while (true)
873
{
874
if (cancellationToken.IsCancellationRequested)
875
yield break;
876
877
SearchResponse response;
878
try
879
{
880
_log.LogTrace("Sending LDAP request for {Filter}", ldapFilter);
881
response = (SearchResponse)conn.SendRequest(request);
882
if (response != null)
883
pageResponse = (PageResultResponseControl)response.Controls
884
.Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault();
885
}
886
catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown &&
887
retryCount < MaxRetries)
888
{
889
/*A ServerDown exception indicates that our connection is no longer valid for one of many reasons.
890
However, this function is generally called by multiple threads, so we need to be careful in recreati
891
the connection. Using a semaphore, we can ensure that only one thread is actually recreating the con
892
while the other threads that hit the ServerDown exception simply wait. The initial caller will hold
893
and do a backoff delay before trying to make a new connection which will replace the existing connec
894
_ldapConnections cache. Other threads will retrieve the new connection from the cache instead of mak
895
This minimizes overhead of new connections while still fixing our core problem.*/
896
897
//Always increment retry count
898
retryCount++;
899
900
//Attempt to acquire a lock
901
if (Monitor.TryEnter(_lockObj))
902
{
903
//If we've acquired the lock, we want to immediately signal our reset event so everyone else wai
904
_connectionResetEvent.Reset();
905
try
906
{
907
//Sleep for our backoff
908
Thread.Sleep(backoffDelay);
909
//Explicitly skip the cache so we don't get the same connection back
910
conn = CreateNewConnection(domainName, globalCatalog, true);
911
if (conn == null)
912
{
913
_log.LogError(
914
"Unable to create replacement ldap connection for ServerDown exception. Breaking loo
915
yield break;
916
}
917
918
_log.LogInformation("Created new LDAP connection after receiving ServerDown from server");
919
}
920
finally
921
{
922
//Reset our event + release the lock
923
_connectionResetEvent.Set();
924
Monitor.Exit(_lockObj);
925
}
926
}
927
else
928
{
929
//If someone else is holding the reset event, we want to just wait and then pull the newly creat
930
//This event will be released after the first entrant thread is done making a new connection
931
//The thread.sleep is to prevent a potential, very unlikely race
932
Thread.Sleep(50);
933
_connectionResetEvent.WaitOne();
934
conn = CreateNewConnection(domainName, globalCatalog);
935
}
936
937
backoffDelay = GetNextBackoff(retryCount);
938
continue;
939
}
940
catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.Busy && retryCount < MaxRetries)
941
{
942
retryCount++;
943
backoffDelay = GetNextBackoff(retryCount);
944
continue;
945
}
946
catch (LdapException le)
947
{
948
if (le.ErrorCode != (int)LdapErrorCodes.LocalError)
949
{
950
if (throwException)
951
{
952
throw new LDAPQueryException(
953
$"LDAP Exception in Loop: {le.ErrorCode}. {le.ServerErrorMessage}. {le.Message}. Filter:
954
le);
955
}
956
957
_log.LogWarning(le,
958
"LDAP Exception in Loop: {ErrorCode}. {ServerErrorMessage}. {Message}. Filter: {Filter}. Dom
959
le.ErrorCode, le.ServerErrorMessage, le.Message, ldapFilter, domainName);
960
}
961
962
if (le.ErrorCode == (int)LdapErrorCodes.ServerDown)
963
{
964
throw new LDAPQueryException(
965
$"LDAP Exception in Loop: {le.ErrorCode}. {le.ServerErrorMessage}. {le.Message}. Filter: {ld
966
le);
967
}
968
969
yield break;
970
}
971
catch (Exception e)
972
{
973
_log.LogWarning(e, "Exception in LDAP loop for {Filter} and {Domain}", ldapFilter, domainName);
974
if (throwException)
975
throw new LDAPQueryException($"Exception in LDAP loop for {ldapFilter} and {domainName}", e);
976
977
yield break;
978
}
979
980
if (cancellationToken.IsCancellationRequested)
981
yield break;
982
983
if (response == null || pageResponse == null)
984
continue;
985
986
foreach (SearchResultEntry entry in response.Entries)
987
{
988
if (cancellationToken.IsCancellationRequested)
989
yield break;
990
991
yield return new SearchResultEntryWrapper(entry, this);
992
}
993
994
var backoffDelay = MinBackoffDelay;
995
var retryCount = 0;
996
997
while (true)
998
{
999
SearchResponse response;
1000
1001
try
1002
{
1003
_log.LogTrace("Sending LDAP request for {Filter}", ldapFilter);
1004
response = (SearchResponse)conn.SendRequest(request);
1005
if (response != null)
1006
pageResponse = (PageResultResponseControl)response.Controls
1007
.Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault();
1008
}
1009
catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.Busy && retryCount < MaxRetries)
1010
{
1011
retryCount++;
1012
Thread.Sleep(backoffDelay);
1013
backoffDelay = TimeSpan.FromSeconds(Math.Min(
1014
backoffDelay.TotalSeconds * BackoffDelayMultiplier.TotalSeconds, MaxBackoffDelay.TotalSeconds));
1015
continue;
1016
}
1017
catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown &&
1018
retryCount < MaxRetries)
1019
{
1020
retryCount++;
1021
Thread.Sleep(backoffDelay);
1022
backoffDelay = TimeSpan.FromSeconds(Math.Min(
1023
backoffDelay.TotalSeconds * BackoffDelayMultiplier.TotalSeconds, MaxBackoffDelay.TotalSeconds));
1024
conn = CreateNewConnection(domainName, globalCatalog, skipCache);
1025
if (conn == null)
1026
{
1027
_log.LogError("Unable to create replacement ldap connection for ServerDown exception. Breaking l
1028
yield break;
1029
}
1030
1031
_log.LogInformation("Created new LDAP connection after receiving ServerDown from server");
1032
continue;
1033
}
1034
catch (LdapException le)
1035
{
1036
if (le.ErrorCode != 82)
1037
if (throwException)
1038
throw new LDAPQueryException(
1039
$"LDAP Exception in Loop: {le.ErrorCode}. {le.ServerErrorMessage}. {le.Message}. Filter:
1040
le);
1041
else
1042
_log.LogWarning(le,
1043
"LDAP Exception in Loop: {ErrorCode}. {ServerErrorMessage}. {Message}. Filter: {Filter}.
1044
le.ErrorCode, le.ServerErrorMessage, le.Message, ldapFilter, domainName);
1045
yield break;
1046
}
1047
catch (Exception e)
1048
{
1049
if (throwException)
1050
throw new LDAPQueryException(
1051
$"Exception in LDAP loop for {ldapFilter} and {domainName ?? "Default Domain"}", e);
1052
1053
_log.LogWarning(e, "Exception in LDAP loop for {Filter} and {Domain}", ldapFilter,
1054
domainName ?? "Default Domain");
1055
yield break;
1056
}
1057
1058
if (response == null || pageResponse == null) continue;
1059
1060
if (response.Entries == null)
1061
yield break;
1062
1063
foreach (SearchResultEntry entry in response.Entries)
1064
yield return new SearchResultEntryWrapper(entry, this);
1065
1066
if (pageResponse.Cookie.Length == 0 || response.Entries.Count == 0)
1067
yield break;
1068
1069
pageControl.Cookie = pageResponse.Cookie;
1070
}
1071
}
1072
1073
/// <summary>
1074
/// Gets the forest associated with a domain.
1075
/// If no domain is provided, defaults to current domain
1076
/// </summary>
1077
/// <param name="domainName"></param>
1078
/// <returns></returns>
1079
public virtual Forest GetForest(string domainName = null)
1080
{
1081
try
1082
{
1083
if (domainName == null && _ldapConfig.Username == null)
1084
return Forest.GetCurrentForest();
994
if (pageResponse.Cookie.Length == 0 || response.Entries.Count == 0 ||
995
cancellationToken.IsCancellationRequested)
996
yield break;
997
998
pageControl.Cookie = pageResponse.Cookie;
999
}
1000
}
1001
1002
private LdapConnection CreateNewConnection(string domainName = null, bool globalCatalog = false,
1003
bool skipCache = false)
1004
{
1005
var task = globalCatalog
1006
? Task.Run(() => CreateGlobalCatalogConnection(domainName, _ldapConfig.AuthType))
1007
: Task.Run(() => CreateLDAPConnection(domainName, skipCache, _ldapConfig.AuthType));
1008
1009
try
1010
{
1011
return task.ConfigureAwait(false).GetAwaiter().GetResult();
1012
}
1013
catch
1014
{
1015
return null;
1016
}
1017
}
1018
1019
/// <summary>
1020
/// Performs an LDAP query using the parameters specified by the user.
1021
/// </summary>
1022
/// <param name="ldapFilter">LDAP filter</param>
1023
/// <param name="scope">SearchScope to query</param>
1024
/// <param name="props">LDAP properties to fetch for each object</param>
1025
/// <param name="includeAcl">Include the DACL and Owner values in the NTSecurityDescriptor</param>
1026
/// <param name="showDeleted">Include deleted objects</param>
1027
/// <param name="domainName">Domain to query</param>
1028
/// <param name="adsPath">ADS path to limit the query too</param>
1029
/// <param name="globalCatalog">Use the global catalog instead of the regular LDAP server</param>
1030
/// <param name="skipCache">
1031
/// Skip the connection cache and force a new connection. You must dispose of this connection
1032
/// yourself.
1033
/// </param>
1034
/// <param name="throwException">Throw exceptions rather than logging the errors directly</param>
1035
/// <returns>All LDAP search results matching the specified parameters</returns>
1036
/// <exception cref="LDAPQueryException">
1037
/// Thrown when an error occurs during LDAP query (only when throwException = true)
1038
/// </exception>
1039
public virtual IEnumerable<ISearchResultEntry> QueryLDAP(string ldapFilter, SearchScope scope,
1040
string[] props, string domainName = null, bool includeAcl = false, bool showDeleted = false,
1041
string adsPath = null, bool globalCatalog = false, bool skipCache = false, bool throwException = false)
1042
{
1043
return QueryLDAP(ldapFilter, scope, props, new CancellationToken(), domainName, includeAcl, showDeleted,
1044
adsPath, globalCatalog, skipCache, throwException);
1045
}
1046
1047
private static TimeSpan GetNextBackoff(int retryCount)
1048
{
1049
return TimeSpan.FromSeconds(Math.Min(
1050
MinBackoffDelay.TotalSeconds * Math.Pow(BackoffDelayMultiplier, retryCount),
1051
MaxBackoffDelay.TotalSeconds));
1052
}
1053
1054
/// <summary>
1055
/// Gets the forest associated with a domain.
1056
/// If no domain is provided, defaults to current domain
1057
/// </summary>
1058
/// <param name="domainName"></param>
1059
/// <returns></returns>
1060
public virtual Forest GetForest(string domainName = null)
1061
{
1062
try
1063
{
1064
if (domainName == null && _ldapConfig.Username == null)
1065
return Forest.GetCurrentForest();
1066
1067
var domain = GetDomain(domainName);
1068
return domain?.Forest;
1069
}
1070
catch
1071
{
1072
return null;
1073
}
1074
}
1075
1076
/// <summary>
1077
/// Creates a new ActiveDirectorySecurityDescriptor
1078
/// Function created for testing purposes
1079
/// </summary>
1080
/// <returns></returns>
1081
public ActiveDirectorySecurityDescriptor MakeSecurityDescriptor()
1082
{
1083
return new ActiveDirectorySecurityDescriptor(new ActiveDirectorySecurity());
1084
}
1085
1086
var domain = GetDomain(domainName);
1087
return domain?.Forest;
1088
}
1089
catch
1090
{
1091
return null;
1092
}
1093
}
1094
1095
/// <summary>
1096
/// Creates a new ActiveDirectorySecurityDescriptor
1097
/// Function created for testing purposes
1086
public string BuildLdapPath(string dnPath, string domainName)
1087
{
1088
var domain = GetDomain(domainName)?.Name;
1089
if (domain == null)
1090
return null;
1091
1092
var adPath = $"{dnPath},DC={domain.Replace(".", ",DC=")}";
1093
return adPath;
1094
}
1095
1096
/// <summary>
1097
/// Tests the current LDAP config to ensure its valid by pulling a domain object
1098
/// </summary>
1099
/// <returns></returns>
1100
public ActiveDirectorySecurityDescriptor MakeSecurityDescriptor()
1099
/// <returns>True if connection was successful, else false</returns>
1100
public bool TestLDAPConfig(string domain)
1101
{
1102
return new ActiveDirectorySecurityDescriptor(new ActiveDirectorySecurity());
1103
}
1102
var filter = new LDAPFilter();
1103
filter.AddDomains();
1104
1105
public string BuildLdapPath(string dnPath, string domainName)
1106
{
1107
var domain = GetDomain(domainName)?.Name;
1108
if (domain == null)
1109
return null;
1110
1111
var adPath = $"{dnPath},DC={domain.Replace(".", ",DC=")}";
1112
return adPath;
1113
}
1105
var resDomain = GetDomain(domain)?.Name ?? domain;
1106
_log.LogTrace("Testing LDAP connection for domain {Domain}", resDomain);
1107
1108
var result = QueryLDAP(filter.GetFilter(), SearchScope.Subtree, CommonProperties.ObjectID, resDomain,
1109
throwException: true)
1110
.DefaultIfEmpty(null).FirstOrDefault();
1111
_log.LogTrace("Result object from LDAP connection test is {DN}", result?.DistinguishedName ?? "null");
1112
return result != null;
1113
}
1114
1115
/// <summary>
1116
/// Tests the current LDAP config to ensure its valid by pulling a domain object
1117
/// </summary>
1118
/// <returns>True if connection was successful, else false</returns>
1119
public bool TestLDAPConfig(string domain)
1120
{
1121
var filter = new LDAPFilter();
1122
filter.AddDomains();
1123
1124
var resDomain = GetDomain(domain)?.Name ?? domain;
1125
_log.LogTrace("Testing LDAP connection for domain {Domain}", resDomain);
1126
1127
var result = QueryLDAP(filter.GetFilter(), SearchScope.Subtree, CommonProperties.ObjectID, resDomain,
1128
throwException: true)
1129
.DefaultIfEmpty(null).FirstOrDefault();
1130
_log.LogTrace("Result object from LDAP connection test is {DN}", result?.DistinguishedName ?? "null");
1131
return result != null;
1132
}
1133
1134
/// <summary>
1135
/// Gets the domain object associated with the specified domain name.
1136
/// Defaults to current domain if none specified
1137
/// </summary>
1138
/// <param name="domainName"></param>
1139
/// <returns></returns>
1140
public virtual Domain GetDomain(string domainName = null)
1141
{
1142
var cacheKey = domainName ?? NullCacheKey;
1143
if (_domainCache.TryGetValue(cacheKey, out var domain)) return domain;
1144
1145
try
1146
{
1147
DirectoryContext context;
1148
if (_ldapConfig.Username != null)
1149
context = domainName != null
1150
? new DirectoryContext(DirectoryContextType.Domain, domainName, _ldapConfig.Username,
1151
_ldapConfig.Password)
1152
: new DirectoryContext(DirectoryContextType.Domain, _ldapConfig.Username,
1153
_ldapConfig.Password);
1154
else
1155
context = domainName != null
1156
? new DirectoryContext(DirectoryContextType.Domain, domainName)
1157
: new DirectoryContext(DirectoryContextType.Domain);
1158
1159
domain = Domain.GetDomain(context);
1160
}
1161
catch (Exception e)
1162
{
1163
_log.LogDebug(e, "GetDomain call failed at {StackTrace}", new StackFrame());
1164
domain = null;
1165
}
1166
1167
_domainCache.TryAdd(cacheKey, domain);
1168
return domain;
1169
}
1170
1171
/// <summary>
1172
/// Setup LDAP query for filter
1173
/// </summary>
1174
/// <param name="ldapFilter">LDAP filter</param>
1175
/// <param name="scope">SearchScope to query</param>
1176
/// <param name="props">LDAP properties to fetch for each object</param>
1177
/// <param name="includeAcl">Include the DACL and Owner values in the NTSecurityDescriptor</param>
1178
/// <param name="domainName">Domain to query</param>
1179
/// <param name="showDeleted">Include deleted objects</param>
1180
/// <param name="adsPath">ADS path to limit the query too</param>
1181
/// <param name="globalCatalog">Use the global catalog instead of the regular LDAP server</param>
1182
/// <param name="skipCache">
1183
/// Skip the connection cache and force a new connection. You must dispose of this connection
1184
/// yourself.
1185
/// </param>
1186
/// <returns>Tuple of LdapConnection, SearchRequest, PageResultRequestControl and LDAPQueryException</returns>
1187
// ReSharper disable once MemberCanBePrivate.Global
1188
internal LDAPQueryParams SetupLDAPQueryFilter(
1189
string ldapFilter,
1190
SearchScope scope, string[] props, bool includeAcl = false, string domainName = null,
1191
bool showDeleted = false,
1192
string adsPath = null, bool globalCatalog = false, bool skipCache = false)
1193
{
1194
_log.LogTrace("Creating ldap connection for {Target} with filter {Filter}",
1195
globalCatalog ? "Global Catalog" : "DC", ldapFilter);
1196
var task = globalCatalog
1197
? Task.Run(() => CreateGlobalCatalogConnection(domainName, _ldapConfig.AuthType))
1198
: Task.Run(() => CreateLDAPConnection(domainName, skipCache, _ldapConfig.AuthType));
1199
1200
var queryParams = new LDAPQueryParams();
1201
1202
LdapConnection conn;
1203
try
1204
{
1205
conn = task.ConfigureAwait(false).GetAwaiter().GetResult();
1206
}
1207
catch (LdapException ldapException)
1208
{
1209
var errorString =
1210
$"LDAP Exception {ldapException.ErrorCode} when creating connection for {ldapFilter} and domain {dom
1211
queryParams.Exception = new LDAPQueryException(errorString, ldapException);
1212
return queryParams;
1213
}
1214
catch (LDAPQueryException ldapQueryException)
1215
{
1216
queryParams.Exception = ldapQueryException;
1217
return queryParams;
1218
}
1219
catch (Exception e)
1116
/// Gets the domain object associated with the specified domain name.
1117
/// Defaults to current domain if none specified
1118
/// </summary>
1119
/// <param name="domainName"></param>
1120
/// <returns></returns>
1121
public virtual Domain GetDomain(string domainName = null)
1122
{
1123
var cacheKey = domainName ?? NullCacheKey;
1124
if (_domainCache.TryGetValue(cacheKey, out var domain)) return domain;
1125
1126
try
1127
{
1128
DirectoryContext context;
1129
if (_ldapConfig.Username != null)
1130
context = domainName != null
1131
? new DirectoryContext(DirectoryContextType.Domain, domainName, _ldapConfig.Username,
1132
_ldapConfig.Password)
1133
: new DirectoryContext(DirectoryContextType.Domain, _ldapConfig.Username,
1134
_ldapConfig.Password);
1135
else
1136
context = domainName != null
1137
? new DirectoryContext(DirectoryContextType.Domain, domainName)
1138
: new DirectoryContext(DirectoryContextType.Domain);
1139
1140
domain = Domain.GetDomain(context);
1141
}
1142
catch (Exception e)
1143
{
1144
_log.LogDebug(e, "GetDomain call failed at {StackTrace}", new StackFrame());
1145
domain = null;
1146
}
1147
1148
_domainCache.TryAdd(cacheKey, domain);
1149
return domain;
1150
}
1151
1152
/// <summary>
1153
/// Setup LDAP query for filter
1154
/// </summary>
1155
/// <param name="ldapFilter">LDAP filter</param>
1156
/// <param name="scope">SearchScope to query</param>
1157
/// <param name="props">LDAP properties to fetch for each object</param>
1158
/// <param name="includeAcl">Include the DACL and Owner values in the NTSecurityDescriptor</param>
1159
/// <param name="domainName">Domain to query</param>
1160
/// <param name="showDeleted">Include deleted objects</param>
1161
/// <param name="adsPath">ADS path to limit the query too</param>
1162
/// <param name="globalCatalog">Use the global catalog instead of the regular LDAP server</param>
1163
/// <param name="skipCache">
1164
/// Skip the connection cache and force a new connection. You must dispose of this connection
1165
/// yourself.
1166
/// </param>
1167
/// <returns>Tuple of LdapConnection, SearchRequest, PageResultRequestControl and LDAPQueryException</returns>
1168
// ReSharper disable once MemberCanBePrivate.Global
1169
internal LDAPQueryParams SetupLDAPQueryFilter(
1170
string ldapFilter,
1171
SearchScope scope, string[] props, bool includeAcl = false, string domainName = null,
1172
bool showDeleted = false,
1173
string adsPath = null, bool globalCatalog = false, bool skipCache = false)
1174
{
1175
_log.LogTrace("Creating ldap connection for {Target} with filter {Filter}",
1176
globalCatalog ? "Global Catalog" : "DC", ldapFilter);
1177
var task = globalCatalog
1178
? Task.Run(() => CreateGlobalCatalogConnection(domainName, _ldapConfig.AuthType))
1179
: Task.Run(() => CreateLDAPConnection(domainName, skipCache, _ldapConfig.AuthType));
1180
1181
var queryParams = new LDAPQueryParams();
1182
1183
LdapConnection conn;
1184
try
1185
{
1186
conn = task.ConfigureAwait(false).GetAwaiter().GetResult();
1187
}
1188
catch (LdapException ldapException)
1189
{
1190
var errorString =
1191
$"LDAP Exception {ldapException.ErrorCode} when creating connection for {ldapFilter} and domain {dom
1192
queryParams.Exception = new LDAPQueryException(errorString, ldapException);
1193
return queryParams;
1194
}
1195
catch (LDAPQueryException ldapQueryException)
1196
{
1197
queryParams.Exception = ldapQueryException;
1198
return queryParams;
1199
}
1200
catch (Exception e)
1201
{
1202
var errorString =
1203
$"Exception getting LDAP connection for {ldapFilter} and domain {domainName ?? "Default Domain"}";
1204
queryParams.Exception = new LDAPQueryException(errorString, e);
1205
return queryParams;
1206
}
1207
1208
//If we get a null connection, something went wrong, but we don't have an error to go with it for whatever r
1209
if (conn == null)
1210
{
1211
var errorString =
1212
$"LDAP connection is null for filter {ldapFilter} and domain {domainName ?? "Default Domain"}";
1213
queryParams.Exception = new LDAPQueryException(errorString);
1214
return queryParams;
1215
}
1216
1217
SearchRequest request;
1218
1219
try
1220
{
1221
var errorString =
1222
$"Exception getting LDAP connection for {ldapFilter} and domain {domainName ?? "Default Domain"}";
1223
queryParams.Exception = new LDAPQueryException(errorString, e);
1224
return queryParams;
1225
}
1226
1227
//If we get a null connection, something went wrong, but we don't have an error to go with it for whatever r
1228
if (conn == null)
1229
{
1230
var errorString =
1231
$"LDAP connection is null for filter {ldapFilter} and domain {domainName ?? "Default Domain"}";
1232
queryParams.Exception = new LDAPQueryException(errorString);
1233
return queryParams;
1234
}
1235
1236
SearchRequest request;
1237
1238
try
1239
{
1240
request = CreateSearchRequest(ldapFilter, scope, props, domainName, adsPath, showDeleted);
1241
}
1242
catch (LDAPQueryException ldapQueryException)
1243
{
1244
queryParams.Exception = ldapQueryException;
1245
return queryParams;
1246
}
1247
1248
if (request == null)
1249
{
1250
var errorString =
1251
$"Search request is null for filter {ldapFilter} and domain {domainName ?? "Default Domain"}";
1252
queryParams.Exception = new LDAPQueryException(errorString);
1253
return queryParams;
1254
}
1255
1256
var pageControl = new PageResultRequestControl(500);
1257
request.Controls.Add(pageControl);
1258
1259
if (includeAcl)
1260
request.Controls.Add(new SecurityDescriptorFlagControl
1261
{
1262
SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner
1263
});
1264
1265
queryParams.Connection = conn;
1266
queryParams.SearchRequest = request;
1267
queryParams.PageControl = pageControl;
1268
1269
return queryParams;
1270
}
1271
1272
private Group GetBaseEnterpriseDC(string domain)
1273
{
1274
var forest = GetForest(domain)?.Name;
1275
if (forest == null) _log.LogWarning("Error getting forest, ENTDC sid is likely incorrect");
1276
var g = new Group { ObjectIdentifier = $"{forest}-S-1-5-9".ToUpper() };
1277
g.Properties.Add("name", $"ENTERPRISE DOMAIN CONTROLLERS@{forest ?? "UNKNOWN"}".ToUpper());
1278
g.Properties.Add("domainsid", GetSidFromDomainName(forest));
1279
g.Properties.Add("domain", forest);
1280
return g;
1281
}
1282
1283
/// <summary>
1284
/// Updates the config for querying LDAP
1285
/// </summary>
1286
/// <param name="config"></param>
1287
public void UpdateLDAPConfig(LDAPConfig config)
1288
{
1289
_ldapConfig = config;
1290
}
1291
1292
private string GetDomainNameFromSidLdap(string sid)
1293
{
1294
var hexSid = Helpers.ConvertSidToHexSid(sid);
1221
request = CreateSearchRequest(ldapFilter, scope, props, domainName, adsPath, showDeleted);
1222
}
1223
catch (LDAPQueryException ldapQueryException)
1224
{
1225
queryParams.Exception = ldapQueryException;
1226
return queryParams;
1227
}
1228
1229
if (request == null)
1230
{
1231
var errorString =
1232
$"Search request is null for filter {ldapFilter} and domain {domainName ?? "Default Domain"}";
1233
queryParams.Exception = new LDAPQueryException(errorString);
1234
return queryParams;
1235
}
1236
1237
var pageControl = new PageResultRequestControl(500);
1238
request.Controls.Add(pageControl);
1239
1240
if (includeAcl)
1241
request.Controls.Add(new SecurityDescriptorFlagControl
1242
{
1243
SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner
1244
});
1245
1246
queryParams.Connection = conn;
1247
queryParams.SearchRequest = request;
1248
queryParams.PageControl = pageControl;
1249
1250
return queryParams;
1251
}
1252
1253
private Group GetBaseEnterpriseDC(string domain)
1254
{
1255
var forest = GetForest(domain)?.Name;
1256
if (forest == null) _log.LogWarning("Error getting forest, ENTDC sid is likely incorrect");
1257
var g = new Group { ObjectIdentifier = $"{forest}-S-1-5-9".ToUpper() };
1258
g.Properties.Add("name", $"ENTERPRISE DOMAIN CONTROLLERS@{forest ?? "UNKNOWN"}".ToUpper());
1259
g.Properties.Add("domainsid", GetSidFromDomainName(forest));
1260
g.Properties.Add("domain", forest);
1261
return g;
1262
}
1263
1264
/// <summary>
1265
/// Updates the config for querying LDAP
1266
/// </summary>
1267
/// <param name="config"></param>
1268
public void UpdateLDAPConfig(LDAPConfig config)
1269
{
1270
_ldapConfig = config;
1271
}
1272
1273
private string GetDomainNameFromSidLdap(string sid)
1274
{
1275
var hexSid = Helpers.ConvertSidToHexSid(sid);
1276
1277
if (hexSid == null)
1278
return null;
1279
1280
//Search using objectsid first
1281
var result =
1282
QueryLDAP($"(&(objectclass=domain)(objectsid={hexSid}))", SearchScope.Subtree,
1283
new[] { "distinguishedname" }, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault();
1284
1285
if (result != null)
1286
{
1287
var domainName = Helpers.DistinguishedNameToDomain(result.DistinguishedName);
1288
return domainName;
1289
}
1290
1291
//Try trusteddomain objects with the securityidentifier attribute
1292
result =
1293
QueryLDAP($"(&(objectclass=trusteddomain)(securityidentifier={sid}))", SearchScope.Subtree,
1294
new[] { "cn" }, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault();
1295
1296
if (hexSid == null)
1297
return null;
1298
1299
//Search using objectsid first
1300
var result =
1301
QueryLDAP($"(&(objectclass=domain)(objectsid={hexSid}))", SearchScope.Subtree,
1302
new[] { "distinguishedname" }, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault();
1303
1304
if (result != null)
1305
{
1306
var domainName = Helpers.DistinguishedNameToDomain(result.DistinguishedName);
1307
return domainName;
1308
}
1309
1310
//Try trusteddomain objects with the securityidentifier attribute
1311
result =
1312
QueryLDAP($"(&(objectclass=trusteddomain)(securityidentifier={sid}))", SearchScope.Subtree,
1313
new[] { "cn" }, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault();
1314
1315
if (result != null)
1316
{
1317
var domainName = result.GetProperty(LDAPProperties.CanonicalName);
1318
return domainName;
1319
}
1320
1321
//We didn't find anything so just return null
1322
return null;
1323
}
1324
1325
/// <summary>
1326
/// Uses a socket and a set of bytes to request the NETBIOS name from a remote computer
1327
/// </summary>
1328
/// <param name="server"></param>
1329
/// <param name="domain"></param>
1330
/// <param name="netbios"></param>
1331
/// <returns></returns>
1332
private static bool RequestNETBIOSNameFromComputer(string server, string domain, out string netbios)
1333
{
1334
var receiveBuffer = new byte[1024];
1335
var requestSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
1336
try
1337
{
1338
//Set receive timeout to 1 second
1339
requestSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1000);
1340
EndPoint remoteEndpoint;
1341
1342
//We need to create an endpoint to bind too. If its an IP, just use that.
1343
if (IPAddress.TryParse(server, out var parsedAddress))
1344
remoteEndpoint = new IPEndPoint(parsedAddress, 137);
1345
else
1346
//If its not an IP, we're going to try and resolve it from DNS
1347
try
1348
{
1349
IPAddress address;
1350
if (server.Contains("."))
1351
address = Dns
1352
.GetHostAddresses(server).First(x => x.AddressFamily == AddressFamily.InterNetwork);
1353
else
1354
address = Dns.GetHostAddresses($"{server}.{domain}")[0];
1355
1356
if (address == null)
1357
{
1358
netbios = null;
1359
return false;
1360
}
1361
1362
remoteEndpoint = new IPEndPoint(address, 137);
1363
}
1364
catch
1365
{
1366
//Failed to resolve an IP, so return null
1367
netbios = null;
1368
return false;
1369
}
1370
1371
var originEndpoint = new IPEndPoint(IPAddress.Any, 0);
1372
requestSocket.Bind(originEndpoint);
1373
1374
try
1375
{
1376
requestSocket.SendTo(NameRequest, remoteEndpoint);
1377
var receivedByteCount = requestSocket.ReceiveFrom(receiveBuffer, ref remoteEndpoint);
1378
if (receivedByteCount >= 90)
1379
{
1380
netbios = new ASCIIEncoding().GetString(receiveBuffer, 57, 16).Trim('\0', ' ');
1381
return true;
1382
}
1383
1384
netbios = null;
1385
return false;
1386
}
1387
catch (SocketException)
1388
{
1389
netbios = null;
1390
return false;
1391
}
1392
}
1393
finally
1394
{
1395
//Make sure we close the socket if its open
1396
requestSocket.Close();
1397
}
1398
}
1399
1400
/// <summary>
1401
/// Calls the NetWkstaGetInfo API on a hostname
1402
/// </summary>
1403
/// <param name="hostname"></param>
1404
/// <returns></returns>
1405
private async Task<NetAPIStructs.WorkstationInfo100?> GetWorkstationInfo(string hostname)
1406
{
1407
if (!await _portScanner.CheckPort(hostname))
1408
return null;
1409
1410
var result = NetAPIMethods.NetWkstaGetInfo(hostname);
1411
if (result.IsSuccess) return result.Value;
1412
1413
return null;
1414
}
1296
if (result != null)
1297
{
1298
var domainName = result.GetProperty(LDAPProperties.CanonicalName);
1299
return domainName;
1300
}
1301
1302
//We didn't find anything so just return null
1303
return null;
1304
}
1305
1306
/// <summary>
1307
/// Uses a socket and a set of bytes to request the NETBIOS name from a remote computer
1308
/// </summary>
1309
/// <param name="server"></param>
1310
/// <param name="domain"></param>
1311
/// <param name="netbios"></param>
1312
/// <returns></returns>
1313
private static bool RequestNETBIOSNameFromComputer(string server, string domain, out string netbios)
1314
{
1315
var receiveBuffer = new byte[1024];
1316
var requestSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
1317
try
1318
{
1319
//Set receive timeout to 1 second
1320
requestSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1000);
1321
EndPoint remoteEndpoint;
1322
1323
//We need to create an endpoint to bind too. If its an IP, just use that.
1324
if (IPAddress.TryParse(server, out var parsedAddress))
1325
remoteEndpoint = new IPEndPoint(parsedAddress, 137);
1326
else
1327
//If its not an IP, we're going to try and resolve it from DNS
1328
try
1329
{
1330
IPAddress address;
1331
if (server.Contains("."))
1332
address = Dns
1333
.GetHostAddresses(server).First(x => x.AddressFamily == AddressFamily.InterNetwork);
1334
else
1335
address = Dns.GetHostAddresses($"{server}.{domain}")[0];
1336
1337
if (address == null)
1338
{
1339
netbios = null;
1340
return false;
1341
}
1342
1343
remoteEndpoint = new IPEndPoint(address, 137);
1344
}
1345
catch
1346
{
1347
//Failed to resolve an IP, so return null
1348
netbios = null;
1349
return false;
1350
}
1351
1352
var originEndpoint = new IPEndPoint(IPAddress.Any, 0);
1353
requestSocket.Bind(originEndpoint);
1354
1355
try
1356
{
1357
requestSocket.SendTo(NameRequest, remoteEndpoint);
1358
var receivedByteCount = requestSocket.ReceiveFrom(receiveBuffer, ref remoteEndpoint);
1359
if (receivedByteCount >= 90)
1360
{
1361
netbios = new ASCIIEncoding().GetString(receiveBuffer, 57, 16).Trim('\0', ' ');
1362
return true;
1363
}
1364
1365
netbios = null;
1366
return false;
1367
}
1368
catch (SocketException)
1369
{
1370
netbios = null;
1371
return false;
1372
}
1373
}
1374
finally
1375
{
1376
//Make sure we close the socket if its open
1377
requestSocket.Close();
1378
}
1379
}
1380
1381
/// <summary>
1382
/// Calls the NetWkstaGetInfo API on a hostname
1383
/// </summary>
1384
/// <param name="hostname"></param>
1385
/// <returns></returns>
1386
private async Task<NetAPIStructs.WorkstationInfo100?> GetWorkstationInfo(string hostname)
1387
{
1388
if (!await _portScanner.CheckPort(hostname))
1389
return null;
1390
1391
var result = NetAPIMethods.NetWkstaGetInfo(hostname);
1392
if (result.IsSuccess) return result.Value;
1393
1394
return null;
1395
}
1396
1397
/// <summary>
1398
/// Creates a SearchRequest object for use in querying LDAP.
1399
/// </summary>
1400
/// <param name="filter">LDAP filter</param>
1401
/// <param name="scope">SearchScope to query</param>
1402
/// <param name="attributes">LDAP properties to fetch for each object</param>
1403
/// <param name="domainName">Domain to query</param>
1404
/// <param name="adsPath">ADS path to limit the query too</param>
1405
/// <param name="showDeleted">Include deleted objects in results</param>
1406
/// <returns>A built SearchRequest</returns>
1407
private SearchRequest CreateSearchRequest(string filter, SearchScope scope, string[] attributes,
1408
string domainName = null, string adsPath = null, bool showDeleted = false)
1409
{
1410
var domain = GetDomain(domainName)?.Name ?? domainName;
1411
1412
if (domain == null)
1413
throw new LDAPQueryException(
1414
$"Unable to create search request: GetDomain call failed for {domainName}");
1415
1416
/// <summary>
1417
/// Creates a SearchRequest object for use in querying LDAP.
1418
/// </summary>
1419
/// <param name="filter">LDAP filter</param>
1420
/// <param name="scope">SearchScope to query</param>
1421
/// <param name="attributes">LDAP properties to fetch for each object</param>
1422
/// <param name="domainName">Domain to query</param>
1423
/// <param name="adsPath">ADS path to limit the query too</param>
1424
/// <param name="showDeleted">Include deleted objects in results</param>
1425
/// <returns>A built SearchRequest</returns>
1426
private SearchRequest CreateSearchRequest(string filter, SearchScope scope, string[] attributes,
1427
string domainName = null, string adsPath = null, bool showDeleted = false)
1428
{
1429
var domain = GetDomain(domainName)?.Name ?? domainName;
1430
1431
if (domain == null)
1432
throw new LDAPQueryException($"Unable to create search request: GetDomain call failed for {domainName}")
1433
1434
var adPath = adsPath?.Replace("LDAP://", "") ?? $"DC={domain.Replace(".", ",DC=")}";
1435
1436
var request = new SearchRequest(adPath, filter, scope, attributes);
1437
request.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope));
1438
if (showDeleted)
1439
request.Controls.Add(new ShowDeletedControl());
1440
1441
return request;
1442
}
1443
1444
/// <summary>
1445
/// Creates a LDAP connection to a global catalog server
1446
/// </summary>
1447
/// <param name="domainName">Domain to connect too</param>
1448
/// <param name="authType">Auth type to use. Defaults to Kerberos. Use Negotiate for netonly scenarios</param>
1449
/// <returns>A connected LdapConnection or null</returns>
1450
private async Task<LdapConnection> CreateGlobalCatalogConnection(string domainName = null,
1451
AuthType authType = AuthType.Kerberos)
1452
{
1453
string targetServer;
1454
if (_ldapConfig.Server != null)
1455
{
1456
targetServer = _ldapConfig.Server;
1457
}
1458
else
1459
{
1460
var domain = GetDomain(domainName);
1461
if (domain == null)
1462
{
1463
_log.LogDebug("Unable to create global catalog connection for domain {DomainName}: GetDomain failed"
1464
throw new LDAPQueryException($"GetDomain call failed for {domainName}");
1465
}
1466
1467
if (!_domainControllerCache.TryGetValue(domain.Name, out targetServer))
1468
targetServer = await GetUsableDomainController(domain);
1416
var adPath = adsPath?.Replace("LDAP://", "") ?? $"DC={domain.Replace(".", ",DC=")}";
1417
1418
var request = new SearchRequest(adPath, filter, scope, attributes);
1419
request.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope));
1420
if (showDeleted)
1421
request.Controls.Add(new ShowDeletedControl());
1422
1423
return request;
1424
}
1425
1426
/// <summary>
1427
/// Creates a LDAP connection to a global catalog server
1428
/// </summary>
1429
/// <param name="domainName">Domain to connect too</param>
1430
/// <param name="authType">Auth type to use. Defaults to Kerberos. Use Negotiate for netonly scenarios</param>
1431
/// <returns>A connected LdapConnection or null</returns>
1432
private async Task<LdapConnection> CreateGlobalCatalogConnection(string domainName = null,
1433
AuthType authType = AuthType.Kerberos)
1434
{
1435
string targetServer;
1436
if (_ldapConfig.Server != null)
1437
{
1438
targetServer = _ldapConfig.Server;
1439
}
1440
else
1441
{
1442
var domain = GetDomain(domainName);
1443
if (domain == null)
1444
{
1445
_log.LogDebug(
1446
"Unable to create global catalog connection for domain {DomainName}: GetDomain failed",
1447
domainName);
1448
throw new LDAPQueryException($"GetDomain call failed for {domainName}");
1449
}
1450
1451
if (!_domainControllerCache.TryGetValue(domain.Name, out targetServer))
1452
targetServer = await GetUsableDomainController(domain);
1453
}
1454
1455
if (targetServer == null)
1456
throw new LDAPQueryException($"No usable global catalog found for {domainName}");
1457
1458
if (_globalCatalogConnections.TryGetValue(targetServer, out var connection))
1459
return connection;
1460
1461
connection = new LdapConnection(new LdapDirectoryIdentifier(targetServer, 3268));
1462
1463
connection.SessionOptions.ProtocolVersion = 3;
1464
1465
if (_ldapConfig.Username != null)
1466
{
1467
var cred = new NetworkCredential(_ldapConfig.Username, _ldapConfig.Password);
1468
connection.Credential = cred;
1469
}
1470
1471
if (targetServer == null)
1472
throw new LDAPQueryException($"No usable global catalog found for {domainName}");
1473
1474
if (_globalCatalogConnections.TryGetValue(targetServer, out var connection))
1475
return connection;
1471
if (_ldapConfig.DisableSigning)
1472
{
1473
connection.SessionOptions.Sealing = false;
1474
connection.SessionOptions.Signing = false;
1475
}
1476
1477
connection = new LdapConnection(new LdapDirectoryIdentifier(targetServer, 3268));
1477
connection.AuthType = authType;
1478
1479
connection.SessionOptions.ProtocolVersion = 3;
1480
1481
if (_ldapConfig.Username != null)
1482
{
1483
var cred = new NetworkCredential(_ldapConfig.Username, _ldapConfig.Password);
1484
connection.Credential = cred;
1485
}
1486
1487
if (_ldapConfig.DisableSigning)
1488
{
1489
connection.SessionOptions.Sealing = false;
1490
connection.SessionOptions.Signing = false;
1491
}
1492
1493
connection.AuthType = authType;
1494
1495
_globalCatalogConnections.TryAdd(targetServer, connection);
1496
return connection;
1497
}
1498
1499
/// <summary>
1500
/// Creates an LDAP connection with appropriate options based off the ldap configuration. Caches connections
1501
/// </summary>
1502
/// <param name="domainName">The domain to connect too</param>
1503
/// <param name="skipCache">Skip the connection cache</param>
1504
/// <param name="authType">Auth type to use. Defaults to Kerberos. Use Negotiate for netonly scenarios</param>
1505
/// <returns>A connected LDAP connection or null</returns>
1506
private async Task<LdapConnection> CreateLDAPConnection(string domainName = null, bool skipCache = false,
1507
AuthType authType = AuthType.Kerberos)
1508
{
1509
string targetServer;
1510
if (_ldapConfig.Server != null)
1511
targetServer = _ldapConfig.Server;
1512
else
1513
{
1514
var domain = GetDomain(domainName);
1515
if (domain == null)
1516
{
1517
_log.LogDebug("Unable to create ldap connection for domain {DomainName}: GetDomain failed", domainNa
1518
throw new LDAPQueryException($"Error creating LDAP connection: GetDomain call failed for {domainName
1519
}
1520
1521
if (!_domainControllerCache.TryGetValue(domain.Name, out targetServer))
1522
targetServer = await GetUsableDomainController(domain);
1523
}
1524
1525
if (targetServer == null)
1526
throw new LDAPQueryException($"No usable domain controller found for {domainName}");
1527
1528
if (!skipCache)
1529
if (_ldapConnections.TryGetValue(targetServer, out var conn))
1530
return conn;
1531
1532
var port = _ldapConfig.GetPort();
1533
var ident = new LdapDirectoryIdentifier(targetServer, port, false, false);
1534
var connection = new LdapConnection(ident) { Timeout = new TimeSpan(0, 0, 5, 0) };
1535
if (_ldapConfig.Username != null)
1536
{
1537
var cred = new NetworkCredential(_ldapConfig.Username, _ldapConfig.Password);
1538
connection.Credential = cred;
1539
}
1540
1541
//These options are important!
1542
connection.SessionOptions.ProtocolVersion = 3;
1543
connection.SessionOptions.ReferralChasing = ReferralChasingOptions.None;
1479
_globalCatalogConnections.TryAdd(targetServer, connection);
1480
return connection;
1481
}
1482
1483
/// <summary>
1484
/// Creates an LDAP connection with appropriate options based off the ldap configuration. Caches connections
1485
/// </summary>
1486
/// <param name="domainName">The domain to connect too</param>
1487
/// <param name="skipCache">Skip the connection cache</param>
1488
/// <param name="authType">Auth type to use. Defaults to Kerberos. Use Negotiate for netonly scenarios</param>
1489
/// <returns>A connected LDAP connection or null</returns>
1490
private async Task<LdapConnection> CreateLDAPConnection(string domainName = null, bool skipCache = false,
1491
AuthType authType = AuthType.Kerberos)
1492
{
1493
string targetServer;
1494
if (_ldapConfig.Server != null)
1495
targetServer = _ldapConfig.Server;
1496
else
1497
{
1498
var domain = GetDomain(domainName);
1499
if (domain == null)
1500
{
1501
_log.LogDebug("Unable to create ldap connection for domain {DomainName}: GetDomain failed",
1502
domainName);
1503
throw new LDAPQueryException(
1504
$"Error creating LDAP connection: GetDomain call failed for {domainName}");
1505
}
1506
1507
if (!_domainControllerCache.TryGetValue(domain.Name, out targetServer))
1508
targetServer = await GetUsableDomainController(domain);
1509
}
1510
1511
if (targetServer == null)
1512
throw new LDAPQueryException($"No usable domain controller found for {domainName}");
1513
1514
if (!skipCache)
1515
if (_ldapConnections.TryGetValue(targetServer, out var conn))
1516
return conn;
1517
1518
var port = _ldapConfig.GetPort();
1519
var ident = new LdapDirectoryIdentifier(targetServer, port, false, false);
1520
var connection = new LdapConnection(ident) { Timeout = new TimeSpan(0, 0, 5, 0) };
1521
if (_ldapConfig.Username != null)
1522
{
1523
var cred = new NetworkCredential(_ldapConfig.Username, _ldapConfig.Password);
1524
connection.Credential = cred;
1525
}
1526
1527
//These options are important!
1528
connection.SessionOptions.ProtocolVersion = 3;
1529
connection.SessionOptions.ReferralChasing = ReferralChasingOptions.None;
1530
1531
if (_ldapConfig.DisableSigning)
1532
{
1533
connection.SessionOptions.Sealing = false;
1534
connection.SessionOptions.Signing = false;
1535
}
1536
1537
if (_ldapConfig.SSL)
1538
connection.SessionOptions.SecureSocketLayer = true;
1539
1540
if (_ldapConfig.DisableCertVerification)
1541
connection.SessionOptions.VerifyServerCertificate = (ldapConnection, certificate) => true;
1542
1543
connection.AuthType = authType;
1544
1545
if (_ldapConfig.DisableSigning)
1545
_ldapConnections.AddOrUpdate(targetServer, connection, (s, ldapConnection) =>
1546
{
1547
connection.SessionOptions.Sealing = false;
1548
connection.SessionOptions.Signing = false;
1549
}
1547
ldapConnection.Dispose();
1548
return connection;
1549
});
1550
1551
if (_ldapConfig.SSL)
1552
connection.SessionOptions.SecureSocketLayer = true;
1551
return connection;
1552
}
1553
1554
if (_ldapConfig.DisableCertVerification)
1555
connection.SessionOptions.VerifyServerCertificate = (ldapConnection, certificate) => true;
1556
1557
connection.AuthType = authType;
1554
private async Task<string> GetUsableDomainController(Domain domain, bool gc = false)
1555
{
1556
if (!gc && _domainControllerCache.TryGetValue(domain.Name.ToUpper(), out var dc))
1557
return dc;
1558
1559
if (!skipCache)
1560
_ldapConnections.TryAdd(targetServer, connection);
1561
1562
return connection;
1563
}
1564
1565
private async Task<string> GetUsableDomainController(Domain domain, bool gc = false)
1566
{
1567
if (!gc && _domainControllerCache.TryGetValue(domain.Name.ToUpper(), out var dc))
1568
return dc;
1569
1570
var port = gc ? 3268 : _ldapConfig.GetPort();
1571
var pdc = domain.PdcRoleOwner.Name;
1572
if (await _portScanner.CheckPort(pdc, port))
1573
{
1574
_domainControllerCache.TryAdd(domain.Name.ToUpper(), pdc);
1575
_log.LogInformation("Found usable Domain Controller for {Domain} : {PDC}", domain.Name, pdc);
1576
return pdc;
1577
}
1578
1579
//If the PDC isn't reachable loop through the rest
1580
foreach (DomainController domainController in domain.DomainControllers)
1581
{
1582
var name = domainController.Name;
1583
if (!await _portScanner.CheckPort(name, port)) continue;
1584
_log.LogInformation("Found usable Domain Controller for {Domain} : {PDC}", domain.Name, name);
1585
_domainControllerCache.TryAdd(domain.Name.ToUpper(), name);
1586
return name;
1587
}
1588
1589
//If we get here, somehow we didn't get any usable DCs. Save it off as null
1590
_domainControllerCache.TryAdd(domain.Name.ToUpper(), null);
1591
_log.LogWarning("Unable to find usable domain controller for {Domain}", domain.Name);
1592
return null;
1593
}
1594
1595
/// <summary>
1596
/// Normalizes a domain name to its full DNS name
1597
/// </summary>
1598
/// <param name="domain"></param>
1599
/// <returns></returns>
1600
internal string NormalizeDomainName(string domain)
1601
{
1602
if (domain == null)
1603
return null;
1604
1605
var resolved = domain;
1606
1607
if (resolved.Contains("."))
1608
return domain.ToUpper();
1609
1610
resolved = ResolveDomainNetbiosToDns(domain) ?? domain;
1611
1612
return resolved.ToUpper();
1613
}
1559
var port = gc ? 3268 : _ldapConfig.GetPort();
1560
var pdc = domain.PdcRoleOwner.Name;
1561
if (await _portScanner.CheckPort(pdc, port))
1562
{
1563
_domainControllerCache.TryAdd(domain.Name.ToUpper(), pdc);
1564
_log.LogInformation("Found usable Domain Controller for {Domain} : {PDC}", domain.Name, pdc);
1565
return pdc;
1566
}
1567
1568
//If the PDC isn't reachable loop through the rest
1569
foreach (DomainController domainController in domain.DomainControllers)
1570
{
1571
var name = domainController.Name;
1572
if (!await _portScanner.CheckPort(name, port)) continue;
1573
_log.LogInformation("Found usable Domain Controller for {Domain} : {PDC}", domain.Name, name);
1574
_domainControllerCache.TryAdd(domain.Name.ToUpper(), name);
1575
return name;
1576
}
1577
1578
//If we get here, somehow we didn't get any usable DCs. Save it off as null
1579
_domainControllerCache.TryAdd(domain.Name.ToUpper(), null);
1580
_log.LogWarning("Unable to find usable domain controller for {Domain}", domain.Name);
1581
return null;
1582
}
1583
1584
/// <summary>
1585
/// Normalizes a domain name to its full DNS name
1586
/// </summary>
1587
/// <param name="domain"></param>
1588
/// <returns></returns>
1589
internal string NormalizeDomainName(string domain)
1590
{
1591
if (domain == null)
1592
return null;
1593
1594
var resolved = domain;
1595
1596
if (resolved.Contains("."))
1597
return domain.ToUpper();
1598
1599
resolved = ResolveDomainNetbiosToDns(domain) ?? domain;
1600
1601
return resolved.ToUpper();
1602
}
1603
1604
/// <summary>
1605
/// Turns a domain Netbios name into its FQDN using the DsGetDcName function (TESTLAB -> TESTLAB.LOCAL)
1606
/// </summary>
1607
/// <param name="domainName"></param>
1608
/// <returns></returns>
1609
internal string ResolveDomainNetbiosToDns(string domainName)
1610
{
1611
var key = domainName.ToUpper();
1612
if (_netbiosCache.TryGetValue(key, out var flatName))
1613
return flatName;
1614
1615
/// <summary>
1616
/// Turns a domain Netbios name into its FQDN using the DsGetDcName function (TESTLAB -> TESTLAB.LOCAL)
1617
/// </summary>
1618
/// <param name="domainName"></param>
1619
/// <returns></returns>
1620
internal string ResolveDomainNetbiosToDns(string domainName)
1621
{
1622
var key = domainName.ToUpper();
1623
if (_netbiosCache.TryGetValue(key, out var flatName))
1624
return flatName;
1625
1626
var domain = GetDomain(domainName);
1627
if (domain != null)
1628
{
1629
_netbiosCache.TryAdd(key, domain.Name);
1630
return domain.Name;
1631
}
1632
1633
var computerName = _ldapConfig.Server;
1615
var domain = GetDomain(domainName);
1616
if (domain != null)
1617
{
1618
_netbiosCache.TryAdd(key, domain.Name);
1619
return domain.Name;
1620
}
1621
1622
var computerName = _ldapConfig.Server;
1623
1624
var dci = _nativeMethods.CallDsGetDcName(computerName, domainName);
1625
if (dci.IsSuccess)
1626
{
1627
flatName = dci.Value.DomainName;
1628
_netbiosCache.TryAdd(key, flatName);
1629
return flatName;
1630
}
1631
1632
return domainName.ToUpper();
1633
}
1634
1635
var dci = _nativeMethods.CallDsGetDcName(computerName, domainName);
1636
if (dci.IsSuccess)
1637
{
1638
flatName = dci.Value.DomainName;
1639
_netbiosCache.TryAdd(key, flatName);
1640
return flatName;
1641
}
1642
1643
return domainName.ToUpper();
1644
}
1645
1646
/// <summary>
1647
/// Gets the range retrieval limit for a domain
1648
/// </summary>
1649
/// <param name="domainName"></param>
1650
/// <param name="defaultRangeSize"></param>
1651
/// <returns></returns>
1652
public int GetDomainRangeSize(string domainName = null, int defaultRangeSize = 750)
1653
{
1654
var domainPath = DomainNameToDistinguishedName(domainName);
1655
//Default to a page size of 750 for safety
1656
if (domainPath == null)
1657
{
1658
_log.LogDebug("Unable to resolve domain {Domain} to distinguishedname to get page size", domainName ?? "
1659
return defaultRangeSize;
1660
}
1661
1662
if (_ldapRangeSizeCache.TryGetValue(domainPath.ToUpper(), out var parsedPageSize))
1663
{
1664
return parsedPageSize;
1665
}
1666
1667
var configPath = CommonPaths.CreateDNPath(CommonPaths.QueryPolicyPath, domainPath);
1668
var enumerable = QueryLDAP("(objectclass=*)", SearchScope.Base, null, adsPath: configPath);
1669
var config = enumerable.DefaultIfEmpty(null).FirstOrDefault();
1670
var pageSize = config?.GetArrayProperty(LDAPProperties.LdapAdminLimits).FirstOrDefault(x => x.StartsWith("Ma
1671
if (pageSize == null)
1672
{
1673
_log.LogDebug("No LDAPAdminLimits object found for {Domain}", domainName);
1674
_ldapRangeSizeCache.TryAdd(domainPath.ToUpper(), defaultRangeSize);
1675
return defaultRangeSize;
1676
}
1677
1678
if (int.TryParse(pageSize.Split('=').Last(), out parsedPageSize))
1679
{
1680
_ldapRangeSizeCache.TryAdd(domainPath.ToUpper(), parsedPageSize);
1681
_log.LogInformation("Found page size {PageSize} for {Domain}", parsedPageSize, domainName ?? "current do
1682
return parsedPageSize;
1683
}
1684
1685
_log.LogDebug("Failed to parse pagesize for {Domain}, returning default", domainName ?? "current domain");
1686
1687
_ldapRangeSizeCache.TryAdd(domainPath.ToUpper(), defaultRangeSize);
1688
return defaultRangeSize;
1689
}
1690
1691
private string DomainNameToDistinguishedName(string domain)
1692
{
1693
var resolvedDomain = GetDomain(domain)?.Name ?? domain;
1694
return resolvedDomain == null ? null : $"DC={resolvedDomain.Replace(".", ",DC=")}";
1695
}
1696
1697
private class ResolvedWellKnownPrincipal
1698
{
1699
public string DomainName { get; set; }
1700
public string WkpId { get; set; }
1701
}
1702
1703
public string GetConfigurationPath(string domainName = null)
1704
{
1705
string path = domainName == null
1706
? "LDAP://RootDSE"
1707
: $"LDAP://{NormalizeDomainName(domainName)}/RootDSE";
1708
1709
DirectoryEntry rootDse;
1710
if (_ldapConfig.Username != null)
1711
rootDse = new DirectoryEntry(path, _ldapConfig.Username, _ldapConfig.Password);
1712
else
1713
rootDse = new DirectoryEntry(path);
1714
1715
return $"{rootDse.Properties["configurationNamingContext"]?[0]}";
1716
}
1717
1718
public string GetSchemaPath(string domainName)
1719
{
1720
string path = domainName == null
1721
? "LDAP://RootDSE"
1722
: $"LDAP://{NormalizeDomainName(domainName)}/RootDSE";
1723
1724
DirectoryEntry rootDse;
1725
if (_ldapConfig.Username != null)
1726
rootDse = new DirectoryEntry(path, _ldapConfig.Username, _ldapConfig.Password);
1727
else
1728
rootDse = new DirectoryEntry(path);
1729
1730
return $"{rootDse.Properties["schemaNamingContext"]?[0]}";
1731
}
1732
1733
public bool IsDomainController(string computerObjectId, string domainName)
1734
{
1735
var filter = new LDAPFilter().AddFilter(LDAPProperties.ObjectSID + "=" + computerObjectId, true).AddFilter(C
1736
var res = QueryLDAP(filter.GetFilter(), SearchScope.Subtree,
1737
CommonProperties.ObjectID, domainName: domainName);
1738
if (res.Count() > 0)
1739
return true;
1740
return false;
1741
}
1742
}
1743
}
1635
/// <summary>
1636
/// Gets the range retrieval limit for a domain
1637
/// </summary>
1638
/// <param name="domainName"></param>
1639
/// <param name="defaultRangeSize"></param>
1640
/// <returns></returns>
1641
public int GetDomainRangeSize(string domainName = null, int defaultRangeSize = 750)
1642
{
1643
var domainPath = DomainNameToDistinguishedName(domainName);
1644
//Default to a page size of 750 for safety
1645
if (domainPath == null)
1646
{
1647
_log.LogDebug("Unable to resolve domain {Domain} to distinguishedname to get page size",
1648
domainName ?? "current domain");
1649
return defaultRangeSize;
1650
}
1651
1652
if (_ldapRangeSizeCache.TryGetValue(domainPath.ToUpper(), out var parsedPageSize))
1653
{
1654
return parsedPageSize;
1655
}
1656
1657
var configPath = CommonPaths.CreateDNPath(CommonPaths.QueryPolicyPath, domainPath);
1658
var enumerable = QueryLDAP("(objectclass=*)", SearchScope.Base, null, adsPath: configPath);
1659
var config = enumerable.DefaultIfEmpty(null).FirstOrDefault();
1660
var pageSize = config?.GetArrayProperty(LDAPProperties.LdapAdminLimits)
1661
.FirstOrDefault(x => x.StartsWith("MaxPageSize", StringComparison.OrdinalIgnoreCase));
1662
if (pageSize == null)
1663
{
1664
_log.LogDebug("No LDAPAdminLimits object found for {Domain}", domainName);
1665
_ldapRangeSizeCache.TryAdd(domainPath.ToUpper(), defaultRangeSize);
1666
return defaultRangeSize;
1667
}
1668
1669
if (int.TryParse(pageSize.Split('=').Last(), out parsedPageSize))
1670
{
1671
_ldapRangeSizeCache.TryAdd(domainPath.ToUpper(), parsedPageSize);
1672
_log.LogInformation("Found page size {PageSize} for {Domain}", parsedPageSize,
1673
domainName ?? "current domain");
1674
return parsedPageSize;
1675
}
1676
1677
_log.LogDebug("Failed to parse pagesize for {Domain}, returning default", domainName ?? "current domain");
1678
1679
_ldapRangeSizeCache.TryAdd(domainPath.ToUpper(), defaultRangeSize);
1680
return defaultRangeSize;
1681
}
1682
1683
private string DomainNameToDistinguishedName(string domain)
1684
{
1685
var resolvedDomain = GetDomain(domain)?.Name ?? domain;
1686
return resolvedDomain == null ? null : $"DC={resolvedDomain.Replace(".", ",DC=")}";
1687
}
1688
1689
private class ResolvedWellKnownPrincipal
1690
{
1691
public string DomainName { get; set; }
1692
public string WkpId { get; set; }
1693
}
1694
1695
public string GetConfigurationPath(string domainName = null)
1696
{
1697
string path = domainName == null
1698
? "LDAP://RootDSE"
1699
: $"LDAP://{NormalizeDomainName(domainName)}/RootDSE";
1700
1701
DirectoryEntry rootDse;
1702
if (_ldapConfig.Username != null)
1703
rootDse = new DirectoryEntry(path, _ldapConfig.Username, _ldapConfig.Password);
1704
else
1705
rootDse = new DirectoryEntry(path);
1706
1707
return $"{rootDse.Properties["configurationNamingContext"]?[0]}";
1708
}
1709
1710
public string GetSchemaPath(string domainName)
1711
{
1712
string path = domainName == null
1713
? "LDAP://RootDSE"
1714
: $"LDAP://{NormalizeDomainName(domainName)}/RootDSE";
1715
1716
DirectoryEntry rootDse;
1717
if (_ldapConfig.Username != null)
1718
rootDse = new DirectoryEntry(path, _ldapConfig.Username, _ldapConfig.Password);
1719
else
1720
rootDse = new DirectoryEntry(path);
1721
1722
return $"{rootDse.Properties["schemaNamingContext"]?[0]}";
1723
}
1724
1725
public bool IsDomainController(string computerObjectId, string domainName)
1726
{
1727
var filter = new LDAPFilter().AddFilter(LDAPProperties.ObjectSID + "=" + computerObjectId, true)
1728
.AddFilter(CommonFilters.DomainControllers, true);
1729
var res = QueryLDAP(filter.GetFilter(), SearchScope.Subtree,
1730
CommonProperties.ObjectID, domainName: domainName);
1731
if (res.Count() > 0)
1732
return true;
1733
return false;
1734
}
1735
}
1736
}