From be72fecfa8df246ebb8dae51927db87ea15121b6 Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Mon, 29 Dec 2025 01:42:21 +0000 Subject: [PATCH] Add winpeas privilege escalation checks from: Kerberoasting: Low-Tech, High-Impact Attacks from Legacy Kerberos Crypto --- winPEAS/winPEASexe/README.md | 1 + .../winPEAS/Checks/ActiveDirectoryInfo.cs | 448 ++++++++++++++++++ 2 files changed, 449 insertions(+) diff --git a/winPEAS/winPEASexe/README.md b/winPEAS/winPEASexe/README.md index 8dd211c..1e07746 100755 --- a/winPEAS/winPEASexe/README.md +++ b/winPEAS/winPEASexe/README.md @@ -86,6 +86,7 @@ The tool is based on **[SeatBelt](https://github.com/GhostPack/Seatbelt)**. - Active Directory quick checks now include: - gMSA readable managed passwords: enumerate msDS-GroupManagedServiceAccount objects and report those where the current user/group is allowed to retrieve the managed password (PrincipalsAllowedToRetrieveManagedPassword). + - Kerberoasting risk scan: read domain Kerberos defaults and surface SPN accounts that still allow RC4/NT hash tickets or rely on long-lived, human passwords (prime Kerberoast targets). - AD CS (ESC4) hygiene: enumerate published certificate templates and highlight templates where the current user/group has dangerous control rights (GenericAll/WriteDacl/WriteOwner/WriteProperty/ExtendedRight) that could allow template abuse (e.g., ESC4 -> ESC1). These checks are lightweight, read-only, and only run when the host is domain-joined. diff --git a/winPEAS/winPEASexe/winPEAS/Checks/ActiveDirectoryInfo.cs b/winPEAS/winPEASexe/winPEAS/Checks/ActiveDirectoryInfo.cs index 123cfc8..8ced4f4 100644 --- a/winPEAS/winPEASexe/winPEAS/Checks/ActiveDirectoryInfo.cs +++ b/winPEAS/winPEASexe/winPEAS/Checks/ActiveDirectoryInfo.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.DirectoryServices; +using System.Reflection; using System.Security.AccessControl; using System.Security.Principal; using winPEAS.Helpers; @@ -19,6 +20,7 @@ namespace winPEAS.Checks new List { PrintGmsaReadableByCurrentPrincipal, + PrintKerberoastableServiceAccounts, PrintAdcsMisconfigurations }.ForEach(action => CheckRunner.Run(action, isDebug)); } @@ -154,6 +156,37 @@ namespace winPEAS.Checks } } + private void PrintKerberoastableServiceAccounts() + { + try + { + Beaprint.MainPrint("Kerberoasting / service ticket risks"); + Beaprint.LinkPrint("https://book.hacktricks.wiki/en/windows-hardening/active-directory-methodology/kerberoast.html", + "Enumerate weak SPN accounts and legacy Kerberos crypto"); + + if (!Checks.IsPartOfDomain) + { + Beaprint.GrayPrint(" [-] Host is not domain-joined. Skipping."); + return; + } + + var defaultNC = GetRootDseProp("defaultNamingContext"); + if (string.IsNullOrEmpty(defaultNC)) + { + Beaprint.GrayPrint(" [-] Could not resolve defaultNamingContext."); + return; + } + + PrintDomainKerberosDefaults(defaultNC); + EnumerateKerberoastCandidates(defaultNC); + } + catch (Exception ex) + { + Beaprint.GrayPrint(" [-] Kerberoasting check failed: " + ex.Message); + } + } + + // Detect AD CS misconfigurations private void PrintAdcsMisconfigurations() { @@ -342,5 +375,420 @@ namespace winPEAS.Checks Beaprint.PrintException(ex.Message); } } + private void PrintDomainKerberosDefaults(string defaultNc) + { + try + { + using (var domainEntry = new DirectoryEntry("LDAP://" + defaultNc)) + { + var encValue = GetDirectoryEntryInt(domainEntry, "msDS-DefaultSupportedEncryptionTypes"); + if (encValue.HasValue) + { + var desc = DescribeEncTypes(encValue); + if (IsRc4Allowed(encValue)) + Beaprint.BadPrint($" Domain default supported encryption types: {desc} — RC4/NT hash tickets allowed."); + else + Beaprint.GoodPrint($" Domain default supported encryption types: {desc} — RC4 disabled."); + } + else + { + Beaprint.GrayPrint(" [-] Domain default supported encryption types not set (legacy compatibility defaults to RC4)."); + } + } + + using (var baseDe = new DirectoryEntry("LDAP://" + defaultNc)) + using (var ds = new DirectorySearcher(baseDe)) + { + ds.Filter = "(&(objectClass=user)(sAMAccountName=krbtgt))"; + ds.PropertiesToLoad.Add("msDS-SupportedEncryptionTypes"); + var result = ds.FindOne(); + if (result != null) + { + var encValue = GetIntProp(result, "msDS-SupportedEncryptionTypes"); + if (encValue.HasValue) + { + var desc = DescribeEncTypes(encValue); + if (IsRc4Allowed(encValue)) + Beaprint.BadPrint($" krbtgt supports: {desc} — RC4 TGTs can still be issued."); + else + Beaprint.GoodPrint($" krbtgt supports: {desc}."); + } + else + { + Beaprint.GrayPrint(" [-] krbtgt enc types inherit domain defaults (unspecified)."); + } + } + } + } + catch (Exception ex) + { + Beaprint.GrayPrint(" [-] Unable to query Kerberos defaults: " + ex.Message); + } + } + + private void EnumerateKerberoastCandidates(string defaultNc) + { + int checkedAccounts = 0; + int highTotal = 0; + int mediumTotal = 0; + var high = new List(); + var medium = new List(); + + try + { + using (var baseDe = new DirectoryEntry("LDAP://" + defaultNc)) + using (var ds = new DirectorySearcher(baseDe)) + { + ds.PageSize = 500; + ds.Filter = "(servicePrincipalName=*)"; + ds.PropertiesToLoad.Add("sAMAccountName"); + ds.PropertiesToLoad.Add("displayName"); + ds.PropertiesToLoad.Add("distinguishedName"); + ds.PropertiesToLoad.Add("servicePrincipalName"); + ds.PropertiesToLoad.Add("msDS-SupportedEncryptionTypes"); + ds.PropertiesToLoad.Add("userAccountControl"); + ds.PropertiesToLoad.Add("pwdLastSet"); + ds.PropertiesToLoad.Add("memberOf"); + ds.PropertiesToLoad.Add("objectClass"); + + foreach (SearchResult r in ds.FindAll()) + { + checkedAccounts++; + var candidate = BuildKerberoastCandidate(r); + if (candidate == null) + { + continue; + } + + if (candidate.IsHighRisk) + { + highTotal++; + if (high.Count < 15) high.Add(candidate); + } + else + { + mediumTotal++; + if (medium.Count < 12) medium.Add(candidate); + } + } + } + + Beaprint.InfoPrint($"Checked {checkedAccounts} SPN-bearing accounts. High-risk RC4/privileged targets: {highTotal}, long-lived AES-only targets: {mediumTotal}."); + + if (highTotal == 0 && mediumTotal == 0) + { + Beaprint.GoodPrint(" No obvious Kerberoastable service accounts detected with current visibility."); + return; + } + + if (high.Count > 0) + { + Beaprint.BadPrint(" [!] RC4-enabled or privileged SPN accounts:"); + foreach (var c in high) + { + Beaprint.ColorPrint($" - {c.Label} | SPNs: {c.SpnSummary} | Enc: {c.Encryption} | {c.Reason}", Beaprint.LRED); + } + if (highTotal > high.Count) + { + Beaprint.GrayPrint($" ... {highTotal - high.Count} additional high-risk accounts omitted."); + } + } + + if (medium.Count > 0) + { + Beaprint.ColorPrint(" [~] Long-lived SPN accounts (still Kerberoastable via AES tickets):", Beaprint.YELLOW); + foreach (var c in medium) + { + Beaprint.ColorPrint($" - {c.Label} | SPNs: {c.SpnSummary} | Enc: {c.Encryption} | {c.Reason}", Beaprint.YELLOW); + } + if (mediumTotal > medium.Count) + { + Beaprint.GrayPrint($" ... {mediumTotal - medium.Count} additional medium-risk accounts omitted."); + } + } + } + catch (Exception ex) + { + Beaprint.GrayPrint(" [-] LDAP error while enumerating SPNs: " + ex.Message); + } + } + + private KerberoastCandidate BuildKerberoastCandidate(SearchResult r) + { + var sam = GetProp(r, "sAMAccountName"); + var displayName = GetProp(r, "displayName"); + var dn = GetProp(r, "distinguishedName"); + + if (IsComputerObject(r) || IsManagedServiceAccount(r)) + return null; + + var uac = GetIntProp(r, "userAccountControl"); + if (uac.HasValue && (uac.Value & 0x2) != 0) + return null; + + var encValue = GetIntProp(r, "msDS-SupportedEncryptionTypes"); + bool rc4Allowed = IsRc4Allowed(encValue); + bool aesPresent = HasAes(encValue); + bool passwordNeverExpires = uac.HasValue && (uac.Value & 0x10000) != 0; + DateTime? pwdLastSet = GetFileTimeProp(r, "pwdLastSet"); + bool stalePassword = pwdLastSet.HasValue && pwdLastSet.Value < DateTime.UtcNow.AddDays(-365); + var privilegeHits = GetPrivilegedGroups(r); + var reasons = new List(); + + if (rc4Allowed) + reasons.Add("RC4 allowed"); + else if (!aesPresent) + reasons.Add("No AES flag"); + if (passwordNeverExpires) + reasons.Add("PasswordNeverExpires"); + if (stalePassword) + reasons.Add("PwdLastSet " + pwdLastSet.Value.ToString("yyyy-MM-dd")); + if (privilegeHits.Count > 0) + reasons.Add("Privileged: " + string.Join("/", privilegeHits)); + + if (reasons.Count == 0) + return null; + + bool isHigh = rc4Allowed || privilegeHits.Count > 0; + if (!isHigh && !(passwordNeverExpires || stalePassword)) + return null; + + var label = !string.IsNullOrEmpty(sam) ? sam : dn; + if (!string.IsNullOrEmpty(displayName) && !string.Equals(displayName, sam, StringComparison.OrdinalIgnoreCase)) + { + label = string.IsNullOrEmpty(sam) ? displayName : $"{sam} ({displayName})"; + } + + return new KerberoastCandidate + { + Label = label ?? "", + SpnSummary = BuildSpnSummary(r), + Encryption = DescribeEncTypes(encValue), + Reason = string.Join("; ", reasons), + IsHighRisk = isHigh + }; + } + + private static string BuildSpnSummary(SearchResult r) + { + if (!r.Properties.Contains("servicePrincipalName") || r.Properties["servicePrincipalName"].Count == 0) + return ""; + + var values = r.Properties["servicePrincipalName"]; + var list = new List(); + int limit = values.Count < 3 ? values.Count : 3; + for (int i = 0; i < limit; i++) + { + var spn = values[i]?.ToString(); + if (!string.IsNullOrEmpty(spn)) + list.Add(spn); + } + + string summary = list.Count > 0 ? string.Join(", ", list) : ""; + if (values.Count > limit) + summary += $" (+{values.Count - limit} more)"; + return summary; + } + + private static List GetPrivilegedGroups(SearchResult r) + { + var hits = new List(); + if (!r.Properties.Contains("memberOf")) + return hits; + + var memberships = r.Properties["memberOf"]; + foreach (var membership in memberships) + { + var cn = ExtractCn(membership?.ToString()); + if (string.IsNullOrEmpty(cn)) + continue; + + foreach (var keyword in PrivilegedGroupKeywords) + { + if (cn.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0) + { + if (!hits.Contains(cn)) + hits.Add(cn); + break; + } + } + } + return hits; + } + + private static string ExtractCn(string dn) + { + if (string.IsNullOrEmpty(dn)) + return null; + + var parts = dn.Split(','); + foreach (var part in parts) + { + var trimmed = part.Trim(); + if (trimmed.StartsWith("CN=", StringComparison.OrdinalIgnoreCase)) + return trimmed.Substring(3); + } + return dn; + } + + private static bool IsComputerObject(SearchResult r) + { + return HasObjectClass(r, "computer"); + } + + private static bool IsManagedServiceAccount(SearchResult r) + { + return HasObjectClass(r, "msDS-ManagedServiceAccount") || HasObjectClass(r, "msDS-GroupManagedServiceAccount"); + } + + private static bool HasObjectClass(SearchResult r, string className) + { + if (!r.Properties.Contains("objectClass")) + return false; + + foreach (var val in r.Properties["objectClass"]) + { + if (string.Equals(val?.ToString(), className, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + private static DateTime? GetFileTimeProp(SearchResult r, string propName) + { + if (!r.Properties.Contains(propName) || r.Properties[propName].Count == 0) + return null; + return ConvertFileTime(r.Properties[propName][0]); + } + + private static DateTime? ConvertFileTime(object value) + { + if (value == null) + return null; + try + { + if (value is long longVal) + { + if (longVal <= 0) return null; + return DateTime.FromFileTimeUtc(longVal); + } + + if (value is IConvertible convertible) + { + long converted = convertible.ToInt64(null); + if (converted > 0) + return DateTime.FromFileTimeUtc(converted); + } + + var type = value.GetType(); + var highProp = type.GetProperty("HighPart", BindingFlags.Public | BindingFlags.Instance); + var lowProp = type.GetProperty("LowPart", BindingFlags.Public | BindingFlags.Instance); + if (highProp != null && lowProp != null) + { + int high = Convert.ToInt32(highProp.GetValue(value, null)); + int low = Convert.ToInt32(lowProp.GetValue(value, null)); + long fileTime = ((long)high << 32) | (uint)low; + if (fileTime > 0) + return DateTime.FromFileTimeUtc(fileTime); + } + } + catch + { + return null; + } + return null; + } + + private static int? GetIntProp(SearchResult r, string name) + { + if (!r.Properties.Contains(name) || r.Properties[name].Count == 0) + return null; + return ConvertToNullableInt(r.Properties[name][0]); + } + + private static int? GetDirectoryEntryInt(DirectoryEntry entry, string name) + { + try + { + return ConvertToNullableInt(entry.Properties[name]?.Value); + } + catch + { + return null; + } + } + + private static int? ConvertToNullableInt(object value) + { + if (value == null) + return null; + if (value is int intValue) + return intValue; + if (value is long longValue) + return unchecked((int)longValue); + if (int.TryParse(value.ToString(), out var parsed)) + return parsed; + return null; + } + + private static bool IsRc4Allowed(int? encValue) + { + if (!encValue.HasValue || encValue.Value == 0) + return true; + return (encValue.Value & EncFlagRc4) != 0; + } + + private static bool HasAes(int? encValue) + { + if (!encValue.HasValue) + return false; + return (encValue.Value & (EncFlagAes128 | EncFlagAes256)) != 0; + } + + private static string DescribeEncTypes(int? encValue) + { + if (!encValue.HasValue || encValue.Value == 0) + return "Unspecified (inherits defaults / RC4 compatible)"; + + var parts = new List(); + if ((encValue.Value & EncFlagDesCrc) != 0) parts.Add("DES-CBC-CRC"); + if ((encValue.Value & EncFlagDesMd5) != 0) parts.Add("DES-CBC-MD5"); + if ((encValue.Value & EncFlagRc4) != 0) parts.Add("RC4-HMAC"); + if ((encValue.Value & EncFlagAes128) != 0) parts.Add("AES128"); + if ((encValue.Value & EncFlagAes256) != 0) parts.Add("AES256"); + if ((encValue.Value & 0x20) != 0) parts.Add("FAST"); + if (parts.Count == 0) parts.Add($"0x{encValue.Value:X}"); + return string.Join(", ", parts); + } + + private class KerberoastCandidate + { + public string Label { get; set; } + public string SpnSummary { get; set; } + public string Encryption { get; set; } + public string Reason { get; set; } + public bool IsHighRisk { get; set; } + } + + private static readonly string[] PrivilegedGroupKeywords = new[] + { + "Domain Admin", + "Enterprise Admin", + "Administrators", + "Exchange", + "Schema Admin", + "Account Operator", + "Server Operator", + "Backup Operator", + "DnsAdmin" + }; + + private const int EncFlagDesCrc = 0x1; + private const int EncFlagDesMd5 = 0x2; + private const int EncFlagRc4 = 0x4; + private const int EncFlagAes128 = 0x8; + private const int EncFlagAes256 = 0x10; + + } }