< Summary

Class:SharpHoundCommonLib.Helpers
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Helpers.cs
Covered lines:116
Uncovered lines:48
Coverable lines:164
Total lines:309
Line coverage:70.7% (116 of 164)
Covered branches:49
Total branches:60
Branch coverage:81.6% (49 of 60)

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%10100%
RemoveDistinguishedNamePrefix(...)70%10076.92%
SplitGPLinkProperty()80%10092.85%
SamAccountTypeToType(...)100%60100%
ConvertSidToHexSid(...)100%100%
ConvertGuidToHexGuid(...)100%10100%
DistinguishedNameToDomain(...)100%40100%
DomainNameToDistinguishedName(...)100%100%
StripServicePrincipalName(...)100%20100%
Base64(...)100%10100%
ConvertFileTimeToUnixEpoch(...)75%4092.3%
ConvertTimestampToUnixEpoch(...)100%1085.71%
ConvertLdapTimeToLong(...)50%2083.33%
PreProcessSID(...)100%120100%
IsSidFiltered(...)60%10088.88%
GetRegistryKeyData(...)100%100%
OpenRemoteRegistry(...)100%100%

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 {
 117        private static readonly HashSet<string> Groups = new() { "268435456", "268435457", "536870912", "536870913" };
 118        private static readonly HashSet<string> Computers = new() { "805306369" };
 119        private static readonly HashSet<string> Users = new() { "805306368" };
 20
 121        private static readonly Regex DCReplaceRegex = new("DC=", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 122        private static readonly Regex SPNRegex = new(@".*\/.*", RegexOptions.Compiled);
 123        private static readonly DateTime EpochDiff = new(1970, 1, 1);
 24
 125        private static readonly string[] FilteredSids = {
 126            "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",
 127            "S-1-5-19", "S-1-5-20", "S-1-0-0", "S-1-0", "S-1-2-1"
 128        };
 29
 830        public static string RemoveDistinguishedNamePrefix(string distinguishedName) {
 931            if (!distinguishedName.Contains(",")) {
 132                return "";
 33            }
 34
 735            if (distinguishedName.IndexOf("DC=", StringComparison.OrdinalIgnoreCase) < 0) {
 036                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
 2441            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
 843                if (i == 0) {
 044                    continue;
 45                }
 46
 47                //This indicates an escaped comma, which we should not use to split a DN
 948                if (distinguishedName[i - 1] == '\\') {
 149                    continue;
 50                }
 51
 52                //This is an unescaped comma, so snip our DN from this comma onwards and return this as the cleaned dist
 753                return distinguishedName.Substring(i + 1);
 54            }
 55
 056            return "";
 857        }
 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>
 866        public static IEnumerable<ParsedGPLink> SplitGPLinkProperty(string linkProp, bool filterDisabled = true) {
 4667            foreach (var link in linkProp.Split(']', '[')
 5568                         .Where(x => x.StartsWith("LDAP", StringComparison.OrdinalIgnoreCase))) {
 1169                var s = link.Split(';');
 1170                var dn = s[0].Substring(s[0].IndexOf("CN=", StringComparison.OrdinalIgnoreCase));
 1171                var status = s[1];
 72
 1173                if (filterDisabled)
 74                    // 1 and 3 represent Disabled, Not Enforced and Disabled, Enforced respectively.
 975                    if (status is "3" or "1")
 076                        continue;
 77
 1178                yield return new ParsedGPLink {
 1179                    Status = status.TrimStart().TrimEnd(),
 1180                    DistinguishedName = dn.TrimStart().TrimEnd()
 1181                };
 1182            }
 883        }
 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>
 1690        public static Label SamAccountTypeToType(string samAccountType) {
 1691            if (Groups.Contains(samAccountType))
 692                return Label.Group;
 93
 1094            if (Users.Contains(samAccountType))
 295                return Label.User;
 96
 897            if (Computers.Contains(samAccountType))
 798                return Label.Computer;
 99
 1100            return Label.Base;
 16101        }
 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>
 0108        public static string ConvertSidToHexSid(string sid) {
 0109            var securityIdentifier = new SecurityIdentifier(sid);
 0110            var sidBytes = new byte[securityIdentifier.BinaryLength];
 0111            securityIdentifier.GetBinaryForm(sidBytes, 0);
 112
 0113            var output = $"\\{BitConverter.ToString(sidBytes).Replace('-', '\\')}";
 0114            return output;
 0115        }
 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>
 1122        public static string ConvertGuidToHexGuid(string guid) {
 1123            var guidObj = new Guid(guid);
 1124            var guidBytes = guidObj.ToByteArray();
 1125            var output = $"\\{BitConverter.ToString(guidBytes).Replace('-', '\\')}";
 1126            return output;
 1127        }
 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>
 16134        public static string DistinguishedNameToDomain(string distinguishedName) {
 135            int idx;
 17136            if (distinguishedName.ToUpper().Contains("DELETED OBJECTS")) {
 1137                idx = distinguishedName.IndexOf("DC=", 3, StringComparison.Ordinal);
 16138            } else {
 15139                idx = distinguishedName.IndexOf("DC=",
 15140                    StringComparison.CurrentCultureIgnoreCase);
 15141            }
 142
 16143            if (idx < 0)
 1144                return null;
 145
 15146            var temp = distinguishedName.Substring(idx);
 15147            temp = DCReplaceRegex.Replace(temp, "").Replace(",", ".").ToUpper();
 15148            return temp;
 16149        }
 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>
 0156        public static string DomainNameToDistinguishedName(string domainName) {
 0157            return $"DC={domainName.Replace(".", ",DC=")}";
 0158        }
 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>
 17165        public static string StripServicePrincipalName(string target) {
 17166            return SPNRegex.IsMatch(target) ? target.Split('/')[1].Split(':')[0] : target;
 17167        }
 168
 169        /// <summary>
 170        ///     Converts a string to its base64 representation
 171        /// </summary>
 172        /// <param name="input"></param>
 173        /// <returns></returns>
 1174        public static string Base64(string input) {
 1175            var plainBytes = Encoding.UTF8.GetBytes(input);
 1176            return Convert.ToBase64String(plainBytes);
 1177        }
 178
 179        /// <summary>
 180        ///     Converts a windows file time to unix epoch time
 181        /// </summary>
 182        /// <param name="ldapTime"></param>
 183        /// <returns></returns>
 28184        public static long ConvertFileTimeToUnixEpoch(string ldapTime) {
 28185            if (ldapTime == null)
 1186                return -1;
 187
 27188            var time = long.Parse(ldapTime);
 25189            if (time == 0)
 0190                return 0;
 191
 192            long toReturn;
 193
 25194            try {
 25195                toReturn = (long)Math.Floor(DateTime.FromFileTimeUtc(time).Subtract(EpochDiff).TotalSeconds);
 26196            } catch {
 1197                toReturn = -1;
 1198            }
 199
 25200            return toReturn;
 26201        }
 202
 203        /// <summary>
 204        ///     Converts a windows file time to unix epoch time
 205        /// </summary>
 206        /// <param name="ldapTime"></param>
 207        /// <returns></returns>
 6208        public static long ConvertTimestampToUnixEpoch(string ldapTime) {
 6209            try {
 6210                var dt = DateTime.ParseExact(ldapTime, "yyyyMMddHHmmss.0K", CultureInfo.CurrentCulture);
 0211                return (long)dt.Subtract(EpochDiff).TotalSeconds;
 12212            } catch {
 6213                return 0;
 214            }
 6215        }
 216
 217        /// <summary>
 218        ///     Converts an LDAP time string into a long
 219        /// </summary>
 220        /// <param name="ldapTime"></param>
 221        /// <returns></returns>
 6222        public static long ConvertLdapTimeToLong(string ldapTime) {
 6223            if (ldapTime == null)
 0224                return -1;
 225
 6226            var time = long.Parse(ldapTime);
 6227            return time;
 6228        }
 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>
 65235        internal static string PreProcessSID(string sid) {
 65236            sid = sid?.ToUpper();
 65237            if (sid != null)
 238                //Ignore Local System/Creator Owner/Principal Self
 39239                return sid is "S-1-5-18" or "S-1-3-0" or "S-1-5-10" ? null : sid;
 240
 26241            return null;
 65242        }
 243
 22244        public static bool IsSidFiltered(string sid) {
 245            //Uppercase just in case we get a lowercase s
 22246            sid = sid.ToUpper();
 22247            if (sid.StartsWith("S-1-5-80") || sid.StartsWith("S-1-5-82") ||
 22248                sid.StartsWith("S-1-5-90") || sid.StartsWith("S-1-5-96"))
 0249                return true;
 250
 22251            if (FilteredSids.Contains(sid))
 1252                return true;
 253
 21254            return false;
 22255        }
 256
 0257        public static RegistryResult GetRegistryKeyData(string target, string subkey, string subvalue, ILogger log) {
 0258            var data = new RegistryResult();
 259
 0260            try {
 0261                var baseKey = OpenRemoteRegistry(target);
 0262                var value = baseKey.GetValue(subkey, subvalue);
 0263                data.Value = value;
 264
 0265                data.Collected = true;
 0266            } catch (IOException e) {
 0267                log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 0268                    target, subkey, subvalue);
 0269                data.FailureReason = "Target machine was not found or not connectable";
 0270            } catch (SecurityException e) {
 0271                log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 0272                    target, subkey, subvalue);
 0273                data.FailureReason = "User does not have the proper permissions to perform this operation";
 0274            } catch (UnauthorizedAccessException e) {
 0275                log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 0276                    target, subkey, subvalue);
 0277                data.FailureReason = "User does not have the necessary registry rights";
 0278            } catch (Exception e) {
 0279                log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 0280                    target, subkey, subvalue);
 0281                data.FailureReason = e.Message;
 0282            }
 283
 0284            return data;
 0285        }
 286
 0287        public static IRegistryKey OpenRemoteRegistry(string target) {
 0288            var key = new SHRegistryKey(RegistryHive.LocalMachine, target);
 0289            return key;
 0290        }
 291
 1292        public static string[] AuthenticationOIDs = new string[] {
 1293            CommonOids.ClientAuthentication,
 1294            CommonOids.PKINITClientAuthentication,
 1295            CommonOids.SmartcardLogon,
 1296            CommonOids.AnyPurpose
 1297        };
 298
 1299        public static string[] SchannelAuthenticationOIDs = new string[] {
 1300            CommonOids.ClientAuthentication,
 1301            CommonOids.AnyPurpose
 1302        };
 303    }
 304
 305    public class ParsedGPLink {
 306        public string DistinguishedName { get; set; }
 307        public string Status { get; set; }
 308    }
 309}