< Summary

Class:SharpHoundCommonLib.Helpers
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Helpers.cs
Covered lines:123
Uncovered lines:55
Coverable lines:178
Total lines:337
Line coverage:69.1% (123 of 178)
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%10072.22%
SplitGPLinkProperty()80%10093.75%
SamAccountTypeToType(...)100%60100%
ConvertSidToHexSid(...)100%100%
ConvertGuidToHexGuid(...)100%10100%
DistinguishedNameToDomain(...)100%40100%
StripServicePrincipalName(...)100%20100%
Base64(...)100%10100%
ConvertFileTimeToUnixEpoch(...)75%4093.33%
ConvertTimestampToUnixEpoch(...)100%1087.5%
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{
 17    public static class Helpers
 18    {
 119        private static readonly HashSet<string> Groups = new() {"268435456", "268435457", "536870912", "536870913"};
 120        private static readonly HashSet<string> Computers = new() {"805306369"};
 121        private static readonly HashSet<string> Users = new() {"805306368"};
 22
 123        private static readonly Regex DCReplaceRegex = new("DC=", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 124        private static readonly Regex SPNRegex = new(@".*\/.*", RegexOptions.Compiled);
 125        private static readonly DateTime EpochDiff = new(1970, 1, 1);
 126        private static readonly string[] FilteredSids =
 127        {
 128            "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",
 129            "S-1-5-19", "S-1-5-20", "S-1-0-0", "S-1-0", "S-1-2-1"
 130        };
 31
 32        public static string RemoveDistinguishedNamePrefix(string distinguishedName)
 833        {
 834            if (!distinguishedName.Contains(","))
 135            {
 136                return "";
 37            }
 738            if (distinguishedName.IndexOf("DC=", StringComparison.OrdinalIgnoreCase) < 0)
 039            {
 040                return "";
 41            }
 42
 43            //Start at the first instance of a comma, and continue to loop while we still have commas. If we get -1, it 
 44            //This allows us to cleanly iterate over all indexes of commas in our DNs and find the first non-escaped one
 1645            for (var i = distinguishedName.IndexOf(','); i > -1; i = distinguishedName.IndexOf(',', i + 1))
 846            {
 47                //If theres a comma at the beginning of the DN, something screwy is going on. Just ignore it
 848                if (i == 0)
 049                {
 050                    continue;
 51                }
 52
 53                //This indicates an escaped comma, which we should not use to split a DN
 854                if (distinguishedName[i-1] == '\\')
 155                {
 156                    continue;
 57                }
 58
 59                //This is an unescaped comma, so snip our DN from this comma onwards and return this as the cleaned dist
 760                return distinguishedName.Substring(i + 1);
 61            }
 62
 063            return "";
 864        }
 65
 66        /// <summary>
 67        ///     Splits a GPLink property into its representative parts
 68        ///     Filters disabled links by default
 69        /// </summary>
 70        /// <param name="linkProp"></param>
 71        /// <param name="filterDisabled"></param>
 72        /// <returns></returns>
 73        public static IEnumerable<ParsedGPLink> SplitGPLinkProperty(string linkProp, bool filterDisabled = true)
 874        {
 4675            foreach (var link in linkProp.Split(']', '[')
 4476                         .Where(x => x.StartsWith("LDAP", StringComparison.OrdinalIgnoreCase)))
 1177            {
 1178                var s = link.Split(';');
 1179                var dn = s[0].Substring(s[0].IndexOf("CN=", StringComparison.OrdinalIgnoreCase));
 1180                var status = s[1];
 81
 1182                if (filterDisabled)
 83                    // 1 and 3 represent Disabled, Not Enforced and Disabled, Enforced respectively.
 984                    if (status is "3" or "1")
 085                        continue;
 86
 1187                yield return new ParsedGPLink
 1188                {
 1189                    Status = status.TrimStart().TrimEnd(),
 1190                    DistinguishedName = dn.TrimStart().TrimEnd()
 1191                };
 1192            }
 893        }
 94
 95        /// <summary>
 96        ///     Attempts to convert a SamAccountType value to the appropriate type enum
 97        /// </summary>
 98        /// <param name="samAccountType"></param>
 99        /// <returns><c>Label</c> value representing type</returns>
 100        public static Label SamAccountTypeToType(string samAccountType)
 7101        {
 7102            if (Groups.Contains(samAccountType))
 4103                return Label.Group;
 104
 3105            if (Users.Contains(samAccountType))
 1106                return Label.User;
 107
 2108            if (Computers.Contains(samAccountType))
 1109                return Label.Computer;
 110
 1111            return Label.Base;
 7112        }
 113
 114        /// <summary>
 115        ///     Converts a string SID to its hex representation for LDAP searches
 116        /// </summary>
 117        /// <param name="sid">String security identifier to convert</param>
 118        /// <returns>String representation to use in LDAP filters</returns>
 119        public static string ConvertSidToHexSid(string sid)
 0120        {
 0121            var securityIdentifier = new SecurityIdentifier(sid);
 0122            var sidBytes = new byte[securityIdentifier.BinaryLength];
 0123            securityIdentifier.GetBinaryForm(sidBytes, 0);
 124
 0125            var output = $"\\{BitConverter.ToString(sidBytes).Replace('-', '\\')}";
 0126            return output;
 0127        }
 128
 129        /// <summary>
 130        ///     Converts a string GUID to its hex representation for LDAP searches
 131        /// </summary>
 132        /// <param name="guid"></param>
 133        /// <returns></returns>
 134        public static string ConvertGuidToHexGuid(string guid)
 1135        {
 1136            var guidObj = new Guid(guid);
 1137            var guidBytes = guidObj.ToByteArray();
 1138            var output = $"\\{BitConverter.ToString(guidBytes).Replace('-', '\\')}";
 1139            return output;
 1140        }
 141
 142        /// <summary>
 143        ///     Extracts an active directory domain name from a DistinguishedName
 144        /// </summary>
 145        /// <param name="distinguishedName">Distinguished Name to extract domain from</param>
 146        /// <returns>String representing the domain name of this object</returns>
 147        public static string DistinguishedNameToDomain(string distinguishedName)
 23148        {
 149            int idx;
 23150            if (distinguishedName.ToUpper().Contains("DELETED OBJECTS"))
 1151            {
 1152                idx = distinguishedName.IndexOf("DC=", 3, StringComparison.Ordinal);
 1153            }
 154            else
 22155            {
 22156                idx = distinguishedName.IndexOf("DC=",
 22157                    StringComparison.CurrentCultureIgnoreCase);
 22158            }
 159
 23160            if (idx < 0)
 1161                return null;
 162
 22163            var temp = distinguishedName.Substring(idx);
 22164            temp = DCReplaceRegex.Replace(temp, "").Replace(",", ".").ToUpper();
 22165            return temp;
 23166        }
 167
 168        /// <summary>
 169        ///     Strips a "serviceprincipalname" entry down to just its hostname
 170        /// </summary>
 171        /// <param name="target">Raw service principal name</param>
 172        /// <returns>Stripped service principal name with (hopefully) just the hostname</returns>
 173        public static string StripServicePrincipalName(string target)
 16174        {
 16175            return SPNRegex.IsMatch(target) ? target.Split('/')[1].Split(':')[0] : target;
 16176        }
 177
 178        /// <summary>
 179        ///     Converts a string to its base64 representation
 180        /// </summary>
 181        /// <param name="input"></param>
 182        /// <returns></returns>
 183        public static string Base64(string input)
 1184        {
 1185            var plainBytes = Encoding.UTF8.GetBytes(input);
 1186            return Convert.ToBase64String(plainBytes);
 1187        }
 188
 189        /// <summary>
 190        ///     Converts a windows file time to unix epoch time
 191        /// </summary>
 192        /// <param name="ldapTime"></param>
 193        /// <returns></returns>
 194        public static long ConvertFileTimeToUnixEpoch(string ldapTime)
 28195        {
 28196            if (ldapTime == null)
 1197                return -1;
 198
 27199            var time = long.Parse(ldapTime);
 25200            if (time == 0)
 0201                return 0;
 202
 203            long toReturn;
 204
 205            try
 25206            {
 25207                toReturn = (long) Math.Floor(DateTime.FromFileTimeUtc(time).Subtract(EpochDiff).TotalSeconds);
 24208            }
 1209            catch
 1210            {
 1211                toReturn = -1;
 1212            }
 213
 25214            return toReturn;
 26215        }
 216
 217        /// <summary>
 218        ///     Converts a windows file time to unix epoch time
 219        /// </summary>
 220        /// <param name="ldapTime"></param>
 221        /// <returns></returns>
 222        public static long ConvertTimestampToUnixEpoch(string ldapTime)
 20223        {
 224            try
 20225            {
 20226                var dt = DateTime.ParseExact(ldapTime, "yyyyMMddHHmmss.0K", CultureInfo.CurrentCulture);
 0227                return (long) dt.Subtract(EpochDiff).TotalSeconds;
 228            }
 20229            catch
 20230            {
 20231                return 0;
 232            }
 20233        }
 234
 235        /// <summary>
 236        ///     Converts an LDAP time string into a long
 237        /// </summary>
 238        /// <param name="ldapTime"></param>
 239        /// <returns></returns>
 240        public static long ConvertLdapTimeToLong(string ldapTime)
 3241        {
 3242            if (ldapTime == null)
 0243                return -1;
 244
 3245            var time = long.Parse(ldapTime);
 3246            return time;
 3247        }
 248
 249        /// <summary>
 250        ///     Removes some commonly seen SIDs that have no use in the schema
 251        /// </summary>
 252        /// <param name="sid"></param>
 253        /// <returns></returns>
 254        internal static string PreProcessSID(string sid)
 81255        {
 81256            sid = sid?.ToUpper();
 81257            if (sid != null)
 258                //Ignore Local System/Creator Owner/Principal Self
 56259                return sid is "S-1-5-18" or "S-1-3-0" or "S-1-5-10" ? null : sid;
 260
 25261            return null;
 81262        }
 263
 264        public static bool IsSidFiltered(string sid)
 22265        {
 266            //Uppercase just in case we get a lowercase s
 22267            sid = sid.ToUpper();
 22268            if (sid.StartsWith("S-1-5-80") || sid.StartsWith("S-1-5-82") ||
 22269                sid.StartsWith("S-1-5-90") || sid.StartsWith("S-1-5-96"))
 0270                return true;
 271
 22272            if (FilteredSids.Contains(sid))
 1273                return true;
 274
 21275            return false;
 22276        }
 277
 278        public static RegistryResult GetRegistryKeyData(string target, string subkey, string subvalue, ILogger log)
 0279        {
 0280            var data = new RegistryResult();
 281
 282            try
 0283            {
 0284                var baseKey = OpenRemoteRegistry(target);
 0285                var value = baseKey.GetValue(subkey, subvalue);
 0286                data.Value = value;
 287
 0288                data.Collected = true;
 0289            }
 0290            catch (IOException e)
 0291            {
 0292                log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 0293                    target, subkey, subvalue);
 0294                data.FailureReason = "Target machine was not found or not connectable";
 0295            }
 0296            catch (SecurityException e)
 0297            {
 0298                log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 0299                    target, subkey, subvalue);
 0300                data.FailureReason = "User does not have the proper permissions to perform this operation";
 0301            }
 0302            catch (UnauthorizedAccessException e)
 0303            {
 0304                log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 0305                    target, subkey, subvalue);
 0306                data.FailureReason = "User does not have the necessary registry rights";
 0307            }
 0308            catch (Exception e)
 0309            {
 0310                log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 0311                    target, subkey, subvalue);
 0312                data.FailureReason = e.Message;
 0313            }
 314
 0315            return data;
 0316        }
 317
 318        public static IRegistryKey OpenRemoteRegistry(string target)
 0319        {
 0320            var key = new SHRegistryKey(RegistryHive.LocalMachine, target);
 0321            return key;
 0322        }
 323
 1324        public static string[] AuthenticationOIDs = new string[] {
 1325            CommonOids.ClientAuthentication,
 1326            CommonOids.PKINITClientAuthentication,
 1327            CommonOids.SmartcardLogon,
 1328            CommonOids.AnyPurpose
 1329        };
 330    }
 331
 332    public class ParsedGPLink
 333    {
 334        public string DistinguishedName { get; set; }
 335        public string Status { get; set; }
 336    }
 337}