diff --git a/SharpHoundCommon.sln b/SharpHoundCommon.sln index 73a7798c..8d0c35a8 100644 --- a/SharpHoundCommon.sln +++ b/SharpHoundCommon.sln @@ -1,12 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# +# Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpHoundCommonLib", "src\CommonLib\SharpHoundCommonLib.csproj", "{88EB8B09-EB8A-4E59-BBF7-CA5374DFA9EB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommonLibTest", "test\unit\CommonLibTest.csproj", "{F1E060CB-58D0-42A7-9BBC-E08C6FD5DD43}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docfx", "docfx\Docfx.csproj", "{BD8C7EB8-F357-4499-8C08-76B42F600076}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpHoundRPC", "src\SharpHoundRPC\SharpHoundRPC.csproj", "{4F06116D-88A7-4601-AB28-B48F2857D458}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,5 +27,9 @@ Global {BD8C7EB8-F357-4499-8C08-76B42F600076}.Debug|Any CPU.Build.0 = Debug|Any CPU {BD8C7EB8-F357-4499-8C08-76B42F600076}.Release|Any CPU.ActiveCfg = Release|Any CPU {BD8C7EB8-F357-4499-8C08-76B42F600076}.Release|Any CPU.Build.0 = Release|Any CPU + {4F06116D-88A7-4601-AB28-B48F2857D458}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F06116D-88A7-4601-AB28-B48F2857D458}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F06116D-88A7-4601-AB28-B48F2857D458}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F06116D-88A7-4601-AB28-B48F2857D458}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/CommonLib/Cache.cs b/src/CommonLib/Cache.cs index 802abc78..bf311aeb 100644 --- a/src/CommonLib/Cache.cs +++ b/src/CommonLib/Cache.cs @@ -4,26 +4,38 @@ namespace SharpHoundCommonLib { + [DataContract] public class Cache { - [DataMember] private ConcurrentDictionary _globalCatalogCache; + //Leave these here until we switch back to Newtonsoft which doesn't suck + // [DataMember]private ConcurrentDictionary _globalCatalogCache; + // + // [DataMember]private ConcurrentDictionary _idToTypeCache; + // + // [DataMember]private ConcurrentDictionary _machineSidCache; + // + // [DataMember]private ConcurrentDictionary _sidToDomainCache; + // + // [DataMember]private ConcurrentDictionary _valueToIDCache; - [DataMember] private ConcurrentDictionary _idToTypeCache; + private Cache() + { + ValueToIdCache = new ConcurrentDictionary(); + IdToTypeCache = new ConcurrentDictionary(); + GlobalCatalogCache = new ConcurrentDictionary(); + MachineSidCache = new ConcurrentDictionary(); + SIDToDomainCache = new ConcurrentDictionary(); + } - [DataMember] private ConcurrentDictionary _machineSidCache; + [DataMember] public ConcurrentDictionary GlobalCatalogCache { get; private set; } - [DataMember] private ConcurrentDictionary _sidToDomainCache; + [DataMember] public ConcurrentDictionary IdToTypeCache { get; private set; } - [DataMember] private ConcurrentDictionary _valueToIDCache; + [DataMember] public ConcurrentDictionary MachineSidCache { get; private set; } - private Cache() - { - _valueToIDCache = new ConcurrentDictionary(); - _idToTypeCache = new ConcurrentDictionary(); - _globalCatalogCache = new ConcurrentDictionary(); - _machineSidCache = new ConcurrentDictionary(); - _sidToDomainCache = new ConcurrentDictionary(); - } + [DataMember] public ConcurrentDictionary SIDToDomainCache { get; private set; } + + [DataMember] public ConcurrentDictionary ValueToIdCache { get; private set; } [IgnoreDataMember] private static Cache CacheInstance { get; set; } @@ -34,7 +46,7 @@ private Cache() /// internal static void AddSidToDomain(string key, string value) { - CacheInstance?._sidToDomainCache.TryAdd(key, value); + CacheInstance?.SIDToDomainCache.TryAdd(key, value); } /// @@ -45,7 +57,7 @@ internal static void AddSidToDomain(string key, string value) /// internal static bool GetDomainSidMapping(string key, out string value) { - if (CacheInstance != null) return CacheInstance._machineSidCache.TryGetValue(key, out value); + if (CacheInstance != null) return CacheInstance.MachineSidCache.TryGetValue(key, out value); value = null; return false; } @@ -57,46 +69,46 @@ internal static bool GetDomainSidMapping(string key, out string value) /// internal static void AddMachineSid(string key, string value) { - CacheInstance?._machineSidCache.TryAdd(key, value); + CacheInstance?.MachineSidCache.TryAdd(key, value); } internal static bool GetMachineSid(string key, out string value) { - if (CacheInstance != null) return CacheInstance._machineSidCache.TryGetValue(key, out value); + if (CacheInstance != null) return CacheInstance.MachineSidCache.TryGetValue(key, out value); value = null; return false; } internal static void AddConvertedValue(string key, string value) { - CacheInstance?._valueToIDCache.TryAdd(key, value); + CacheInstance?.ValueToIdCache.TryAdd(key, value); } internal static void AddPrefixedValue(string key, string domain, string value) { - CacheInstance?._valueToIDCache.TryAdd(GetPrefixKey(key, domain), value); + CacheInstance?.ValueToIdCache.TryAdd(GetPrefixKey(key, domain), value); } internal static void AddType(string key, Label value) { - CacheInstance?._idToTypeCache.TryAdd(key, value); + CacheInstance?.IdToTypeCache.TryAdd(key, value); } internal static void AddGCCache(string key, string[] value) { - CacheInstance?._globalCatalogCache?.TryAdd(key, value); + CacheInstance?.GlobalCatalogCache?.TryAdd(key, value); } internal static bool GetGCCache(string key, out string[] value) { - if (CacheInstance != null) return CacheInstance._globalCatalogCache.TryGetValue(key, out value); + if (CacheInstance != null) return CacheInstance.GlobalCatalogCache.TryGetValue(key, out value); value = null; return false; } internal static bool GetConvertedValue(string key, out string value) { - if (CacheInstance != null) return CacheInstance._valueToIDCache.TryGetValue(key, out value); + if (CacheInstance != null) return CacheInstance.ValueToIdCache.TryGetValue(key, out value); value = null; return false; } @@ -104,14 +116,14 @@ internal static bool GetConvertedValue(string key, out string value) internal static bool GetPrefixedValue(string key, string domain, out string value) { if (CacheInstance != null) - return CacheInstance._valueToIDCache.TryGetValue(GetPrefixKey(key, domain), out value); + return CacheInstance.ValueToIdCache.TryGetValue(GetPrefixKey(key, domain), out value); value = null; return false; } internal static bool GetIDType(string key, out Label value) { - if (CacheInstance != null) return CacheInstance._idToTypeCache.TryGetValue(key, out value); + if (CacheInstance != null) return CacheInstance.IdToTypeCache.TryGetValue(key, out value); value = Label.Base; return false; } @@ -149,7 +161,7 @@ public string GetCacheStats() try { return - $"{_idToTypeCache.Count} ID to type mappings.\n {_valueToIDCache.Count} name to SID mappings.\n {_machineSidCache.Count} machine sid mappings.\n {_sidToDomainCache.Count} sid to domain mappings.\n {_globalCatalogCache.Count} global catalog mappings."; + $"{IdToTypeCache.Count} ID to type mappings.\n {ValueToIdCache.Count} name to SID mappings.\n {MachineSidCache.Count} machine sid mappings.\n {SIDToDomainCache.Count} sid to domain mappings.\n {GlobalCatalogCache.Count} global catalog mappings."; } catch { @@ -169,11 +181,11 @@ public static Cache GetCacheInstance() private static void CreateMissingDictionaries() { CacheInstance ??= new Cache(); - CacheInstance._idToTypeCache ??= new ConcurrentDictionary(); - CacheInstance._globalCatalogCache ??= new ConcurrentDictionary(); - CacheInstance._machineSidCache ??= new ConcurrentDictionary(); - CacheInstance._sidToDomainCache ??= new ConcurrentDictionary(); - CacheInstance._valueToIDCache ??= new ConcurrentDictionary(); + CacheInstance.IdToTypeCache ??= new ConcurrentDictionary(); + CacheInstance.GlobalCatalogCache ??= new ConcurrentDictionary(); + CacheInstance.MachineSidCache ??= new ConcurrentDictionary(); + CacheInstance.SIDToDomainCache ??= new ConcurrentDictionary(); + CacheInstance.ValueToIdCache ??= new ConcurrentDictionary(); } } } \ No newline at end of file diff --git a/src/CommonLib/Enums/CollectionMethods.cs b/src/CommonLib/Enums/CollectionMethods.cs index d5c111c7..3c9bdb2c 100644 --- a/src/CommonLib/Enums/CollectionMethods.cs +++ b/src/CommonLib/Enums/CollectionMethods.cs @@ -21,10 +21,11 @@ public enum ResolvedCollectionMethod DCOM = 1 << 12, SPNTargets = 1 << 13, PSRemote = 1 << 14, + UserRights = 1 << 15, LocalGroups = DCOM | RDP | LocalAdmin | PSRemote, - ComputerOnly = LocalGroups | Session, + ComputerOnly = LocalGroups | Session | UserRights, DCOnly = ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup, Default = Group | Session | Trusts | ACL | ObjectProps | LocalGroups | SPNTargets | Container, - All = Default | LoggedOn | GPOLocalGroup + All = Default | LoggedOn | GPOLocalGroup | UserRights } } \ No newline at end of file diff --git a/src/CommonLib/Exceptions/SharpHoundCommonException.cs b/src/CommonLib/Exceptions/LDAPQueryException.cs similarity index 65% rename from src/CommonLib/Exceptions/SharpHoundCommonException.cs rename to src/CommonLib/Exceptions/LDAPQueryException.cs index 2e66668e..b463a51e 100644 --- a/src/CommonLib/Exceptions/SharpHoundCommonException.cs +++ b/src/CommonLib/Exceptions/LDAPQueryException.cs @@ -4,8 +4,16 @@ namespace SharpHoundCommonLib.Exceptions { public class LDAPQueryException : Exception { - public LDAPQueryException() { } - public LDAPQueryException(string message) : base(message) { } - public LDAPQueryException(string message, Exception inner) : base(message, inner) { } + public LDAPQueryException() + { + } + + public LDAPQueryException(string message) : base(message) + { + } + + public LDAPQueryException(string message, Exception inner) : base(message, inner) + { + } } -} +} \ No newline at end of file diff --git a/src/CommonLib/LDAPProperties.cs b/src/CommonLib/LDAPProperties.cs index ea09e266..27a894b5 100644 --- a/src/CommonLib/LDAPProperties.cs +++ b/src/CommonLib/LDAPProperties.cs @@ -45,5 +45,6 @@ public class LDAPProperties public const string UnixUserPassword = "unixuserpassword"; public const string UnicodePassword = "unicodepwd"; public const string MsSFU30Password = "msSFU30Password"; + public const string ScriptPath = "scriptpath"; } } \ No newline at end of file diff --git a/src/CommonLib/LDAPQueries/CommonProperties.cs b/src/CommonLib/LDAPQueries/CommonProperties.cs index 5cc9c57e..c8c7d1fd 100644 --- a/src/CommonLib/LDAPQueries/CommonProperties.cs +++ b/src/CommonLib/LDAPQueries/CommonProperties.cs @@ -11,7 +11,7 @@ public static class CommonProperties public static readonly string[] BaseQueryProps = { - "objectsid", "distiguishedname", "objectguid", "ms-mcs-admpwdexpirationtime", "isDeleted", + "objectsid", "distinguishedname", "objectguid", "ms-mcs-admpwdexpirationtime", "isDeleted", "useraccountcontrol" }; diff --git a/src/CommonLib/LDAPUtils.cs b/src/CommonLib/LDAPUtils.cs index 8d835622..b01e18ac 100644 --- a/src/CommonLib/LDAPUtils.cs +++ b/src/CommonLib/LDAPUtils.cs @@ -13,10 +13,11 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.Exceptions; using SharpHoundCommonLib.LDAPQueries; using SharpHoundCommonLib.OutputTypes; using SharpHoundCommonLib.Processors; -using SharpHoundCommonLib.Exceptions; +using SharpHoundRPC.NetAPINative; using Domain = System.DirectoryServices.ActiveDirectory.Domain; using SearchScope = System.DirectoryServices.Protocols.SearchScope; using SecurityMasks = System.DirectoryServices.Protocols.SecurityMasks; @@ -183,7 +184,7 @@ public string[] GetUserGlobalCatalogMatches(string name) return sids; var query = new LDAPFilter().AddUsers($"samaccountname={tempName}").GetFilter(); - var results = QueryLDAP(query, SearchScope.Subtree, new[] { "objectsid" }, globalCatalog: true) + var results = QueryLDAP(query, SearchScope.Subtree, new[] {"objectsid"}, globalCatalog: true) .Select(x => x.GetSid()).Where(x => x != null).ToArray(); Cache.AddGCCache(tempName, results); return results; @@ -219,15 +220,11 @@ public Label LookupSidType(string sid, string domain) { if (Cache.GetIDType(sid, out var type)) return type; - - var hex = Helpers.ConvertSidToHexSid(sid); - if (hex == null) - return Label.Base; - + var rDomain = GetDomainNameFromSid(sid) ?? domain; var result = - QueryLDAP($"(objectsid={hex})", SearchScope.Subtree, CommonProperties.TypeResolutionProps, rDomain) + QueryLDAP(CommonFilters.SpecificSID(sid), SearchScope.Subtree, CommonProperties.TypeResolutionProps, rDomain) .DefaultIfEmpty(null).FirstOrDefault(); type = result?.GetLabel() ?? Label.Base; @@ -359,7 +356,7 @@ public IEnumerable DoRangedRetrieval(string distinguishedName, string at var currentRange = $"{baseString};range={index}-*"; var complete = false; - var searchRequest = CreateSearchRequest($"{attributeName}=*", SearchScope.Base, new[] { currentRange }, + var searchRequest = CreateSearchRequest($"{attributeName}=*", SearchScope.Base, new[] {currentRange}, domainName, distinguishedName); if (searchRequest == null) @@ -369,7 +366,7 @@ public IEnumerable DoRangedRetrieval(string distinguishedName, string at { SearchResponse response; - response = (SearchResponse)conn.SendRequest(searchRequest); + response = (SearchResponse) conn.SendRequest(searchRequest); //If we ever get more than one response from here, something is horribly wrong if (response?.Entries.Count == 1) @@ -454,11 +451,11 @@ public async Task ResolveHostToSid(string hostname, string domain) //Step 2: Try NetWkstaGetInfo //Next we'll try calling NetWkstaGetInfo in hopes of getting the NETBIOS name directly from the computer //We'll use the hostname that we started with instead of the one from our previous step - var workstationInfo = await CallNetWkstaGetInfo(strippedHost); + var workstationInfo = await GetWorkstationInfo(strippedHost); if (workstationInfo.HasValue) { - tempName = workstationInfo.Value.computer_name; - tempDomain = workstationInfo.Value.lan_group; + tempName = workstationInfo.Value.ComputerName; + tempDomain = workstationInfo.Value.LanGroup; if (string.IsNullOrEmpty(tempDomain)) tempDomain = normalDomain; @@ -470,8 +467,8 @@ public async Task ResolveHostToSid(string hostname, string domain) var principal = ResolveAccountName(tempName, tempDomain); if (principal != null) { - _hostResolutionMap.TryAdd(strippedHost, sid); - return sid; + _hostResolutionMap.TryAdd(strippedHost, principal.ObjectIdentifier); + return principal.ObjectIdentifier; } } } @@ -633,75 +630,6 @@ public TypedPrincipal ResolveDistinguishedName(string dn) }; } - /// - /// Setup LDAP query for filter - /// - /// LDAP filter - /// SearchScope to query - /// LDAP properties to fetch for each object - /// Include the DACL and Owner values in the NTSecurityDescriptor - /// Domain to query - /// Include deleted objects - /// ADS path to limit the query too - /// Use the global catalog instead of the regular LDAP server - /// - /// Skip the connection cache and force a new connection. You must dispose of this connection - /// yourself. - /// - /// Tuple of LdapConnection, SearchRequest, PageResultRequestControl and LDAPQueryException - public Tuple SetupLDAPQueryFilter(string ldapFilter, - SearchScope scope, string[] props, bool includeAcl = false, string domainName = null, bool showDeleted = false, - string adsPath = null, bool globalCatalog = false, bool skipCache = false) - { - _log.LogTrace("Creating ldap connection for {Target} with filter {Filter}", - globalCatalog ? "Global Catalog" : "DC", ldapFilter); - var task = globalCatalog - ? Task.Run(() => CreateGlobalCatalogConnection(domainName, _ldapConfig.AuthType)) - : Task.Run(() => CreateLDAPConnection(domainName, skipCache, _ldapConfig.AuthType)); - - LdapConnection conn; - try - { - conn = task.ConfigureAwait(false).GetAwaiter().GetResult(); - } - catch (Exception e) - { - var errorString = String.Format("Exception getting LDAP connection for {0} and domain {1}", ldapFilter, - domainName ?? "Default Domain"); - return Tuple.Create( - null, null, null, new LDAPQueryException(errorString, e)); - } - - if (conn == null) - { - var errorString = String.Format("LDAP connection is null for filter {0} and domain {1}", ldapFilter, - domainName ?? "Default Domain"); - return Tuple.Create( - null, null, null, new LDAPQueryException(errorString)); - } - - var request = CreateSearchRequest(ldapFilter, scope, props, domainName, adsPath, showDeleted); - - if (request == null) - { - var errorString = String.Format("Search request is null for filter {0} and domain {1}", ldapFilter, - domainName ?? "Default Domain"); - return Tuple.Create( - null, null, null, new LDAPQueryException(errorString)); - } - - var pageControl = new PageResultRequestControl(500); - request.Controls.Add(pageControl); - - if (includeAcl) - request.Controls.Add(new SecurityDescriptorFlagControl - { - SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner - }); - - return Tuple.Create(conn, request, pageControl, null); - } - /// /// Queries LDAP using LDAPQueryOptions /// @@ -760,14 +688,10 @@ public IEnumerable QueryLDAP(string ldapFilter, SearchScope if (error != null) { - if (throwException) - { - throw new LDAPQueryException("Failed to setup LDAP Query Filter", error); - } - else - { - _log.LogWarning(error, "Failed to setup LDAP Query Filter"); - } + if (throwException) throw new LDAPQueryException("Failed to setup LDAP Query Filter", error); + + _log.LogWarning(error, "Failed to setup LDAP Query Filter"); + yield break; } PageResultResponseControl pageResponse = null; @@ -780,26 +704,22 @@ public IEnumerable QueryLDAP(string ldapFilter, SearchScope try { _log.LogTrace("Sending LDAP request for {Filter}", ldapFilter); - response = (SearchResponse)conn.SendRequest(request); + response = (SearchResponse) conn.SendRequest(request); if (response != null) - pageResponse = (PageResultResponseControl)response.Controls + pageResponse = (PageResultResponseControl) response.Controls .Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault(); } catch (LdapException le) { if (le.ErrorCode != 82) if (throwException) - { - throw new LDAPQueryException(String.Format( + throw new LDAPQueryException(string.Format( "LDAP Exception in Loop: {0}. {1}. {2}. Filter: {3}. Domain: {4}.", le.ErrorCode, le.ServerErrorMessage, le.Message, ldapFilter, domainName), le); - } else - { _log.LogWarning(le, "LDAP Exception in Loop: {ErrorCode}. {ServerErrorMessage}. {Message}. Filter: {Filter}. Domain: {Domain}", le.ErrorCode, le.ServerErrorMessage, le.Message, ldapFilter, domainName); - } yield break; } @@ -807,14 +727,12 @@ public IEnumerable QueryLDAP(string ldapFilter, SearchScope { if (throwException) { - throw new LDAPQueryException(String.Format("Exception in LDAP loop for {0} and {1}", + throw new LDAPQueryException(string.Format("Exception in LDAP loop for {0} and {1}", ldapFilter, domainName)); } - else - { - _log.LogWarning(e, "Exception in LDAP loop for {Filter} and {Domain}", ldapFilter, domainName); - yield break; - } + + _log.LogWarning(e, "Exception in LDAP loop for {Filter} and {Domain}", ldapFilter, domainName); + yield break; } if (cancellationToken.IsCancellationRequested) @@ -873,15 +791,10 @@ public IEnumerable QueryLDAP(string ldapFilter, SearchScope if (error != null) { - if (throwException) - { - throw new LDAPQueryException("Failed to setup LDAP Query Filter", error); - } - else - { - _log.LogWarning(error, "Failed to setup LDAP Query Filter"); - yield break; - } + if (throwException) throw new LDAPQueryException("Failed to setup LDAP Query Filter", error); + + _log.LogWarning(error, "Failed to setup LDAP Query Filter"); + yield break; } PageResultResponseControl pageResponse = null; @@ -891,40 +804,35 @@ public IEnumerable QueryLDAP(string ldapFilter, SearchScope try { _log.LogTrace("Sending LDAP request for {Filter}", ldapFilter); - response = (SearchResponse)conn.SendRequest(request); + response = (SearchResponse) conn.SendRequest(request); if (response != null) - pageResponse = (PageResultResponseControl)response.Controls + pageResponse = (PageResultResponseControl) response.Controls .Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault(); } catch (LdapException le) { if (le.ErrorCode != 82) if (throwException) - { - throw new LDAPQueryException(String.Format( + throw new LDAPQueryException(string.Format( "LDAP Exception in Loop: {0}. {1}. {2}. Filter: {3}. Domain: {4}", le.ErrorCode, le.ServerErrorMessage, le.Message, ldapFilter, domainName), le); - } else - { _log.LogWarning(le, "LDAP Exception in Loop: {ErrorCode}. {ServerErrorMessage}. {Message}. Filter: {Filter}. Domain: {Domain}", le.ErrorCode, le.ServerErrorMessage, le.Message, ldapFilter, domainName); - } yield break; } catch (Exception e) { if (throwException) { - throw new LDAPQueryException(String.Format( + throw new LDAPQueryException(string.Format( "Exception in LDAP loop for {0} and {1}", ldapFilter, domainName ?? "Default Domain"), e); } - else - { - _log.LogWarning(e, "Exception in LDAP loop for {Filter} and {Domain}", ldapFilter, domainName ?? "Default Domain"); - yield break; - } + + _log.LogWarning(e, "Exception in LDAP loop for {Filter} and {Domain}", ldapFilter, + domainName ?? "Default Domain"); + yield break; } if (response == null || pageResponse == null) continue; @@ -1030,11 +938,83 @@ public Domain GetDomain(string domainName = null) return domain; } + /// + /// Setup LDAP query for filter + /// + /// LDAP filter + /// SearchScope to query + /// LDAP properties to fetch for each object + /// Include the DACL and Owner values in the NTSecurityDescriptor + /// Domain to query + /// Include deleted objects + /// ADS path to limit the query too + /// Use the global catalog instead of the regular LDAP server + /// + /// Skip the connection cache and force a new connection. You must dispose of this connection + /// yourself. + /// + /// Tuple of LdapConnection, SearchRequest, PageResultRequestControl and LDAPQueryException + public Tuple SetupLDAPQueryFilter( + string ldapFilter, + SearchScope scope, string[] props, bool includeAcl = false, string domainName = null, + bool showDeleted = false, + string adsPath = null, bool globalCatalog = false, bool skipCache = false) + { + _log.LogTrace("Creating ldap connection for {Target} with filter {Filter}", + globalCatalog ? "Global Catalog" : "DC", ldapFilter); + var task = globalCatalog + ? Task.Run(() => CreateGlobalCatalogConnection(domainName, _ldapConfig.AuthType)) + : Task.Run(() => CreateLDAPConnection(domainName, skipCache, _ldapConfig.AuthType)); + + LdapConnection conn; + try + { + conn = task.ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (Exception e) + { + var errorString = string.Format("Exception getting LDAP connection for {0} and domain {1}", ldapFilter, + domainName ?? "Default Domain"); + return Tuple.Create( + null, null, null, new LDAPQueryException(errorString, e)); + } + + if (conn == null) + { + var errorString = string.Format("LDAP connection is null for filter {0} and domain {1}", ldapFilter, + domainName ?? "Default Domain"); + return Tuple.Create( + null, null, null, new LDAPQueryException(errorString)); + } + + var request = CreateSearchRequest(ldapFilter, scope, props, domainName, adsPath, showDeleted); + + if (request == null) + { + var errorString = string.Format("Search request is null for filter {0} and domain {1}", ldapFilter, + domainName ?? "Default Domain"); + return Tuple.Create( + null, null, null, new LDAPQueryException(errorString)); + } + + var pageControl = new PageResultRequestControl(500); + request.Controls.Add(pageControl); + + if (includeAcl) + request.Controls.Add(new SecurityDescriptorFlagControl + { + SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner + }); + + return Tuple.Create(conn, + request, pageControl, null); + } + private Group GetBaseEnterpriseDC(string domain) { var forest = GetForest(domain)?.Name; if (forest == null) _log.LogWarning("Error getting forest, ENTDC sid is likely incorrect"); - var g = new Group { ObjectIdentifier = $"{forest}-S-1-5-9".ToUpper() }; + var g = new Group {ObjectIdentifier = $"{forest}-S-1-5-9".ToUpper()}; g.Properties.Add("name", $"ENTERPRISE DOMAIN CONTROLLERS@{forest ?? "UNKNOWN"}".ToUpper()); g.Properties.Add("domainsid", GetSidFromDomainName(forest)); g.Properties.Add("domain", forest); @@ -1060,7 +1040,7 @@ private string GetDomainNameFromSidLdap(string sid) //Search using objectsid first var result = QueryLDAP($"(&(objectclass=domain)(objectsid={hexSid}))", SearchScope.Subtree, - new[] { "distinguishedname" }, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault(); + new[] {"distinguishedname"}, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault(); if (result != null) { @@ -1071,7 +1051,7 @@ private string GetDomainNameFromSidLdap(string sid) //Try trusteddomain objects with the securityidentifier attribute result = QueryLDAP($"(&(objectclass=trusteddomain)(securityidentifier={sid}))", SearchScope.Subtree, - new[] { "cn" }, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault(); + new[] {"cn"}, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault(); if (result != null) { @@ -1163,19 +1143,15 @@ private static bool RequestNETBIOSNameFromComputer(string server, string domain, /// /// /// - private async Task CallNetWkstaGetInfo(string hostname) + private async Task GetWorkstationInfo(string hostname) { if (!await _portScanner.CheckPort(hostname)) return null; - try - { - return _nativeMethods.CallNetWkstaGetInfo(hostname); - } - catch - { - return null; - } + var result = NetAPIMethods.NetWkstaGetInfo(hostname); + if (result.IsSuccess) return result.Value; + + return null; } /// @@ -1298,7 +1274,7 @@ private async Task CreateLDAPConnection(string domainName = null var port = _ldapConfig.GetPort(); var ident = new LdapDirectoryIdentifier(targetServer, port, false, false); - var connection = new LdapConnection(ident) { Timeout = new TimeSpan(0, 0, 5, 0) }; + var connection = new LdapConnection(ident) {Timeout = new TimeSpan(0, 0, 5, 0)}; if (_ldapConfig.Username != null) { var cred = new NetworkCredential(_ldapConfig.Username, _ldapConfig.Password); @@ -1397,7 +1373,7 @@ internal string ResolveDomainNetbiosToDns(string domainName) var computerName = _ldapConfig.Server; var dci = _nativeMethods.CallDsGetDcName(computerName, domainName); - if (dci.HasValue) + if (dci.IsSuccess) { flatName = dci.Value.DomainName; _netbiosCache.TryAdd(key, flatName); diff --git a/src/CommonLib/NativeMethods.cs b/src/CommonLib/NativeMethods.cs index 80e6a58e..fb0c4c69 100644 --- a/src/CommonLib/NativeMethods.cs +++ b/src/CommonLib/NativeMethods.cs @@ -1,38 +1,14 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; -using System.Security; -using System.Security.Principal; using Microsoft.Extensions.Logging; -using SharpHoundCommonLib.Processors; +using SharpHoundRPC.NetAPINative; namespace SharpHoundCommonLib { [ExcludeFromCodeCoverage] + // This class exists entirely as a shim for testing public class NativeMethods { - public enum NtStatus - { - StatusSuccess = 0x0, - StatusMoreEntries = 0x105, - StatusSomeMapped = 0x107, - StatusInvalidHandle = unchecked((int) 0xC0000008), - StatusInvalidParameter = unchecked((int) 0xC000000D), - StatusAccessDenied = unchecked((int) 0xC0000022), - StatusObjectTypeMismatch = unchecked((int) 0xC0000024), - StatusNoSuchDomain = unchecked((int) 0xC00000DF), - StatusRpcServerUnavailable = unchecked((int) 0xC0020017), - StatusNoSuchAlias = unchecked((int) 0xC0000151) - } - - private const string NetWkstaUserEnumQueryName = "NetWkstaUserEnum"; - private const string NetSessionEnumQueryName = "NetSessionEnum"; - private const string NetWkstaGetInfoQueryName = "NetWkstaGetInfo"; - - private const int NetWkstaUserEnumQueryLevel = 1; - private const int NetSessionEnumLevel = 10; - private const int NetWkstaGetInfoQueryLevel = 100; private readonly ILogger _log; public NativeMethods(ILogger log = null) @@ -45,809 +21,20 @@ public NativeMethods() _log = Logging.LogProvider.CreateLogger("NativeMethods"); } - public virtual WorkstationInfo100 CallNetWkstaGetInfo(string serverName) - { - var ptr = IntPtr.Zero; - - try - { - var result = NetWkstaGetInfo(serverName, NetWkstaGetInfoQueryLevel, out ptr); - if (result != NERR.NERR_Success) - throw new APIException - { - Status = result.ToString(), - APICall = NetWkstaGetInfoQueryName - }; - - var wkstaInfo = Marshal.PtrToStructure(ptr); - return wkstaInfo; - } - finally - { - if (ptr != IntPtr.Zero) - NetApiBufferFree(ptr); - } - } - - public virtual IEnumerable CallNetSessionEnum(string serverName) - { - var ptr = IntPtr.Zero; - - _log.LogTrace("Beginning NetSessionEnum for {ServerName}", serverName); - try - { - var resumeHandle = 0; - var result = NetSessionEnum(serverName, null, null, NetSessionEnumLevel, out ptr, -1, - out var entriesread, - out _, ref resumeHandle); - - _log.LogTrace("Result of NetSessionEnum for {ServerName} is {Result}", serverName, result); - - if (result != NERR.NERR_Success) - throw new APIException - { - APICall = NetSessionEnumQueryName, - Status = result.ToString() - }; - - var iter = ptr; - for (var i = 0; i < entriesread; i++) - { - var data = Marshal.PtrToStructure(iter); - iter = (IntPtr) (iter.ToInt64() + Marshal.SizeOf()); - - yield return data; - } - } - finally - { - if (ptr != IntPtr.Zero) - NetApiBufferFree(ptr); - } - } - - public virtual IEnumerable CallNetWkstaUserEnum(string servername) - { - var ptr = IntPtr.Zero; - try - { - var resumeHandle = 0; - _log.LogTrace("Beginning NetWkstaUserEnum for {ServerName}", servername); - var result = NetWkstaUserEnum(servername, NetWkstaUserEnumQueryLevel, out ptr, -1, out var entriesread, - out _, - ref resumeHandle); - - _log.LogTrace("Result of NetWkstaUserEnum for computer {ServerName} is {Result}", servername, result); - - if (result != NERR.NERR_Success && result != NERR.ERROR_MORE_DATA) - throw new APIException - { - APICall = NetWkstaUserEnumQueryName, - Status = result.ToString() - }; - - var iter = ptr; - for (var i = 0; i < entriesread; i++) - { - var data = Marshal.PtrToStructure(iter); - iter = (IntPtr) (iter.ToInt64() + Marshal.SizeOf()); - yield return data; - } - } - finally - { - if (ptr != IntPtr.Zero) - NetApiBufferFree(ptr); - } - } - - public virtual DOMAIN_CONTROLLER_INFO? CallDsGetDcName(string computerName, string domainName) - { - var ptr = IntPtr.Zero; - try - { - var result = DsGetDcName(computerName, domainName, null, null, - (uint) (DSGETDCNAME_FLAGS.DS_IS_FLAT_NAME | DSGETDCNAME_FLAGS.DS_RETURN_DNS_NAME), out ptr); - - if (result != 0) return null; - var info = Marshal.PtrToStructure(ptr); - return info; - } - finally - { - if (ptr != IntPtr.Zero) - NetApiBufferFree(ptr); - } - } - - public virtual NtStatus CallSamConnect(ref UNICODE_STRING serverName, out IntPtr serverHandle, - SamAccessMasks desiredAccess, ref OBJECT_ATTRIBUTES objectAttributes) - { - return SamConnect(ref serverName, out serverHandle, desiredAccess, ref objectAttributes); - } - - internal virtual NtStatus CallSamOpenDomain(IntPtr serverHandle, DomainAccessMask desiredAccess, - byte[] domainSid, out IntPtr domainHandle) - { - return SamOpenDomain(serverHandle, desiredAccess, domainSid, out domainHandle); - } - - internal virtual NtStatus CallSamOpenAlias(IntPtr domainHandle, AliasOpenFlags desiredAccess, int aliasId, - out IntPtr aliasHandle) - { - return SamOpenAlias(domainHandle, desiredAccess, aliasId, out aliasHandle); - } - - internal virtual NtStatus CallSamGetMembersInAlias(IntPtr aliasHandle, out IntPtr members, out int count) - { - return SamGetMembersInAlias(aliasHandle, out members, out count); - } - - internal virtual NtStatus CallSamLookupDomainInSamServer(IntPtr serverHandle, ref UNICODE_STRING name, - out IntPtr sid) - { - return SamLookupDomainInSamServer(serverHandle, ref name, out sid); - } - - internal virtual NtStatus CallSamEnumerateAliasesInDomain(IntPtr domainHandle, out IntPtr rids, out int count) - { - var enumContext = 0; - return SamEnumerateAliasesInDomain(domainHandle, ref enumContext, out rids, -1, out count); - } - - - internal virtual NtStatus CallSamFreeMemory(IntPtr handle) - { - return SamFreeMemory(handle); - } - - internal virtual NtStatus CallSamCloseHandle(IntPtr handle) - { - return SamCloseHandle(handle); - } - - internal virtual NtStatus CallSamEnumerateDomainsInSamServer(IntPtr serverHandle, out IntPtr domains, - out int count) - { - var enumContext = 0; - return SamEnumerateDomainsInSamServer(serverHandle, ref enumContext, out domains, -1, out count); - } - - internal virtual NtStatus CallLSAOpenPolicy(ref LSA_UNICODE_STRING serverName, - ref LSA_OBJECT_ATTRIBUTES lsaObjectAttributes, - LsaOpenMask openMask, out IntPtr policyHandle) - { - return LsaOpenPolicy(ref serverName, ref lsaObjectAttributes, openMask, out policyHandle); - } - - internal virtual NtStatus CallLSAEnumerateAccountsWithUserRight(IntPtr policyHandle, string privilege, - out IntPtr buffer, - out int count) - { - var privileges = new LSA_UNICODE_STRING[1]; - privileges[0] = new LSA_UNICODE_STRING(privilege); - return LsaEnumerateAccountsWithUserRight(policyHandle, privileges, out buffer, out count); - } - - internal virtual NtStatus CallSamLookupIdsInDomain(IntPtr domainHandle, int[] rids, out IntPtr names, - out IntPtr use) - { - var count = rids.Length; - - return SamLookupIdsInDomain(domainHandle, count, rids, out names, out use); - } - - public virtual NtStatus CallLSAClose(IntPtr handle) - { - return LsaClose(handle); - } - - public virtual NtStatus CallLSAFreeMemory(IntPtr handle) - { - return LsaFreeMemory(handle); - } - - public virtual NtStatus CallLSALookupSids(IntPtr policyHandle, SecurityIdentifier[] sids, - out IntPtr referencedDomains, out IntPtr names) - { - var count = sids.Length; - var gcHandles = new GCHandle[count]; - var pSids = new IntPtr[count]; - - for (var i = 0; i < count; i++) - { - var sid = sids[i]; - var b = new byte[sid.BinaryLength]; - sid.GetBinaryForm(b, 0); - gcHandles[i] = GCHandle.Alloc(b, GCHandleType.Pinned); - pSids[i] = gcHandles[i].AddrOfPinnedObject(); - } - - try - { - return LsaLookupSids(policyHandle, (uint) count, pSids, out referencedDomains, out names); - } - finally - { - foreach (var handle in gcHandles) - if (handle.IsAllocated) - handle.Free(); - } - } - - public virtual NtStatus CallLSALookupSids2(IntPtr policyHandle, LsaLookupOptions lookupOptions, - SecurityIdentifier[] sids, out IntPtr referencedDomains, out IntPtr names) - { - var count = sids.Length; - var gcHandles = new GCHandle[count]; - var pSids = new IntPtr[count]; - - for (var i = 0; i < count; i++) - { - var sid = sids[i]; - var b = new byte[sid.BinaryLength]; - sid.GetBinaryForm(b, 0); - gcHandles[i] = GCHandle.Alloc(b, GCHandleType.Pinned); - pSids[i] = gcHandles[i].AddrOfPinnedObject(); - } - - try - { - return LsaLookupSids2(policyHandle, lookupOptions, (uint) count, pSids, out referencedDomains, - out names); - } - finally - { - foreach (var handle in gcHandles) - if (handle.IsAllocated) - handle.Free(); - } - } - - public virtual NtStatus CallLsaQueryInformationPolicy(IntPtr policyHandle, - LSAPolicyInformation policyInformation, out IntPtr buffer) - { - return LsaQueryInformationPolicy(policyHandle, policyInformation, out buffer); - } - - [StructLayout(LayoutKind.Sequential)] - public struct POLICY_ACCOUNT_DOMAIN_INFO - { - public LSA_UNICODE_STRING DomainName; - public IntPtr DomainSid; - } - - public struct OBJECT_ATTRIBUTES : IDisposable - { - public void Dispose() - { - if (objectName == IntPtr.Zero) return; - Marshal.DestroyStructure(objectName, typeof(UNICODE_STRING)); - Marshal.FreeHGlobal(objectName); - objectName = IntPtr.Zero; - } - - public int len; - public IntPtr rootDirectory; - public uint attribs; - public IntPtr sid; - public IntPtr qos; - private IntPtr objectName; - public UNICODE_STRING ObjectName; - } - - [StructLayout(LayoutKind.Sequential)] - public struct UNICODE_STRING : IDisposable - { - private readonly ushort Length; - private readonly ushort MaximumLength; - private IntPtr Buffer; - - public UNICODE_STRING(string s) - : this() - { - if (s == null) return; - Length = (ushort) (s.Length * 2); - MaximumLength = (ushort) (Length + 2); - Buffer = Marshal.StringToHGlobalUni(s); - } - - public void Dispose() - { - if (Buffer == IntPtr.Zero) return; - Marshal.FreeHGlobal(Buffer); - Buffer = IntPtr.Zero; - } - - public override string ToString() - { - return (Buffer != IntPtr.Zero ? Marshal.PtrToStringUni(Buffer, Length / 2) : null) ?? - throw new InvalidOperationException(); - } - } - - #region SAMR Imports - - [DllImport("samlib.dll")] - private static extern NtStatus SamCloseHandle( - IntPtr handle - ); - - [DllImport("samlib.dll")] - private static extern NtStatus SamFreeMemory( - IntPtr handle - ); - - [DllImport("samlib.dll", CharSet = CharSet.Unicode)] - private static extern NtStatus SamLookupDomainInSamServer( - IntPtr serverHandle, - ref UNICODE_STRING name, - out IntPtr sid); - - [DllImport("samlib.dll", CharSet = CharSet.Unicode)] - private static extern NtStatus SamGetMembersInAlias( - IntPtr aliasHandle, - out IntPtr members, - out int count); - - [DllImport("samlib.dll", CharSet = CharSet.Unicode)] - private static extern NtStatus SamOpenAlias( - IntPtr domainHandle, - AliasOpenFlags desiredAccess, - int aliasId, - out IntPtr aliasHandle - ); - - [DllImport("samlib.dll", CharSet = CharSet.Unicode)] - private static extern NtStatus SamConnect( - ref UNICODE_STRING serverName, - out IntPtr serverHandle, - SamAccessMasks desiredAccess, - ref OBJECT_ATTRIBUTES objectAttributes - ); - - [DllImport("samlib.dll", CharSet = CharSet.Unicode)] - private static extern NtStatus SamOpenDomain( - IntPtr serverHandle, - DomainAccessMask desiredAccess, - [MarshalAs(UnmanagedType.LPArray)] byte[] domainSid, - out IntPtr domainHandle - ); - - [DllImport("samlib.dll", CharSet = CharSet.Unicode)] - private static extern NtStatus SamLookupIdsInDomain(IntPtr domainHandle, - int count, - int[] rids, - out IntPtr names, - out IntPtr use); - - [DllImport("samlib.dll", CharSet = CharSet.Unicode)] - private static extern NtStatus SamEnumerateAliasesInDomain( - IntPtr domainHandle, - ref int enumerationContext, - out IntPtr buffer, - int prefMaxLen, - out int count); - - [DllImport("samlib.dll", CharSet = CharSet.Unicode)] - private static extern NtStatus SamEnumerateDomainsInSamServer( - IntPtr serverHandle, - ref int enumerationContext, - out IntPtr buffer, - int prefMaxLen, - out int count); - - [Flags] - [SuppressMessage("ReSharper", "UnusedMember.Local")] - internal enum AliasOpenFlags - { - AddMember = 0x1, - RemoveMember = 0x2, - ListMembers = 0x4, - ReadInfo = 0x8, - WriteAccount = 0x10, - AllAccess = 0xf001f, - Read = 0x20004, - Write = 0x20013, - Execute = 0x20008 - } - - [Flags] - [SuppressMessage("ReSharper", "UnusedMember.Local")] - public enum DomainAccessMask - { - ReadPasswordParameters = 0x1, - WritePasswordParameters = 0x2, - ReadOtherParameters = 0x4, - WriteOtherParameters = 0x8, - CreateUser = 0x10, - CreateGroup = 0x20, - CreateAlias = 0x40, - GetAliasMembership = 0x80, - ListAccounts = 0x100, - Lookup = 0x200, - AdministerServer = 0x400, - AllAccess = 0xf07ff, - Read = 0x20084, - Write = 0x2047A, - Execute = 0x20301 - } - - [Flags] - [SuppressMessage("ReSharper", "UnusedMember.Local")] - internal enum SamAliasFlags - { - AddMembers = 0x1, - RemoveMembers = 0x2, - ListMembers = 0x4, - ReadInfo = 0x8, - WriteAccount = 0x10, - AllAccess = 0xf001f, - Read = 0x20004, - Write = 0x20013, - Execute = 0x20008 - } - - [Flags] - [SuppressMessage("ReSharper", "UnusedMember.Local")] - public enum SamAccessMasks + public virtual NetAPIResult> NetSessionEnum(string serverName) { - SamServerConnect = 0x1, - SamServerShutdown = 0x2, - SamServerInitialize = 0x4, - SamServerCreateDomains = 0x8, - SamServerEnumerateDomains = 0x10, - SamServerLookupDomain = 0x20, - SamServerAllAccess = 0xf003f, - SamServerRead = 0x20010, - SamServerWrite = 0x2000e, - SamServerExecute = 0x20021 + return NetAPIMethods.NetSessionEnum(serverName); } - [StructLayout(LayoutKind.Sequential)] - public struct SamRidEnumeration + public virtual NetAPIResult> NetWkstaUserEnum(string servername) { - internal static readonly int SizeOf = Marshal.SizeOf(); - public int Rid; - public UNICODE_STRING Name; + return NetAPIMethods.NetWkstaUserEnum(servername); } - public enum SidNameUse + public virtual NetAPIResult CallDsGetDcName(string computerName, + string domainName) { - User = 1, - Group, - Domain, - Alias, - WellKnownGroup, - DeletedAccount, - Invalid, - Unknown, - Computer, - Label, - LogonSession + return NetAPIMethods.DsGetDcName(computerName, domainName); } - - #endregion - - #region Session Enum Imports - - [DllImport("NetAPI32.dll", SetLastError = true)] - private static extern NERR NetSessionEnum( - [MarshalAs(UnmanagedType.LPWStr)] string ServerName, - [MarshalAs(UnmanagedType.LPWStr)] string UncClientName, - [MarshalAs(UnmanagedType.LPWStr)] string UserName, - int Level, - out IntPtr bufptr, - int prefmaxlen, - out int entriesread, - out int totalentries, - ref int resume_handle); - - [StructLayout(LayoutKind.Sequential)] - public struct SESSION_INFO_10 - { - [MarshalAs(UnmanagedType.LPWStr)] public string sesi10_cname; - [MarshalAs(UnmanagedType.LPWStr)] public string sesi10_username; - public uint sesi10_time; - public uint sesi10_idle_time; - } - - public enum NERR - { - NERR_Success = 0, - ERROR_MORE_DATA = 234, - ERROR_NO_BROWSER_SERVERS_FOUND = 6118, - ERROR_INVALID_LEVEL = 124, - ERROR_ACCESS_DENIED = 5, - ERROR_INVALID_PARAMETER = 87, - ERROR_NOT_ENOUGH_MEMORY = 8, - ERROR_NETWORK_BUSY = 54, - ERROR_BAD_NETPATH = 53, - ERROR_NO_NETWORK = 1222, - ERROR_INVALID_HANDLE_STATE = 1609, - ERROR_EXTENDED_ERROR = 1208, - NERR_BASE = 2100, - NERR_UnknownDevDir = NERR_BASE + 16, - NERR_DuplicateShare = NERR_BASE + 18, - NERR_BufTooSmall = NERR_BASE + 23 - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct WKSTA_USER_INFO_1 - { - public string wkui1_username; - public string wkui1_logon_domain; - public string wkui1_oth_domains; - public string wkui1_logon_server; - } - - [DllImport("netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern NERR NetWkstaUserEnum( - string servername, - int level, - out IntPtr bufptr, - int prefmaxlen, - out int entriesread, - out int totalentries, - ref int resume_handle); - - [DllImport("netapi32.dll")] - private static extern int NetApiBufferFree( - IntPtr Buff); - - #endregion - - #region NetAPI PInvoke Calls - - [DllImport("netapi32.dll", SetLastError = true)] - private static extern NERR NetWkstaGetInfo( - [MarshalAs(UnmanagedType.LPWStr)] string serverName, - uint level, - out IntPtr bufPtr); - - public struct WorkstationInfo100 - { - public int platform_id; - [MarshalAs(UnmanagedType.LPWStr)] public string computer_name; - [MarshalAs(UnmanagedType.LPWStr)] public string lan_group; - public int ver_major; - public int ver_minor; - } - - #endregion - - #region DSGetDcName Imports - - [DllImport("Netapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] - internal static extern int DsGetDcName - ( - [MarshalAs(UnmanagedType.LPTStr)] string ComputerName, - [MarshalAs(UnmanagedType.LPTStr)] string DomainName, - [In] GuidClass DomainGuid, - [MarshalAs(UnmanagedType.LPTStr)] string SiteName, - uint Flags, - out IntPtr pDOMAIN_CONTROLLER_INFO - ); - - [StructLayout(LayoutKind.Sequential)] - public class GuidClass - { - public Guid TheGuid; - } - - [Flags] - public enum DSGETDCNAME_FLAGS : uint - { - DS_FORCE_REDISCOVERY = 0x00000001, - DS_DIRECTORY_SERVICE_REQUIRED = 0x00000010, - DS_DIRECTORY_SERVICE_PREFERRED = 0x00000020, - DS_GC_SERVER_REQUIRED = 0x00000040, - DS_PDC_REQUIRED = 0x00000080, - DS_BACKGROUND_ONLY = 0x00000100, - DS_IP_REQUIRED = 0x00000200, - DS_KDC_REQUIRED = 0x00000400, - DS_TIMESERV_REQUIRED = 0x00000800, - DS_WRITABLE_REQUIRED = 0x00001000, - DS_GOOD_TIMESERV_PREFERRED = 0x00002000, - DS_AVOID_SELF = 0x00004000, - DS_ONLY_LDAP_NEEDED = 0x00008000, - DS_IS_FLAT_NAME = 0x00010000, - DS_IS_DNS_NAME = 0x00020000, - DS_RETURN_DNS_NAME = 0x40000000, - DS_RETURN_FLAT_NAME = 0x80000000 - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct DOMAIN_CONTROLLER_INFO - { - [MarshalAs(UnmanagedType.LPTStr)] public string DomainControllerName; - [MarshalAs(UnmanagedType.LPTStr)] public string DomainControllerAddress; - public uint DomainControllerAddressType; - public Guid DomainGuid; - [MarshalAs(UnmanagedType.LPTStr)] public string DomainName; - [MarshalAs(UnmanagedType.LPTStr)] public string DnsForestName; - public uint Flags; - [MarshalAs(UnmanagedType.LPTStr)] public string DcSiteName; - [MarshalAs(UnmanagedType.LPTStr)] public string ClientSiteName; - } - - #endregion - - #region LSA Imports - - [DllImport("advapi32.dll")] - private static extern NtStatus LsaOpenPolicy( - ref LSA_UNICODE_STRING server, - ref LSA_OBJECT_ATTRIBUTES objectAttributes, - LsaOpenMask desiredAccess, - out IntPtr policyHandle - ); - - [DllImport("advapi32.dll")] - private static extern NtStatus LsaEnumerateAccountRights( - IntPtr policyHandle, - IntPtr accountSid, - IntPtr accountRights, - out int count); - - [DllImport("advapi32.dll")] - private static extern NtStatus LsaLookupSids( - IntPtr policyHandle, - uint count, - [MarshalAs(UnmanagedType.LPArray)] IntPtr[] sidArray, - out IntPtr referencedDomains, - out IntPtr names - ); - - [DllImport("advapi32.dll")] - private static extern NtStatus LsaLookupSids2( - IntPtr policyHandle, - LsaLookupOptions lookupOptions, - uint count, - [MarshalAs(UnmanagedType.LPArray)] IntPtr[] sidArray, - out IntPtr referencedDomains, - out IntPtr names - ); - - [DllImport("advapi32.dll")] - private static extern NtStatus LsaQueryInformationPolicy( - IntPtr policyHandle, LSAPolicyInformation policyInformation, out IntPtr buffer); - - [DllImport("advapi32.dll")] - private static extern NtStatus LsaClose( - IntPtr buffer - ); - - [DllImport("advapi32.dll")] - public static extern NtStatus LsaFreeMemory( - IntPtr buffer - ); - - [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)] - [SuppressUnmanagedCodeSecurity] - public static extern NtStatus LsaEnumerateAccountsWithUserRight( - IntPtr PolicyHandle, - LSA_UNICODE_STRING[] UserRights, - out IntPtr EnumerationBuffer, - out int CountReturned - ); - - [Flags] - public enum LsaLookupOptions : uint - { - ReturnLocalNames = 0, - PreferInternetNames = 0x40000000, - DisallowsConnectedAccountInternetSid = 0x80000000 - } - - [Flags] - public enum LSAPolicyInformation - { - PolicyAuditLogInformation = 1, - PolicyAuditEventsInformation, - PolicyPrimaryDomainInformation, - PolicyPdAccountInformation, - PolicyAccountDomainInformation, - PolicyLsaServerRoleInformation, - PolicyReplicaSourceInformation, - PolicyDefaultQuotaInformation, - PolicyModificationInformation, - PolicyAuditFullSetInformation, - PolicyAuditFullQueryInformation, - PolicyDnsDomainInformation - } - - [Flags] - [SuppressMessage("ReSharper", "UnusedMember.Local")] - public enum LsaOpenMask - { - ViewLocalInfo = 0x1, - ViewAuditInfo = 0x2, - GetPrivateInfo = 0x4, - TrustAdmin = 0x8, - CreateAccount = 0x10, - CreateSecret = 0x20, - CreatePrivilege = 0x40, - SetDefaultQuotaLimits = 0x80, - SetAuditRequirements = 0x100, - AuditLogAdmin = 0x200, - ServerAdmin = 0x400, - LookupNames = 0x800, - Notification = 0x1000, - POLICY_READ = 0x20006, - POLICY_ALL_ACCESS = 0x00F0FFF, - POLICY_EXECUTE = 0X20801, - POLICY_WRITE = 0X207F8 - } - - [StructLayout(LayoutKind.Sequential)] - public struct LSA_UNICODE_STRING : IDisposable - { - private readonly ushort Length; - private readonly ushort MaximumLength; - private IntPtr Buffer; - - public LSA_UNICODE_STRING(string s) - : this() - { - if (s == null) return; - Length = (ushort) (s.Length * 2); - MaximumLength = (ushort) (Length + 2); - Buffer = Marshal.StringToHGlobalUni(s); - } - - public void Dispose() - { - if (Buffer == IntPtr.Zero) return; - Marshal.FreeHGlobal(Buffer); - Buffer = IntPtr.Zero; - } - - public override string ToString() - { - return (Buffer != IntPtr.Zero ? Marshal.PtrToStringUni(Buffer, Length / 2) : null) ?? - throw new InvalidOperationException(); - } - } - - [StructLayout(LayoutKind.Sequential)] - public struct LSA_OBJECT_ATTRIBUTES - { - public int Length; - public IntPtr RootDirectory; - public IntPtr ObjectName; - public int Attributes; - public IntPtr SecurityDescriptor; - public IntPtr SecurityQualityOfService; - - public void Dispose() - { - if (ObjectName == IntPtr.Zero) return; - Marshal.DestroyStructure(ObjectName, typeof(LSA_UNICODE_STRING)); - Marshal.FreeHGlobal(ObjectName); - ObjectName = IntPtr.Zero; - } - } - - [StructLayout(LayoutKind.Sequential)] - public struct LSATranslatedNames - { - public SidNameUse Use; - public LSA_UNICODE_STRING Name; - public int DomainIndex; - } - - public class SidPointer - { - private readonly byte[] data; - - public SidPointer(SecurityIdentifier identifier) - { - data = new byte[identifier.BinaryLength]; - identifier.GetBinaryForm(data, 0); - } - } - - #endregion } } \ No newline at end of file diff --git a/src/CommonLib/OutputTypes/LocalGroupAPIResult.cs b/src/CommonLib/OutputTypes/LocalGroupAPIResult.cs index ceb0da01..e80dea1a 100644 --- a/src/CommonLib/OutputTypes/LocalGroupAPIResult.cs +++ b/src/CommonLib/OutputTypes/LocalGroupAPIResult.cs @@ -5,7 +5,6 @@ namespace SharpHoundCommonLib.OutputTypes { public class LocalGroupAPIResult : APIResult { - public int GroupRID { get; set; } public string ObjectIdentifier { get; set; } public string Name { get; set; } public TypedPrincipal[] Results { get; set; } = Array.Empty(); diff --git a/src/CommonLib/OutputTypes/MetaTag.cs b/src/CommonLib/OutputTypes/MetaTag.cs index e340f8c8..c11a4653 100644 --- a/src/CommonLib/OutputTypes/MetaTag.cs +++ b/src/CommonLib/OutputTypes/MetaTag.cs @@ -2,6 +2,7 @@ namespace SharpHoundCommonLib.OutputTypes { + [DataContract] public class MetaTag { [DataMember(Name = "methods")] public long CollectionMethods { get; set; } diff --git a/src/CommonLib/Processors/CachedLocalItem.cs b/src/CommonLib/Processors/CachedLocalItem.cs new file mode 100644 index 00000000..7cc91500 --- /dev/null +++ b/src/CommonLib/Processors/CachedLocalItem.cs @@ -0,0 +1,16 @@ +using SharpHoundCommonLib.Enums; + +namespace SharpHoundCommonLib.Processors +{ + internal class CachedLocalItem + { + public CachedLocalItem(string name, Label type) + { + Name = name; + Type = type; + } + + public string Name { get; set; } + public Label Type { get; set; } + } +} \ No newline at end of file diff --git a/src/CommonLib/Processors/ComputerSessionProcessor.cs b/src/CommonLib/Processors/ComputerSessionProcessor.cs index 13b34a4b..952558a7 100644 --- a/src/CommonLib/Processors/ComputerSessionProcessor.cs +++ b/src/CommonLib/Processors/ComputerSessionProcessor.cs @@ -12,6 +12,8 @@ namespace SharpHoundCommonLib.Processors { public class ComputerSessionProcessor { + public delegate void ComputerStatusDelegate(CSVComputerStatus status); + private static readonly Regex SidRegex = new(@"S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$", RegexOptions.Compiled); private readonly string _currentUserName; private readonly ILogger _log; @@ -27,6 +29,8 @@ public ComputerSessionProcessor(ILDAPUtils utils, string currentUserName = null, _log = log ?? Logging.LogProvider.CreateLogger("CompSessions"); } + public event ComputerStatusDelegate ComputerStatusEvent; + /// /// Uses the NetSessionEnum Win32 API call to get network sessions from a remote computer. /// These are usually from SMB share accesses or other network sessions of the sort @@ -39,27 +43,23 @@ public async Task ReadUserSessions(string computerName, string string computerDomain) { var ret = new SessionAPIResult(); - NativeMethods.SESSION_INFO_10[] apiResult; - try + var result = _nativeMethods.NetSessionEnum(computerName); + if (result.IsFailed) { - apiResult = _nativeMethods.CallNetSessionEnum(computerName).ToArray(); - } - catch (APIException e) - { - _log.LogDebug("NetSessionEnum failed on {ComputerName}: {Status}", computerName, e.Status); + _log.LogDebug("NetSessionEnum failed on {ComputerName}: {Status}", computerName, result.Status); ret.Collected = false; - ret.FailureReason = e.Status; + ret.FailureReason = result.Status.ToString(); return ret; } ret.Collected = true; var results = new List(); - foreach (var sesInfo in apiResult) + foreach (var sesInfo in result.Value) { - var username = sesInfo.sesi10_username; - var computerSessionName = sesInfo.sesi10_cname; + var username = sesInfo.Username; + var computerSessionName = sesInfo.ComputerName; _log.LogTrace("NetSessionEnum Entry: {Username}@{ComputerSessionName} from {ComputerName}", username, computerSessionName, computerName); @@ -135,27 +135,23 @@ public SessionAPIResult ReadUserSessionsPrivileged(string computerName, string computerSamAccountName, string computerSid) { var ret = new SessionAPIResult(); - NativeMethods.WKSTA_USER_INFO_1[] apiResult; + var result = _nativeMethods.NetWkstaUserEnum(computerName); - try + if (result.IsFailed) { - apiResult = _nativeMethods.CallNetWkstaUserEnum(computerName).ToArray(); - } - catch (APIException e) - { - _log.LogTrace("NetWkstaUserEnum failed on {ComputerName}: {Status}", computerName, e.Status); + _log.LogTrace("NetWkstaUserEnum failed on {ComputerName}: {Status}", computerName, result.Status); ret.Collected = false; - ret.FailureReason = e.Status; + ret.FailureReason = result.Status.ToString(); return ret; } ret.Collected = true; var results = new List(); - foreach (var wkstaUserInfo in apiResult) + foreach (var wkstaUserInfo in result.Value) { - var domain = wkstaUserInfo.wkui1_logon_domain; - var username = wkstaUserInfo.wkui1_username; + var domain = wkstaUserInfo.LogonDomain; + var username = wkstaUserInfo.Username; _log.LogTrace("NetWkstaUserEnum entry: {Username}@{Domain} from {ComputerName}", username, domain, computerName); @@ -236,5 +232,10 @@ public SessionAPIResult ReadUserSessionsRegistry(string computerName, string com key?.Dispose(); } } + + private void SendComputerStatus(CSVComputerStatus status) + { + ComputerStatusEvent?.Invoke(status); + } } } \ No newline at end of file diff --git a/src/CommonLib/Processors/GPOLocalGroupProcessor.cs b/src/CommonLib/Processors/GPOLocalGroupProcessor.cs index c51dc3c8..e133eaaa 100644 --- a/src/CommonLib/Processors/GPOLocalGroupProcessor.cs +++ b/src/CommonLib/Processors/GPOLocalGroupProcessor.cs @@ -604,5 +604,14 @@ internal enum GroupActionTarget RestrictedMember, LocalGroup } + + internal enum LocalGroupRids + { + None = 0, + Administrators = 544, + RemoteDesktopUsers = 555, + DcomUsers = 562, + PSRemote = 580 + } } } \ No newline at end of file diff --git a/src/CommonLib/Processors/LDAPPropertyProcessor.cs b/src/CommonLib/Processors/LDAPPropertyProcessor.cs index 4fc0ab93..919551c8 100644 --- a/src/CommonLib/Processors/LDAPPropertyProcessor.cs +++ b/src/CommonLib/Processors/LDAPPropertyProcessor.cs @@ -22,7 +22,7 @@ public class LDAPPropertyProcessor "msds-behavior-version", "objectguid", "name", "gpoptions", "msds-allowedtodelegateto", "msDS-allowedtoactonbehalfofotheridentity", "displayname", "sidhistory", "samaccountname", "samaccounttype", "objectsid", "objectguid", "objectclass", - "samaccountname", "msds-groupmsamembership", + "msds-groupmsamembership", "distinguishedname", "memberof", "logonhours", "ntsecuritydescriptor", "dsasignature", "repluptodatevector", "member", "whenCreated" }; @@ -225,6 +225,7 @@ public async Task ReadUserProperties(ISearchResultEntry entry) props.Add("unixpassword", entry.GetProperty(LDAPProperties.UnixUserPassword)); props.Add("unicodepassword", entry.GetProperty(LDAPProperties.UnicodePassword)); props.Add("sfupassword", entry.GetProperty(LDAPProperties.MsSFU30Password)); + props.Add("logonscript", entry.GetProperty(LDAPProperties.ScriptPath)); var ac = entry.GetProperty(LDAPProperties.AdminCount); if (ac != null) @@ -387,7 +388,7 @@ public async Task ReadComputerProperties(ISearchResultEntry /// format using a best guess /// /// - private static Dictionary ParseAllProperties(ISearchResultEntry entry) + public Dictionary ParseAllProperties(ISearchResultEntry entry) { var flag = IsTextUnicodeFlags.IS_TEXT_UNICODE_STATISTICS; var props = new Dictionary(); diff --git a/src/CommonLib/Processors/LSAServer.cs b/src/CommonLib/Processors/LSAServer.cs deleted file mode 100644 index f55b2007..00000000 --- a/src/CommonLib/Processors/LSAServer.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Security.Principal; -using Microsoft.Extensions.Logging; -using SharpHoundCommonLib.Enums; -using SharpHoundCommonLib.OutputTypes; - -namespace SharpHoundCommonLib.Processors -{ - public class LSAServer : IDisposable - { - private readonly string _computerName; - private readonly ILogger _log; - private readonly NativeMethods _nativeMethods; - private readonly NativeMethods.LSA_OBJECT_ATTRIBUTES _obj; - - private readonly IntPtr _policyHandle; - private readonly ILDAPUtils _utils; - - /// - /// Creates an instance of an RPCServer which is used for making SharpHound specific LSA API calls for computers - /// - /// - /// - /// - /// - public LSAServer(string computerName, ILDAPUtils utils = null, NativeMethods methods = null, ILogger log = null) - { - _computerName = computerName; - _nativeMethods = methods ?? new NativeMethods(); - _utils = utils ?? new LDAPUtils(); - _log = log ?? Logging.LogProvider.CreateLogger("SAMRPCServer"); - _log.LogTrace($"Opening LSA server for {computerName}"); - - var us = new NativeMethods.LSA_UNICODE_STRING(computerName); - var status = _nativeMethods.CallLSAOpenPolicy(ref us, ref _obj, - NativeMethods.LsaOpenMask.LookupNames | NativeMethods.LsaOpenMask.ViewLocalInfo, - out var policyHandle); - _log.LogTrace($"LSAOpenPolicy returned {status} for {computerName}"); - if (status != NativeMethods.NtStatus.StatusSuccess) - { - _nativeMethods.CallLSAClose(policyHandle); - - throw new APIException - { - Status = status.ToString(), - APICall = "LSAOpenPolicy" - }; - } - - _policyHandle = policyHandle; - } - - public void Dispose() - { - if (_policyHandle != IntPtr.Zero) _nativeMethods.CallLSAClose(_policyHandle); - - _obj.Dispose(); - } - - /// - /// Reads principals granted the specified privilege. Refer to for a list of relevant - /// privileges. - /// - /// - /// - public IEnumerable ReadLSAPrivilege(string privilege) - { - var result = new LSAPrivilegeAPIResult(); - var status = - _nativeMethods.CallLSAEnumerateAccountsWithUserRight(_policyHandle, privilege, out var accounts, - out var count); - - _log.LogTrace($"LSAEnumerateAccountsWithUserRight returned {status} for {privilege} on {_computerName}"); - if (status != NativeMethods.NtStatus.StatusSuccess) - { - _nativeMethods.CallLSAClose(accounts); - result.FailureReason = $"LSAEnumerateAccountsWithUserRight returned {status}"; - yield break; - } - - _log.LogTrace( - $"LSAEnumerateAccountsWithUserRight returned {count} objects for privilege {privilege} on {_computerName}"); - - if (count == 0) - { - _nativeMethods.CallLSAClose(accounts); - result.Collected = true; - yield break; - } - - var sids = new List(); - for (var i = 0; i < count; i++) - try - { - var raw = Marshal.ReadIntPtr(accounts, Marshal.SizeOf() * i); - var sid = new SecurityIdentifier(raw).Value; - sids.Add(sid); - } - catch (Exception e) - { - _log.LogTrace($"Exception converting sid: {e}"); - } - - _nativeMethods.CallLSAClose(accounts); - - foreach (var sid in sids) yield return sid; - } - } -} \ No newline at end of file diff --git a/src/CommonLib/Processors/LocalGroupProcessor.cs b/src/CommonLib/Processors/LocalGroupProcessor.cs new file mode 100644 index 00000000..be0ec9f5 --- /dev/null +++ b/src/CommonLib/Processors/LocalGroupProcessor.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.OutputTypes; +using SharpHoundRPC.Shared; +using SharpHoundRPC.Wrappers; + +namespace SharpHoundCommonLib.Processors +{ + public class LocalGroupProcessor + { + public delegate void ComputerStatusDelegate(CSVComputerStatus status); + + private readonly string[] _filteredSids = + { + "S-1-5-2", "S-1-5-2", "S-1-5-3", "S-1-5-4", "S-1-5-6", "S-1-5-7", "S-1-2", "S-1-2-0", "S-1-5-18", + "S-1-5-19", "S-1-5-20" + }; + + private readonly ILogger _log; + private readonly ILDAPUtils _utils; + + public LocalGroupProcessor(ILDAPUtils utils, ILogger log = null) + { + _utils = utils; + _log = log ?? Logging.LogProvider.CreateLogger("LocalGroupProcessor"); + } + + public event ComputerStatusDelegate ComputerStatusEvent; + + public IEnumerable GetLocalGroups(string computerName, string computerDomainSid, + string computerDomain) + { + var openServerResult = SAMServer.OpenServer(computerName); + if (openServerResult.IsFailed) + { + SendComputerStatus(new CSVComputerStatus + { + Task = "SamConnect", + ComputerName = computerName, + Status = openServerResult.Status.ToString() + }); + yield break; + } + + var server = openServerResult.Value; + var typeCache = new ConcurrentDictionary(); + var computerSid = new SecurityIdentifier(computerDomainSid); + + if (!Cache.GetMachineSid(computerDomainSid, out var machineSid)) + { + var getMachineSidResult = server.GetMachineSid(); + if (getMachineSidResult.IsFailed) + { + machineSid = "UNKNOWN"; + } + else + { + machineSid = getMachineSidResult.Value.Value; + Cache.AddMachineSid(computerDomainSid, machineSid); + } + } + + + var getDomainsResult = server.GetDomains(); + if (getDomainsResult.IsFailed) + { + SendComputerStatus(new CSVComputerStatus + { + Task = "GetDomains", + ComputerName = computerName, + Status = getDomainsResult.Status.ToString() + }); + yield break; + } + + var isDc = server.IsDomainController(computerSid); + + foreach (var domainResult in getDomainsResult.Value) + { + var openDomainResult = server.OpenDomain(domainResult.Name); + if (openDomainResult.IsFailed) + { + SendComputerStatus(new CSVComputerStatus + { + Task = $"OpenDomain - {domainResult.Name}", + ComputerName = computerName, + Status = openDomainResult.Status.ToString() + }); + continue; + } + + var domain = openDomainResult.Value; + + var getAliasesResult = domain.GetAliases(); + + if (getAliasesResult.IsFailed) + { + SendComputerStatus(new CSVComputerStatus + { + Task = $"GetAliases - {domainResult.Name}", + ComputerName = computerName, + Status = getAliasesResult.Status.ToString() + }); + continue; + } + + foreach (var alias in getAliasesResult.Value) + { + var resolvedName = ResolveGroupName(alias.Name, computerName, machineSid, computerDomain, alias.Rid, isDc, + domainResult.Name.Equals("builtin", StringComparison.OrdinalIgnoreCase)); + var ret = new LocalGroupAPIResult + { + Name = resolvedName.PrincipalName, + ObjectIdentifier = resolvedName.ObjectId + }; + var openAliasResult = domain.OpenAlias(alias.Rid); + if (openAliasResult.IsFailed) + { + SendComputerStatus(new CSVComputerStatus + { + Task = $"OpenAlias - {alias.Name}", + ComputerName = computerName, + Status = openAliasResult.Status.ToString() + }); + ret.Collected = false; + ret.FailureReason = $"SamOpenAliasInDomain failed with status {openAliasResult.Status}"; + yield return ret; + } + + var results = new List(); + var names = new List(); + + var localGroup = openAliasResult.Value; + var getMembersResult = localGroup.GetMembers(); + if (getMembersResult.IsFailed) + { + SendComputerStatus(new CSVComputerStatus + { + Task = $"GetMembersInAlias - {alias.Name}", + ComputerName = computerName, + Status = getMembersResult.Status.ToString() + }); + ret.Collected = false; + ret.FailureReason = $"SamGetMembersInAlias failed with status {getMembersResult.Status}"; + yield return ret; + } + + foreach (var securityIdentifier in getMembersResult.Value) + { + if (IsSidFiltered(securityIdentifier)) + continue; + + var sidValue = securityIdentifier.Value; + + if (server.IsDomainController(computerSid)) + { + if (_utils.GetWellKnownPrincipal(sidValue, computerDomain, out var wellKnown)) + results.Add(wellKnown); + else + results.Add(_utils.ResolveIDAndType(sidValue, computerDomain)); + } + else + { + if (WellKnownPrincipal.GetWellKnownPrincipal(sidValue, out var wellKnown)) + { + if (machineSid == "UNKNOWN") + continue; + wellKnown.ObjectIdentifier = $"{machineSid}-{securityIdentifier.Rid()}"; + wellKnown.ObjectType = wellKnown.ObjectType switch + { + Label.User => Label.LocalUser, + Label.Group => Label.LocalGroup, + _ => wellKnown.ObjectType + }; + results.Add(wellKnown); + } + else + { + if (securityIdentifier.IsEqualDomainSid(computerSid)) + { + results.Add(_utils.ResolveIDAndType(sidValue, computerDomain)); + } + else + { + if (typeCache.TryGetValue(sidValue, out var item)) + { + results.Add(new TypedPrincipal + { + ObjectIdentifier = sidValue, + ObjectType = item.Type + }); + + names.Add(new NamedPrincipal + { + ObjectId = sidValue, + PrincipalName = item.Name + }); + } + else + { + var lookupUserResult = server.LookupPrincipalBySid(securityIdentifier); + if (lookupUserResult.IsFailed) + { + _log.LogTrace("Unable to resolve local sid {SID}", sidValue); + continue; + } + + var (name, use) = lookupUserResult.Value; + var objectType = use switch + { + SharedEnums.SidNameUse.User => Label.LocalUser, + SharedEnums.SidNameUse.Group => Label.LocalGroup, + SharedEnums.SidNameUse.Alias => Label.LocalGroup, + _ => Label.Base + }; + + typeCache.TryAdd(sidValue, new CachedLocalItem(name, objectType)); + + results.Add(new TypedPrincipal + { + ObjectIdentifier = sidValue, + ObjectType = objectType + }); + + names.Add(new NamedPrincipal + { + PrincipalName = name, + ObjectId = sidValue + }); + } + } + } + } + } + + ret.LocalNames = names.ToArray(); + ret.Results = results.ToArray(); + yield return ret; + } + } + } + + private NamedPrincipal ResolveGroupName(string baseName, string computerName, string machineSid, string domainName, int groupRid, bool isDc, bool isBuiltIn) + { + if (isDc) + { + if (isBuiltIn) + { + _utils.GetWellKnownPrincipal($"S-1-5-32-{groupRid}".ToUpper(), domainName, out var principal); + return new NamedPrincipal + { + ObjectId = principal.ObjectIdentifier, + PrincipalName = "IGNOREME" + }; + } + + return new NamedPrincipal + { + ObjectId = $"{machineSid}-{groupRid}".ToUpper(), + PrincipalName = "IGNOREME" + }; + } + + return new NamedPrincipal + { + ObjectId = $"{machineSid}-{groupRid}", + PrincipalName = $"{baseName}@{computerName}".ToUpper() + }; + } + + private bool IsSidFiltered(SecurityIdentifier identifier) + { + var value = identifier.Value; + + if (value.StartsWith("S-1-5-80") || value.StartsWith("S-1-5-82") || + value.StartsWith("S-1-5-90") || value.StartsWith("S-1-5-96")) + return true; + + if (_filteredSids.Contains(value)) + return true; + + return false; + } + + private void SendComputerStatus(CSVComputerStatus status) + { + ComputerStatusEvent?.Invoke(status); + } + } +} \ No newline at end of file diff --git a/src/CommonLib/Processors/RPCServer.cs b/src/CommonLib/Processors/RPCServer.cs deleted file mode 100644 index 89c600ea..00000000 --- a/src/CommonLib/Processors/RPCServer.cs +++ /dev/null @@ -1,1122 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Security.Principal; -using System.Threading; -using Microsoft.Extensions.Logging; -using SharpHoundCommonLib.Enums; -using SharpHoundCommonLib.OutputTypes; - -namespace SharpHoundCommonLib.Processors -{ - public class RPCServer : IDisposable - { - private const string DummyMachineSid = "DUMMYSTRING"; - - private static readonly Lazy WellKnownSidBytes = new(() => - { - var sid = new SecurityIdentifier("S-1-5-32"); - var sidBytes = new byte[sid.BinaryLength]; - sid.GetBinaryForm(sidBytes, 0); - return sidBytes; - }, LazyThreadSafetyMode.PublicationOnly); - - private readonly string _computerDomain; - private readonly string _computerDomainSid; - - private readonly string _computerName; - private readonly string _computerSAMAccountName; - private readonly SecurityIdentifier _computerSID; - - private readonly DomainHandleManager _domainHandleManager; - - private readonly string[] _filteredSids = - { - "S-1-5-2", "S-1-5-2", "S-1-5-3", "S-1-5-4", "S-1-5-6", "S-1-5-7", "S-1-2", "S-1-2-0", "S-1-5-18", - "S-1-5-19", "S-1-5-20" - }; - - private readonly ILogger _log; - - private readonly NativeMethods _nativeMethods; - private readonly ConcurrentDictionary _typeCache = new(); - private readonly ILDAPUtils _utils; - private string _cachedMachineSid; - private NativeMethods.LSA_OBJECT_ATTRIBUTES _lsaObj; - private IntPtr _lsaPolicyHandle; - - private bool _lsaServerOpen; - private NativeMethods.OBJECT_ATTRIBUTES _obj; - private IntPtr _samServerHandle; - private bool _samServerOpen; - - /// - /// Creates an instance of an RPCServer which is used for making SharpHound specific SAMRPC/LSA API calls for - /// computers. - /// OpenSAMServer should be called before any other operations - /// - /// The name of the computer to connect too. This should be the network name of the computer - /// The samaccountname of the computer - /// The security identifier for the computer - /// The domain of the computer - /// LDAPUtils instance - /// NativeMethods instance - /// ILogger instance - public RPCServer(string computerName, string samAccountName, string computerSid, string computerDomain, - ILDAPUtils utils = null, - NativeMethods methods = null, ILogger log = null) - { - //Remove the trailing '$' from the SAMAccountName - _computerSAMAccountName = samAccountName.Remove(samAccountName.Length - 1, 1); - _computerSID = new SecurityIdentifier(computerSid); - _computerDomainSid = _computerSID.AccountDomainSid.Value; - _computerName = computerName; - _computerDomain = computerDomain; - _utils = utils; - _nativeMethods = methods ?? new NativeMethods(); - _utils = utils ?? new LDAPUtils(); - _domainHandleManager = new DomainHandleManager(_nativeMethods); - _log = log ?? Logging.LogProvider.CreateLogger("RPCServer"); - } - - public void Dispose() - { - if (_samServerHandle != IntPtr.Zero) - { - _nativeMethods.CallSamCloseHandle(_samServerHandle); - _samServerHandle = IntPtr.Zero; - } - - if (_lsaPolicyHandle != IntPtr.Zero) - { - _nativeMethods.CallLSAClose(_lsaPolicyHandle); - _lsaPolicyHandle = IntPtr.Zero; - } - - _domainHandleManager.Dispose(); - - _obj.Dispose(); - _lsaObj.Dispose(); - } - - /// - /// Opens the SAMRPC server for further use. This needs to be called before any other operations. - /// Refer to https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/87bacbd0-7b8b-429f-abc6-4b3d895d4e90 - /// for access masks - /// - /// - /// - /// - /// An exception indicates that we failed to open a connection, as well as the reason - /// - public void OpenSAMServer( - NativeMethods.SamAccessMasks requestedConnectAccess = NativeMethods.SamAccessMasks.SamServerConnect | - NativeMethods.SamAccessMasks - .SamServerEnumerateDomains | - NativeMethods.SamAccessMasks.SamServerLookupDomain) - { - _log.LogTrace("Opening SAM Server for {ComputerName}", _computerName); - - var us = new NativeMethods.UNICODE_STRING(_computerName); - //Every API call we make relies on both SamConnect - //Make this call immediately and save the handle. If either fails, nothing else is going to work - var status = _nativeMethods.CallSamConnect(ref us, out _samServerHandle, - requestedConnectAccess, - ref _obj); - _log.LogTrace("SamConnect returned {Status} for {ComputerName}", status, _computerName); - if (status != NativeMethods.NtStatus.StatusSuccess) - { - _nativeMethods.CallSamCloseHandle(_samServerHandle); - throw new APIException - { - Status = status.ToString(), - APICall = "SamConnect" - }; - } - - _samServerOpen = true; - } - - /// - /// Opens the LSA policy for further use. - /// - /// - /// - /// An exception indicates a failure to open the LSA policy - /// - public void OpenLSAServer( - NativeMethods.LsaOpenMask desiredAccess = - NativeMethods.LsaOpenMask.LookupNames | NativeMethods.LsaOpenMask.ViewLocalInfo) - { - var us = new NativeMethods.LSA_UNICODE_STRING(_computerName); - var status = _nativeMethods.CallLSAOpenPolicy(ref us, ref _lsaObj, desiredAccess, - out _lsaPolicyHandle); - _log.LogTrace($"LSAOpenPolicy returned {status} for {_computerName}"); - if (status != NativeMethods.NtStatus.StatusSuccess) - throw new APIException - { - Status = status.ToString(), - APICall = "LSAOpenPolicy" - }; - - _lsaServerOpen = true; - } - - /// - /// Gets principals with specified User Rights. If no rights are provided, defaults to the list in - /// LSAPrivileges.DesiredPrivileges - /// - /// - /// - public IEnumerable GetUserRightsAssignments(string[] desiredPrivileges = null) - { - if (!_lsaServerOpen) OpenLSAServer(); - - desiredPrivileges ??= LSAPrivileges.DesiredPrivileges; - - foreach (var privilege in desiredPrivileges) - { - var result = new UserRightsAssignmentAPIResult - { - Collected = false, - Privilege = privilege - }; - - var status = _nativeMethods.CallLSAEnumerateAccountsWithUserRight(_lsaPolicyHandle, privilege, - out var buffer, out var count); - - if (status != NativeMethods.NtStatus.StatusSuccess) - { - if (buffer != IntPtr.Zero) _nativeMethods.CallLSAFreeMemory(buffer); - - result.FailureReason = $"LSAEnumerateAccountsWithUserRight returned {status}"; - yield return result; - } - - result.Collected = true; - - var sids = new List(); - for (var i = 0; i < count; i++) - try - { - var raw = Marshal.ReadIntPtr(buffer, Marshal.SizeOf() * i); - var sid = new SecurityIdentifier(raw); - _log.LogTrace("Got sid {sid} for {ura}", sid.Value, privilege); - sids.Add(sid); - } - catch (Exception e) - { - _log.LogTrace(e, "Exception converting sid"); - } - - GetMachineSidLSA(out var machineSid); - - var resolved = new List(); - var toResolve = new List(); - var isDc = IsDomainController(); - foreach (var sid in sids) - { - var value = sid.Value; - - if (value.StartsWith("S-1-5-80") || value.StartsWith("S-1-5-82") || - value.StartsWith("S-1-5-90") || value.StartsWith("S-1-5-96")) continue; - - if (_filteredSids.Contains(value)) continue; - - if (isDc) - { - if (_utils.GetWellKnownPrincipal(sid.Value, _computerDomain, out var principal)) - { - _log.LogInformation("Found WKP {sid} on DC", sid.Value); - resolved.Add(principal); - } - else - { - var res = _utils.ResolveIDAndType(sid.Value, _computerDomain); - _log.LogInformation("Resolved {sid} on DC", res.ObjectIdentifier); - resolved.Add(res); - } - } - else - { - if (WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common)) - { - common.ObjectIdentifier = $"{machineSid}-{sid.Rid()}"; - if (common.ObjectType == Label.User) - common.ObjectType = Label.LocalUser; - else if (common.ObjectType == Label.Group) common.ObjectType = Label.LocalGroup; - resolved.Add(common); - } - else - { - toResolve.Add(sid); - } - } - } - - if (toResolve.Count == 0) - { - result.Results = resolved.ToArray(); - yield return result; - } - - var resolvedNames = new List(); - status = _nativeMethods.CallLSALookupSids(_lsaPolicyHandle, toResolve.ToArray(), out var domains, - out var names); - - if (status != NativeMethods.NtStatus.StatusSuccess && status != NativeMethods.NtStatus.StatusSomeMapped) - { - _log.LogError("LSALookupSids returned {status} for {computer}, unable to resolve local sids", - status, _computerName); - resolved.AddRange(toResolve.Select(x => new TypedPrincipal(x.Value, Label.Base))); - result.Results = resolved.ToArray(); - yield return result; - } - - for (var i = 0; i < toResolve.Count; i++) - { - var translated = Marshal.PtrToStructure(names + - Marshal.SizeOf() * i); - - Label objectType; - switch (translated.Use) - { - case NativeMethods.SidNameUse.User: - objectType = Label.LocalUser; - break; - case NativeMethods.SidNameUse.Group: - objectType = Label.LocalGroup; - break; - case NativeMethods.SidNameUse.Alias: - objectType = Label.LocalGroup; - break; - case NativeMethods.SidNameUse.Domain: - case NativeMethods.SidNameUse.WellKnownGroup: - case NativeMethods.SidNameUse.DeletedAccount: - case NativeMethods.SidNameUse.Invalid: - case NativeMethods.SidNameUse.Unknown: - case NativeMethods.SidNameUse.Computer: - case NativeMethods.SidNameUse.Label: - case NativeMethods.SidNameUse.LogonSession: - default: - objectType = Label.Base; - break; - } - - resolved.Add(new TypedPrincipal(toResolve[i].Value, objectType)); - try - { - resolvedNames.Add(new NamedPrincipal(translated.Name.ToString(), toResolve[i].Value)); - } - catch - { - } - } - - _nativeMethods.CallLSAFreeMemory(names); - _nativeMethods.CallLSAFreeMemory(domains); - - result.Results = resolved.ToArray(); - result.LocalNames = resolvedNames.ToArray(); - - yield return result; - } - } - - private bool IsMachineAccount(SecurityIdentifier sid) - { - var stringSid = sid.Value; - return IsMachineAccount(stringSid); - } - - private bool IsDomainController() - { - string machineSid; - if (_lsaServerOpen) - { - GetMachineSidLSA(out machineSid); - } - else if (_samServerOpen) - { - GetMachineSid(out machineSid); - } - else - { - OpenSAMServer(); - GetMachineSid(out machineSid); - } - - return machineSid == _computerDomainSid; - } - - private bool IsMachineAccount(string sid) - { - if (sid.StartsWith(_computerDomainSid)) - return false; - - string machineSid; - - if (_lsaServerOpen) - { - GetMachineSidLSA(out machineSid); - } - else if (_samServerOpen) - { - GetMachineSid(out machineSid); - } - else - { - OpenSAMServer(); - GetMachineSid(out machineSid); - } - - if (machineSid.StartsWith(_computerDomainSid)) - return false; - - return true; - } - - /// - /// Gets all local groups and their members - /// - /// - public IEnumerable GetGroupsAndMembers() - { - //First open the SAM server if it hasn't already been - if (!_samServerOpen) OpenSAMServer(); - - var groupCache = new ConcurrentBag(); - - GetMachineSid(out var machineSid); - - foreach (var domainName in ListDomainsInServer()) - if (OpenDomainHandle(domainName, out var domainHandle)) - foreach (var group in GetGroupsFromDomain(domainHandle, - domainName.Equals("builtin", StringComparison.OrdinalIgnoreCase))) - { - _typeCache.TryAdd(group.ObjectID, new CachedLocalItem(group.Name, Label.LocalGroup)); - var result = GetLocalGroupMembers(domainHandle, group); - groupCache.Add(result); - } - - _log.LogTrace("Beginning resolution of local types"); - // We've got all our local groups, now we need to resolve unresolved types - foreach (var group in groupCache) - { - if (!group.Collected) - continue; - - var names = new List(); - var resolvedPrincipals = new List(); - - foreach (var result in group.Results) - { - if (result.ObjectType != Label.Base || !result.ObjectIdentifier.StartsWith(machineSid) || - result.ObjectIdentifier.StartsWith(_computerSID.AccountDomainSid.Value)) - { - resolvedPrincipals.Add(result); - continue; - } - - var sid = new SecurityIdentifier(result.ObjectIdentifier); - _log.LogTrace("Resolving {Sid} in local", sid.Value); - if (!ResolveLocalSid(sid, out var name, out var type)) - { - resolvedPrincipals.Add(result); - continue; - } - - names.Add(new NamedPrincipal(name, result.ObjectIdentifier)); - resolvedPrincipals.Add(new TypedPrincipal(result.ObjectIdentifier, type)); - } - - group.Results = resolvedPrincipals.ToArray(); - group.LocalNames = names.ToArray(); - - yield return group; - } - } - - /// - /// Opens a domain handle in the SAM server - /// - /// - /// - /// - /// - public bool OpenDomainHandle(string domainName, out IntPtr domainHandle, - NativeMethods.DomainAccessMask requestedDomainAccess = NativeMethods.DomainAccessMask.Lookup | - NativeMethods.DomainAccessMask.ListAccounts) - { - if (_domainHandleManager.GetHandleByName(domainName, out domainHandle)) - return true; - - domainHandle = IntPtr.Zero; - if (!LookupDomainSid(domainName, out var sid)) return false; - - var bytes = new byte[sid.BinaryLength]; - sid.GetBinaryForm(bytes, 0); - var status = - _nativeMethods.CallSamOpenDomain(_samServerHandle, requestedDomainAccess, bytes, out domainHandle); - if (status != NativeMethods.NtStatus.StatusSuccess) - { - if (domainHandle != IntPtr.Zero) - _nativeMethods.CallSamFreeMemory(domainHandle); - return false; - } - - _log.LogTrace("Adding domain to manager: {domain}, {sid}", domainName, sid.ToString()); - _domainHandleManager.AddMappedDomain(sid, domainName, domainHandle); - - return true; - } - - /// - /// Opens the built in domain - /// - /// - /// - /// - public bool OpenBuiltInDomain(out IntPtr domainHandle, - NativeMethods.DomainAccessMask requestedDomainAccess = NativeMethods.DomainAccessMask.Lookup | - NativeMethods.DomainAccessMask.ListAccounts) - { - if (_domainHandleManager.GetHandleByName("BUILTIN", out domainHandle)) - return true; - - var status = - _nativeMethods.CallSamOpenDomain(_samServerHandle, requestedDomainAccess, WellKnownSidBytes.Value, - out domainHandle); - if (status != NativeMethods.NtStatus.StatusSuccess) - { - if (domainHandle != IntPtr.Zero) - _nativeMethods.CallSamFreeMemory(domainHandle); - return false; - } - - _domainHandleManager.AddMappedDomain("S-1-5-32", "BUILTIN", domainHandle); - - return true; - } - - ~RPCServer() - { - Dispose(); - } - - /// - /// Gets the list of aliases in a domain - /// - /// - /// - /// - public IEnumerable GetGroupsFromDomain(IntPtr domainHandle, bool isBuiltIn) - { - if (!_samServerOpen) - { - _log.LogError("SAM Server is not open. Call OpenSamServer before calling GetLocalGroups"); - yield break; - } - - if (!GetMachineSid(out var machineSid)) - { - _log.LogError("Failed to get machine sid for {ComputerName}, cannot get local groups", _computerName); - yield break; - } - - var status = _nativeMethods.CallSamEnumerateAliasesInDomain(domainHandle, out var rids, out var count); - _log.LogTrace("SamEnumerateAliasesInDomain returned {Status} on {ComputerName}", status, - _computerName); - if (status != NativeMethods.NtStatus.StatusSuccess) - throw new APIException("SamEnumerateAliasesInDomain", status); - - if (count == 0) - yield break; - - for (var i = 0; i < count; i++) - { - var data = Marshal.PtrToStructure(rids + - i * NativeMethods.SamRidEnumeration.SizeOf); - _log.LogTrace("Got entry {Name} with RID {Rid}", data.Name, data.Rid); - LocalGroup group; - if (IsDomainController()) - { - if (isBuiltIn) - { - var sid = $"S-1-5-32-{data.Rid}"; - _utils.GetWellKnownPrincipal(sid, _computerDomain, out var principal); - group = new LocalGroup - { - ObjectID = principal.ObjectIdentifier, - Rid = data.Rid, - Name = "IGNOREME" - }; - } - else - { - group = new LocalGroup - { - Name = "IGNOREME", - Rid = data.Rid, - ObjectID = $"{machineSid}-{data.Rid}" - }; - } - } - else - { - group = new LocalGroup - { - Name = $"{data.Name.ToString()}@{_computerName}".ToUpper(), - Rid = data.Rid, - ObjectID = $"{machineSid}-{data.Rid}" - }; - } - - yield return group; - } - } - - /// - /// Lists the domains in the SAM Server - /// - /// - /// - public IEnumerable ListDomainsInServer() - { - if (!_samServerOpen) - { - _log.LogError("SAM Server is not open. Call OpenSamServer before calling ListDomainsInServer"); - yield break; - } - - var domains = IntPtr.Zero; - try - { - var status = - _nativeMethods.CallSamEnumerateDomainsInSamServer(_samServerHandle, out domains, out var count); - _log.LogTrace("SamEnumerateDomainsInSamServer returned {Status} on {ComputerName}", status, - _computerName); - if (status != NativeMethods.NtStatus.StatusSuccess || count == 0) - throw new APIException - { - Status = status.ToString(), - APICall = "SamEnumerateDomainsInSamServer" - }; - - for (var i = 0; i < count; i++) - { - var data = Marshal.PtrToStructure(domains + - i * NativeMethods.SamRidEnumeration.SizeOf); - yield return data.Name.ToString(); - } - } - finally - { - if (domains != IntPtr.Zero) - _nativeMethods.CallSamFreeMemory(domains); - } - } - - - /// - /// Reads the members in a specified local group from the open domain. The group is referenced by its RID (Relative - /// Identifier). - /// Groups current used by SharpHound can be found in LocalGroupRids - /// - /// - /// - /// - public LocalGroupAPIResult GetLocalGroupMembers(IntPtr domainHandle, LocalGroup group) - { - if (!_samServerOpen) - { - _log.LogError("SAM Server is not open. Call OpenSamServer before calling GetLocalGroupMembers"); - throw new ServerNotOpenException("SAM Server is not open"); - } - - var result = new LocalGroupAPIResult - { - GroupRID = group.Rid, - ObjectIdentifier = group.ObjectID, - Name = group.Name - }; - - var status = _nativeMethods.CallSamOpenAlias(domainHandle, NativeMethods.AliasOpenFlags.ListMembers, - group.Rid, out var aliasHandle); - _log.LogTrace("SamOpenAlias returned {Status} for RID {GroupRID} on {ComputerName}", status, group.Rid, - _computerName); - if (status != NativeMethods.NtStatus.StatusSuccess) - { - _nativeMethods.CallSamCloseHandle(aliasHandle); - result.FailureReason = $"SamOpenAlias returned {status.ToString()}"; - return result; - } - - status = _nativeMethods.CallSamGetMembersInAlias(aliasHandle, out var members, out var count); - _log.LogTrace("SamGetMembersInAlias returned {Status} for RID {GroupRID} on {ComputerName}", status, - group.Rid, _computerName); - _nativeMethods.CallSamCloseHandle(aliasHandle); - - if (status != NativeMethods.NtStatus.StatusSuccess) - { - _nativeMethods.CallSamFreeMemory(members); - result.FailureReason = $"SamGetMembersInAlias returned {status.ToString()}"; - return result; - } - - _log.LogTrace("SamGetMembersInAlias returned {Count} items for RID {GroupRID} on {ComputerName}", count, - group.Rid, _computerName); - - if (count == 0) - { - _nativeMethods.CallSamFreeMemory(members); - result.Collected = true; - return result; - } - - var sids = new List(); - for (var i = 0; i < count; i++) - try - { - var raw = Marshal.ReadIntPtr(members, Marshal.SizeOf() * i); - var sid = new SecurityIdentifier(raw); - var value = sid.Value; - - //Filter out sids we dont care about/explicitly ignore - if (value.StartsWith("S-1-5-80") || value.StartsWith("S-1-5-82") || - value.StartsWith("S-1-5-90") || value.StartsWith("S-1-5-96")) continue; - - if (_filteredSids.Contains(value)) continue; - - sids.Add(sid); - } - catch (Exception e) - { - _log.LogTrace(e, "Exception converting sid"); - } - - _nativeMethods.CallSamFreeMemory(members); - - GetMachineSid(out var machineSid); - - var isDc = IsDomainController(); - - var converted = sids.Select(x => - { - var sid = x.Value.ToUpper(); - if (isDc || !sid.StartsWith(machineSid) || sid.StartsWith(_computerSID.AccountDomainSid.Value)) - { - _log.LogTrace("Resolving {Sid} in domain", sid); - return _utils.ResolveIDAndType(sid, _computerDomain); - } - - return new TypedPrincipal - { - ObjectIdentifier = sid, - ObjectType = Label.Base - }; - }).Where(x => x != null); - - result.Collected = true; - result.Results = converted.ToArray(); - - return result; - } - - private bool ResolveLocalSid(SecurityIdentifier identifier, out string name, out Label objectType) - { - if (_typeCache.TryGetValue(identifier.Value, out var item)) - { - _log.LogTrace("ResolveLocalSid - Cache hit for {ID}", identifier.Value); - name = item.Name; - objectType = item.Type; - return true; - } - - var domainSid = identifier.AccountDomainSid.Value; - var rid = identifier.Rid(); - - _log.LogTrace("ResolveLocalSid - Starting resolution for {ID} in domain {Domain} with RID {rid}", - identifier.Value, domainSid, rid); - - name = null; - objectType = Label.Base; - - if (!_domainHandleManager.GetHandleBySid(domainSid, out var handle)) - { - _log.LogTrace("ResolveLocalSid - Failed to get handle for {SID}", domainSid); - return false; - } - - var ridArray = new int[1]; - ridArray[0] = rid; - var status = - _nativeMethods.CallSamLookupIdsInDomain(handle, ridArray, out var names, out var use); - try - { - if (status != NativeMethods.NtStatus.StatusSuccess) - { - _log.LogTrace("SamLookupIdsInDomain returned {Status} for {Rid} in domain {Domain}", status, rid, - domainSid); - name = null; - objectType = Label.Base; - return false; - } - - var convertedName = Marshal.PtrToStructure(names); - name = convertedName.ToString(); - - var snu = (NativeMethods.SidNameUse) Marshal.ReadInt32(use, 0); - - switch (snu) - { - case NativeMethods.SidNameUse.User: - objectType = Label.LocalUser; - break; - case NativeMethods.SidNameUse.Group: - objectType = Label.LocalGroup; - break; - case NativeMethods.SidNameUse.Alias: - objectType = Label.LocalGroup; - break; - case NativeMethods.SidNameUse.Domain: - case NativeMethods.SidNameUse.WellKnownGroup: - case NativeMethods.SidNameUse.DeletedAccount: - case NativeMethods.SidNameUse.Invalid: - case NativeMethods.SidNameUse.Unknown: - case NativeMethods.SidNameUse.Computer: - case NativeMethods.SidNameUse.Label: - case NativeMethods.SidNameUse.LogonSession: - default: - objectType = Label.Base; - break; - } - - return true; - } - finally - { - if (names != IntPtr.Zero) _nativeMethods.CallSamFreeMemory(names); - - if (use != IntPtr.Zero) _nativeMethods.CallSamFreeMemory(use); - } - } - - /// - /// Gets the machine SID using LSA calls - /// - /// - /// - public bool GetMachineSidLSA(out string machineSid) - { - if (Cache.GetMachineSid(_computerSID.Value, out machineSid)) - return true; - - if (_cachedMachineSid != null) - { - machineSid = _cachedMachineSid; - return true; - } - - if (!_lsaServerOpen) OpenLSAServer(); - - var status = _nativeMethods.CallLsaQueryInformationPolicy(_lsaPolicyHandle, - NativeMethods.LSAPolicyInformation.PolicyAccountDomainInformation, out var buffer); - if (status != NativeMethods.NtStatus.StatusSuccess) - { - machineSid = DummyMachineSid; - return false; - } - - var data = Marshal.PtrToStructure(buffer); - _nativeMethods.CallLSAFreeMemory(buffer); - - var sid = new SecurityIdentifier(data.DomainSid); - machineSid = sid.Value; - _cachedMachineSid = machineSid; - Cache.AddMachineSid(_computerSID.Value, machineSid); - return true; - } - - /// - /// Uses API calls and caching to attempt to get the local SID of a computer. - /// The local SID of a computer will not match its domain SID, and is used to denote local machine accounts - /// - /// - public bool GetMachineSid(out string machineSid) - { - if (_cachedMachineSid != null) - { - machineSid = _cachedMachineSid; - return machineSid != DummyMachineSid; - } - - if (Cache.GetMachineSid(_computerSID.Value, out machineSid)) - { - _cachedMachineSid = machineSid; - return true; - } - - if (LookupDomainSid(_computerSAMAccountName, out var tempSid)) - { - machineSid = tempSid.Value; - Cache.AddMachineSid(_computerSID.Value, tempSid.Value); - _cachedMachineSid = tempSid.Value; - return true; - } - - var domain = ListDomainsInServer().DefaultIfEmpty(null).FirstOrDefault(); - if (domain != null && LookupDomainSid(domain, out tempSid)) - { - machineSid = tempSid.Value; - Cache.AddMachineSid(_computerSID.Value, tempSid.Value); - _cachedMachineSid = tempSid.Value; - return true; - } - - machineSid = DummyMachineSid; - _cachedMachineSid = machineSid; - - if (!OpenBuiltInDomain(out var domainHandle)) return false; - - //As a fallback, try and retrieve the local administrators group and get the first account with a rid of 500 - //If at any time we encounter a failure, just return a dummy sid that wont match anything - var status = _nativeMethods.CallSamOpenAlias(domainHandle, NativeMethods.AliasOpenFlags.ListMembers, - (int) LocalGroupRids.Administrators, out var aliasHandle); - _log.LogTrace("SamOpenAlias returned {Status} for Administrators on {ComputerName}", status, _computerName); - if (status != NativeMethods.NtStatus.StatusSuccess) - { - _nativeMethods.CallSamCloseHandle(aliasHandle); - return false; - } - - status = _nativeMethods.CallSamGetMembersInAlias(aliasHandle, out var members, out var count); - _log.LogTrace("SamGetMembersInAlias returned {Status} for Administrators on {ComputerName}", status, - _computerName); - if (status != NativeMethods.NtStatus.StatusSuccess) - { - _nativeMethods.CallSamCloseHandle(aliasHandle); - return false; - } - - _nativeMethods.CallSamCloseHandle(aliasHandle); - - if (count == 0) - { - _nativeMethods.CallSamFreeMemory(members); - return false; - } - - var sids = new List(); - for (var i = 0; i < count; i++) - try - { - var ptr = Marshal.ReadIntPtr(members, Marshal.SizeOf() * i); - var sid = new SecurityIdentifier(ptr).Value; - sids.Add(sid); - } - catch (Exception e) - { - _log.LogDebug(e, "GetMachineSid - Exception converting sid"); - } - - _nativeMethods.CallSamFreeMemory(members); - - var domainSid = _computerSID.AccountDomainSid.Value.ToUpper(); - - machineSid = sids.Select(x => - { - try - { - return new SecurityIdentifier(x).Value; - } - catch - { - return null; - } - }).Where(x => x != null).DefaultIfEmpty(null) - .FirstOrDefault(x => x.EndsWith("-500") && !x.ToUpper().StartsWith(domainSid)); - - if (machineSid == null) - { - _log.LogTrace("Did not get a machine SID for {ComputerName}", _computerName); - return false; - } - - machineSid = new SecurityIdentifier(machineSid).AccountDomainSid.Value; - - Cache.AddMachineSid(_computerSID.Value, machineSid); - _cachedMachineSid = machineSid; - return true; - } - - private bool LookupDomainSid(string domainName, out SecurityIdentifier sid) - { - var unicodeString = new NativeMethods.UNICODE_STRING(domainName); - sid = null; - try - { - var status = - _nativeMethods.CallSamLookupDomainInSamServer(_samServerHandle, ref unicodeString, out var ptrSid); - _log.LogTrace("SamLookupDomainInSamServer returned {Status} on {ComputerName} for {Domain}", status, - _computerName, domainName); - if (status == NativeMethods.NtStatus.StatusSuccess) - { - sid = new SecurityIdentifier(ptrSid); - _nativeMethods.CallSamFreeMemory(ptrSid); - return true; - } - else - { - return false; - } - } - catch - { - return false; - } - finally - { - unicodeString.Dispose(); - } - } - } - - public class ServerNotOpenException : Exception - { - public ServerNotOpenException(string message) : base(message) - { - } - } - - public class APIException : Exception - { - public APIException() - { - } - - public APIException(string apiCall, NativeMethods.NtStatus status) - { - Status = status.ToString(); - APICall = apiCall; - } - - public string Status { get; set; } - public string APICall { get; set; } - - public override string ToString() - { - return $"Call to {APICall} returned {Status}"; - } - } - - public class LocalItem - { - public int RelativeID { get; set; } - public Label ObjectType { get; set; } - public string ObjectID { get; set; } - public string Domain { get; set; } - } - - public enum LocalGroupRids - { - None = 0, - Administrators = 544, - RemoteDesktopUsers = 555, - DcomUsers = 562, - PSRemote = 580 - } - - internal class DomainHandleManager - { - private readonly Dictionary _nameToHandleMap = new(); - private readonly NativeMethods _nativeMethods; - private readonly Dictionary _sidToDomainMap = new(); - - internal DomainHandleManager(NativeMethods nativeMethods = null) - { - _nativeMethods = nativeMethods ?? new NativeMethods(); - } - - internal void AddMappedDomain(SecurityIdentifier sid, string name, IntPtr handle) - { - if (name.Equals("Builtin", StringComparison.CurrentCultureIgnoreCase)) - { - _sidToDomainMap.Add(sid.Value.ToUpper(), name.ToUpper()); - _sidToDomainMap.Add(name.ToUpper(), sid.Value.ToUpper()); - } - else - { - _sidToDomainMap.Add(sid.AccountDomainSid.Value.ToUpper(), name.ToUpper()); - _sidToDomainMap.Add(name.ToUpper(), sid.AccountDomainSid.Value.ToUpper()); - } - - - _nameToHandleMap.Add(name.ToUpper(), handle); - } - - internal void AddMappedDomain(string sid, string name, IntPtr handle) - { - _sidToDomainMap.Add(sid.ToUpper(), name.ToUpper()); - _sidToDomainMap.Add(name.ToUpper(), sid.ToUpper()); - _nameToHandleMap.Add(name.ToUpper(), handle); - } - - internal bool GetHandleByName(string name, out IntPtr domainHandle) - { - return _nameToHandleMap.TryGetValue(name.ToUpper(), out domainHandle); - } - - internal bool GetHandleBySid(string sid, out IntPtr domainHandle) - { - if (_sidToDomainMap.TryGetValue(sid.ToUpper(), out var domainName)) - return _nameToHandleMap.TryGetValue(domainName, out domainHandle); - - domainHandle = IntPtr.Zero; - return false; - } - - internal bool GetHandleBySid(SecurityIdentifier sid, out IntPtr domainHandle) - { - var sidString = sid.AccountDomainSid.Value.ToUpper(); - if (_sidToDomainMap.TryGetValue(sidString, out var domainName)) - return _nameToHandleMap.TryGetValue(domainName, out domainHandle); - - domainHandle = IntPtr.Zero; - return false; - } - - internal void Dispose() - { - foreach (var kv in _nameToHandleMap.ToList()) - { - if (kv.Value == IntPtr.Zero) continue; - _nativeMethods.CallSamCloseHandle(kv.Value); - _nameToHandleMap[kv.Key] = IntPtr.Zero; - } - - foreach (var kv in _nameToHandleMap) - { - } - } - - ~DomainHandleManager() - { - Dispose(); - } - } - - internal class CachedLocalItem - { - public CachedLocalItem(string name, Label type) - { - Name = name; - Type = type; - } - - public string Name { get; set; } - public Label Type { get; set; } - } -} \ No newline at end of file diff --git a/src/CommonLib/Processors/UserRightsAssignmentProcessor.cs b/src/CommonLib/Processors/UserRightsAssignmentProcessor.cs new file mode 100644 index 00000000..6fa1b45c --- /dev/null +++ b/src/CommonLib/Processors/UserRightsAssignmentProcessor.cs @@ -0,0 +1,186 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.OutputTypes; +using SharpHoundRPC.Shared; +using SharpHoundRPC.Wrappers; + +namespace SharpHoundCommonLib.Processors +{ + public class UserRightsAssignmentProcessor + { + public delegate void ComputerStatusDelegate(CSVComputerStatus status); + + private readonly string[] _filteredSids = + { + "S-1-5-2", "S-1-5-2", "S-1-5-3", "S-1-5-4", "S-1-5-6", "S-1-5-7", "S-1-2", "S-1-2-0", "S-1-5-18", + "S-1-5-19", "S-1-5-20" + }; + + private readonly ILogger _log; + private readonly ILDAPUtils _utils; + + public UserRightsAssignmentProcessor(ILDAPUtils utils, ILogger log = null) + { + _utils = utils; + _log = log ?? Logging.LogProvider.CreateLogger("UserRightsAssignmentProcessor"); + } + + public event ComputerStatusDelegate ComputerStatusEvent; + + public IEnumerable GetUserRightsAssignments(string computerName, + string computerDomainSid, string computerDomain, string[] desiredPrivileges = null) + { + var computerSid = new SecurityIdentifier(computerDomainSid); + var policyOpenResult = LSAPolicy.OpenPolicy(computerName); + if (policyOpenResult.IsFailed) + { + SendComputerStatus(new CSVComputerStatus + { + Task = "LSAOpenPolicy", + ComputerName = computerName, + Status = policyOpenResult.Status.ToString() + }); + yield break; + } + + var server = policyOpenResult.Value; + desiredPrivileges ??= LSAPrivileges.DesiredPrivileges; + foreach (var privilege in desiredPrivileges) + { + var result = new UserRightsAssignmentAPIResult + { + Collected = false, + Privilege = privilege + }; + + var enumerateAccountsResult = server.GetResolvedPrincipalsWithPrivilege(privilege); + if (enumerateAccountsResult.IsFailed) + { + SendComputerStatus(new CSVComputerStatus + { + ComputerName = computerName, + Status = enumerateAccountsResult.Status.ToString(), + Task = "LSAEnumerateAccountsWithUserRight" + }); + result.FailureReason = + $"LSAEnumerateAccountsWithUserRights returned {enumerateAccountsResult.Status}"; + yield return result; + continue; + } + + if (!Cache.GetMachineSid(computerDomainSid, out var machineSid)) + { + var getMachineSidResult = server.GetLocalDomainInformation(); + if (getMachineSidResult.IsFailed) + { + machineSid = "UNKNOWN"; + } + else + { + machineSid = getMachineSidResult.Value.Sid; + Cache.AddMachineSid(computerDomainSid, machineSid); + } + } + + var isDc = computerSid.IsEqualDomainSid(new SecurityIdentifier(machineSid)); + + var resolved = new List(); + var names = new List(); + + foreach (var value in enumerateAccountsResult.Value) + { + var (sid, name, use, domain) = value; + if (IsSidFiltered(sid)) + continue; + + if (isDc) + { + if (_utils.GetWellKnownPrincipal(sid.Value, computerDomain, out var principal)) + { + resolved.Add(principal); + } + else + { + var res = _utils.ResolveIDAndType(sid.Value, computerDomain); + resolved.Add(res); + } + } + else + { + if (WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common)) + { + if (machineSid == "UNKNOWN") + continue; + var convertedId = $"{machineSid}-{sid.Rid()}"; + names.Add(new NamedPrincipal + { + ObjectId = convertedId, + PrincipalName = common.ObjectIdentifier + }); + + var objectType = common.ObjectType switch + { + Label.User => Label.LocalUser, + Label.Group => Label.LocalGroup, + _ => common.ObjectType + }; + resolved.Add(new TypedPrincipal + { + ObjectIdentifier = convertedId, + ObjectType = objectType + }); + } + else + { + var objectType = use switch + { + SharedEnums.SidNameUse.User => Label.LocalUser, + SharedEnums.SidNameUse.Group => Label.LocalGroup, + SharedEnums.SidNameUse.Alias => Label.LocalGroup, + _ => Label.Base + }; + + names.Add(new NamedPrincipal + { + ObjectId = sid.ToString(), + PrincipalName = name + }); + + resolved.Add(new TypedPrincipal + { + ObjectIdentifier = sid.ToString(), + ObjectType = objectType + }); + } + } + } + + result.LocalNames = names.ToArray(); + result.Results = resolved.ToArray(); + yield return result; + } + } + + private bool IsSidFiltered(SecurityIdentifier identifier) + { + var value = identifier.Value; + + if (value.StartsWith("S-1-5-80") || value.StartsWith("S-1-5-82") || + value.StartsWith("S-1-5-90") || value.StartsWith("S-1-5-96")) + return true; + + if (_filteredSids.Contains(value)) + return true; + + return false; + } + + private void SendComputerStatus(CSVComputerStatus status) + { + ComputerStatusEvent?.Invoke(status); + } + } +} \ No newline at end of file diff --git a/src/CommonLib/ResolvedSearchResult.cs b/src/CommonLib/ResolvedSearchResult.cs index 2287c8bd..adb40a3c 100644 --- a/src/CommonLib/ResolvedSearchResult.cs +++ b/src/CommonLib/ResolvedSearchResult.cs @@ -9,7 +9,7 @@ public class ResolvedSearchResult public string DisplayName { - get => _displayName.ToUpper(); + get => _displayName?.ToUpper(); set => _displayName = value; } diff --git a/src/CommonLib/SharpHoundCommonLib.csproj b/src/CommonLib/SharpHoundCommonLib.csproj index 88e6acc6..e6fa9fe8 100644 --- a/src/CommonLib/SharpHoundCommonLib.csproj +++ b/src/CommonLib/SharpHoundCommonLib.csproj @@ -28,4 +28,7 @@ + + + diff --git a/src/SharpHoundRPC/Extensions.cs b/src/SharpHoundRPC/Extensions.cs new file mode 100644 index 00000000..50656bb1 --- /dev/null +++ b/src/SharpHoundRPC/Extensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Security.Principal; + +namespace SharpHoundRPC +{ + public static class Extensions + { + public static bool IsError(this NtStatus status) + { + if (status != NtStatus.StatusSuccess && status != NtStatus.StatusMoreEntries && + status != NtStatus.StatusSomeMapped && status != NtStatus.StatusNoMoreEntries) + return true; + + return false; + } + + /// + /// Gets the relative identifier for a SID + /// + /// + /// + public static int Rid(this SecurityIdentifier securityIdentifier) + { + var value = securityIdentifier.Value; + var rid = int.Parse(value.Substring(value.LastIndexOf("-", StringComparison.Ordinal) + 1)); + return rid; + } + + public static byte[] GetBytes(this SecurityIdentifier identifier) + { + var bytes = new byte[identifier.BinaryLength]; + identifier.GetBinaryForm(bytes, 0); + return bytes; + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Handles/BasePointer.cs b/src/SharpHoundRPC/Handles/BasePointer.cs new file mode 100644 index 00000000..92b63c47 --- /dev/null +++ b/src/SharpHoundRPC/Handles/BasePointer.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Security.Principal; +using Microsoft.Win32.SafeHandles; + +namespace SharpHoundRPC.Handles +{ + public abstract class BasePointer : SafeHandleZeroOrMinusOneIsInvalid + { + protected BasePointer() : base(true) + { + } + + protected BasePointer(bool ownsHandle) : base(ownsHandle) + { + } + + protected BasePointer(IntPtr handle) : base(true) + { + SetHandle(handle); + } + + protected BasePointer(IntPtr handle, bool ownsHandle) : base(ownsHandle) + { + SetHandle(handle); + } + + public IEnumerable GetEnumerable(int count) + { + for (var i = 0; i < count; i++) + if (typeof(T) == typeof(int)) + yield return (T) (object) ReadInt32(i); + else if (typeof(T) == typeof(long)) + yield return (T) (object) ReadInt64(i); + else if (typeof(T) == typeof(SecurityIdentifier)) + yield return (T) (object) new SecurityIdentifier(ReadIntPtr(i)); + else + yield return Marshal.PtrToStructure(handle + Marshal.SizeOf() * i); + } + + public T GetData() + { + if (typeof(T) == typeof(int)) return (T) (object) ReadInt32(); + + if (typeof(T) == typeof(long)) return (T) (object) ReadInt64(); + + if (typeof(T) == typeof(SecurityIdentifier)) return (T) (object) new SecurityIdentifier(handle); + + return Marshal.PtrToStructure(handle); + } + + private int ReadInt32(int offset = 0) + { + return Marshal.ReadInt32(handle + offset * Marshal.SizeOf()); + } + + private long ReadInt64(int offset = 0) + { + return Marshal.ReadInt64(handle + offset * Marshal.SizeOf()); + } + + private IntPtr ReadIntPtr(int offset = 0) + { + return Marshal.ReadIntPtr(handle + offset * Marshal.SizeOf()); + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Handles/LSAHandle.cs b/src/SharpHoundRPC/Handles/LSAHandle.cs new file mode 100644 index 00000000..0bd941ca --- /dev/null +++ b/src/SharpHoundRPC/Handles/LSAHandle.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.Win32.SafeHandles; +using SharpHoundRPC.LSANative; + +namespace SharpHoundRPC.Handles +{ + public class LSAHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public LSAHandle() : base(true) + { + } + + public LSAHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle) + { + SetHandle(handle); + } + + public LSAHandle(bool ownsHandle) : base(true) + { + } + + protected override bool ReleaseHandle() + { + if (handle == IntPtr.Zero) return true; + return LSAMethods.LsaClose(handle) == NtStatus.StatusSuccess; + } + + ~LSAHandle() + { + Dispose(); + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Handles/LSAPointer.cs b/src/SharpHoundRPC/Handles/LSAPointer.cs new file mode 100644 index 00000000..499541b0 --- /dev/null +++ b/src/SharpHoundRPC/Handles/LSAPointer.cs @@ -0,0 +1,26 @@ +using System; +using SharpHoundRPC.LSANative; + +namespace SharpHoundRPC.Handles +{ + public class LSAPointer : BasePointer + { + public LSAPointer() : base(true) + { + } + + public LSAPointer(IntPtr handle) : base(handle, true) + { + } + + public LSAPointer(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle) + { + } + + protected override bool ReleaseHandle() + { + if (handle == IntPtr.Zero) return true; + return LSAMethods.LsaFreeMemory(handle) == NtStatus.StatusSuccess; + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Handles/NetAPIPointer.cs b/src/SharpHoundRPC/Handles/NetAPIPointer.cs new file mode 100644 index 00000000..1a498114 --- /dev/null +++ b/src/SharpHoundRPC/Handles/NetAPIPointer.cs @@ -0,0 +1,26 @@ +using System; +using SharpHoundRPC.NetAPINative; + +namespace SharpHoundRPC.Handles +{ + public class NetAPIPointer : BasePointer + { + public NetAPIPointer() : base(true) + { + } + + public NetAPIPointer(IntPtr handle) : base(handle, true) + { + } + + public NetAPIPointer(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle) + { + } + + protected override bool ReleaseHandle() + { + if (handle == IntPtr.Zero) return true; + return NetAPIMethods.NetApiBufferFree(handle) == NetAPIEnums.NetAPIStatus.Success; + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Handles/SAMHandle.cs b/src/SharpHoundRPC/Handles/SAMHandle.cs new file mode 100644 index 00000000..101dd3a9 --- /dev/null +++ b/src/SharpHoundRPC/Handles/SAMHandle.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Win32.SafeHandles; +using SharpHoundRPC.SAMRPCNative; + +namespace SharpHoundRPC.Handles +{ + public class SAMHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SAMHandle() : base(true) + { + } + + public SAMHandle(IntPtr handle) : base(true) + { + SetHandle(handle); + } + + public SAMHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle) + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + if (handle == IntPtr.Zero) return true; + return SAMMethods.SamCloseHandle(handle) == NtStatus.StatusSuccess; + } + + ~SAMHandle() + { + Dispose(); + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Handles/SAMPointer.cs b/src/SharpHoundRPC/Handles/SAMPointer.cs new file mode 100644 index 00000000..932b1376 --- /dev/null +++ b/src/SharpHoundRPC/Handles/SAMPointer.cs @@ -0,0 +1,25 @@ +using System; +using SharpHoundRPC.SAMRPCNative; + +namespace SharpHoundRPC.Handles +{ + public class SAMPointer : BasePointer + { + public SAMPointer() : base(true) + { + } + + public SAMPointer(IntPtr handle) : base(handle, true) + { + } + + public SAMPointer(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle) + { + } + + protected override bool ReleaseHandle() + { + return SAMMethods.SamFreeMemory(handle) == NtStatus.StatusSuccess; + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Handles/SAMSidArray.cs b/src/SharpHoundRPC/Handles/SAMSidArray.cs new file mode 100644 index 00000000..492fe3fb --- /dev/null +++ b/src/SharpHoundRPC/Handles/SAMSidArray.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace SharpHoundRPC.Handles +{ + public class SAMSidArray : SAMPointer + { + public SAMSidArray() + { + } + + public SAMSidArray(IntPtr handle) : base(handle) + { + } + + public SAMSidArray(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle) + { + } + + public IEnumerable GetData(int count) + { + for (var i = 0; i < count; i++) + { + var rawPtr = Marshal.ReadIntPtr(handle, Marshal.SizeOf() * i); + var sid = new SecurityIdentifier(rawPtr); + yield return sid; + } + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/LSANative/LSAEnums.cs b/src/SharpHoundRPC/LSANative/LSAEnums.cs new file mode 100644 index 00000000..25616adc --- /dev/null +++ b/src/SharpHoundRPC/LSANative/LSAEnums.cs @@ -0,0 +1,45 @@ +using System; + +namespace SharpHoundRPC.LSANative +{ + public class LSAEnums + { + [Flags] + public enum LsaOpenMask + { + ViewLocalInfo = 0x1, + ViewAuditInfo = 0x2, + GetPrivateInfo = 0x4, + TrustAdmin = 0x8, + CreateAccount = 0x10, + CreateSecret = 0x20, + CreatePrivilege = 0x40, + SetDefaultQuotaLimits = 0x80, + SetAuditRequirements = 0x100, + AuditLogAdmin = 0x200, + ServerAdmin = 0x400, + LookupNames = 0x800, + Notification = 0x1000, + POLICY_READ = 0x20006, + POLICY_ALL_ACCESS = 0x00F0FFF, + POLICY_EXECUTE = 0X20801, + POLICY_WRITE = 0X207F8 + } + + public enum LSAPolicyInformation + { + PolicyAuditLogInformation = 1, + PolicyAuditEventsInformation, + PolicyPrimaryDomainInformation, + PolicyPdAccountInformation, + PolicyAccountDomainInformation, + PolicyLsaServerRoleInformation, + PolicyReplicaSourceInformation, + PolicyDefaultQuotaInformation, + PolicyModificationInformation, + PolicyAuditFullSetInformation, + PolicyAuditFullQueryInformation, + PolicyDnsDomainInformation + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/LSANative/LSAMethods.cs b/src/SharpHoundRPC/LSANative/LSAMethods.cs new file mode 100644 index 00000000..f01602cc --- /dev/null +++ b/src/SharpHoundRPC/LSANative/LSAMethods.cs @@ -0,0 +1,135 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Principal; +using SharpHoundRPC.Handles; +using SharpHoundRPC.Shared; + +namespace SharpHoundRPC.LSANative +{ + [SuppressUnmanagedCodeSecurity] + public class LSAMethods + { + internal static (NtStatus status, LSAHandle policyHandle) LsaOpenPolicy(string computerName, + LSAEnums.LsaOpenMask desiredAccess) + { + var us = new SharedStructs.UnicodeString(computerName); + var objectAttributes = default(LSAStructs.ObjectAttributes); + var status = LsaOpenPolicy(ref us, ref objectAttributes, desiredAccess, out var policyHandle); + + return (status, policyHandle); + } + + [DllImport("advapi32.dll")] + private static extern NtStatus LsaOpenPolicy( + ref SharedStructs.UnicodeString server, + ref LSAStructs.ObjectAttributes objectAttributes, + LSAEnums.LsaOpenMask desiredAccess, + out LSAHandle policyHandle + ); + + [DllImport("advapi32.dll")] + internal static extern NtStatus LsaClose( + IntPtr handle + ); + + [DllImport("advapi32.dll")] + internal static extern NtStatus LsaFreeMemory( + IntPtr buffer + ); + + internal static (NtStatus status, LSAPointer pointer) LsaQueryInformationPolicy(LSAHandle policyHandle, + LSAEnums.LSAPolicyInformation policyInformation) + { + var status = LsaQueryInformationPolicy(policyHandle, policyInformation, out var pointer); + + return (status, pointer); + } + + [DllImport("advapi32.dll")] + private static extern NtStatus LsaQueryInformationPolicy( + LSAHandle policyHandle, + LSAEnums.LSAPolicyInformation policyInformation, + out LSAPointer buffer + ); + + internal static (NtStatus status, LSAPointer sids, int count) LsaEnumerateAccountsWithUserRight( + LSAHandle policyHandle, + string userRight) + { + var arr = new SharedStructs.UnicodeString[1]; + arr[0] = new SharedStructs.UnicodeString(userRight); + + var status = LsaEnumerateAccountsWithUserRight(policyHandle, arr, out var sids, out var count); + + return (status, sids, count); + } + + [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern NtStatus LsaEnumerateAccountsWithUserRight( + LSAHandle policyHandle, + SharedStructs.UnicodeString[] userRight, + out LSAPointer sids, + out int count + ); + + internal static (NtStatus status, LSAPointer referencedDomains, LSAPointer names, int count) + LsaLookupSids(LSAHandle policyHandle, + LSAPointer sids, int count) + { + var status = LsaLookupSids(policyHandle, count, sids, out var referencedDomains, out var names); + return (status, referencedDomains, names, count); + } + + internal static (NtStatus status, LSAPointer referencedDomains, LSAPointer names, int count) + LsaLookupSids(LSAHandle policyHandle, + SecurityIdentifier[] sids) + { + var count = sids.Length; + if (count == 0) + return (NtStatus.StatusInvalidParameter, null, null, 0); + + var gcHandles = new GCHandle[count]; + var pSids = new IntPtr[count]; + + for (var i = 0; i < count; i++) + { + var sid = sids[i]; + var b = new byte[sid.BinaryLength]; + sid.GetBinaryForm(b, 0); + gcHandles[i] = GCHandle.Alloc(b, GCHandleType.Pinned); + pSids[i] = gcHandles[i].AddrOfPinnedObject(); + } + + try + { + var status = LsaLookupSids(policyHandle, count, pSids, out var referencedDomains, out var names); + return (status, referencedDomains, names, count); + } + finally + { + foreach (var handle in gcHandles) + if (handle.IsAllocated) + handle.Free(); + } + } + + [DllImport("advapi32.dll")] + private static extern NtStatus LsaLookupSids( + LSAHandle policyHandle, + int count, + LSAPointer sidArray, + out LSAPointer referencedDomains, + out LSAPointer names + ); + + [DllImport("advapi32.dll")] + private static extern NtStatus LsaLookupSids( + LSAHandle policyHandle, + int count, + [MarshalAs(UnmanagedType.LPArray)] IntPtr[] sidArray, + out LSAPointer referencedDomains, + out LSAPointer names + ); + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/LSANative/LSAStructs.cs b/src/SharpHoundRPC/LSANative/LSAStructs.cs new file mode 100644 index 00000000..dd00e1b9 --- /dev/null +++ b/src/SharpHoundRPC/LSANative/LSAStructs.cs @@ -0,0 +1,57 @@ +using System; +using System.Runtime.InteropServices; +using SharpHoundRPC.Shared; + +namespace SharpHoundRPC.LSANative +{ + public class LSAStructs + { + [StructLayout(LayoutKind.Sequential)] + public struct ObjectAttributes + { + public int Length; + public IntPtr RootDirectory; + public IntPtr ObjectName; + public int Attributes; + public IntPtr SecurityDescriptor; + public IntPtr SecurityQualityOfService; + + public void Dispose() + { + if (ObjectName == IntPtr.Zero) return; + Marshal.DestroyStructure(ObjectName, typeof(SharedStructs.UnicodeString)); + Marshal.FreeHGlobal(ObjectName); + ObjectName = IntPtr.Zero; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct PolicyAccountDomainInfo + { + public SharedStructs.UnicodeString DomainName; + public IntPtr DomainSid; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LSATranslatedNames + { + public SharedEnums.SidNameUse Use; + public SharedStructs.UnicodeString Name; + public int DomainIndex; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LSAReferencedDomains + { + public int Entries; + public IntPtr Domains; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LSATrustInformation + { + public SharedStructs.UnicodeString Name; + public IntPtr Sid; + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/LSANative/UserRights.cs b/src/SharpHoundRPC/LSANative/UserRights.cs new file mode 100644 index 00000000..fe29dcff --- /dev/null +++ b/src/SharpHoundRPC/LSANative/UserRights.cs @@ -0,0 +1,61 @@ +namespace SharpHoundRPC.LSANative +{ + public class UserRights + { + public const string AssignPrimaryToken = "SeAssignPrimaryTokenPrivilege"; + public const string Audit = "SeAuditPrivilege"; + public const string Backup = "SeBackupPrivilege"; + public const string BatchLogon = "SeBatchLogonRight"; + public const string ChangeNotify = "SeChangeNotifyPrivilege"; + public const string CreateGlobal = "SeCreateGlobalPrivilege"; + public const string CreatePagefile = "SeCreatePagefilePrivilege"; + public const string CreatePermanent = "SeCreatePermanentPrivilege"; + public const string CreateSymbolicLink = "SeCreateSymbolicLinkPrivilege"; + public const string CreateToken = "SeCreateTokenPrivilege"; + public const string Debug = "SeDebugPrivilege"; + public const string DenyBatchLogon = "SeDenyBatchLogonRight"; + public const string DenyInteractiveLogon = "SeDenyInteractiveLogonRight"; + public const string DenyNetworkLogon = "SeDenyNetworkLogonRight"; + public const string DenyRemoteInteractiveLogon = "SeDenyRemoteInteractiveLogonRight"; + public const string DenyServiceLogon = "SeDenyServiceLogonRight"; + public const string EnableDelegation = "SeEnableDelegationPrivilege"; + public const string Impersonate = "SeImpersonatePrivilege"; + public const string IncreaseBasePriority = "SeIncreaseBasePriorityPrivilege"; + public const string IncreaseQuota = "SeIncreaseQuotaPrivilege"; + public const string IncreaseWorkingSet = "SeIncreaseWorkingSetPrivilege"; + public const string InteractiveLogon = "SeInteractiveLogonRight"; + public const string LoadDriver = "SeLoadDriverPrivilege"; + public const string LockMemory = "SeLockMemoryPrivilege"; + public const string MachineAccount = "SeMachineAccountPrivilege"; + public const string ManageVolume = "SeManageVolumePrivilege"; + public const string NetworkLogon = "SeNetworkLogonRight"; + public const string ProfileSingleProcess = "SeProfileSingleProcessPrivilege"; + public const string Relabel = "SeRelabelPrivilege"; + public const string RemoteInteractiveLogon = "SeRemoteInteractiveLogonRight"; + public const string RemoteShutdown = "SeRemoteShutdownPrivilege"; + public const string Restore = "SeRestorePrivilege"; + public const string Security = "SeSecurityPrivilege"; + public const string ServiceLogon = "SeServiceLogonRight"; + public const string Shutdown = "SeShutdownPrivilege"; + public const string SyncAgent = "SeSyncAgentPrivilege"; + public const string SystemEnvironment = "SeSystemEnvironmentPrivilege"; + public const string SystemProfile = "SeSystemProfilePrivilege"; + public const string SystemTime = "SeSystemtimePrivilege"; + public const string TakeOwnership = "SeTakeOwnershipPrivilege"; + public const string Tcb = "SeTcbPrivilege"; + public const string TimeZone = "SeTimeZonePrivilege"; + public const string TrustedCredManAccess = "SeTrustedCredManAccessPrivilege"; + public const string Undock = "SeUndockPrivilege"; + + public static readonly string[] AllPrivileges = + { + AssignPrimaryToken, Audit, Backup, BatchLogon, ChangeNotify, CreateGlobal, CreatePagefile, CreatePermanent, + CreateSymbolicLink, CreateToken, Debug, DenyBatchLogon, DenyInteractiveLogon, DenyNetworkLogon, + DenyRemoteInteractiveLogon, DenyServiceLogon, EnableDelegation, Impersonate, IncreaseBasePriority, + IncreaseQuota, IncreaseWorkingSet, InteractiveLogon, LoadDriver, LockMemory, MachineAccount, ManageVolume, + NetworkLogon, ProfileSingleProcess, Relabel, RemoteInteractiveLogon, RemoteShutdown, Restore, Security, + ServiceLogon, Shutdown, SyncAgent, SystemEnvironment, SystemProfile, SystemTime, TakeOwnership, Tcb, + TimeZone, TrustedCredManAccess, Undock + }; + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/NTStatus.cs b/src/SharpHoundRPC/NTStatus.cs new file mode 100644 index 00000000..f5391bd3 --- /dev/null +++ b/src/SharpHoundRPC/NTStatus.cs @@ -0,0 +1,17 @@ +namespace SharpHoundRPC +{ + public enum NtStatus + { + StatusSuccess = 0x0, + StatusMoreEntries = 0x105, + StatusSomeMapped = 0x107, + StatusInvalidHandle = unchecked((int) 0xC0000008), + StatusInvalidParameter = unchecked((int) 0xC000000D), + StatusAccessDenied = unchecked((int) 0xC0000022), + StatusObjectTypeMismatch = unchecked((int) 0xC0000024), + StatusNoSuchDomain = unchecked((int) 0xC00000DF), + StatusRpcServerUnavailable = unchecked((int) 0xC0020017), + StatusNoSuchAlias = unchecked((int) 0xC0000151), + StatusNoMoreEntries = unchecked((int) 0x8000001A) + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/NetAPINative/NetAPIEnums.cs b/src/SharpHoundRPC/NetAPINative/NetAPIEnums.cs new file mode 100644 index 00000000..7410bd69 --- /dev/null +++ b/src/SharpHoundRPC/NetAPINative/NetAPIEnums.cs @@ -0,0 +1,89 @@ +using System; + +namespace SharpHoundRPC.NetAPINative +{ + public class NetAPIEnums + { + [Flags] + public enum DSGETDCNAME_FLAGS : uint + { + DS_FORCE_REDISCOVERY = 0x00000001, + DS_DIRECTORY_SERVICE_REQUIRED = 0x00000010, + DS_DIRECTORY_SERVICE_PREFERRED = 0x00000020, + DS_GC_SERVER_REQUIRED = 0x00000040, + DS_PDC_REQUIRED = 0x00000080, + DS_BACKGROUND_ONLY = 0x00000100, + DS_IP_REQUIRED = 0x00000200, + DS_KDC_REQUIRED = 0x00000400, + DS_TIMESERV_REQUIRED = 0x00000800, + DS_WRITABLE_REQUIRED = 0x00001000, + DS_GOOD_TIMESERV_PREFERRED = 0x00002000, + DS_AVOID_SELF = 0x00004000, + DS_ONLY_LDAP_NEEDED = 0x00008000, + DS_IS_FLAT_NAME = 0x00010000, + DS_IS_DNS_NAME = 0x00020000, + DS_RETURN_DNS_NAME = 0x40000000, + DS_RETURN_FLAT_NAME = 0x80000000 + } + + public enum NetAPIStatus : uint + { + Success = 0, + + /// + /// This computer name is invalid. + /// + InvalidComputer = 2351, + + /// + /// This operation is only allowed on the primary domain controller of the domain. + /// + NotPrimary = 2226, + + /// + /// This operation is not allowed on this special group. + /// + SpeGroupOp = 2234, + + /// + /// This operation is not allowed on the last administrative account. + /// + LastAdmin = 2452, + + /// + /// The password parameter is invalid. + /// + BadPassword = 2203, + + /// + /// The password does not meet the password policy requirements. + /// Check the minimum password length, password complexity and password history requirements. + /// + PasswordTooShort = 2245, + + /// + /// The user name could not be found. + /// + UserNotFound = 2221, + ErrorAccessDenied = 5, + ErrorNotEnoughMemory = 8, + ErrorInvalidParameter = 87, + ErrorInvalidName = 123, + ErrorInvalidLevel = 124, + ErrorMoreData = 234, + ErrorSessionCredentialConflict = 1219, + + /// + /// The RPC server is not available. This error is returned if a remote computer was specified in + /// the lpServer parameter and the RPC server is not available. + /// + RpcSServerUnavailable = 2147944122, // 0x800706BA + + /// + /// Remote calls are not allowed for this process. This error is returned if a remote computer was + /// specified in the lpServer parameter and remote calls are not allowed for this process. + /// + RpcERemoteDisabled = 2147549468 // 0x8001011C + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/NetAPINative/NetAPIMethods.cs b/src/SharpHoundRPC/NetAPINative/NetAPIMethods.cs new file mode 100644 index 00000000..a2dafc8f --- /dev/null +++ b/src/SharpHoundRPC/NetAPINative/NetAPIMethods.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using SharpHoundRPC.Handles; + +namespace SharpHoundRPC.NetAPINative +{ + public static class NetAPIMethods + { + private const int NetWkstaUserEnumQueryLevel = 1; + private const int NetSessionEnumLevel = 10; + private const int NetWkstaGetInfoQueryLevel = 100; + + [DllImport("netapi32.dll")] + internal static extern NetAPIEnums.NetAPIStatus NetApiBufferFree( + IntPtr buffer); + + public static NetAPIResult> NetWkstaUserEnum(string computerName) + { + var resumeHandle = 0; + var result = NetWkstaUserEnum(computerName, NetWkstaUserEnumQueryLevel, out var buffer, -1, + out var entriesRead, out _, ref resumeHandle); + + if (result != NetAPIEnums.NetAPIStatus.Success && result != NetAPIEnums.NetAPIStatus.ErrorMoreData) + return result; + + return NetAPIResult>.Ok(buffer + .GetEnumerable(entriesRead) + .Select(x => new NetWkstaUserEnumResults(x.Username, x.LogonDomain))); + } + + [DllImport("netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern NetAPIEnums.NetAPIStatus NetWkstaUserEnum( + string servername, + int level, + out NetAPIPointer buffer, + int preferredMaxLength, + out int entriesRead, + out int totalEntries, + ref int resumeHandle); + + public static NetAPIResult> NetSessionEnum(string computerName) + { + var resumeHandle = 0; + var result = NetSessionEnum(computerName, null, null, NetSessionEnumLevel, out var buffer, -1, + out var entriesRead, out _, ref resumeHandle); + + if (result != NetAPIEnums.NetAPIStatus.Success && result != NetAPIEnums.NetAPIStatus.ErrorMoreData) + return result; + + return NetAPIResult>.Ok(buffer + .GetEnumerable(entriesRead) + .Select(x => new NetSessionEnumResults(x.Username, x.CName))); + } + + [DllImport("NetAPI32.dll", SetLastError = true)] + private static extern NetAPIEnums.NetAPIStatus NetSessionEnum( + [MarshalAs(UnmanagedType.LPWStr)] string serverName, + [MarshalAs(UnmanagedType.LPWStr)] string uncClientName, + [MarshalAs(UnmanagedType.LPWStr)] string userName, + int level, + out NetAPIPointer buffer, + int preferredMaxLength, + out int entriesRead, + out int totalEntries, + ref int resumeHandle); + + public static NetAPIResult NetWkstaGetInfo(string computerName) + { + var result = NetWkstaGetInfo(computerName, NetWkstaGetInfoQueryLevel, out var buffer); + + if (result != NetAPIEnums.NetAPIStatus.Success) return result; + + return buffer.GetData(); + } + + [DllImport("netapi32.dll", SetLastError = true)] + private static extern NetAPIEnums.NetAPIStatus NetWkstaGetInfo( + [MarshalAs(UnmanagedType.LPWStr)] string serverName, + uint level, + out NetAPIPointer bufPtr); + + public static NetAPIResult DsGetDcName(string computerName, + string domainName) + { + var result = DsGetDcName(computerName, domainName, null, null, + (uint) (NetAPIEnums.DSGETDCNAME_FLAGS.DS_IS_FLAT_NAME | + NetAPIEnums.DSGETDCNAME_FLAGS.DS_RETURN_DNS_NAME), out var buffer); + if (result != NetAPIEnums.NetAPIStatus.Success) return result; + + return buffer.GetData(); + } + + [DllImport("Netapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern NetAPIEnums.NetAPIStatus DsGetDcName + ( + [MarshalAs(UnmanagedType.LPTStr)] string computerName, + [MarshalAs(UnmanagedType.LPTStr)] string domainName, + [In] NetAPIStructs.GuidClass domainGuid, + [MarshalAs(UnmanagedType.LPTStr)] string siteName, + uint flags, + out NetAPIPointer pDomainControllerInfo + ); + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/NetAPINative/NetAPIResult.cs b/src/SharpHoundRPC/NetAPINative/NetAPIResult.cs new file mode 100644 index 00000000..d5d1f461 --- /dev/null +++ b/src/SharpHoundRPC/NetAPINative/NetAPIResult.cs @@ -0,0 +1,41 @@ +namespace SharpHoundRPC.NetAPINative +{ + public class NetAPIResult + { + public bool IsSuccess { get; private set; } + public NetAPIEnums.NetAPIStatus Status { get; private set; } + public T Value { get; private set; } + public string Error { get; private set; } + public bool IsFailed => !IsSuccess; + + public static NetAPIResult Ok(T value) + { + return new NetAPIResult {IsSuccess = true, Value = value}; + } + + public static NetAPIResult Fail(NetAPIEnums.NetAPIStatus status) + { + return new NetAPIResult {Status = status}; + } + + public static NetAPIResult Fail(string error) + { + return new NetAPIResult {Error = error}; + } + + public static implicit operator NetAPIResult(T input) + { + return Ok(input); + } + + public static implicit operator NetAPIResult(NetAPIEnums.NetAPIStatus status) + { + return Fail(status); + } + + public static implicit operator NetAPIResult(string error) + { + return Fail(error); + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/NetAPINative/NetAPIReturns.cs b/src/SharpHoundRPC/NetAPINative/NetAPIReturns.cs new file mode 100644 index 00000000..9ccf117d --- /dev/null +++ b/src/SharpHoundRPC/NetAPINative/NetAPIReturns.cs @@ -0,0 +1,26 @@ +namespace SharpHoundRPC.NetAPINative +{ + public class NetWkstaUserEnumResults + { + public NetWkstaUserEnumResults(string username, string domain) + { + LogonDomain = domain; + Username = username; + } + + public string LogonDomain { get; } + public string Username { get; } + } + + public class NetSessionEnumResults + { + public NetSessionEnumResults(string username, string cname) + { + Username = username; + ComputerName = cname; + } + + public string Username { get; } + public string ComputerName { get; } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/NetAPINative/NetAPIStructs.cs b/src/SharpHoundRPC/NetAPINative/NetAPIStructs.cs new file mode 100644 index 00000000..adf5b277 --- /dev/null +++ b/src/SharpHoundRPC/NetAPINative/NetAPIStructs.cs @@ -0,0 +1,56 @@ +using System; +using System.Runtime.InteropServices; + +namespace SharpHoundRPC.NetAPINative +{ + public class NetAPIStructs + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct WkstaUserInfo1 + { + public string Username; + public string LogonDomain; + public string OtherDomains; + public string LogonServer; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SessionInfo10 + { + [MarshalAs(UnmanagedType.LPWStr)] public string CName; + [MarshalAs(UnmanagedType.LPWStr)] public string Username; + public uint Time; + public uint IdleTIme; + } + + [StructLayout(LayoutKind.Sequential)] + public struct WorkstationInfo100 + { + public int PlatformId; + [MarshalAs(UnmanagedType.LPWStr)] public string ComputerName; + [MarshalAs(UnmanagedType.LPWStr)] public string LanGroup; + public int MajorVersion; + public int MinorVersion; + } + + [StructLayout(LayoutKind.Sequential)] + public class GuidClass + { + public Guid TheGuid; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct DomainControllerInfo + { + [MarshalAs(UnmanagedType.LPTStr)] public string DomainControllerName; + [MarshalAs(UnmanagedType.LPTStr)] public string DomainControllerAddress; + public uint DomainControllerAddressType; + public Guid DomainGuid; + [MarshalAs(UnmanagedType.LPTStr)] public string DomainName; + [MarshalAs(UnmanagedType.LPTStr)] public string DnsForestName; + public uint Flags; + [MarshalAs(UnmanagedType.LPTStr)] public string DcSiteName; + [MarshalAs(UnmanagedType.LPTStr)] public string ClientSiteName; + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/RPCException.cs b/src/SharpHoundRPC/RPCException.cs new file mode 100644 index 00000000..10571580 --- /dev/null +++ b/src/SharpHoundRPC/RPCException.cs @@ -0,0 +1,39 @@ +using System; + +namespace SharpHoundRPC +{ + public class RPCException : Exception + { + public const string Connect = "SamConnect"; + public const string EnumerateDomains = "SamEnumerateDomainsInSamServer"; + public const string ServerNotInitialized = "Server Not Initialized"; + public const string OpenAlias = "SamOpenAlias"; + public const string OpenDomain = "SamOpenDomain"; + public const string AliasNotFound = "Alias Not Found"; + public const string DomainNotFound = "Domain Not Found"; + public const string LookupIds = "SamLookupIdsInDomain"; + public const string EnumerateAliases = "SamEnumerateAliasesInDomain"; + public const string GetAliasMembers = "SamGetMembersinAlias"; + public const string LookupDomain = "SamLookupDomainInSamServer"; + public const string GetMachineSid = "GetMachineSid"; + private readonly string APICall; + private readonly string Status; + + public RPCException(string apiCall, NtStatus status) + { + APICall = apiCall; + Status = status.ToString(); + } + + public RPCException(string apiCall, string status) + { + APICall = apiCall; + Status = status; + } + + public override string ToString() + { + return $"Call to {APICall} returned {Status}"; + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Result.cs b/src/SharpHoundRPC/Result.cs new file mode 100644 index 00000000..ca92045a --- /dev/null +++ b/src/SharpHoundRPC/Result.cs @@ -0,0 +1,41 @@ +namespace SharpHoundRPC +{ + public class Result + { + public bool IsSuccess { get; private set; } + public NtStatus Status { get; private set; } + public T Value { get; private set; } + public string Error { get; private set; } + public bool IsFailed => !IsSuccess; + + public static Result Ok(T value) + { + return new() {IsSuccess = true, Value = value}; + } + + public static Result Fail(NtStatus status) + { + return new() {Status = status}; + } + + public static Result Fail(string error) + { + return new() {Error = error}; + } + + public static implicit operator Result(T input) + { + return Ok(input); + } + + public static implicit operator Result(NtStatus status) + { + return Fail(status); + } + + public static implicit operator Result(string error) + { + return Fail(error); + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/SAMRPCNative/SAMEnums.cs b/src/SharpHoundRPC/SAMRPCNative/SAMEnums.cs new file mode 100644 index 00000000..c6e60290 --- /dev/null +++ b/src/SharpHoundRPC/SAMRPCNative/SAMEnums.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SharpHoundRPC.SAMRPCNative +{ + public class SAMEnums + { + [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + public enum AliasOpenFlags + { + AddMember = 0x1, + RemoveMember = 0x2, + ListMembers = 0x4, + ReadInfo = 0x8, + WriteAccount = 0x10, + AllAccess = 0xf001f, + Read = 0x20004, + Write = 0x20013, + Execute = 0x20008 + } + + [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + public enum DomainAccessMask + { + ReadPasswordParameters = 0x1, + WritePasswordParameters = 0x2, + ReadOtherParameters = 0x4, + WriteOtherParameters = 0x8, + CreateUser = 0x10, + CreateGroup = 0x20, + CreateAlias = 0x40, + GetAliasMembership = 0x80, + ListAccounts = 0x100, + Lookup = 0x200, + AdministerServer = 0x400, + AllAccess = 0xf07ff, + Read = 0x20084, + Write = 0x2047A, + Execute = 0x20301 + } + + [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + public enum SamAccessMasks + { + SamServerConnect = 0x1, + SamServerShutdown = 0x2, + SamServerInitialize = 0x4, + SamServerCreateDomains = 0x8, + SamServerEnumerateDomains = 0x10, + SamServerLookupDomain = 0x20, + SamServerAllAccess = 0xf003f, + SamServerRead = 0x20010, + SamServerWrite = 0x2000e, + SamServerExecute = 0x20021 + } + + [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + internal enum SamAliasFlags + { + AddMembers = 0x1, + RemoveMembers = 0x2, + ListMembers = 0x4, + ReadInfo = 0x8, + WriteAccount = 0x10, + AllAccess = 0xf001f, + Read = 0x20004, + Write = 0x20013, + Execute = 0x20008 + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/SAMRPCNative/SAMMethods.cs b/src/SharpHoundRPC/SAMRPCNative/SAMMethods.cs new file mode 100644 index 00000000..e17de5b6 --- /dev/null +++ b/src/SharpHoundRPC/SAMRPCNative/SAMMethods.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Principal; +using SharpHoundRPC.Handles; +using SharpHoundRPC.Shared; + +namespace SharpHoundRPC.SAMRPCNative +{ + [SuppressUnmanagedCodeSecurity] + public static class SAMMethods + { + internal static (NtStatus status, SAMHandle handle) SamConnect(string serverName, + SAMEnums.SamAccessMasks requestedConnectAccess) + { + var us = new SharedStructs.UnicodeString(serverName); + var objectAttributes = default(SAMStructs.ObjectAttributes); + + var status = SamConnect(ref us, out var handle, requestedConnectAccess, ref objectAttributes); + objectAttributes.Dispose(); + + return (status, handle); + } + + [DllImport("samlib.dll", CharSet = CharSet.Unicode)] + private static extern NtStatus SamConnect( + ref SharedStructs.UnicodeString serverName, + out SAMHandle serverHandle, + SAMEnums.SamAccessMasks desiredAccess, + ref SAMStructs.ObjectAttributes objectAttributes + ); + + internal static (NtStatus status, SAMPointer domainRids, int count) + SamEnumerateDomainsInSamServer(SAMHandle serverHandle) + { + var enumerationContext = 0; + var status = + SamEnumerateDomainsInSamServer(serverHandle, ref enumerationContext, out var domains, -1, + out var count); + + return (status, domains, count); + } + + [DllImport("samlib.dll", CharSet = CharSet.Unicode)] + private static extern NtStatus SamEnumerateDomainsInSamServer( + SAMHandle serverHandle, + ref int enumerationContext, + out SAMPointer buffer, + int prefMaxLen, + out int count + ); + + internal static (NtStatus status, SAMPointer securityIdentifier) SamLookupDomainInSamServer( + SAMHandle serverHandle, string name) + { + var us = new SharedStructs.UnicodeString(name); + var status = SamLookupDomainInSamServer(serverHandle, ref us, out var sid); + + return (status, sid); + } + + [DllImport("samlib.dll", CharSet = CharSet.Unicode)] + private static extern NtStatus SamLookupDomainInSamServer( + SAMHandle serverHandle, + ref SharedStructs.UnicodeString name, + out SAMPointer sid); + + internal static (NtStatus status, SAMHandle domainHandle) SamOpenDomain(SAMHandle serverHandle, + SAMEnums.DomainAccessMask desiredAccess, byte[] domainSid) + { + var status = SamOpenDomain(serverHandle, desiredAccess, domainSid, out var domainHandle); + return (status, domainHandle); + } + + [DllImport("samlib.dll", CharSet = CharSet.Unicode)] + private static extern NtStatus SamOpenDomain( + SAMHandle serverHandle, + SAMEnums.DomainAccessMask desiredAccess, + [MarshalAs(UnmanagedType.LPArray)] byte[] domainSid, + out SAMHandle domainHandle + ); + + internal static (NtStatus status, SAMSidArray members, int count) SamGetMembersInAlias(SAMHandle aliasHandle) + { + var status = SamGetMembersInAlias(aliasHandle, out var members, out var count); + return (status, members, count); + } + + [DllImport("samlib.dll", CharSet = CharSet.Unicode)] + private static extern NtStatus SamGetMembersInAlias( + SAMHandle aliasHandle, + out SAMSidArray members, + out int count + ); + + internal static (NtStatus status, SAMHandle aliasHandle) SamOpenAlias(SAMHandle domainHandle, + SAMEnums.AliasOpenFlags desiredAccess, int aliasId) + { + var status = SamOpenAlias(domainHandle, desiredAccess, aliasId, out var aliasHandle); + return (status, aliasHandle); + } + + [DllImport("samlib.dll", CharSet = CharSet.Unicode)] + private static extern NtStatus SamOpenAlias( + SAMHandle domainHandle, + SAMEnums.AliasOpenFlags desiredAccess, + int aliasId, + out SAMHandle aliasHandle + ); + + internal static (NtStatus status, SAMPointer pointer, int count) SamEnumerateAliasesInDomain( + SAMHandle domainHandle) + { + var enumerationContext = 0; + var status = SamEnumerateAliasesInDomain(domainHandle, ref enumerationContext, out var buffer, -1, + out var count); + return (status, buffer, count); + } + + [DllImport("samlib.dll", CharSet = CharSet.Unicode)] + private static extern NtStatus SamEnumerateAliasesInDomain( + SAMHandle domainHandle, + ref int enumerationContext, + out SAMPointer buffer, + int prefMaxLen, + out int count + ); + + internal static (NtStatus status, SAMPointer names, SAMPointer use) SamLookupIdsInDomain(SAMHandle domainHandle, + int rid) + { + var rids = new[] {rid}; + var status = SamLookupIdsInDomain(domainHandle, 1, rids, out var names, out var use); + return (status, names, use); + } + + [DllImport("samlib.dll", CharSet = CharSet.Unicode)] + private static extern NtStatus SamLookupIdsInDomain(SAMHandle domainHandle, + int count, + int[] rids, + out SAMPointer names, + out SAMPointer use); + + #region Cleanup + + [DllImport("samlib.dll")] + internal static extern NtStatus SamFreeMemory( + IntPtr handle + ); + + [DllImport("samlib.dll")] + internal static extern NtStatus SamCloseHandle( + IntPtr handle + ); + + #endregion + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/SAMRPCNative/SAMStructs.cs b/src/SharpHoundRPC/SAMRPCNative/SAMStructs.cs new file mode 100644 index 00000000..0bbc643f --- /dev/null +++ b/src/SharpHoundRPC/SAMRPCNative/SAMStructs.cs @@ -0,0 +1,35 @@ +using System; +using System.Runtime.InteropServices; +using SharpHoundRPC.Shared; + +namespace SharpHoundRPC.SAMRPCNative +{ + public static class SAMStructs + { + public struct ObjectAttributes : IDisposable + { + public void Dispose() + { + if (_objectName == IntPtr.Zero) return; + Marshal.DestroyStructure(_objectName, typeof(SharedStructs.UnicodeString)); + Marshal.FreeHGlobal(_objectName); + _objectName = IntPtr.Zero; + } + + public int Length; + public IntPtr RootDirectory; + public uint Attributes; + public IntPtr SID; + public IntPtr Qos; + private IntPtr _objectName; + public SharedStructs.UnicodeString ObjectName; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SamRidEnumeration + { + public int Rid; + public SharedStructs.UnicodeString Name; + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Shared/SharedEnums.cs b/src/SharpHoundRPC/Shared/SharedEnums.cs new file mode 100644 index 00000000..5b752f1d --- /dev/null +++ b/src/SharpHoundRPC/Shared/SharedEnums.cs @@ -0,0 +1,20 @@ +namespace SharpHoundRPC.Shared +{ + public class SharedEnums + { + public enum SidNameUse + { + User = 1, + Group, + Domain, + Alias, + WellKnownGroup, + DeletedAccount, + Invalid, + Unknown, + Computer, + Label, + LogonSession + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Shared/SharedStructs.cs b/src/SharpHoundRPC/Shared/SharedStructs.cs new file mode 100644 index 00000000..0ee9a085 --- /dev/null +++ b/src/SharpHoundRPC/Shared/SharedStructs.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.InteropServices; + +namespace SharpHoundRPC.Shared +{ + public class SharedStructs + { + [StructLayout(LayoutKind.Sequential)] + public struct UnicodeString : IDisposable + { + private readonly ushort Length; + private readonly ushort MaximumLength; + private IntPtr Buffer; + + public UnicodeString(string s) + : this() + { + if (s == null) return; + Length = (ushort) (s.Length * 2); + MaximumLength = (ushort) (Length + 2); + Buffer = Marshal.StringToHGlobalUni(s); + } + + public void Dispose() + { + if (Buffer == IntPtr.Zero) return; + Marshal.FreeHGlobal(Buffer); + Buffer = IntPtr.Zero; + } + + public override string ToString() + { + return (Buffer != IntPtr.Zero ? Marshal.PtrToStringUni(Buffer, Length / 2) : null) ?? + throw new InvalidOperationException(); + } + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/SharpHoundRPC.csproj b/src/SharpHoundRPC/SharpHoundRPC.csproj new file mode 100644 index 00000000..98e82a66 --- /dev/null +++ b/src/SharpHoundRPC/SharpHoundRPC.csproj @@ -0,0 +1,25 @@ + + + net462 + library + SharpHoundRPC + latest + Rohan Vazarkar + SpecterOps + SAM/LSA Wrapper for C# BloodHound tasks + GPL-3.0-only + 1.0.0 + SharpHoundRPC + SharpHoundRPC + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + full + + + + + + + + \ No newline at end of file diff --git a/src/SharpHoundRPC/Wrappers/LSABase.cs b/src/SharpHoundRPC/Wrappers/LSABase.cs new file mode 100644 index 00000000..47e451ef --- /dev/null +++ b/src/SharpHoundRPC/Wrappers/LSABase.cs @@ -0,0 +1,33 @@ +using System; +using SharpHoundRPC.Handles; + +namespace SharpHoundRPC.Wrappers +{ + public class LSABase : IDisposable + { + protected LSAHandle Handle; + + protected LSABase(LSAHandle handle) + { + Handle = handle; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) ReleaseHandle(); + } + + protected virtual void ReleaseHandle() + { + Handle?.Dispose(); + Handle = null; + //Call suppressfinalize to prevent finalization, since we've already cleaned up our own stuff + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Wrappers/LSAPolicy.cs b/src/SharpHoundRPC/Wrappers/LSAPolicy.cs new file mode 100644 index 00000000..5f3d98a4 --- /dev/null +++ b/src/SharpHoundRPC/Wrappers/LSAPolicy.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using SharpHoundRPC.Handles; +using SharpHoundRPC.LSANative; +using SharpHoundRPC.Shared; + +namespace SharpHoundRPC.Wrappers +{ + public class LSAPolicy : LSABase + { + private string _computerName; + + public LSAPolicy(string computerName, LSAHandle handle) : base(handle) + { + _computerName = computerName; + } + + public static Result OpenPolicy(string computerName, LSAEnums.LsaOpenMask desiredAccess = + LSAEnums.LsaOpenMask.LookupNames | LSAEnums.LsaOpenMask.ViewLocalInfo) + { + var (status, handle) = LSAMethods.LsaOpenPolicy(computerName, desiredAccess); + if (status.IsError()) return status; + + return new LSAPolicy(computerName, handle); + } + + public Result<(string Name, string Sid)> GetLocalDomainInformation() + { + var result = LSAMethods.LsaQueryInformationPolicy(Handle, + LSAEnums.LSAPolicyInformation.PolicyAccountDomainInformation); + + if (result.status.IsError()) return result.status; + + var domainInfo = result.pointer.GetData(); + var domainSid = new SecurityIdentifier(domainInfo.DomainSid); + return (domainInfo.DomainName.ToString(), domainSid.Value.ToUpper()); + } + + public Result> GetPrincipalsWithPrivilege(string userRight) + { + var (status, sids, count) = LSAMethods.LsaEnumerateAccountsWithUserRight(Handle, userRight); + + if (status.IsError()) return status; + + return Result>.Ok(sids.GetEnumerable(count)); + } + + public Result> + GetResolvedPrincipalsWithPrivilege(string userRight) + { + var (status, sids, count) = LSAMethods.LsaEnumerateAccountsWithUserRight(Handle, userRight); + using (sids) + { + if (status.IsError()) return status; + + var (lookupStatus, referencedDomains, names, lookupCount) = + LSAMethods.LsaLookupSids(Handle, sids, count); + if (lookupStatus.IsError()) + { + referencedDomains.Dispose(); + names.Dispose(); + return lookupStatus; + } + + var translatedNames = names.GetEnumerable(count).ToArray(); + var domainList = referencedDomains.GetData(); + var safeDomains = new LSAPointer(domainList.Domains); + var domains = safeDomains.GetEnumerable(domainList.Entries).ToArray(); + var convertedSids = sids.GetEnumerable(lookupCount).ToArray(); + + var ret = new List<(SecurityIdentifier sid, string Name, SharedEnums.SidNameUse Use, string Domain)>(); + for (var i = 0; i < count; i++) + ret.Add((convertedSids[i], translatedNames[i].Name.ToString(), translatedNames[i].Use, + domains[translatedNames[i].DomainIndex].Name.ToString())); + + referencedDomains.Dispose(); + names.Dispose(); + safeDomains.Dispose(); + + return ret; + } + } + + public Result<(string Name, SharedEnums.SidNameUse Use, string Domains)> LookupSid(SecurityIdentifier sid) + { + if (sid == null) + return "SID cannot be null"; + + var (status, referencedDomains, names, count) = LSAMethods.LsaLookupSids(Handle, new[] {sid}); + if (status.IsError()) + { + names.Dispose(); + referencedDomains.Dispose(); + return status; + } + + var translatedNames = names.GetEnumerable(count).ToArray(); + var domainList = referencedDomains.GetData(); + var safeDomains = new LSAPointer(domainList.Domains); + var domains = safeDomains.GetEnumerable(domainList.Entries).ToArray(); + names.Dispose(); + referencedDomains.Dispose(); + safeDomains.Dispose(); + return (translatedNames[0].Name.ToString(), translatedNames[0].Use, + domains[translatedNames[0].DomainIndex].Name.ToString()); + } + + public Result> + LookupSids( + SecurityIdentifier[] sids) + { + sids = sids.Where(x => x != null).ToArray(); + if (sids.Length == 0) + return "No non-null SIDs specified"; + + var (status, referencedDomains, names, count) = LSAMethods.LsaLookupSids(Handle, sids); + if (status.IsError()) + { + referencedDomains.Dispose(); + names.Dispose(); + return status; + } + + var translatedNames = names.GetEnumerable(count).ToArray(); + var domainList = referencedDomains.GetData(); + var safeDomains = new LSAPointer(domainList.Domains); + var domains = safeDomains.GetEnumerable(domainList.Entries).ToArray(); + + var ret = new List<(SecurityIdentifier Sid, string Name, SharedEnums.SidNameUse Use, string Domain)>(); + for (var i = 0; i < count; i++) + ret.Add((sids[i], translatedNames[i].Name.ToString(), translatedNames[i].Use, + domains[translatedNames[i].DomainIndex].Name.ToString())); + + referencedDomains.Dispose(); + names.Dispose(); + safeDomains.Dispose(); + + return ret.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Wrappers/SAMAlias.cs b/src/SharpHoundRPC/Wrappers/SAMAlias.cs new file mode 100644 index 00000000..1c70f5b2 --- /dev/null +++ b/src/SharpHoundRPC/Wrappers/SAMAlias.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Security.Principal; +using SharpHoundRPC.Handles; +using SharpHoundRPC.SAMRPCNative; + +namespace SharpHoundRPC.Wrappers +{ + public class SAMAlias : SAMBase + { + public SAMAlias(SAMHandle handle) : base(handle) + { + } + + public string Name { get; set; } + public int Rid { get; set; } + + public Result> GetMembers() + { + var (status, members, count) = SAMMethods.SamGetMembersInAlias(Handle); + + if (status.IsError()) + { + return status; + } + + return Result>.Ok(members.GetData(count)); + + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Wrappers/SAMBase.cs b/src/SharpHoundRPC/Wrappers/SAMBase.cs new file mode 100644 index 00000000..d0dd1166 --- /dev/null +++ b/src/SharpHoundRPC/Wrappers/SAMBase.cs @@ -0,0 +1,33 @@ +using System; +using SharpHoundRPC.Handles; + +namespace SharpHoundRPC.Wrappers +{ + public class SAMBase : IDisposable + { + protected SAMHandle Handle; + + protected SAMBase(SAMHandle handle) + { + Handle = handle; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) ReleaseHandle(); + } + + protected virtual void ReleaseHandle() + { + Handle?.Dispose(); + Handle = null; + //Call suppressfinalize to prevent finalization, since we've already cleaned up our own stuff + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Wrappers/SAMDomain.cs b/src/SharpHoundRPC/Wrappers/SAMDomain.cs new file mode 100644 index 00000000..3a3a0be8 --- /dev/null +++ b/src/SharpHoundRPC/Wrappers/SAMDomain.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SharpHoundRPC.Handles; +using SharpHoundRPC.SAMRPCNative; +using SharpHoundRPC.Shared; + +namespace SharpHoundRPC.Wrappers +{ + public class SAMDomain : SAMBase + { + public SAMDomain(SAMHandle handle) : base(handle) + { + } + + public Result<(string Name, SharedEnums.SidNameUse Type)> LookupPrincipalByRid(int rid) + { + var (status, namePointer, usePointer) = SAMMethods.SamLookupIdsInDomain(Handle, rid); + using (namePointer) + { + using (usePointer) + { + if (status.IsError()) return status; + + return (namePointer.GetData().ToString(), + (SharedEnums.SidNameUse) usePointer.GetData()); + } + } + } + + public Result> GetAliases() + { + var (status, ridPointer, count) = SAMMethods.SamEnumerateAliasesInDomain(Handle); + + if (status.IsError()) + { + return status; + } + + var ret = Result>.Ok(ridPointer + .GetEnumerable(count) + .Select(x => (x.Name.ToString(), x.Rid))); + + return ret; + } + + public Result OpenAlias(int rid, + SAMEnums.AliasOpenFlags desiredAccess = SAMEnums.AliasOpenFlags.ListMembers) + { + var (status, aliasHandle) = SAMMethods.SamOpenAlias(Handle, desiredAccess, rid); + if (status.IsError()) return status; + + return new SAMAlias(aliasHandle); + } + + public Result OpenAlias(string name) + { + var getAliasesResult = GetAliases(); + if (getAliasesResult.IsFailed) return getAliasesResult.Status; + + foreach (var alias in getAliasesResult.Value) + if (alias.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + return OpenAlias(alias.Rid); + + return $"Alias {name} was not found"; + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Wrappers/SAMServer.cs b/src/SharpHoundRPC/Wrappers/SAMServer.cs new file mode 100644 index 00000000..23a15121 --- /dev/null +++ b/src/SharpHoundRPC/Wrappers/SAMServer.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using SharpHoundRPC.Handles; +using SharpHoundRPC.SAMRPCNative; +using SharpHoundRPC.Shared; + +namespace SharpHoundRPC.Wrappers +{ + public class SAMServer : SAMBase + { + private readonly ConcurrentDictionary _domainHandleCache; + private SecurityIdentifier _cachedMachineSid; + + public SAMServer(SAMHandle handle, string computerName) : base(handle) + { + _domainHandleCache = new ConcurrentDictionary(); + ComputerName = computerName; + } + + public string ComputerName { get; } + + public static Result OpenServer(string computerName, SAMEnums.SamAccessMasks requestedConnectAccess = + SAMEnums.SamAccessMasks.SamServerConnect | + SAMEnums.SamAccessMasks + .SamServerEnumerateDomains | + SAMEnums.SamAccessMasks.SamServerLookupDomain) + { + var (status, handle) = SAMMethods.SamConnect(computerName, requestedConnectAccess); + + return status.IsError() + ? status + : new SAMServer(handle, computerName); + } + + public Result> GetDomains() + { + var (status, rids, count) = SAMMethods.SamEnumerateDomainsInSamServer(Handle); + if (status.IsError()) + { + return status; + } + + var ret = Result>.Ok(rids.GetEnumerable(count).Select(x => (x.Name.ToString(), x.Rid))); + return ret; + } + + public Result LookupDomain(string name) + { + var (status, sid) = SAMMethods.SamLookupDomainInSamServer(Handle, name); + using (sid) + return status.IsError() ? status : sid.GetData(); + } + + public Result GetMachineSid(string testName = null) + { + if (_cachedMachineSid != null) + return _cachedMachineSid; + + SecurityIdentifier sid = null; + + if (testName != null) + { + var result = LookupDomain(testName); + if (result.IsSuccess) sid = result.Value; + } + + if (sid == null) + { + var domainResult = GetDomains(); + if (domainResult.IsSuccess) + { + var result = LookupDomain(domainResult.Value.FirstOrDefault().Name); + if (result.IsSuccess) sid = result.Value; + } + } + + if (sid == null) return "Unable to get machine sid"; + _cachedMachineSid = sid; + return sid; + } + + public bool IsDomainController(SecurityIdentifier domainMachineSid) + { + return domainMachineSid.AccountDomainSid.Equals(GetMachineSid().Value.AccountDomainSid); + } + + public Result<(string Name, SharedEnums.SidNameUse Type)> LookupPrincipalBySid( + SecurityIdentifier securityIdentifier) + { + var openDomainResult = OpenDomain(securityIdentifier); + if (openDomainResult.IsFailed) return $"OpenDomain returned {openDomainResult.Status}"; + + var domain = openDomainResult.Value; + + return domain.LookupPrincipalByRid(securityIdentifier.Rid()); + } + + public Result OpenDomain(string domainName, SAMEnums.DomainAccessMask requestedDomainAccess = + SAMEnums.DomainAccessMask.Lookup | + SAMEnums.DomainAccessMask.ListAccounts) + { + var lookupResult = LookupDomain(domainName); + if (lookupResult.IsFailed) return $"LookupDomain returned {lookupResult.Error}"; + + var sid = lookupResult.Value; + + if (_domainHandleCache.TryGetValue(sid.Value, out var domain)) return domain; + + var (status, domainHandle) = SAMMethods.SamOpenDomain(Handle, requestedDomainAccess, sid.GetBytes()); + if (status.IsError()) return status; + + domain = new SAMDomain(domainHandle); + + _domainHandleCache.TryAdd(sid.Value, domain); + return domain; + } + + public Result OpenDomain(SecurityIdentifier securityIdentifier, + SAMEnums.DomainAccessMask requestedDomainAccess = + SAMEnums.DomainAccessMask.Lookup | + SAMEnums.DomainAccessMask.ListAccounts) + { + if (_domainHandleCache.TryGetValue(securityIdentifier.Value, out var domain)) return domain; + + var (status, domainHandle) = + SAMMethods.SamOpenDomain(Handle, requestedDomainAccess, securityIdentifier.GetBytes()); + if (status.IsError()) return status.ToString(); + + domain = new SAMDomain(domainHandle); + _domainHandleCache.TryAdd(securityIdentifier.Value, domain); + return domain; + } + + protected override void Dispose(bool disposing) + { + foreach (var domainHandle in _domainHandleCache.Values) domainHandle.Dispose(); + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/test/unit/ACLProcessorTest.cs b/test/unit/ACLProcessorTest.cs index d095a3c1..f5654234 100644 --- a/test/unit/ACLProcessorTest.cs +++ b/test/unit/ACLProcessorTest.cs @@ -55,7 +55,7 @@ public void SanityCheck() public void ACLProcessor_IsACLProtected_NullNTSD_ReturnsFalse() { var processor = new ACLProcessor(new MockLDAPUtils(), true); - var result = processor.IsACLProtected((byte[])null); + var result = processor.IsACLProtected((byte[]) null); Assert.False(result); } @@ -206,7 +206,7 @@ public void ACLProcessor_ProcessGMSAReaders_Null_PrincipalID() var collection = new List(); mockRule.Setup(x => x.AccessControlType()).Returns(AccessControlType.Allow); - mockRule.Setup(x => x.IdentityReference()).Returns((string)null); + mockRule.Setup(x => x.IdentityReference()).Returns((string) null); collection.Add(mockRule.Object); mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) @@ -267,7 +267,7 @@ public void ACLProcessor_ProcessACL_Null_SID() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); var processor = new ACLProcessor(mockLDAPUtils.Object, true); @@ -287,7 +287,7 @@ public void ACLProcessor_ProcessACL_Null_ACE() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); var processor = new ACLProcessor(mockLDAPUtils.Object, true); @@ -309,7 +309,7 @@ public void ACLProcessor_ProcessACL_Deny_ACE() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); var processor = new ACLProcessor(mockLDAPUtils.Object, true); @@ -332,7 +332,7 @@ public void ACLProcessor_ProcessACL_Unmatched_Inheritance_ACE() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); var processor = new ACLProcessor(mockLDAPUtils.Object, true); @@ -351,12 +351,12 @@ public void ACLProcessor_ProcessACL_Null_SID_ACE() var collection = new List(); mockRule.Setup(x => x.AccessControlType()).Returns(AccessControlType.Allow); mockRule.Setup(x => x.IsAceInheritedFrom(It.IsAny())).Returns(true); - mockRule.Setup(x => x.IdentityReference()).Returns((string)null); + mockRule.Setup(x => x.IdentityReference()).Returns((string) null); collection.Add(mockRule.Object); mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); var processor = new ACLProcessor(mockLDAPUtils.Object, true); @@ -386,7 +386,7 @@ public void ACLProcessor_ProcessACL_GenericAll_Unmatched_Guid() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -417,7 +417,7 @@ public void ACLProcessor_ProcessACL_GenericAll() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -454,7 +454,7 @@ public void ACLProcessor_ProcessACL_WriteDacl() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -491,7 +491,7 @@ public void ACLProcessor_ProcessACL_WriteOwner() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -528,7 +528,7 @@ public void ACLProcessor_ProcessACL_Self() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -565,7 +565,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_Unmatched() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -597,7 +597,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetChanges mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -634,7 +634,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_All() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -671,7 +671,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetChanges mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -709,7 +709,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_User_Unmatched() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -741,7 +741,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_User_UserForceChangePassword() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -778,7 +778,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_User_All() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -815,7 +815,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Computer_NoLAPS() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -847,7 +847,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Computer_All() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -889,7 +889,7 @@ public void ACLProcessor_ProcessACL_GenericWrite_Unmatched() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -921,7 +921,7 @@ public void ACLProcessor_ProcessACL_GenericWrite_User_All() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -958,7 +958,7 @@ public void ACLProcessor_ProcessACL_GenericWrite_User_WriteMember() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); @@ -997,7 +997,7 @@ public void ACLProcessor_ProcessACL_GenericWrite_Computer_WriteAllowedToAct() mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); - mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); diff --git a/test/unit/CommonLibTest.csproj b/test/unit/CommonLibTest.csproj index abbbd657..be39b0c0 100644 --- a/test/unit/CommonLibTest.csproj +++ b/test/unit/CommonLibTest.csproj @@ -1,47 +1,48 @@ - - net5.0 - false - true - ..\..\docfx\coverage\ - OpenCover - + + net5.0 + false + true + ..\..\docfx\coverage\ + OpenCover + - - - - + + + + + - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + - - - - - - - + + + + + + + diff --git a/test/unit/ComputerSessionProcessorTest.cs b/test/unit/ComputerSessionProcessorTest.cs index be509a00..c120610c 100644 --- a/test/unit/ComputerSessionProcessorTest.cs +++ b/test/unit/ComputerSessionProcessorTest.cs @@ -6,6 +6,7 @@ using SharpHoundCommonLib; using SharpHoundCommonLib.OutputTypes; using SharpHoundCommonLib.Processors; +using SharpHoundRPC.NetAPINative; using Xunit; using Xunit.Abstractions; @@ -37,25 +38,14 @@ public void Dispose() public async Task ComputerSessionProcessor_ReadUserSessions_FilteringWorks() { var mockNativeMethods = new Mock(); - var apiResult = new NativeMethods.SESSION_INFO_10[] + + var apiResult = new NetSessionEnumResults[] { - new() - { - sesi10_username = "dfm", - sesi10_cname = "\\\\192.168.92.110" - }, - new() - { - sesi10_cname = "", - sesi10_username = "admin" - }, - new() - { - sesi10_username = "admin", - sesi10_cname = "\\\\192.168.92.110" - } + new("dfm", "\\\\192.168.92.110"), + new("admin", ""), + new("admin", "\\\\192.168.92.110") }; - mockNativeMethods.Setup(x => x.CallNetSessionEnum(It.IsAny())).Returns(apiResult); + mockNativeMethods.Setup(x => x.NetSessionEnum(It.IsAny())).Returns(apiResult); var processor = new ComputerSessionProcessor(new MockLDAPUtils(), "dfm", mockNativeMethods.Object); var result = await processor.ReadUserSessions("win10", _computerSid, _computerDomain); @@ -67,15 +57,11 @@ public async Task ComputerSessionProcessor_ReadUserSessions_FilteringWorks() public async Task ComputerSessionProcessor_ReadUserSessions_ResolvesHost() { var mockNativeMethods = new Mock(); - var apiResult = new NativeMethods.SESSION_INFO_10[] + var apiResult = new NetSessionEnumResults[] { - new() - { - sesi10_username = "admin", - sesi10_cname = "\\\\192.168.1.1" - } + new("admin", "\\\\192.168.1.1") }; - mockNativeMethods.Setup(x => x.CallNetSessionEnum(It.IsAny())).Returns(apiResult); + mockNativeMethods.Setup(x => x.NetSessionEnum(It.IsAny())).Returns(apiResult); var expected = new Session[] { @@ -96,15 +82,11 @@ public async Task ComputerSessionProcessor_ReadUserSessions_ResolvesHost() public async Task ComputerSessionProcessor_ReadUserSessions_ResolvesLocalHostEquivalent() { var mockNativeMethods = new Mock(); - var apiResult = new NativeMethods.SESSION_INFO_10[] + var apiResult = new NetSessionEnumResults[] { - new() - { - sesi10_username = "admin", - sesi10_cname = "\\\\127.0.0.1" - } + new("admin", "\\\\127.0.0.1") }; - mockNativeMethods.Setup(x => x.CallNetSessionEnum(It.IsAny())).Returns(apiResult); + mockNativeMethods.Setup(x => x.NetSessionEnum(It.IsAny())).Returns(apiResult); var expected = new Session[] { @@ -125,15 +107,11 @@ public async Task ComputerSessionProcessor_ReadUserSessions_ResolvesLocalHostEqu public async Task ComputerSessionProcessor_ReadUserSessions_MultipleMatches_AddsAll() { var mockNativeMethods = new Mock(); - var apiResult = new NativeMethods.SESSION_INFO_10[] + var apiResult = new NetSessionEnumResults[] { - new() - { - sesi10_username = "administrator", - sesi10_cname = "\\\\127.0.0.1" - } + new("administrator", "\\\\127.0.0.1") }; - mockNativeMethods.Setup(x => x.CallNetSessionEnum(It.IsAny())).Returns(apiResult); + mockNativeMethods.Setup(x => x.NetSessionEnum(It.IsAny())).Returns(apiResult); var expected = new Session[] { @@ -159,15 +137,11 @@ public async Task ComputerSessionProcessor_ReadUserSessions_MultipleMatches_Adds public async Task ComputerSessionProcessor_ReadUserSessions_NoGCMatch_TriesResolve() { var mockNativeMethods = new Mock(); - var apiResult = new NativeMethods.SESSION_INFO_10[] + var apiResult = new NetSessionEnumResults[] { - new() - { - sesi10_username = "test", - sesi10_cname = "\\\\127.0.0.1" - } + new("test", "\\\\127.0.0.1") }; - mockNativeMethods.Setup(x => x.CallNetSessionEnum(It.IsAny())).Returns(apiResult); + mockNativeMethods.Setup(x => x.NetSessionEnum(It.IsAny())).Returns(apiResult); var expected = new Session[] { @@ -185,19 +159,16 @@ public async Task ComputerSessionProcessor_ReadUserSessions_NoGCMatch_TriesResol } [WindowsOnlyFact] - public async Task ComputerSessionProcessor_ReadUserSessions_ComputerAccessDenied_ExceptionCaught() + public async Task ComputerSessionProcessor_ReadUserSessions_ComputerAccessDenied_Handled() { var mockNativeMethods = new Mock(); //mockNativeMethods.Setup(x => x.CallSamConnect(ref It.Ref.IsAny, out It.Ref.IsAny, It.IsAny(), ref It.Ref.IsAny)).Returns(NativeMethods.NtStatus.StatusAccessDenied); - var ex = new APIException - { - Status = NativeMethods.NERR.ERROR_ACCESS_DENIED.ToString() - }; - mockNativeMethods.Setup(x => x.CallNetSessionEnum(It.IsAny())).Throws(ex); + mockNativeMethods.Setup(x => x.NetSessionEnum(It.IsAny())) + .Returns(NetAPIEnums.NetAPIStatus.ErrorAccessDenied); var processor = new ComputerSessionProcessor(new MockLDAPUtils(), "dfm", mockNativeMethods.Object); var test = await processor.ReadUserSessions("test", "test", "test"); Assert.False(test.Collected); - Assert.Equal(NativeMethods.NERR.ERROR_ACCESS_DENIED.ToString(), test.FailureReason); + Assert.Equal(NetAPIEnums.NetAPIStatus.ErrorAccessDenied.ToString(), test.FailureReason); } [WindowsOnlyFact] @@ -205,15 +176,12 @@ public async Task ComputerSessionProcessor_ReadUserSessionsPrivileged_ComputerAc { var mockNativeMethods = new Mock(); //mockNativeMethods.Setup(x => x.CallSamConnect(ref It.Ref.IsAny, out It.Ref.IsAny, It.IsAny(), ref It.Ref.IsAny)).Returns(NativeMethods.NtStatus.StatusAccessDenied); - var ex = new APIException - { - Status = NativeMethods.NERR.ERROR_ACCESS_DENIED.ToString() - }; - mockNativeMethods.Setup(x => x.CallNetWkstaUserEnum(It.IsAny())).Throws(ex); + mockNativeMethods.Setup(x => x.NetWkstaUserEnum(It.IsAny())) + .Returns(NetAPIEnums.NetAPIStatus.ErrorAccessDenied); var processor = new ComputerSessionProcessor(new MockLDAPUtils(), "dfm", mockNativeMethods.Object); var test = processor.ReadUserSessionsPrivileged("test", "test", "test"); Assert.False(test.Collected); - Assert.Equal(NativeMethods.NERR.ERROR_ACCESS_DENIED.ToString(), test.FailureReason); + Assert.Equal(NetAPIEnums.NetAPIStatus.ErrorAccessDenied.ToString(), test.FailureReason); } [WindowsOnlyFact] @@ -223,80 +191,20 @@ public async Task ComputerSessionProcessor_ReadUserSessionsPrivileged_FilteringW const string samAccountName = "WIN10"; //This is a sample response from a computer in a test environment. The duplicates are intentional - var apiResults = new NativeMethods.WKSTA_USER_INFO_1[] + var apiResults = new NetWkstaUserEnumResults[] { - new() - { - wkui1_logon_domain = "TESTLAB", - wkui1_logon_server = "PRIMARY", - wkui1_oth_domains = "", - wkui1_username = "dfm" - }, - new() - { - wkui1_logon_domain = "PRIMARY", - wkui1_logon_server = "", - wkui1_oth_domains = "", - wkui1_username = "Administrator" - }, - new() - { - wkui1_logon_domain = "", - wkui1_logon_server = "PRIMARY", - wkui1_oth_domains = "", - wkui1_username = "Administrator" - }, - new() - { - wkui1_logon_domain = "TESTLAB", - wkui1_logon_server = "", - wkui1_oth_domains = "", - wkui1_username = "WIN10$" - }, - new() - { - wkui1_logon_domain = "TESTLAB", - wkui1_logon_server = "", - wkui1_oth_domains = "", - wkui1_username = "WIN10$" - }, - new() - { - wkui1_logon_domain = "TESTLAB", - wkui1_logon_server = "", - wkui1_oth_domains = "", - wkui1_username = "WIN10$" - }, - new() - { - wkui1_logon_domain = "TESTLAB", - wkui1_logon_server = "", - wkui1_oth_domains = "", - wkui1_username = "WIN10$" - }, - new() - { - wkui1_logon_domain = "WIN10", - wkui1_logon_server = "", - wkui1_oth_domains = "", - wkui1_username = "JOHN" - }, - new() - { - wkui1_logon_domain = "NT AUTHORITY", - wkui1_logon_server = "", - wkui1_oth_domains = "", - wkui1_username = "SYSTEM" - }, - new() - { - wkui1_logon_domain = "TESTLAB", - wkui1_logon_server = "", - wkui1_oth_domains = "", - wkui1_username = "ABC" - } + new("dfm", "TESTLAB"), + new("Administrator", "PRIMARY"), + new("Administrator", ""), + new("WIN10$", "TESTLAB"), + new("WIN10$", "TESTLAB"), + new("WIN10$", "TESTLAB"), + new("WIN10$", "TESTLAB"), + new("JOHN", "WIN10"), + new("SYSTEM", "NT AUTHORITY"), + new("ABC", "TESTLAB") }; - mockNativeMethods.Setup(x => x.CallNetWkstaUserEnum(It.IsAny())).Returns(apiResults); + mockNativeMethods.Setup(x => x.NetWkstaUserEnum(It.IsAny())).Returns(apiResults); var expected = new Session[] { diff --git a/test/unit/Facades/MockLDAPUtils.cs b/test/unit/Facades/MockLDAPUtils.cs index 8d92e58a..7f184371 100644 --- a/test/unit/Facades/MockLDAPUtils.cs +++ b/test/unit/Facades/MockLDAPUtils.cs @@ -10,7 +10,7 @@ using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; -using Domain = SharpHoundCommonLib.OutputTypes.Domain; +using Domain = System.DirectoryServices.ActiveDirectory.Domain; namespace CommonLibTest.Facades { @@ -40,10 +40,10 @@ public string[] GetUserGlobalCatalogMatches(string name) name = name.ToLower(); return name switch { - "dfm" => new[] { "S-1-5-21-3130019616-2776909439-2417379446-1105" }, + "dfm" => new[] {"S-1-5-21-3130019616-2776909439-2417379446-1105"}, "administrator" => new[] {"S-1-5-21-3130019616-2776909439-2417379446-500", "S-1-5-21-3084884204-958224920-2707782874-500"}, - "admin" => new[] { "S-1-5-21-3130019616-2776909439-2417379446-2116" }, + "admin" => new[] {"S-1-5-21-3130019616-2776909439-2417379446-2116"}, _ => Array.Empty() }; } @@ -711,7 +711,7 @@ public void AddDomainController(string domainControllerSID) _domainControllers.TryAdd(domainControllerSID, new byte()); } - public System.DirectoryServices.ActiveDirectory.Domain GetDomain(string domainName = null) + public Domain GetDomain(string domainName = null) { throw new NotImplementedException(); } @@ -727,7 +727,7 @@ public IEnumerable GetWellKnownPrincipalOutput(string domain) Label.Computer => new Computer(), Label.Group => new Group(), Label.GPO => new GPO(), - Label.Domain => new Domain(), + Label.Domain => new SharpHoundCommonLib.OutputTypes.Domain(), Label.OU => new OU(), Label.Container => new Container(), _ => throw new ArgumentOutOfRangeException() @@ -1049,7 +1049,7 @@ public ActiveDirectorySecurityDescriptor MakeSecurityDescriptor() private Group GetBaseEnterpriseDC() { - var g = new Group { ObjectIdentifier = "TESTLAB.LOCAL-S-1-5-9".ToUpper() }; + var g = new Group {ObjectIdentifier = "TESTLAB.LOCAL-S-1-5-9".ToUpper()}; g.Properties.Add("name", "ENTERPRISE DOMAIN CONTROLLERS@TESTLAB.LOCAL".ToUpper()); return g; } diff --git a/test/unit/GPOLocalGroupProcessorTest.cs b/test/unit/GPOLocalGroupProcessorTest.cs index ef9e0496..9cf1f6d6 100644 --- a/test/unit/GPOLocalGroupProcessorTest.cs +++ b/test/unit/GPOLocalGroupProcessorTest.cs @@ -145,13 +145,13 @@ public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups_Null_Gpcfilesyspath( var mockSearchResults = new List(); mockSearchResults.Add(mockSearchResultEntry.Object); mockLDAPUtils.Setup(x => x.QueryLDAP( - new LDAPQueryOptions - { - Filter = "(samaccounttype=805306369)", - Scope = SearchScope.Subtree, - Properties = CommonProperties.ObjectSID, - AdsPath = null - })) + new LDAPQueryOptions + { + Filter = "(samaccounttype=805306369)", + Scope = SearchScope.Subtree, + Properties = CommonProperties.ObjectSID, + AdsPath = null + })) .Returns(mockSearchResults.ToArray()); var processor = new GPOLocalGroupProcessor(mockLDAPUtils.Object); diff --git a/test/unit/LDAPUtilsTest.cs b/test/unit/LDAPUtilsTest.cs index 5e39cf4e..c68cc132 100644 --- a/test/unit/LDAPUtilsTest.cs +++ b/test/unit/LDAPUtilsTest.cs @@ -1,11 +1,11 @@ using System; +using System.DirectoryServices.Protocols; +using System.Threading; using CommonLibTest.Facades; using Moq; using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.Exceptions; -using System.DirectoryServices.Protocols; -using System.Threading; using Xunit; using Xunit.Abstractions; @@ -120,10 +120,11 @@ public void QueryLDAP_With_Exception() Assert.Throws( () => { - foreach (var sre in _utils.QueryLDAP(null, new SearchScope(), null, new CancellationToken(), null, false, false, null, false, false, true)) + foreach (var sre in _utils.QueryLDAP(null, new SearchScope(), null, new CancellationToken(), null, + false, false, null, false, false, true)) { // We shouldn't reach this anyway, and all we care about is if exceptions are bubbling - }; + } }); Assert.Throws( @@ -132,7 +133,7 @@ public void QueryLDAP_With_Exception() foreach (var sre in _utils.QueryLDAP(options)) { // We shouldn't reach this anyway, and all we care about is if exceptions are bubbling - }; + } }); } @@ -152,7 +153,7 @@ public void QueryLDAP_Without_Exception() foreach (var sre in _utils.QueryLDAP(null, new SearchScope(), null, new CancellationToken())) { // We shouldn't reach this anyway, and all we care about is if exceptions are bubbling - }; + } }); Assert.Null(exception); @@ -162,7 +163,7 @@ public void QueryLDAP_Without_Exception() foreach (var sre in _utils.QueryLDAP(options)) { // We shouldn't reach this anyway, and all we care about is if exceptions are bubbling - }; + } }); Assert.Null(exception); } diff --git a/test/unit/SPNProcessorsTest.cs b/test/unit/SPNProcessorsTest.cs index 2c6dc1cb..0ba7411b 100644 --- a/test/unit/SPNProcessorsTest.cs +++ b/test/unit/SPNProcessorsTest.cs @@ -29,7 +29,8 @@ public async Task ReadSPNTargets_NoPortSupplied_ParsedCorrectly() var expected = new SPNPrivilege { - ComputerSID = "S-1-5-21-3130019616-2776909439-2417379446-1001", Port = 1433, Service = EdgeNames.SQLAdmin + ComputerSID = "S-1-5-21-3130019616-2776909439-2417379446-1001", Port = 1433, + Service = EdgeNames.SQLAdmin }; await foreach (var actual in processor.ReadSPNTargets(servicePrincipalNames, distinguishedName)) @@ -49,7 +50,8 @@ public async Task ReadSPNTargets_BadPortSupplied_ParsedCorrectly() var expected = new SPNPrivilege { - ComputerSID = "S-1-5-21-3130019616-2776909439-2417379446-1001", Port = 1433, Service = EdgeNames.SQLAdmin + ComputerSID = "S-1-5-21-3130019616-2776909439-2417379446-1001", Port = 1433, + Service = EdgeNames.SQLAdmin }; await foreach (var actual in processor.ReadSPNTargets(servicePrincipalNames, distinguishedName)) @@ -69,7 +71,8 @@ public async void ReadSPNTargets_SuppliedPort_ParsedCorrectly() var expected = new SPNPrivilege { - ComputerSID = "S-1-5-21-3130019616-2776909439-2417379446-1001", Port = 2345, Service = EdgeNames.SQLAdmin + ComputerSID = "S-1-5-21-3130019616-2776909439-2417379446-1001", Port = 2345, + Service = EdgeNames.SQLAdmin }; await foreach (var actual in processor.ReadSPNTargets(servicePrincipalNames, distinguishedName))