< Summary

Class:SharpHoundCommonLib.ParsedGPLink
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Helpers.cs
Covered lines:2
Uncovered lines:0
Coverable lines:2
Total lines:309
Line coverage:100% (2 of 2)
Covered branches:0
Total branches:0

File(s)

D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Helpers.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.Linq;
 5using System.Security.Principal;
 6using System.Text;
 7using System.Text.RegularExpressions;
 8using SharpHoundCommonLib.Enums;
 9using Microsoft.Extensions.Logging;
 10using System.IO;
 11using System.Security;
 12using SharpHoundCommonLib.Processors;
 13using Microsoft.Win32;
 14
 15namespace SharpHoundCommonLib {
 16    public static class Helpers {
 17        private static readonly HashSet<string> Groups = new() { "268435456", "268435457", "536870912", "536870913" };
 18        private static readonly HashSet<string> Computers = new() { "805306369" };
 19        private static readonly HashSet<string> Users = new() { "805306368" };
 20
 21        private static readonly Regex DCReplaceRegex = new("DC=", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 22        private static readonly Regex SPNRegex = new(@".*\/.*", RegexOptions.Compiled);
 23        private static readonly DateTime EpochDiff = new(1970, 1, 1);
 24
 25        private static readonly string[] FilteredSids = {
 26            "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",
 27            "S-1-5-19", "S-1-5-20", "S-1-0-0", "S-1-0", "S-1-2-1"
 28        };
 29
 30        public static string RemoveDistinguishedNamePrefix(string distinguishedName) {
 31            if (!distinguishedName.Contains(",")) {
 32                return "";
 33            }
 34
 35            if (distinguishedName.IndexOf("DC=", StringComparison.OrdinalIgnoreCase) < 0) {
 36                return "";
 37            }
 38
 39            //Start at the first instance of a comma, and continue to loop while we still have commas. If we get -1, it 
 40            //This allows us to cleanly iterate over all indexes of commas in our DNs and find the first non-escaped one
 41            for (var i = distinguishedName.IndexOf(','); i > -1; i = distinguishedName.IndexOf(',', i + 1)) {
 42                //If theres a comma at the beginning of the DN, something screwy is going on. Just ignore it
 43                if (i == 0) {
 44                    continue;
 45                }
 46
 47                //This indicates an escaped comma, which we should not use to split a DN
 48                if (distinguishedName[i - 1] == '\\') {
 49                    continue;
 50                }
 51
 52                //This is an unescaped comma, so snip our DN from this comma onwards and return this as the cleaned dist
 53                return distinguishedName.Substring(i + 1);
 54            }
 55
 56            return "";
 57        }
 58
 59        /// <summary>
 60        ///     Splits a GPLink property into its representative parts
 61        ///     Filters disabled links by default
 62        /// </summary>
 63        /// <param name="linkProp"></param>
 64        /// <param name="filterDisabled"></param>
 65        /// <returns></returns>
 66        public static IEnumerable<ParsedGPLink> SplitGPLinkProperty(string linkProp, bool filterDisabled = true) {
 67            foreach (var link in linkProp.Split(']', '[')
 68                         .Where(x => x.StartsWith("LDAP", StringComparison.OrdinalIgnoreCase))) {
 69                var s = link.Split(';');
 70                var dn = s[0].Substring(s[0].IndexOf("CN=", StringComparison.OrdinalIgnoreCase));
 71                var status = s[1];
 72
 73                if (filterDisabled)
 74                    // 1 and 3 represent Disabled, Not Enforced and Disabled, Enforced respectively.
 75                    if (status is "3" or "1")
 76                        continue;
 77
 78                yield return new ParsedGPLink {
 79                    Status = status.TrimStart().TrimEnd(),
 80                    DistinguishedName = dn.TrimStart().TrimEnd()
 81                };
 82            }
 83        }
 84
 85        /// <summary>
 86        ///     Attempts to convert a SamAccountType value to the appropriate type enum
 87        /// </summary>
 88        /// <param name="samAccountType"></param>
 89        /// <returns><c>Label</c> value representing type</returns>
 90        public static Label SamAccountTypeToType(string samAccountType) {
 91            if (Groups.Contains(samAccountType))
 92                return Label.Group;
 93
 94            if (Users.Contains(samAccountType))
 95                return Label.User;
 96
 97            if (Computers.Contains(samAccountType))
 98                return Label.Computer;
 99
 100            return Label.Base;
 101        }
 102
 103        /// <summary>
 104        ///     Converts a string SID to its hex representation for LDAP searches
 105        /// </summary>
 106        /// <param name="sid">String security identifier to convert</param>
 107        /// <returns>String representation to use in LDAP filters</returns>
 108        public static string ConvertSidToHexSid(string sid) {
 109            var securityIdentifier = new SecurityIdentifier(sid);
 110            var sidBytes = new byte[securityIdentifier.BinaryLength];
 111            securityIdentifier.GetBinaryForm(sidBytes, 0);
 112
 113            var output = $"\\{BitConverter.ToString(sidBytes).Replace('-', '\\')}";
 114            return output;
 115        }
 116
 117        /// <summary>
 118        ///     Converts a string GUID to its hex representation for LDAP searches
 119        /// </summary>
 120        /// <param name="guid"></param>
 121        /// <returns></returns>
 122        public static string ConvertGuidToHexGuid(string guid) {
 123            var guidObj = new Guid(guid);
 124            var guidBytes = guidObj.ToByteArray();
 125            var output = $"\\{BitConverter.ToString(guidBytes).Replace('-', '\\')}";
 126            return output;
 127        }
 128
 129        /// <summary>
 130        ///     Extracts an active directory domain name from a DistinguishedName
 131        /// </summary>
 132        /// <param name="distinguishedName">Distinguished Name to extract domain from</param>
 133        /// <returns>String representing the domain name of this object</returns>
 134        public static string DistinguishedNameToDomain(string distinguishedName) {
 135            int idx;
 136            if (distinguishedName.ToUpper().Contains("DELETED OBJECTS")) {
 137                idx = distinguishedName.IndexOf("DC=", 3, StringComparison.Ordinal);
 138            } else {
 139                idx = distinguishedName.IndexOf("DC=",
 140                    StringComparison.CurrentCultureIgnoreCase);
 141            }
 142
 143            if (idx < 0)
 144                return null;
 145
 146            var temp = distinguishedName.Substring(idx);
 147            temp = DCReplaceRegex.Replace(temp, "").Replace(",", ".").ToUpper();
 148            return temp;
 149        }
 150
 151        /// <summary>
 152        /// Converts a domain name to a distinguished name using simple string substitution
 153        /// </summary>
 154        /// <param name="domainName"></param>
 155        /// <returns></returns>
 156        public static string DomainNameToDistinguishedName(string domainName) {
 157            return $"DC={domainName.Replace(".", ",DC=")}";
 158        }
 159
 160        /// <summary>
 161        ///     Strips a "serviceprincipalname" entry down to just its hostname
 162        /// </summary>
 163        /// <param name="target">Raw service principal name</param>
 164        /// <returns>Stripped service principal name with (hopefully) just the hostname</returns>
 165        public static string StripServicePrincipalName(string target) {
 166            return SPNRegex.IsMatch(target) ? target.Split('/')[1].Split(':')[0] : target;
 167        }
 168
 169        /// <summary>
 170        ///     Converts a string to its base64 representation
 171        /// </summary>
 172        /// <param name="input"></param>
 173        /// <returns></returns>
 174        public static string Base64(string input) {
 175            var plainBytes = Encoding.UTF8.GetBytes(input);
 176            return Convert.ToBase64String(plainBytes);
 177        }
 178
 179        /// <summary>
 180        ///     Converts a windows file time to unix epoch time
 181        /// </summary>
 182        /// <param name="ldapTime"></param>
 183        /// <returns></returns>
 184        public static long ConvertFileTimeToUnixEpoch(string ldapTime) {
 185            if (ldapTime == null)
 186                return -1;
 187
 188            var time = long.Parse(ldapTime);
 189            if (time == 0)
 190                return 0;
 191
 192            long toReturn;
 193
 194            try {
 195                toReturn = (long)Math.Floor(DateTime.FromFileTimeUtc(time).Subtract(EpochDiff).TotalSeconds);
 196            } catch {
 197                toReturn = -1;
 198            }
 199
 200            return toReturn;
 201        }
 202
 203        /// <summary>
 204        ///     Converts a windows file time to unix epoch time
 205        /// </summary>
 206        /// <param name="ldapTime"></param>
 207        /// <returns></returns>
 208        public static long ConvertTimestampToUnixEpoch(string ldapTime) {
 209            try {
 210                var dt = DateTime.ParseExact(ldapTime, "yyyyMMddHHmmss.0K", CultureInfo.CurrentCulture);
 211                return (long)dt.Subtract(EpochDiff).TotalSeconds;
 212            } catch {
 213                return 0;
 214            }
 215        }
 216
 217        /// <summary>
 218        ///     Converts an LDAP time string into a long
 219        /// </summary>
 220        /// <param name="ldapTime"></param>
 221        /// <returns></returns>
 222        public static long ConvertLdapTimeToLong(string ldapTime) {
 223            if (ldapTime == null)
 224                return -1;
 225
 226            var time = long.Parse(ldapTime);
 227            return time;
 228        }
 229
 230        /// <summary>
 231        ///     Removes some commonly seen SIDs that have no use in the schema
 232        /// </summary>
 233        /// <param name="sid"></param>
 234        /// <returns></returns>
 235        internal static string PreProcessSID(string sid) {
 236            sid = sid?.ToUpper();
 237            if (sid != null)
 238                //Ignore Local System/Creator Owner/Principal Self
 239                return sid is "S-1-5-18" or "S-1-3-0" or "S-1-5-10" ? null : sid;
 240
 241            return null;
 242        }
 243
 244        public static bool IsSidFiltered(string sid) {
 245            //Uppercase just in case we get a lowercase s
 246            sid = sid.ToUpper();
 247            if (sid.StartsWith("S-1-5-80") || sid.StartsWith("S-1-5-82") ||
 248                sid.StartsWith("S-1-5-90") || sid.StartsWith("S-1-5-96"))
 249                return true;
 250
 251            if (FilteredSids.Contains(sid))
 252                return true;
 253
 254            return false;
 255        }
 256
 257        public static RegistryResult GetRegistryKeyData(string target, string subkey, string subvalue, ILogger log) {
 258            var data = new RegistryResult();
 259
 260            try {
 261                var baseKey = OpenRemoteRegistry(target);
 262                var value = baseKey.GetValue(subkey, subvalue);
 263                data.Value = value;
 264
 265                data.Collected = true;
 266            } catch (IOException e) {
 267                log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 268                    target, subkey, subvalue);
 269                data.FailureReason = "Target machine was not found or not connectable";
 270            } catch (SecurityException e) {
 271                log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 272                    target, subkey, subvalue);
 273                data.FailureReason = "User does not have the proper permissions to perform this operation";
 274            } catch (UnauthorizedAccessException e) {
 275                log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 276                    target, subkey, subvalue);
 277                data.FailureReason = "User does not have the necessary registry rights";
 278            } catch (Exception e) {
 279                log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 280                    target, subkey, subvalue);
 281                data.FailureReason = e.Message;
 282            }
 283
 284            return data;
 285        }
 286
 287        public static IRegistryKey OpenRemoteRegistry(string target) {
 288            var key = new SHRegistryKey(RegistryHive.LocalMachine, target);
 289            return key;
 290        }
 291
 292        public static string[] AuthenticationOIDs = new string[] {
 293            CommonOids.ClientAuthentication,
 294            CommonOids.PKINITClientAuthentication,
 295            CommonOids.SmartcardLogon,
 296            CommonOids.AnyPurpose
 297        };
 298
 299        public static string[] SchannelAuthenticationOIDs = new string[] {
 300            CommonOids.ClientAuthentication,
 301            CommonOids.AnyPurpose
 302        };
 303    }
 304
 305    public class ParsedGPLink {
 22306        public string DistinguishedName { get; set; }
 19307        public string Status { get; set; }
 308    }
 309}

Methods/Properties

DistinguishedName()
Status()