diff --git a/winPEAS/winPEASexe/README.md b/winPEAS/winPEASexe/README.md index 0346409..d9866c7 100755 --- a/winPEAS/winPEASexe/README.md +++ b/winPEAS/winPEASexe/README.md @@ -74,10 +74,6 @@ winpeas.exe -lolbas #Execute also additional LOLBAS search check The goal of this project is to search for possible **Privilege Escalation Paths** in Windows environments. -New in this version: -- Detect potential GPO abuse by flagging writable SYSVOL paths for GPOs applied to the current host and by highlighting membership in the "Group Policy Creator Owners" group. -- Detect SOAPwn-style .NET HTTP client proxies by flagging high-privilege services/processes that embed SoapHttpClientProtocol or ServiceDescriptionImporter indicators. - It should take only a **few seconds** to execute almost all the checks and **some seconds/minutes during the lasts checks searching for known filenames** that could contain passwords (the time depened on the number of files in your home folder). By default only **some** filenames that could contain credentials are searched, you can use the **searchall** parameter to search all the list (this could will add some minutes). @@ -87,6 +83,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). + - AD object control surfaces: parse ACLs for high-value objects plus a sampled set of users/groups/computers and flag when the current security principal already has GenericAll/GenericWrite/WriteDacl/WriteOwner or attribute-specific rights (SPN, UAC, msDS-AllowedToActOnBehalfOfOtherIdentity, sidHistory, member, unicodePwd, replication) that can be abused for password resets, Kerberoasting, delegation/RBCD, DCSync, or stealth persistence. - 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. @@ -154,6 +151,7 @@ Once you have installed and activated it you need to: - [x] Applocker Configuration & bypass suggestions - [x] Printers - [x] Named Pipes + - [x] Named Pipe ACL abuse candidates - [x] AMSI Providers - [x] SysMon - [x] .NET Versions diff --git a/winPEAS/winPEASexe/winPEAS/Checks/ActiveDirectoryInfo.cs b/winPEAS/winPEASexe/winPEAS/Checks/ActiveDirectoryInfo.cs index 123cfc8..f6e81c3 100644 --- a/winPEAS/winPEASexe/winPEAS/Checks/ActiveDirectoryInfo.cs +++ b/winPEAS/winPEASexe/winPEAS/Checks/ActiveDirectoryInfo.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.DirectoryServices; +using System.Linq; using System.Security.AccessControl; using System.Security.Principal; +using System.Text; using winPEAS.Helpers; using winPEAS.Helpers.Registry; -using winPEAS.Info.FilesInfo.Certificates; namespace winPEAS.Checks { @@ -19,10 +20,16 @@ namespace winPEAS.Checks new List { PrintGmsaReadableByCurrentPrincipal, + PrintAdObjectControlPaths, PrintAdcsMisconfigurations }.ForEach(action => CheckRunner.Run(action, isDebug)); } + private const int SampleObjectLimit = 120; + private const int MaxFindingsToPrint = 40; + private static readonly Dictionary GuidNameCache = new Dictionary(); + private static readonly object GuidCacheLock = new object(); + private static HashSet GetCurrentSidSet() { var sids = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -65,6 +72,596 @@ namespace winPEAS.Checks : null; } + // Highlight objects where the current principal already has useful write/control rights + private void PrintAdObjectControlPaths() + { + try + { + Beaprint.MainPrint("AD object control surfaces"); + Beaprint.LinkPrint( + "https://book.hacktricks.wiki/en/windows-hardening/active-directory-methodology/index.html#acl-abuse", + "Look for objects where you have GenericAll/GenericWrite/attribute rights for ACL abuse (password reset, SPN/UAC/RBCD, sidHistory, delegation, DCSync)."); + + if (!Checks.IsPartOfDomain) + { + Beaprint.GrayPrint(" [-] Host is not domain-joined. Skipping."); + return; + } + + var defaultNC = GetRootDseProp("defaultNamingContext"); + var schemaNC = GetRootDseProp("schemaNamingContext"); + var configNC = GetRootDseProp("configurationNamingContext"); + + if (string.IsNullOrEmpty(defaultNC)) + { + Beaprint.GrayPrint(" [-] Could not resolve defaultNamingContext."); + return; + } + + var sidSet = GetCurrentSidSet(); + var processedDns = new HashSet(StringComparer.OrdinalIgnoreCase); + var findings = new List(); + + foreach (var target in EnumerateHighValueTargets(defaultNC)) + { + var finding = AnalyzeDirectoryObject(target.DistinguishedName, target.Label, sidSet, schemaNC, configNC); + if (finding == null) + { + continue; + } + + if (processedDns.Add(finding.DistinguishedName)) + { + findings.Add(finding); + } + } + + try + { + using (var baseDe = new DirectoryEntry("LDAP://" + defaultNC)) + using (var ds = new DirectorySearcher(baseDe)) + { + ds.PageSize = 200; + ds.SizeLimit = SampleObjectLimit; + ds.SearchScope = SearchScope.Subtree; + ds.SecurityMasks = SecurityMasks.Dacl; + ds.Filter = "(|(objectClass=user)(objectClass=group)(objectClass=computer))"; + ds.PropertiesToLoad.Add("distinguishedName"); + ds.PropertiesToLoad.Add("sAMAccountName"); + ds.PropertiesToLoad.Add("name"); + + using (var results = ds.FindAll()) + { + foreach (SearchResult r in results) + { + var dn = GetProp(r, "distinguishedName"); + if (string.IsNullOrEmpty(dn) || processedDns.Contains(dn)) + { + continue; + } + + var label = GetProp(r, "sAMAccountName") ?? GetProp(r, "name") ?? dn; + var finding = AnalyzeDirectoryObject(dn, label, sidSet, schemaNC, configNC); + if (finding != null && processedDns.Add(finding.DistinguishedName)) + { + findings.Add(finding); + } + } + } + } + } + catch (Exception ex) + { + Beaprint.GrayPrint(" [!] LDAP sampling failed: " + ex.Message); + } + + if (findings.Count == 0) + { + Beaprint.GrayPrint(" [-] No impactful ACLs detected for the current principal (sampled set)."); + return; + } + + var ordered = findings + .OrderByDescending(f => f.MaxScore) + .ThenBy(f => f.DisplayName, StringComparer.OrdinalIgnoreCase) + .ToList(); + + var truncated = ordered.Count > MaxFindingsToPrint; + if (truncated) + { + ordered = ordered.Take(MaxFindingsToPrint).ToList(); + } + + Beaprint.GrayPrint($" [+] Found {findings.Count} object(s) where your principal has abuse-friendly rights:"); + foreach (var finding in ordered) + { + Beaprint.BadPrint($" -> {finding.DisplayName} ({finding.ClassName})"); + Beaprint.GrayPrint(" DN: " + finding.DistinguishedName); + foreach (var impact in finding.Impacts.OrderByDescending(i => i.Score)) + { + Beaprint.GrayPrint($" * {impact.Impact}: {impact.Detail}"); + } + } + + if (truncated) + { + Beaprint.GrayPrint($" [!] Additional {findings.Count - MaxFindingsToPrint} object(s) not shown (enable domain mode or run winPEAS with more time to enumerate all objects)."); + } + } + catch (Exception ex) + { + Beaprint.PrintException(ex.Message); + } + } + + private static IEnumerable<(string Label, string DistinguishedName)> EnumerateHighValueTargets(string defaultNC) + { + return new List<(string, string)> + { + ("Domain Root", defaultNC), + ("AdminSDHolder", $"CN=AdminSDHolder,CN=System,{defaultNC}"), + ("Domain Controllers OU", $"OU=Domain Controllers,{defaultNC}"), + ("Domain Controllers group", $"CN=Domain Controllers,CN=Users,{defaultNC}"), + ("Domain Admins", $"CN=Domain Admins,CN=Users,{defaultNC}"), + ("Enterprise Admins", $"CN=Enterprise Admins,CN=Users,{defaultNC}"), + ("Schema Admins", $"CN=Schema Admins,CN=Users,{defaultNC}"), + ("Administrators", $"CN=Administrators,CN=Builtin,{defaultNC}"), + ("Account Operators", $"CN=Account Operators,CN=Builtin,{defaultNC}"), + ("Backup Operators", $"CN=Backup Operators,CN=Builtin,{defaultNC}"), + ("Group Policy Creator Owners", $"CN=Group Policy Creator Owners,CN=Users,{defaultNC}"), + ("krbtgt", $"CN=krbtgt,CN=Users,{defaultNC}") + }; + } + + private static AdObjectFinding AnalyzeDirectoryObject(string dn, string label, HashSet sidSet, string schemaNC, string configNC) + { + if (string.IsNullOrEmpty(dn)) + { + return null; + } + + try + { + using (var entry = new DirectoryEntry("LDAP://" + dn)) + { + entry.Options.SecurityMasks = SecurityMasks.Owner | SecurityMasks.Dacl; + entry.RefreshCache(); + return EvaluateSecurity(entry, label ?? dn, sidSet, schemaNC, configNC); + } + } + catch (Exception) + { + return null; + } + } + + private static AdObjectFinding EvaluateSecurity(DirectoryEntry entry, string label, HashSet sidSet, string schemaNC, string configNC) + { + ActiveDirectorySecurity security; + try + { + security = entry.ObjectSecurity; + } + catch + { + return null; + } + + if (security == null) + { + return null; + } + + var finding = new AdObjectFinding + { + DisplayName = label ?? entry.Name, + DistinguishedName = entry.Properties?["distinguishedName"]?.Value as string ?? entry.Path, + ClassName = entry.SchemaClassName ?? "object" + }; + + var seenImpacts = new HashSet(StringComparer.OrdinalIgnoreCase); + + try + { + var ownerSid = security.GetOwner(typeof(SecurityIdentifier)) as SecurityIdentifier; + if (ownerSid != null && sidSet.Contains(ownerSid.Value)) + { + var impact = new AdAccessImpact + { + Impact = "Object owner", + Detail = "You own this object and can rewrite its ACL to grant full control.", + Score = 3 + }; + finding.Impacts.Add(impact); + seenImpacts.Add(impact.Impact); + } + } + catch + { + // ignore owner lookup issues + } + + AuthorizationRuleCollection rules; + try + { + rules = security.GetAccessRules(true, true, typeof(SecurityIdentifier)); + } + catch + { + return finding.Impacts.Count > 0 ? finding : null; + } + + foreach (ActiveDirectoryAccessRule rule in rules) + { + if (rule == null || rule.AccessControlType != AccessControlType.Allow) + { + continue; + } + + if (!(rule.IdentityReference is SecurityIdentifier sid)) + { + continue; + } + + if (!sidSet.Contains(sid.Value)) + { + continue; + } + + foreach (var impact in MapRuleToImpacts(rule, schemaNC, configNC)) + { + if (impact == null) + { + continue; + } + + var key = impact.Impact + "|" + impact.Detail; + if (seenImpacts.Add(key)) + { + finding.Impacts.Add(impact); + } + } + } + + return finding.Impacts.Count > 0 ? finding : null; + } + + private static IEnumerable MapRuleToImpacts(ActiveDirectoryAccessRule rule, string schemaNC, string configNC) + { + var impacts = new List(); + var rights = rule.ActiveDirectoryRights; + + if ((rights & ActiveDirectoryRights.GenericAll) != 0) + { + impacts.Add(new AdAccessImpact + { + Impact = "GenericAll", + Detail = "Full control -> reset password, add group members, edit SPNs/UAC, change ACLs.", + Score = 5 + }); + return impacts; + } + + if ((rights & ActiveDirectoryRights.GenericWrite) != 0) + { + impacts.Add(new AdAccessImpact + { + Impact = "GenericWrite", + Detail = "Can modify most attributes (logon scripts, SPNs, UAC, etc.).", + Score = 4 + }); + } + + if ((rights & ActiveDirectoryRights.WriteDacl) != 0) + { + impacts.Add(new AdAccessImpact + { + Impact = "WriteDACL", + Detail = "Can edit the ACL to grant yourself additional rights/persistence.", + Score = 4 + }); + } + + if ((rights & ActiveDirectoryRights.WriteOwner) != 0) + { + impacts.Add(new AdAccessImpact + { + Impact = "WriteOwner", + Detail = "Can take ownership and then modify the DACL.", + Score = 3 + }); + } + + if ((rights & ActiveDirectoryRights.CreateChild) != 0) + { + impacts.Add(new AdAccessImpact + { + Impact = "CreateChild", + Detail = "Can create new users/computers/groups under this container (great for planting attack principals).", + Score = 3 + }); + } + + if ((rights & ActiveDirectoryRights.ExtendedRight) != 0) + { + var extImpact = MapExtendedRightImpact(rule.ObjectType, schemaNC, configNC); + if (extImpact != null) + { + impacts.Add(extImpact); + } + } + + if ((rights & ActiveDirectoryRights.WriteProperty) != 0) + { + var attrImpact = MapAttributeWriteImpact(rule.ObjectType, schemaNC, configNC, false); + if (attrImpact != null) + { + impacts.Add(attrImpact); + } + } + + if ((rights & ActiveDirectoryRights.Self) != 0) + { + var validatedImpact = MapAttributeWriteImpact(rule.ObjectType, schemaNC, configNC, true); + if (validatedImpact != null) + { + impacts.Add(validatedImpact); + } + } + + return impacts; + } + + private static AdAccessImpact MapExtendedRightImpact(Guid objectType, string schemaNC, string configNC) + { + if (objectType == Guid.Empty) + { + return null; + } + + var name = GetGuidFriendlyName(objectType, schemaNC, configNC)?.ToLowerInvariant(); + if (string.IsNullOrEmpty(name)) + { + return null; + } + + if (name.Contains("reset password") || name.Contains("user-force-change-password")) + { + return new AdAccessImpact + { + Impact = "ResetPassword right", + Detail = "Can reset the target account password without knowing the current value.", + Score = 5 + }; + } + + if (name.Contains("replicating directory changes")) + { + return new AdAccessImpact + { + Impact = "Replication (DCSync)", + Detail = "Has replication rights (part of DCSync privilege to dump NTDS hashes).", + Score = name.Contains("filtered") ? 5 : 4 + }; + } + + return null; + } + + private static AdAccessImpact MapAttributeWriteImpact(Guid objectType, string schemaNC, string configNC, bool validatedWrite) + { + if (objectType == Guid.Empty) + { + return new AdAccessImpact + { + Impact = validatedWrite ? "Validated write (broad)" : "WriteProperty (broad)", + Detail = "ACE applies to most attributes. Consider SPN/UAC/sidHistory abuse paths.", + Score = 3 + }; + } + + var attributeName = GetGuidFriendlyName(objectType, schemaNC, configNC); + if (string.IsNullOrEmpty(attributeName)) + { + return null; + } + + var lower = attributeName.ToLowerInvariant(); + + if (lower.Contains("member")) + { + return new AdAccessImpact + { + Impact = "Group membership control", + Detail = "Can edit the 'member' attribute -> add principals to this group.", + Score = 5 + }; + } + + if (lower.Contains("serviceprincipalname") || lower.Contains("validated-spn")) + { + return new AdAccessImpact + { + Impact = "SPN control", + Detail = "Can set servicePrincipalName -> Kerberoast or constrained delegation abuse.", + Score = 4 + }; + } + + if (lower.Contains("useraccountcontrol")) + { + return new AdAccessImpact + { + Impact = "UAC control", + Detail = "Can toggle UserAccountControl bits (AS-REP roastable, delegation, unconstrained).", + Score = 4 + }; + } + + if (lower.Contains("msds-allowedtoactonbehalfofotheridentity")) + { + return new AdAccessImpact + { + Impact = "RBCD control", + Detail = "Can edit msDS-AllowedToActOnBehalfOfOtherIdentity -> configure Resource-Based Constrained Delegation.", + Score = 5 + }; + } + + if (lower.Contains("msds-allowedtodelegateto")) + { + return new AdAccessImpact + { + Impact = "Delegation target control", + Detail = "Can edit msDS-AllowedToDelegateTo -> establish constrained delegation paths.", + Score = 4 + }; + } + + if (lower.Contains("sidhistory")) + { + return new AdAccessImpact + { + Impact = "sidHistory control", + Detail = "Can add privileged SIDs into sidHistory for stealth escalation/persistence.", + Score = 4 + }; + } + + if (lower.Contains("unicodepwd") || lower.Contains("userpassword")) + { + return new AdAccessImpact + { + Impact = "Password write", + Detail = "Can directly set unicodePwd/userPassword -> immediate account takeover.", + Score = 5 + }; + } + + return null; + } + + private static string GetGuidFriendlyName(Guid guid, string schemaNC, string configNC) + { + if (guid == Guid.Empty) + { + return null; + } + + lock (GuidCacheLock) + { + if (GuidNameCache.TryGetValue(guid, out var cached)) + { + return cached; + } + } + + string resolved = null; + + if (!string.IsNullOrEmpty(schemaNC)) + { + resolved = LookupGuidInSchema(guid, schemaNC); + } + + if (resolved == null && !string.IsNullOrEmpty(configNC)) + { + resolved = LookupGuidInExtendedRights(guid, configNC); + } + + if (string.IsNullOrEmpty(resolved)) + { + resolved = guid.ToString(); + } + + lock (GuidCacheLock) + { + if (!GuidNameCache.ContainsKey(guid)) + { + GuidNameCache[guid] = resolved; + } + return GuidNameCache[guid]; + } + } + + private static string LookupGuidInSchema(Guid guid, string schemaNC) + { + try + { + using (var schema = new DirectoryEntry("LDAP://" + schemaNC)) + using (var searcher = new DirectorySearcher(schema)) + { + searcher.Filter = $"(schemaIDGUID={GuidToLdapFilter(guid)})"; + searcher.PropertiesToLoad.Add("lDAPDisplayName"); + searcher.PropertiesToLoad.Add("name"); + var result = searcher.FindOne(); + if (result != null) + { + return GetProp(result, "lDAPDisplayName") ?? GetProp(result, "name"); + } + } + } + catch + { + // ignore schema lookup errors + } + + return null; + } + + private static string LookupGuidInExtendedRights(Guid guid, string configNC) + { + try + { + var extendedRightsDn = $"CN=Extended-Rights,{configNC}"; + using (var rights = new DirectoryEntry("LDAP://" + extendedRightsDn)) + using (var searcher = new DirectorySearcher(rights)) + { + searcher.Filter = $"(rightsGuid={guid})"; + searcher.PropertiesToLoad.Add("displayName"); + searcher.PropertiesToLoad.Add("name"); + var result = searcher.FindOne(); + if (result != null) + { + return GetProp(result, "displayName") ?? GetProp(result, "name"); + } + } + } + catch + { + // ignore extended rights lookup issues + } + + return null; + } + + private static string GuidToLdapFilter(Guid guid) + { + var bytes = guid.ToByteArray(); + var sb = new StringBuilder(); + foreach (var b in bytes) + { + sb.Append($"\\{b:X2}"); + } + + return sb.ToString(); + } + + private class AdObjectFinding + { + public string DisplayName { get; set; } + public string DistinguishedName { get; set; } + public string ClassName { get; set; } + public List Impacts { get; } = new List(); + public int MaxScore => Impacts.Count == 0 ? 0 : Impacts.Max(i => i.Score); + } + + private class AdAccessImpact + { + public string Impact { get; set; } + public string Detail { get; set; } + public int Score { get; set; } + } + // Detect gMSA objects where the current principal (or one of its groups) can retrieve the managed password private void PrintGmsaReadableByCurrentPrincipal() { diff --git a/winPEAS/winPEASexe/winPEAS/Checks/ServicesInfo.cs b/winPEAS/winPEASexe/winPEAS/Checks/ServicesInfo.cs index 739be63..1905d6b 100644 --- a/winPEAS/winPEASexe/winPEAS/Checks/ServicesInfo.cs +++ b/winPEAS/winPEASexe/winPEAS/Checks/ServicesInfo.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using winPEAS.Helpers; +using winPEAS.Helpers.Registry; using winPEAS.Info.ServicesInfo; namespace winPEAS.Checks @@ -34,6 +36,8 @@ namespace winPEAS.Checks PrintModifiableServices, PrintWritableRegServices, PrintPathDllHijacking, + PrintLegacySignedKernelDrivers, + PrintKernelQuickIndicators, }.ForEach(action => CheckRunner.Run(action, isDebug)); } @@ -206,5 +210,146 @@ namespace winPEAS.Checks } } + + void PrintLegacySignedKernelDrivers() + { + try + { + Beaprint.MainPrint("Kernel drivers with weak/legacy signatures"); + Beaprint.LinkPrint("https://research.checkpoint.com/2025/cracking-valleyrat-from-builder-secrets-to-kernel-rootkits/", + "Legacy cross-signed drivers (pre-July-2015) can still grant kernel execution on modern Windows"); + + List drivers = ServicesInfoHelper.GetKernelDriverInfos(); + if (drivers.Count == 0) + { + Beaprint.InfoPrint(" Unable to enumerate kernel services"); + return; + } + + var suspiciousDrivers = drivers.Where(d => d.Signature != null && (!d.Signature.IsSigned || d.Signature.IsLegacyExpired)) + .OrderBy(d => d.Name) + .ToList(); + + if (suspiciousDrivers.Count == 0) + { + Beaprint.InfoPrint(" No unsigned or legacy-signed kernel drivers detected"); + return; + } + + foreach (var driver in suspiciousDrivers) + { + var signature = driver.Signature ?? new ServicesInfoHelper.KernelDriverSignatureInfo(); + List reasons = new List(); + + if (!signature.IsSigned) + { + reasons.Add("unsigned or signature missing"); + } + else if (signature.IsLegacyExpired) + { + reasons.Add("signed with certificate that expired before 29-Jul-2015 (legacy exception)"); + } + + if (!string.IsNullOrEmpty(driver.StartMode) && + (driver.StartMode.Equals("System", StringComparison.OrdinalIgnoreCase) || + driver.StartMode.Equals("Boot", StringComparison.OrdinalIgnoreCase))) + { + reasons.Add($"loads at early boot (Start={driver.StartMode})"); + } + + if (string.Equals(driver.Name, "kernelquick", StringComparison.OrdinalIgnoreCase)) + { + reasons.Add("service name matches ValleyRAT rootkit loader"); + } + + string reason = reasons.Count > 0 ? string.Join("; ", reasons) : "Potentially risky driver"; + string signatureLine = signature.IsSigned + ? $"Subject: {signature.Subject}; Issuer: {signature.Issuer}; Valid: {FormatDate(signature.NotBefore)} - {FormatDate(signature.NotAfter)}" + : $"Signature issue: {signature.Error ?? "Unsigned"}"; + + Beaprint.BadPrint($" {driver.Name} ({driver.DisplayName})"); + Beaprint.NoColorPrint($" Path : {driver.PathName}"); + Beaprint.NoColorPrint($" Start/State: {driver.StartMode}/{driver.State}"); + Beaprint.NoColorPrint($" Reason : {reason}"); + Beaprint.NoColorPrint($" Signature : {signatureLine}"); + } + } + catch (Exception ex) + { + Beaprint.PrintException(ex.Message); + } + } + + void PrintKernelQuickIndicators() + { + try + { + Beaprint.MainPrint("KernelQuick / ValleyRAT rootkit indicators"); + + bool found = false; + + Dictionary serviceValues = RegistryHelper.GetRegValues("HKLM", @"SYSTEM\\CurrentControlSet\\Services\\kernelquick"); + if (serviceValues != null) + { + found = true; + string imagePath = serviceValues.ContainsKey("ImagePath") ? serviceValues["ImagePath"].ToString() : "Unknown"; + string start = serviceValues.ContainsKey("Start") ? serviceValues["Start"].ToString() : "Unknown"; + Beaprint.BadPrint(" Service HKLM\\SYSTEM\\CurrentControlSet\\Services\\kernelquick present"); + Beaprint.NoColorPrint($" ImagePath : {imagePath}"); + Beaprint.NoColorPrint($" Start : {start}"); + } + + foreach (var path in new[] { @"SOFTWARE\\KernelQuick", @"SOFTWARE\\WOW6432Node\\KernelQuick", @"SYSTEM\\CurrentControlSet\\Services\\kernelquick" }) + { + Dictionary values = RegistryHelper.GetRegValues("HKLM", path); + if (values == null) + continue; + + var kernelQuickValues = values.Where(k => k.Key.StartsWith("KernelQuick_", StringComparison.OrdinalIgnoreCase)).ToList(); + if (kernelQuickValues.Count == 0) + continue; + + found = true; + Beaprint.BadPrint($" Registry values under HKLM\\{path}"); + foreach (var kv in kernelQuickValues) + { + string displayValue = kv.Value is byte[] bytes ? $"(binary) {bytes.Length} bytes" : string.Format("{0}", kv.Value); + Beaprint.NoColorPrint($" {kv.Key} = {displayValue}"); + } + } + + Dictionary ipdatesValues = RegistryHelper.GetRegValues("HKLM", @"SOFTWARE\\IpDates"); + if (ipdatesValues != null) + { + found = true; + Beaprint.BadPrint(" Possible kernel shellcode staging key HKLM\\SOFTWARE\\IpDates"); + foreach (var kv in ipdatesValues) + { + string displayValue = kv.Value is byte[] bytes ? $"(binary) {bytes.Length} bytes" : string.Format("{0}", kv.Value); + Beaprint.NoColorPrint($" {kv.Key} = {displayValue}"); + } + } + + if (!found) + { + Beaprint.InfoPrint(" No KernelQuick-specific registry indicators were found"); + } + else + { + Beaprint.LinkPrint("https://research.checkpoint.com/2025/cracking-valleyrat-from-builder-secrets-to-kernel-rootkits/", + "KernelQuick_* values and HKLM\\SOFTWARE\\IpDates are used by the ValleyRAT rootkit to hide files and stage APC payloads"); + } + } + catch (Exception ex) + { + Beaprint.PrintException(ex.Message); + } + } + + private string FormatDate(DateTime? dateTime) + { + return dateTime.HasValue ? dateTime.Value.ToString("yyyy-MM-dd HH:mm") : "n/a"; + } } } + diff --git a/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs b/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs index 96f17bd..a15ad23 100644 --- a/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs +++ b/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs @@ -89,6 +89,7 @@ namespace winPEAS.Checks AppLockerHelper.PrintAppLockerPolicy, PrintPrintersWMIInfo, PrintNamedPipes, + PrintNamedPipeAbuseCandidates, PrintAMSIProviders, PrintSysmon, PrintDotNetVersions @@ -857,6 +858,48 @@ namespace winPEAS.Checks } } + + private static void PrintNamedPipeAbuseCandidates() + { + Beaprint.MainPrint("Named Pipes with Low-Priv Write Access to Privileged Servers"); + + try + { + var candidates = NamedPipeSecurityAnalyzer.GetNamedPipeAbuseCandidates().ToList(); + + if (!candidates.Any()) + { + Beaprint.NoColorPrint(" No risky named pipe ACLs were found.\n"); + return; + } + + foreach (var candidate in candidates) + { + var aclSummary = candidate.LowPrivilegeAces.Any() + ? string.Join("; ", candidate.LowPrivilegeAces.Select(ace => + $"{ace.Principal} [{ace.RightsDescription}]").Where(s => !string.IsNullOrEmpty(s))) + : "Unknown"; + + var serverSummary = candidate.Processes.Any() + ? string.Join("; ", candidate.Processes.Select(proc => + $"{proc.ProcessName} (PID {proc.Pid}, {proc.UserName ?? proc.UserSid})")) + : "No privileged handles observed (service idle or access denied)"; + + var color = candidate.HasPrivilegedServer ? Beaprint.ansi_color_bad : Beaprint.ansi_color_yellow; + + Beaprint.ColorPrint($" \\\\.\\pipe\\{candidate.Name}", color); + Beaprint.NoColorPrint($" Low-priv ACLs : {aclSummary}"); + Beaprint.NoColorPrint($" Observed owners: {serverSummary}"); + Beaprint.NoColorPrint($" SDDL : {candidate.Sddl}"); + Beaprint.PrintLineSeparator(); + } + } + catch (Exception ex) + { + Beaprint.PrintException(ex.Message); + } + } + private void PrintAMSIProviders() { Beaprint.MainPrint("Enumerating AMSI registered providers"); diff --git a/winPEAS/winPEASexe/winPEAS/Info/ServicesInfo/ServicesInfoHelper.cs b/winPEAS/winPEASexe/winPEAS/Info/ServicesInfo/ServicesInfoHelper.cs index 324ecfc..3277084 100644 --- a/winPEAS/winPEASexe/winPEAS/Info/ServicesInfo/ServicesInfoHelper.cs +++ b/winPEAS/winPEASexe/winPEAS/Info/ServicesInfo/ServicesInfoHelper.cs @@ -2,11 +2,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Management; using System.Reflection; using System.Runtime.InteropServices; using System.Security.AccessControl; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.ServiceProcess; using System.Text.RegularExpressions; using winPEAS.Helpers; @@ -276,6 +279,109 @@ namespace winPEAS.Info.ServicesInfo } + private static readonly DateTime LegacyDriverCutoff = new DateTime(2015, 7, 29); + + public static List GetKernelDriverInfos() + { + List drivers = new List(); + + try + { + using (ManagementObjectSearcher wmiData = new ManagementObjectSearcher(@"root\cimv2", "SELECT Name,DisplayName,PathName,StartMode,State,ServiceType FROM win32_service")) + { + using (ManagementObjectCollection data = wmiData.Get()) + { + foreach (ManagementObject result in data) + { + string serviceType = GetStringOrEmpty(result["ServiceType"]); + if (string.IsNullOrEmpty(serviceType) || !serviceType.ToLowerInvariant().Contains("kernel driver")) + continue; + + string binaryPath = MyUtils.ReconstructExecPath(GetStringOrEmpty(result["PathName"])); + + drivers.Add(new KernelDriverInfo + { + Name = GetStringOrEmpty(result["Name"]), + DisplayName = GetStringOrEmpty(result["DisplayName"]), + StartMode = GetStringOrEmpty(result["StartMode"]), + State = GetStringOrEmpty(result["State"]), + PathName = binaryPath, + Signature = GetDriverSignatureInfo(binaryPath) + }); + } + } + } + } + catch (Exception ex) + { + Beaprint.PrintException(ex.Message); + } + + return drivers; + } + + private static KernelDriverSignatureInfo GetDriverSignatureInfo(string binaryPath) + { + KernelDriverSignatureInfo info = new KernelDriverSignatureInfo + { + FilePath = binaryPath, + IsSigned = false + }; + + if (string.IsNullOrEmpty(binaryPath) || !File.Exists(binaryPath)) + { + info.Error = "Binary not found"; + return info; + } + + try + { + using (var baseCertificate = X509Certificate.CreateFromSignedFile(binaryPath)) + using (var certificate = new X509Certificate2(baseCertificate)) + { + info.IsSigned = true; + info.Subject = certificate.Subject; + info.Issuer = certificate.Issuer; + info.NotBefore = certificate.NotBefore; + info.NotAfter = certificate.NotAfter; + info.IsLegacyExpired = certificate.NotAfter < LegacyDriverCutoff; + } + } + catch (CryptographicException cryptoEx) + { + info.Error = cryptoEx.Message; + } + catch (Exception ex) + { + info.Error = ex.Message; + } + + return info; + } + + internal class KernelDriverInfo + { + public string Name { get; set; } + public string DisplayName { get; set; } + public string PathName { get; set; } + public string StartMode { get; set; } + public string State { get; set; } + public KernelDriverSignatureInfo Signature { get; set; } + } + + internal class KernelDriverSignatureInfo + { + public string FilePath { get; set; } + public bool IsSigned { get; set; } + public string Subject { get; set; } + public string Issuer { get; set; } + public DateTime? NotBefore { get; set; } + public DateTime? NotAfter { get; set; } + public bool IsLegacyExpired { get; set; } + public string Error { get; set; } + } + + ////////////////////////////////////// //////// PATH DLL Hijacking ///////// ////////////////////////////////////// diff --git a/winPEAS/winPEASexe/winPEAS/Info/SystemInfo/NamedPipes/NamedPipeSecurityAnalyzer.cs b/winPEAS/winPEASexe/winPEAS/Info/SystemInfo/NamedPipes/NamedPipeSecurityAnalyzer.cs new file mode 100644 index 0000000..3796448 --- /dev/null +++ b/winPEAS/winPEASexe/winPEAS/Info/SystemInfo/NamedPipes/NamedPipeSecurityAnalyzer.cs @@ -0,0 +1,508 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.AccessControl; +using System.Security.Principal; +using winPEAS.Helpers; +using winPEAS.Native; + +namespace winPEAS.Info.SystemInfo.NamedPipes +{ + internal static class NamedPipeSecurityAnalyzer + { + private const string DeviceNamedPipePrefix = @"\Device\NamedPipe\"; + private static readonly char[] CandidateSeparators = { '\\', '/', '-', ':', '(' }; + + private static readonly HashSet LowPrivSidSet = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "S-1-1-0", // Everyone + "S-1-5-11", // Authenticated Users + "S-1-5-32-545", // Users + "S-1-5-32-546", // Guests + "S-1-5-32-547", // Power Users + "S-1-5-32-554", // Pre-Windows 2000 Compatible Access + "S-1-5-32-555", // Remote Desktop Users + "S-1-5-32-558", // Performance Log Users + "S-1-5-32-559", // Performance Monitor Users + "S-1-5-32-562", // Distributed COM Users + "S-1-5-32-569", // Remote Management Users + "S-1-5-4", // Interactive + "S-1-5-2", // Network + "S-1-5-1", // Dialup + "S-1-5-7" // Anonymous Logon + }; + + private static readonly HashSet LowPrivPrincipalKeywords = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "everyone", + "authenticated users", + "users", + "guests", + "power users", + "remote desktop users", + "remote management users", + "distributed com users", + "anonymous logon", + "interactive", + "network", + "local", + "batch", + "iis_iusrs" + }; + + private static readonly HashSet PrivilegedSidSet = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "S-1-5-18", // SYSTEM + "S-1-5-19", // LOCAL SERVICE + "S-1-5-20", // NETWORK SERVICE + "S-1-5-32-544" // Administrators + }; + + private static readonly (string Label, FileSystemRights Right)[] DangerousRightsMap = new[] + { + ("FullControl", FileSystemRights.FullControl), + ("Modify", FileSystemRights.Modify), + ("Write", FileSystemRights.Write), + ("WriteData", FileSystemRights.WriteData), + ("AppendData", FileSystemRights.AppendData), + ("CreateFiles", FileSystemRights.CreateFiles), + ("CreateDirectories", FileSystemRights.CreateDirectories), + ("WriteAttributes", FileSystemRights.WriteAttributes), + ("WriteExtendedAttributes", FileSystemRights.WriteExtendedAttributes), + ("Delete", FileSystemRights.Delete), + ("ChangePermissions", FileSystemRights.ChangePermissions), + ("TakeOwnership", FileSystemRights.TakeOwnership) + }; + + public static IEnumerable GetNamedPipeAbuseCandidates() + { + var insecurePipes = DiscoverInsecurePipes(); + if (!insecurePipes.Any()) + { + return Enumerable.Empty(); + } + + AttachProcesses(insecurePipes); + + return insecurePipes.Values + .Where(issue => issue.LowPrivilegeAces.Any()) + .OrderByDescending(issue => issue.HasPrivilegedServer) + .ThenBy(issue => issue.Name) + .ToList(); + } + + private static Dictionary DiscoverInsecurePipes() + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var pipe in NamedPipes.GetNamedPipeInfos()) + { + if (string.IsNullOrWhiteSpace(pipe.Sddl) || pipe.Sddl.Equals("ERROR", StringComparison.OrdinalIgnoreCase)) + continue; + + try + { + var descriptor = new RawSecurityDescriptor(pipe.Sddl); + if (descriptor.DiscretionaryAcl == null) + continue; + + foreach (GenericAce ace in descriptor.DiscretionaryAcl) + { + if (!(ace is CommonAce commonAce)) + continue; + + var sid = commonAce.SecurityIdentifier; + if (sid == null || !IsLowPrivilegePrincipal(sid)) + continue; + + if (!HasDangerousWriteRights(commonAce.AccessMask)) + continue; + + var rights = DescribeRights(commonAce.AccessMask).ToList(); + if (!rights.Any()) + continue; + + if (!result.TryGetValue(pipe.Name, out var issue)) + { + issue = new NamedPipeSecurityIssue(pipe.Name, pipe.Sddl, NormalizePipeName(pipe.Name)); + result[pipe.Name] = issue; + } + + var account = ResolveSidToName(sid); + issue.AddLowPrivPrincipal(account, sid.Value, rights); + } + } + catch + { + // Ignore malformed SDDL strings + } + } + + return result; + } + + private static void AttachProcesses(Dictionary insecurePipes) + { + if (!insecurePipes.Any()) + return; + + var lookup = BuildLookup(insecurePipes.Values); + if (!lookup.Any()) + return; + + List handles; + try + { + handles = HandlesHelper.GetAllHandlers(); + } + catch + { + return; + } + + var currentProcess = Kernel32.GetCurrentProcess(); + var processCache = new Dictionary(); + + foreach (var handle in handles) + { + IntPtr processHandle = IntPtr.Zero; + IntPtr duplicatedHandle = IntPtr.Zero; + + try + { + int pid = GetPid(handle); + if (pid <= 0) + continue; + + processHandle = Kernel32.OpenProcess( + HandlesHelper.ProcessAccessFlags.DupHandle | HandlesHelper.ProcessAccessFlags.QueryLimitedInformation, + false, + pid); + + if (processHandle == IntPtr.Zero) + continue; + + if (!Kernel32.DuplicateHandle(processHandle, handle.HandleValue, currentProcess, out duplicatedHandle, 0, false, HandlesHelper.DUPLICATE_SAME_ACCESS)) + continue; + + var typeName = HandlesHelper.GetObjectType(duplicatedHandle); + if (!string.Equals(typeName, "File", StringComparison.OrdinalIgnoreCase)) + continue; + + var objectName = HandlesHelper.GetObjectName(duplicatedHandle); + if (string.IsNullOrEmpty(objectName) || !objectName.StartsWith(DeviceNamedPipePrefix, StringComparison.OrdinalIgnoreCase)) + continue; + + var normalizedHandleName = NormalizePipeName(objectName.Substring(DeviceNamedPipePrefix.Length)); + var candidates = GetCandidateKeys(normalizedHandleName); + + bool matched = false; + + foreach (var candidate in candidates) + { + if (!lookup.TryGetValue(candidate, out var matchedIssues)) + continue; + + if (!processCache.TryGetValue(pid, out var processInfo)) + { + var raw = HandlesHelper.getProcInfoById(pid); + processInfo = new NamedPipeProcessInfo(raw.pid, raw.name, raw.userName, raw.userSid, IsHighPrivilegeAccount(raw.userSid, raw.userName)); + processCache[pid] = processInfo; + } + + foreach (var issue in matchedIssues) + { + issue.AddProcess(processInfo); + } + + matched = true; + break; + } + + if (!matched) + continue; + } + catch + { + // Ignore per-handle failures + } + finally + { + if (duplicatedHandle != IntPtr.Zero) + { + Kernel32.CloseHandle(duplicatedHandle); + } + if (processHandle != IntPtr.Zero) + { + Kernel32.CloseHandle(processHandle); + } + } + } + } + + private static Dictionary> BuildLookup(IEnumerable issues) + { + var lookup = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (var issue in issues) + { + foreach (var key in GetCandidateKeys(issue.NormalizedName)) + { + if (!lookup.TryGetValue(key, out var list)) + { + list = new List(); + lookup[key] = list; + } + + if (!list.Contains(issue)) + { + list.Add(issue); + } + } + } + + return lookup; + } + + private static IEnumerable GetCandidateKeys(string normalizedName) + { + if (string.IsNullOrEmpty(normalizedName)) + return Array.Empty(); + + var candidates = new HashSet(StringComparer.OrdinalIgnoreCase) + { + normalizedName + }; + + foreach (var separator in CandidateSeparators) + { + var idx = normalizedName.IndexOf(separator); + if (idx > 0) + { + candidates.Add(normalizedName.Substring(0, idx)); + } + } + + return candidates; + } + + private static string NormalizePipeName(string rawName) + { + if (string.IsNullOrWhiteSpace(rawName)) + return string.Empty; + + var normalized = rawName.Replace('/', '\\').Trim(); + while (normalized.StartsWith("\\", StringComparison.Ordinal)) + { + normalized = normalized.Substring(1); + } + + return normalized.ToLowerInvariant(); + } + + private static bool HasDangerousWriteRights(int accessMask) + { + var rights = (FileSystemRights)accessMask; + foreach (var entry in DangerousRightsMap) + { + if ((rights & entry.Right) == entry.Right) + return true; + } + + return false; + } + + private static IEnumerable DescribeRights(int accessMask) + { + var rights = (FileSystemRights)accessMask; + var descriptions = new List(); + + foreach (var entry in DangerousRightsMap) + { + if ((rights & entry.Right) == entry.Right) + { + descriptions.Add(entry.Label); + if (entry.Right == FileSystemRights.FullControl) + break; + } + } + + if (!descriptions.Any()) + { + descriptions.Add($"0x{accessMask:x}"); + } + + return descriptions; + } + + private static bool IsLowPrivilegePrincipal(SecurityIdentifier sid) + { + if (sid == null) + return false; + + if (LowPrivSidSet.Contains(sid.Value)) + return true; + + var accountName = ResolveSidToName(sid); + if (string.IsNullOrEmpty(accountName)) + return false; + + return LowPrivPrincipalKeywords.Any(keyword => accountName.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0); + } + + private static string ResolveSidToName(SecurityIdentifier sid) + { + if (sid == null) + return string.Empty; + + try + { + return sid.Translate(typeof(NTAccount)).Value; + } + catch + { + return sid.Value; + } + } + + private static bool IsHighPrivilegeAccount(string sid, string userName) + { + if (!string.IsNullOrEmpty(sid)) + { + if (PrivilegedSidSet.Contains(sid)) + return true; + + if (sid.StartsWith("S-1-5-80-", StringComparison.OrdinalIgnoreCase)) // Service SID + return true; + + if (sid.StartsWith("S-1-5-82-", StringComparison.OrdinalIgnoreCase)) // AppPool / service-like SIDs + return true; + } + + if (!string.IsNullOrEmpty(userName)) + { + if (string.Equals(userName, HandlesHelper.elevatedProcess, StringComparison.OrdinalIgnoreCase)) + return true; + + var normalized = userName.ToUpperInvariant(); + if (normalized.Contains("SYSTEM") || normalized.Contains("LOCAL SERVICE") || normalized.Contains("NETWORK SERVICE")) + return true; + + if (normalized.StartsWith("NT SERVICE\\", StringComparison.Ordinal)) + return true; + + if (normalized.EndsWith("$", StringComparison.Ordinal) && normalized.Contains("\\")) + return true; + } + + return false; + } + + private static int GetPid(HandlesHelper.SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX handle) + { + unchecked + { + if (IntPtr.Size == 4) + { + return (int)handle.UniqueProcessId.ToUInt32(); + } + + return (int)handle.UniqueProcessId.ToUInt64(); + } + } + } + + internal class NamedPipeSecurityIssue + { + private readonly Dictionary _principalAccess = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _processes = new Dictionary(); + + public NamedPipeSecurityIssue(string name, string sddl, string normalizedName) + { + Name = name; + Sddl = sddl; + NormalizedName = normalizedName; + } + + public string Name { get; } + public string Sddl { get; } + public string NormalizedName { get; } + + public IReadOnlyCollection LowPrivilegeAces => _principalAccess.Values; + public IReadOnlyCollection Processes => _processes.Values; + public bool HasPrivilegedServer => _processes.Values.Any(process => process.IsHighPrivilege); + + public void AddLowPrivPrincipal(string principal, string sid, IEnumerable rights) + { + if (string.IsNullOrEmpty(sid)) + return; + + if (!_principalAccess.TryGetValue(sid, out var access)) + { + access = new NamedPipePrincipalAccess(principal, sid); + _principalAccess[sid] = access; + } + + access.AddRights(rights); + } + + public void AddProcess(NamedPipeProcessInfo process) + { + if (process == null) + return; + + if (!_processes.ContainsKey(process.Pid)) + { + _processes[process.Pid] = process; + } + } + } + + internal class NamedPipePrincipalAccess + { + private readonly HashSet _rights = new HashSet(StringComparer.OrdinalIgnoreCase); + + public NamedPipePrincipalAccess(string principal, string sid) + { + Principal = principal; + Sid = sid; + } + + public string Principal { get; } + public string Sid { get; } + public string RightsDescription => _rights.Count == 0 ? string.Empty : string.Join("|", _rights.OrderBy(r => r)); + public IEnumerable Rights => _rights; + + public void AddRights(IEnumerable rights) + { + if (rights == null) + return; + + foreach (var right in rights) + { + if (!string.IsNullOrWhiteSpace(right)) + { + _rights.Add(right.Trim()); + } + } + } + } + + internal class NamedPipeProcessInfo + { + public NamedPipeProcessInfo(int pid, string processName, string userName, string userSid, bool isHighPrivilege) + { + Pid = pid; + ProcessName = processName; + UserName = userName; + UserSid = userSid; + IsHighPrivilege = isHighPrivilege; + } + + public int Pid { get; } + public string ProcessName { get; } + public string UserName { get; } + public string UserSid { get; } + public bool IsHighPrivilege { get; } + } +} diff --git a/winPEAS/winPEASexe/winPEAS/winPEAS.csproj b/winPEAS/winPEASexe/winPEAS/winPEAS.csproj index a09519f..6f9263c 100644 --- a/winPEAS/winPEASexe/winPEAS/winPEAS.csproj +++ b/winPEAS/winPEASexe/winPEAS/winPEAS.csproj @@ -1293,6 +1293,7 @@ +