| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.DirectoryServices.Protocols; |
| | 4 | | using System.Linq; |
| | 5 | | using System.Security.Cryptography.X509Certificates; |
| | 6 | | using System.Security.Principal; |
| | 7 | | using Microsoft.Extensions.Logging; |
| | 8 | | using SharpHoundCommonLib.Enums; |
| | 9 | |
|
| | 10 | | namespace SharpHoundCommonLib |
| | 11 | | { |
| | 12 | | public interface ISearchResultEntry |
| | 13 | | { |
| | 14 | | string DistinguishedName { get; } |
| | 15 | | ResolvedSearchResult ResolveBloodHoundInfo(); |
| | 16 | | string GetProperty(string propertyName); |
| | 17 | | byte[] GetByteProperty(string propertyName); |
| | 18 | | string[] GetArrayProperty(string propertyName); |
| | 19 | | byte[][] GetByteArrayProperty(string propertyName); |
| | 20 | | bool GetIntProperty(string propertyName, out int value); |
| | 21 | | X509Certificate2[] GetCertificateArrayProperty(string propertyName); |
| | 22 | | string GetObjectIdentifier(); |
| | 23 | | bool IsDeleted(); |
| | 24 | | Label GetLabel(); |
| | 25 | | string GetSid(); |
| | 26 | | string GetGuid(); |
| | 27 | | int PropCount(string prop); |
| | 28 | | IEnumerable<string> PropertyNames(); |
| | 29 | | bool IsMSA(); |
| | 30 | | bool IsGMSA(); |
| | 31 | | bool HasLAPS(); |
| | 32 | | } |
| | 33 | |
|
| | 34 | | public class SearchResultEntryWrapper : ISearchResultEntry |
| | 35 | | { |
| | 36 | | private const string GMSAClass = "msds-groupmanagedserviceaccount"; |
| | 37 | | private const string MSAClass = "msds-managedserviceaccount"; |
| | 38 | | private readonly SearchResultEntry _entry; |
| | 39 | | private readonly ILogger _log; |
| | 40 | | private readonly ILDAPUtils _utils; |
| | 41 | |
|
| 0 | 42 | | public SearchResultEntryWrapper(SearchResultEntry entry, ILDAPUtils utils = null, ILogger log = null) |
| 0 | 43 | | { |
| 0 | 44 | | _entry = entry; |
| 0 | 45 | | _utils = utils ?? new LDAPUtils(); |
| 0 | 46 | | _log = log ?? Logging.LogProvider.CreateLogger("SearchResultWrapper"); |
| 0 | 47 | | } |
| | 48 | |
|
| 0 | 49 | | public string DistinguishedName => _entry.DistinguishedName; |
| | 50 | |
|
| | 51 | | public ResolvedSearchResult ResolveBloodHoundInfo() |
| 0 | 52 | | { |
| 0 | 53 | | var res = new ResolvedSearchResult(); |
| | 54 | |
|
| 0 | 55 | | var objectId = GetObjectIdentifier(); |
| 0 | 56 | | if (objectId == null) |
| 0 | 57 | | { |
| 0 | 58 | | _log.LogWarning("ObjectIdentifier is null for {DN}", DistinguishedName); |
| 0 | 59 | | return null; |
| | 60 | | } |
| | 61 | |
|
| 0 | 62 | | var uac = _entry.GetProperty(LDAPProperties.UserAccountControl); |
| 0 | 63 | | if (int.TryParse(uac, out var flag)) |
| 0 | 64 | | { |
| 0 | 65 | | var flags = (UacFlags) flag; |
| 0 | 66 | | if (flags.HasFlag(UacFlags.ServerTrustAccount)) |
| 0 | 67 | | { |
| 0 | 68 | | _log.LogTrace("Marked {SID} as a domain controller", objectId); |
| 0 | 69 | | res.IsDomainController = true; |
| 0 | 70 | | _utils.AddDomainController(objectId); |
| 0 | 71 | | } |
| 0 | 72 | | } |
| | 73 | |
|
| | 74 | | //Try to resolve the domain |
| 0 | 75 | | var distinguishedName = DistinguishedName; |
| | 76 | | string itemDomain; |
| 0 | 77 | | if (distinguishedName == null) |
| 0 | 78 | | { |
| 0 | 79 | | if (objectId.StartsWith("S-1-")) |
| 0 | 80 | | { |
| 0 | 81 | | itemDomain = _utils.GetDomainNameFromSid(objectId); |
| 0 | 82 | | } |
| | 83 | | else |
| 0 | 84 | | { |
| 0 | 85 | | _log.LogWarning("Failed to resolve domain for {ObjectID}", objectId); |
| 0 | 86 | | return null; |
| | 87 | | } |
| 0 | 88 | | } |
| | 89 | | else |
| 0 | 90 | | { |
| 0 | 91 | | itemDomain = Helpers.DistinguishedNameToDomain(distinguishedName); |
| 0 | 92 | | } |
| | 93 | |
|
| 0 | 94 | | _log.LogTrace("Resolved domain for {SID} to {Domain}", objectId, itemDomain); |
| | 95 | |
|
| 0 | 96 | | res.ObjectId = objectId; |
| 0 | 97 | | res.Domain = itemDomain; |
| 0 | 98 | | if (IsDeleted()) |
| 0 | 99 | | { |
| 0 | 100 | | res.Deleted = IsDeleted(); |
| 0 | 101 | | _log.LogTrace("{SID} is tombstoned, skipping rest of resolution", objectId); |
| 0 | 102 | | return res; |
| | 103 | | } |
| | 104 | |
|
| 0 | 105 | | if (WellKnownPrincipal.GetWellKnownPrincipal(objectId, out var wkPrincipal)) |
| 0 | 106 | | { |
| 0 | 107 | | res.DomainSid = _utils.GetSidFromDomainName(itemDomain); |
| 0 | 108 | | res.DisplayName = $"{wkPrincipal.ObjectIdentifier}@{itemDomain}"; |
| 0 | 109 | | res.ObjectType = wkPrincipal.ObjectType; |
| 0 | 110 | | res.ObjectId = _utils.ConvertWellKnownPrincipal(objectId, itemDomain); |
| | 111 | |
|
| 0 | 112 | | _log.LogTrace("Resolved {DN} to wkp {ObjectID}", DistinguishedName, res.ObjectId); |
| 0 | 113 | | return res; |
| | 114 | | } |
| | 115 | |
|
| 0 | 116 | | if (objectId.StartsWith("S-1-")) |
| | 117 | | try |
| 0 | 118 | | { |
| 0 | 119 | | res.DomainSid = new SecurityIdentifier(objectId).AccountDomainSid.Value; |
| 0 | 120 | | } |
| 0 | 121 | | catch |
| 0 | 122 | | { |
| 0 | 123 | | res.DomainSid = _utils.GetSidFromDomainName(itemDomain); |
| 0 | 124 | | } |
| | 125 | | else |
| 0 | 126 | | res.DomainSid = _utils.GetSidFromDomainName(itemDomain); |
| | 127 | |
|
| 0 | 128 | | var samAccountName = GetProperty(LDAPProperties.SAMAccountName); |
| | 129 | |
|
| 0 | 130 | | var itemType = GetLabel(); |
| 0 | 131 | | res.ObjectType = itemType; |
| | 132 | |
|
| 0 | 133 | | if (IsGMSA() || IsMSA()) |
| 0 | 134 | | { |
| 0 | 135 | | res.ObjectType = Label.User; |
| 0 | 136 | | itemType = Label.User; |
| 0 | 137 | | } |
| | 138 | |
|
| 0 | 139 | | _log.LogTrace("Resolved type for {SID} to {Label}", objectId, itemType); |
| | 140 | |
|
| 0 | 141 | | switch (itemType) |
| | 142 | | { |
| | 143 | | case Label.User: |
| | 144 | | case Label.Group: |
| | 145 | | case Label.Base: |
| 0 | 146 | | res.DisplayName = $"{samAccountName}@{itemDomain}"; |
| 0 | 147 | | break; |
| | 148 | | case Label.Computer: |
| 0 | 149 | | { |
| 0 | 150 | | var shortName = samAccountName?.TrimEnd('$'); |
| 0 | 151 | | var dns = GetProperty(LDAPProperties.DNSHostName); |
| 0 | 152 | | var cn = GetProperty(LDAPProperties.CanonicalName); |
| | 153 | |
|
| 0 | 154 | | if (dns != null) |
| 0 | 155 | | res.DisplayName = dns; |
| 0 | 156 | | else if (shortName == null && cn == null) |
| 0 | 157 | | res.DisplayName = $"UNKNOWN.{itemDomain}"; |
| 0 | 158 | | else if (shortName != null) |
| 0 | 159 | | res.DisplayName = $"{shortName}.{itemDomain}"; |
| | 160 | | else |
| 0 | 161 | | res.DisplayName = $"{cn}.{itemDomain}"; |
| | 162 | |
|
| 0 | 163 | | break; |
| | 164 | | } |
| | 165 | | case Label.GPO: |
| | 166 | | case Label.IssuancePolicy: |
| 0 | 167 | | { |
| 0 | 168 | | var cn = GetProperty(LDAPProperties.CanonicalName); |
| 0 | 169 | | var displayName = GetProperty(LDAPProperties.DisplayName); |
| 0 | 170 | | res.DisplayName = string.IsNullOrEmpty(displayName) ? $"{cn}@{itemDomain}" : $"{GetProperty(LDAPProp |
| 0 | 171 | | break; |
| | 172 | | } |
| | 173 | | case Label.Domain: |
| 0 | 174 | | res.DisplayName = itemDomain; |
| 0 | 175 | | break; |
| | 176 | | case Label.OU: |
| | 177 | | case Label.Container: |
| | 178 | | case Label.Configuration: |
| | 179 | | case Label.RootCA: |
| | 180 | | case Label.AIACA: |
| | 181 | | case Label.NTAuthStore: |
| | 182 | | case Label.EnterpriseCA: |
| | 183 | | case Label.CertTemplate: |
| 0 | 184 | | res.DisplayName = $"{GetProperty(LDAPProperties.Name)}@{itemDomain}"; |
| 0 | 185 | | break; |
| | 186 | | default: |
| 0 | 187 | | throw new ArgumentOutOfRangeException(); |
| | 188 | | } |
| | 189 | |
|
| 0 | 190 | | return res; |
| 0 | 191 | | } |
| | 192 | |
|
| | 193 | | public string GetProperty(string propertyName) |
| 0 | 194 | | { |
| 0 | 195 | | return _entry.GetProperty(propertyName); |
| 0 | 196 | | } |
| | 197 | |
|
| | 198 | | public byte[] GetByteProperty(string propertyName) |
| 0 | 199 | | { |
| 0 | 200 | | return _entry.GetPropertyAsBytes(propertyName); |
| 0 | 201 | | } |
| | 202 | |
|
| | 203 | | public string[] GetArrayProperty(string propertyName) |
| 0 | 204 | | { |
| 0 | 205 | | return _entry.GetPropertyAsArray(propertyName); |
| 0 | 206 | | } |
| | 207 | |
|
| | 208 | | public byte[][] GetByteArrayProperty(string propertyName) |
| 0 | 209 | | { |
| 0 | 210 | | return _entry.GetPropertyAsArrayOfBytes(propertyName); |
| 0 | 211 | | } |
| | 212 | |
|
| | 213 | | public bool GetIntProperty(string propertyName, out int value) |
| 0 | 214 | | { |
| 0 | 215 | | return _entry.GetPropertyAsInt(propertyName, out value); |
| 0 | 216 | | } |
| | 217 | |
|
| | 218 | | public X509Certificate2[] GetCertificateArrayProperty(string propertyName) |
| 0 | 219 | | { |
| 0 | 220 | | return _entry.GetPropertyAsArrayOfCertificates(propertyName); |
| 0 | 221 | | } |
| | 222 | |
|
| | 223 | | public string GetObjectIdentifier() |
| 0 | 224 | | { |
| 0 | 225 | | return _entry.GetObjectIdentifier(); |
| 0 | 226 | | } |
| | 227 | |
|
| | 228 | | public bool IsDeleted() |
| 0 | 229 | | { |
| 0 | 230 | | return _entry.IsDeleted(); |
| 0 | 231 | | } |
| | 232 | |
|
| | 233 | | public Label GetLabel() |
| 0 | 234 | | { |
| 0 | 235 | | return _entry.GetLabel(); |
| 0 | 236 | | } |
| | 237 | |
|
| | 238 | | public string GetSid() |
| 0 | 239 | | { |
| 0 | 240 | | return _entry.GetSid(); |
| 0 | 241 | | } |
| | 242 | |
|
| | 243 | | public string GetGuid() |
| 0 | 244 | | { |
| 0 | 245 | | return _entry.GetGuid(); |
| 0 | 246 | | } |
| | 247 | |
|
| | 248 | | public int PropCount(string prop) |
| 0 | 249 | | { |
| 0 | 250 | | var coll = _entry.Attributes[prop]; |
| 0 | 251 | | return coll.Count; |
| 0 | 252 | | } |
| | 253 | |
|
| | 254 | | public IEnumerable<string> PropertyNames() |
| 0 | 255 | | { |
| 0 | 256 | | foreach (var property in _entry.Attributes.AttributeNames) yield return property.ToString().ToLower(); |
| 0 | 257 | | } |
| | 258 | |
|
| | 259 | | public bool IsMSA() |
| 0 | 260 | | { |
| 0 | 261 | | var classes = GetArrayProperty(LDAPProperties.ObjectClass); |
| 0 | 262 | | return classes.Contains(MSAClass, StringComparer.InvariantCultureIgnoreCase); |
| 0 | 263 | | } |
| | 264 | |
|
| | 265 | | public bool IsGMSA() |
| 0 | 266 | | { |
| 0 | 267 | | var classes = GetArrayProperty(LDAPProperties.ObjectClass); |
| 0 | 268 | | return classes.Contains(GMSAClass, StringComparer.InvariantCultureIgnoreCase); |
| 0 | 269 | | } |
| | 270 | |
|
| | 271 | | public bool HasLAPS() |
| 0 | 272 | | { |
| 0 | 273 | | return GetProperty(LDAPProperties.LAPSExpirationTime) != null || GetProperty(LDAPProperties.LegacyLAPSExpira |
| 0 | 274 | | } |
| | 275 | |
|
| | 276 | | public SearchResultEntry GetEntry() |
| 0 | 277 | | { |
| 0 | 278 | | return _entry; |
| 0 | 279 | | } |
| | 280 | | } |
| | 281 | | } |