< 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:337
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{
 17    public static class Helpers
 18    {
 19        private static readonly HashSet<string> Groups = new() {"268435456", "268435457", "536870912", "536870913"};
 20        private static readonly HashSet<string> Computers = new() {"805306369"};
 21        private static readonly HashSet<string> Users = new() {"805306368"};
 22
 23        private static readonly Regex DCReplaceRegex = new("DC=", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 24        private static readonly Regex SPNRegex = new(@".*\/.*", RegexOptions.Compiled);
 25        private static readonly DateTime EpochDiff = new(1970, 1, 1);
 26        private static readonly string[] FilteredSids =
 27        {
 28            "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",
 29            "S-1-5-19", "S-1-5-20", "S-1-0-0", "S-1-0", "S-1-2-1"
 30        };
 31
 32        public static string RemoveDistinguishedNamePrefix(string distinguishedName)
 33        {
 34            if (!distinguishedName.Contains(","))
 35            {
 36                return "";
 37            }
 38            if (distinguishedName.IndexOf("DC=", StringComparison.OrdinalIgnoreCase) < 0)
 39            {
 40                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
 45            for (var i = distinguishedName.IndexOf(','); i > -1; i = distinguishedName.IndexOf(',', i + 1))
 46            {
 47                //If theres a comma at the beginning of the DN, something screwy is going on. Just ignore it
 48                if (i == 0)
 49                {
 50                    continue;
 51                }
 52
 53                //This indicates an escaped comma, which we should not use to split a DN
 54                if (distinguishedName[i-1] == '\\')
 55                {
 56                    continue;
 57                }
 58
 59                //This is an unescaped comma, so snip our DN from this comma onwards and return this as the cleaned dist
 60                return distinguishedName.Substring(i + 1);
 61            }
 62
 63            return "";
 64        }
 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)
 74        {
 75            foreach (var link in linkProp.Split(']', '[')
 76                         .Where(x => x.StartsWith("LDAP", StringComparison.OrdinalIgnoreCase)))
 77            {
 78                var s = link.Split(';');
 79                var dn = s[0].Substring(s[0].IndexOf("CN=", StringComparison.OrdinalIgnoreCase));
 80                var status = s[1];
 81
 82                if (filterDisabled)
 83                    // 1 and 3 represent Disabled, Not Enforced and Disabled, Enforced respectively.
 84                    if (status is "3" or "1")
 85                        continue;
 86
 87                yield return new ParsedGPLink
 88                {
 89                    Status = status.TrimStart().TrimEnd(),
 90                    DistinguishedName = dn.TrimStart().TrimEnd()
 91                };
 92            }
 93        }
 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)
 101        {
 102            if (Groups.Contains(samAccountType))
 103                return Label.Group;
 104
 105            if (Users.Contains(samAccountType))
 106                return Label.User;
 107
 108            if (Computers.Contains(samAccountType))
 109                return Label.Computer;
 110
 111            return Label.Base;
 112        }
 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)
 120        {
 121            var securityIdentifier = new SecurityIdentifier(sid);
 122            var sidBytes = new byte[securityIdentifier.BinaryLength];
 123            securityIdentifier.GetBinaryForm(sidBytes, 0);
 124
 125            var output = $"\\{BitConverter.ToString(sidBytes).Replace('-', '\\')}";
 126            return output;
 127        }
 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)
 135        {
 136            var guidObj = new Guid(guid);
 137            var guidBytes = guidObj.ToByteArray();
 138            var output = $"\\{BitConverter.ToString(guidBytes).Replace('-', '\\')}";
 139            return output;
 140        }
 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)
 148        {
 149            int idx;
 150            if (distinguishedName.ToUpper().Contains("DELETED OBJECTS"))
 151            {
 152                idx = distinguishedName.IndexOf("DC=", 3, StringComparison.Ordinal);
 153            }
 154            else
 155            {
 156                idx = distinguishedName.IndexOf("DC=",
 157                    StringComparison.CurrentCultureIgnoreCase);
 158            }
 159
 160            if (idx < 0)
 161                return null;
 162
 163            var temp = distinguishedName.Substring(idx);
 164            temp = DCReplaceRegex.Replace(temp, "").Replace(",", ".").ToUpper();
 165            return temp;
 166        }
 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)
 174        {
 175            return SPNRegex.IsMatch(target) ? target.Split('/')[1].Split(':')[0] : target;
 176        }
 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)
 184        {
 185            var plainBytes = Encoding.UTF8.GetBytes(input);
 186            return Convert.ToBase64String(plainBytes);
 187        }
 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)
 195        {
 196            if (ldapTime == null)
 197                return -1;
 198
 199            var time = long.Parse(ldapTime);
 200            if (time == 0)
 201                return 0;
 202
 203            long toReturn;
 204
 205            try
 206            {
 207                toReturn = (long) Math.Floor(DateTime.FromFileTimeUtc(time).Subtract(EpochDiff).TotalSeconds);
 208            }
 209            catch
 210            {
 211                toReturn = -1;
 212            }
 213
 214            return toReturn;
 215        }
 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)
 223        {
 224            try
 225            {
 226                var dt = DateTime.ParseExact(ldapTime, "yyyyMMddHHmmss.0K", CultureInfo.CurrentCulture);
 227                return (long) dt.Subtract(EpochDiff).TotalSeconds;
 228            }
 229            catch
 230            {
 231                return 0;
 232            }
 233        }
 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)
 241        {
 242            if (ldapTime == null)
 243                return -1;
 244
 245            var time = long.Parse(ldapTime);
 246            return time;
 247        }
 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)
 255        {
 256            sid = sid?.ToUpper();
 257            if (sid != null)
 258                //Ignore Local System/Creator Owner/Principal Self
 259                return sid is "S-1-5-18" or "S-1-3-0" or "S-1-5-10" ? null : sid;
 260
 261            return null;
 262        }
 263
 264        public static bool IsSidFiltered(string sid)
 265        {
 266            //Uppercase just in case we get a lowercase s
 267            sid = sid.ToUpper();
 268            if (sid.StartsWith("S-1-5-80") || sid.StartsWith("S-1-5-82") ||
 269                sid.StartsWith("S-1-5-90") || sid.StartsWith("S-1-5-96"))
 270                return true;
 271
 272            if (FilteredSids.Contains(sid))
 273                return true;
 274
 275            return false;
 276        }
 277
 278        public static RegistryResult GetRegistryKeyData(string target, string subkey, string subvalue, ILogger log)
 279        {
 280            var data = new RegistryResult();
 281
 282            try
 283            {
 284                var baseKey = OpenRemoteRegistry(target);
 285                var value = baseKey.GetValue(subkey, subvalue);
 286                data.Value = value;
 287
 288                data.Collected = true;
 289            }
 290            catch (IOException e)
 291            {
 292                log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 293                    target, subkey, subvalue);
 294                data.FailureReason = "Target machine was not found or not connectable";
 295            }
 296            catch (SecurityException e)
 297            {
 298                log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 299                    target, subkey, subvalue);
 300                data.FailureReason = "User does not have the proper permissions to perform this operation";
 301            }
 302            catch (UnauthorizedAccessException e)
 303            {
 304                log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 305                    target, subkey, subvalue);
 306                data.FailureReason = "User does not have the necessary registry rights";
 307            }
 308            catch (Exception e)
 309            {
 310                log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
 311                    target, subkey, subvalue);
 312                data.FailureReason = e.Message;
 313            }
 314
 315            return data;
 316        }
 317
 318        public static IRegistryKey OpenRemoteRegistry(string target)
 319        {
 320            var key = new SHRegistryKey(RegistryHive.LocalMachine, target);
 321            return key;
 322        }
 323
 324        public static string[] AuthenticationOIDs = new string[] {
 325            CommonOids.ClientAuthentication,
 326            CommonOids.PKINITClientAuthentication,
 327            CommonOids.SmartcardLogon,
 328            CommonOids.AnyPurpose
 329        };
 330    }
 331
 332    public class ParsedGPLink
 333    {
 23334        public string DistinguishedName { get; set; }
 19335        public string Status { get; set; }
 336    }
 337}

Methods/Properties

DistinguishedName()
Status()