From 4dad7599e6624844d1fb517d19dfd08fadc78b36 Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Sun, 7 Dec 2025 01:59:18 +0000 Subject: [PATCH 01/26] =?UTF-8?q?Add=20winpeas=20privilege=20escalation=20?= =?UTF-8?q?checks=20from:=20LDAP=20BOF=20Collection=20=E2=80=93=20In?= =?UTF-8?q?=E2=80=91Memory=20LDAP=20Toolkit=20for=20Active=20Directory=20E?= =?UTF-8?q?xploitation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- winPEAS/winPEASexe/README.md | 1 + .../winPEAS/Checks/ActiveDirectoryInfo.cs | 599 +++++++++++++++++- 2 files changed, 599 insertions(+), 1 deletion(-) diff --git a/winPEAS/winPEASexe/README.md b/winPEAS/winPEASexe/README.md index 8dd211c..27cbe24 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). + - 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. 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() { From b7b7aebf1cbf0e6d9a66c76e58fd6907d67d5dee Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Tue, 9 Dec 2025 02:07:57 +0000 Subject: [PATCH 02/26] =?UTF-8?q?Add=20winpeas=20privilege=20escalation=20?= =?UTF-8?q?checks=20from:=20pipetap=20=E2=80=93=20A=20Windows=20Named=20Pi?= =?UTF-8?q?pe=20Multi-tool=20and=20Proxy=20for=20Intercepting=20and=20Repl?= =?UTF-8?q?ayi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- winPEAS/winPEASexe/README.md | 1 + .../winPEASexe/winPEAS/Checks/SystemInfo.cs | 43 ++ .../NamedPipes/NamedPipeSecurityAnalyzer.cs | 508 ++++++++++++++++++ winPEAS/winPEASexe/winPEAS/winPEAS.csproj | 1 + 4 files changed, 553 insertions(+) create mode 100644 winPEAS/winPEASexe/winPEAS/Info/SystemInfo/NamedPipes/NamedPipeSecurityAnalyzer.cs diff --git a/winPEAS/winPEASexe/README.md b/winPEAS/winPEASexe/README.md index 8dd211c..c29acf4 100755 --- a/winPEAS/winPEASexe/README.md +++ b/winPEAS/winPEASexe/README.md @@ -153,6 +153,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/SystemInfo.cs b/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs index 344fd13..b096a86 100644 --- a/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs +++ b/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs @@ -88,6 +88,7 @@ namespace winPEAS.Checks AppLockerHelper.PrintAppLockerPolicy, PrintPrintersWMIInfo, PrintNamedPipes, + PrintNamedPipeAbuseCandidates, PrintAMSIProviders, PrintSysmon, PrintDotNetVersions @@ -791,6 +792,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/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 4259210..e22f5d2 100644 --- a/winPEAS/winPEASexe/winPEAS/winPEAS.csproj +++ b/winPEAS/winPEASexe/winPEAS/winPEAS.csproj @@ -1291,6 +1291,7 @@ + From 9123910f9da88caa8fd899ea56498c19ade4fb65 Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Wed, 10 Dec 2025 19:18:07 +0000 Subject: [PATCH 03/26] Add winpeas privilege escalation checks from: Cracking ValleyRAT: From Builder Secrets to Kernel Rootkits --- winPEAS/winPEASexe/README.md | 1 + .../winPEASexe/winPEAS/Checks/ServicesInfo.cs | 145 ++++++++++++++++++ .../Info/ServicesInfo/ServicesInfoHelper.cs | 106 +++++++++++++ 3 files changed, 252 insertions(+) diff --git a/winPEAS/winPEASexe/README.md b/winPEAS/winPEASexe/README.md index 8dd211c..f1c806d 100755 --- a/winPEAS/winPEASexe/README.md +++ b/winPEAS/winPEASexe/README.md @@ -76,6 +76,7 @@ The goal of this project is to search for possible **Privilege Escalation Paths* 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. +- Flag legacy/expired-signed kernel drivers (e.g., ValleyRAT's kernelquick) and their registry-controlled stealth configuration so you can spot kernel-level persistence. 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). 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/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 ///////// ////////////////////////////////////// From 6100bfaceb2bcf723ca84eba8866918b2ae9d3bc Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Thu, 11 Dec 2025 19:05:05 +0000 Subject: [PATCH 04/26] Add winpeas privilege escalation checks from: SOAPwn: Pwning .NET Framework Applications Through HTTP Client Proxies and WSDL --- winPEAS/winPEASexe/README.md | 1 + winPEAS/winPEASexe/winPEAS/Checks/Checks.cs | 1 + .../winPEAS/Checks/SoapClientInfo.cs | 88 +++++ winPEAS/winPEASexe/winPEAS/Helpers/MyUtils.cs | 63 +-- .../SoapClientProxyAnalyzer.cs | 366 ++++++++++++++++++ winPEAS/winPEASexe/winPEAS/winPEAS.csproj | 2 + 6 files changed, 497 insertions(+), 24 deletions(-) create mode 100644 winPEAS/winPEASexe/winPEAS/Checks/SoapClientInfo.cs create mode 100644 winPEAS/winPEASexe/winPEAS/Info/ApplicationInfo/SoapClientProxyAnalyzer.cs diff --git a/winPEAS/winPEASexe/README.md b/winPEAS/winPEASexe/README.md index 8dd211c..eb49dc8 100755 --- a/winPEAS/winPEASexe/README.md +++ b/winPEAS/winPEASexe/README.md @@ -76,6 +76,7 @@ The goal of this project is to search for possible **Privilege Escalation Paths* 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). diff --git a/winPEAS/winPEASexe/winPEAS/Checks/Checks.cs b/winPEAS/winPEASexe/winPEAS/Checks/Checks.cs index 8e0a8d1..d2cfac0 100644 --- a/winPEAS/winPEASexe/winPEAS/Checks/Checks.cs +++ b/winPEAS/winPEASexe/winPEAS/Checks/Checks.cs @@ -88,6 +88,7 @@ namespace winPEAS.Checks new SystemCheck("userinfo", new UserInfo()), new SystemCheck("processinfo", new ProcessInfo()), new SystemCheck("servicesinfo", new ServicesInfo()), + new SystemCheck("soapclientinfo", new SoapClientInfo()), new SystemCheck("applicationsinfo", new ApplicationsInfo()), new SystemCheck("networkinfo", new NetworkInfo()), new SystemCheck("activedirectoryinfo", new ActiveDirectoryInfo()), diff --git a/winPEAS/winPEASexe/winPEAS/Checks/SoapClientInfo.cs b/winPEAS/winPEASexe/winPEAS/Checks/SoapClientInfo.cs new file mode 100644 index 0000000..4b3ce80 --- /dev/null +++ b/winPEAS/winPEASexe/winPEAS/Checks/SoapClientInfo.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using winPEAS.Helpers; +using winPEAS.Info.ApplicationInfo; + +namespace winPEAS.Checks +{ + internal class SoapClientInfo : ISystemCheck + { + public void PrintInfo(bool isDebug) + { + Beaprint.GreatPrint(".NET SOAP Client Proxies (SOAPwn)"); + + CheckRunner.Run(PrintSoapClientFindings, isDebug); + } + + private static void PrintSoapClientFindings() + { + try + { + Beaprint.MainPrint("Potential SOAPwn / HttpWebClientProtocol abuse surfaces"); + Beaprint.LinkPrint( + "https://labs.watchtowr.com/soapwn-pwning-net-framework-applications-through-http-client-proxies-and-wsdl/", + "Look for .NET services that let attackers control SoapHttpClientProtocol URLs or WSDL imports to coerce NTLM or drop files."); + + List findings = SoapClientProxyAnalyzer.CollectFindings(); + if (findings.Count == 0) + { + Beaprint.NotFoundPrint(); + return; + } + + foreach (SoapClientProxyFinding finding in findings) + { + string severity = finding.BinaryIndicators.Contains("ServiceDescriptionImporter") + ? "Dynamic WSDL import" + : "SOAP proxy usage"; + + Beaprint.BadPrint($" [{severity}] {finding.BinaryPath}"); + + foreach (SoapClientProxyInstance instance in finding.Instances) + { + string instanceInfo = $" -> {instance.SourceType}: {instance.Name}"; + if (!string.IsNullOrEmpty(instance.Account)) + { + instanceInfo += $" ({instance.Account})"; + } + if (!string.IsNullOrEmpty(instance.Extra)) + { + instanceInfo += $" | {instance.Extra}"; + } + + Beaprint.GrayPrint(instanceInfo); + } + + if (finding.BinaryIndicators.Count > 0) + { + Beaprint.BadPrint(" Binary indicators: " + string.Join(", ", finding.BinaryIndicators)); + } + + if (finding.ConfigIndicators.Count > 0) + { + string configLabel = string.IsNullOrEmpty(finding.ConfigPath) + ? "Config indicators" + : $"Config indicators ({finding.ConfigPath})"; + Beaprint.BadPrint(" " + configLabel + ": " + string.Join(", ", finding.ConfigIndicators)); + } + + if (finding.BinaryScanFailed) + { + Beaprint.GrayPrint(" (Binary scan skipped due to access/size limits)"); + } + + if (finding.ConfigScanFailed) + { + Beaprint.GrayPrint(" (Unable to read config file)"); + } + + Beaprint.PrintLineSeparator(); + } + } + catch (Exception ex) + { + Beaprint.PrintException(ex.Message); + } + } + } +} diff --git a/winPEAS/winPEASexe/winPEAS/Helpers/MyUtils.cs b/winPEAS/winPEASexe/winPEAS/Helpers/MyUtils.cs index 5fb7e50..e81ad77 100644 --- a/winPEAS/winPEASexe/winPEAS/Helpers/MyUtils.cs +++ b/winPEAS/winPEASexe/winPEAS/Helpers/MyUtils.cs @@ -24,36 +24,51 @@ namespace winPEAS.Helpers //////////////////////////////////// /////// MISC - Files & Paths /////// //////////////////////////////////// - public static bool CheckIfDotNet(string path) + public static bool CheckIfDotNet(string path, bool ignoreCompanyName = false) { bool isDotNet = false; - FileVersionInfo myFileVersionInfo = FileVersionInfo.GetVersionInfo(path); - string companyName = myFileVersionInfo.CompanyName; - if ((string.IsNullOrEmpty(companyName)) || - (!Regex.IsMatch(companyName, @"^Microsoft.*", RegexOptions.IgnoreCase))) + string companyName = string.Empty; + + try { - try + FileVersionInfo myFileVersionInfo = FileVersionInfo.GetVersionInfo(path); + companyName = myFileVersionInfo.CompanyName; + } + catch + { + // Unable to read version information, continue with assembly inspection + } + + bool shouldInspectAssembly = ignoreCompanyName || + (string.IsNullOrEmpty(companyName)) || + (!Regex.IsMatch(companyName, @"^Microsoft.*", RegexOptions.IgnoreCase)); + + if (!shouldInspectAssembly) + { + return false; + } + + try + { + AssemblyName.GetAssemblyName(path); + isDotNet = true; + } + catch (System.IO.FileNotFoundException) + { + // System.Console.WriteLine("The file cannot be found."); + } + catch (System.BadImageFormatException exception) + { + if (Regex.IsMatch(exception.Message, + ".*This assembly is built by a runtime newer than the currently loaded runtime and cannot be loaded.*", + RegexOptions.IgnoreCase)) { - AssemblyName myAssemblyName = AssemblyName.GetAssemblyName(path); isDotNet = true; } - catch (System.IO.FileNotFoundException) - { - // System.Console.WriteLine("The file cannot be found."); - } - catch (System.BadImageFormatException exception) - { - if (Regex.IsMatch(exception.Message, - ".*This assembly is built by a runtime newer than the currently loaded runtime and cannot be loaded.*", - RegexOptions.IgnoreCase)) - { - isDotNet = true; - } - } - catch - { - // System.Console.WriteLine("The assembly has already been loaded."); - } + } + catch + { + // System.Console.WriteLine("The assembly has already been loaded."); } return isDotNet; diff --git a/winPEAS/winPEASexe/winPEAS/Info/ApplicationInfo/SoapClientProxyAnalyzer.cs b/winPEAS/winPEASexe/winPEAS/Info/ApplicationInfo/SoapClientProxyAnalyzer.cs new file mode 100644 index 0000000..9a499a3 --- /dev/null +++ b/winPEAS/winPEASexe/winPEAS/Info/ApplicationInfo/SoapClientProxyAnalyzer.cs @@ -0,0 +1,366 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management; +using System.Text; +using winPEAS.Helpers; +using winPEAS.Info.ProcessInfo; + +namespace winPEAS.Info.ApplicationInfo +{ + internal class SoapClientProxyInstance + { + public string SourceType { get; set; } + public string Name { get; set; } + public string Account { get; set; } + public string Extra { get; set; } + } + + internal class SoapClientProxyFinding + { + public string BinaryPath { get; set; } + public List Instances { get; } = new List(); + public HashSet BinaryIndicators { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); + public HashSet ConfigIndicators { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); + public string ConfigPath { get; set; } + public bool BinaryScanFailed { get; set; } + public bool ConfigScanFailed { get; set; } + } + + internal static class SoapClientProxyAnalyzer + { + private class SoapClientProxyCandidate + { + public string BinaryPath { get; set; } + public string SourceType { get; set; } + public string Name { get; set; } + public string Account { get; set; } + public string Extra { get; set; } + } + + private static readonly string[] BinaryIndicatorStrings = new[] + { + "SoapHttpClientProtocol", + "HttpWebClientProtocol", + "DiscoveryClientProtocol", + "HttpSimpleClientProtocol", + "HttpGetClientProtocol", + "HttpPostClientProtocol", + "ServiceDescriptionImporter", + "System.Web.Services.Description.ServiceDescriptionImporter", + }; + + private static readonly Dictionary ConfigIndicatorMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "soap:address", "soap:address element present" }, + { "soap12:address", "soap12:address element present" }, + { "?wsdl", "?wsdl reference" }, + { " DotNetCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public static List CollectFindings() + { + var findings = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var candidate in EnumerateServiceCandidates().Concat(EnumerateProcessCandidates())) + { + if (string.IsNullOrEmpty(candidate.BinaryPath) || !File.Exists(candidate.BinaryPath)) + { + continue; + } + + if (!findings.TryGetValue(candidate.BinaryPath, out var finding)) + { + finding = new SoapClientProxyFinding + { + BinaryPath = candidate.BinaryPath, + }; + + findings.Add(candidate.BinaryPath, finding); + } + + finding.Instances.Add(new SoapClientProxyInstance + { + SourceType = candidate.SourceType, + Name = candidate.Name, + Account = string.IsNullOrEmpty(candidate.Account) ? "Unknown" : candidate.Account, + Extra = candidate.Extra ?? string.Empty, + }); + } + + foreach (var finding in findings.Values) + { + ScanBinaryIndicators(finding); + ScanConfigIndicators(finding); + } + + return findings.Values + .Where(f => f.BinaryIndicators.Count > 0 || f.ConfigIndicators.Count > 0) + .OrderByDescending(f => f.BinaryIndicators.Contains("ServiceDescriptionImporter")) + .ThenBy(f => f.BinaryPath, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private static IEnumerable EnumerateServiceCandidates() + { + try + { + using (var searcher = new ManagementObjectSearcher(@"root\\cimv2", "SELECT Name, DisplayName, PathName, StartName FROM Win32_Service")) + using (var services = searcher.Get()) + { + foreach (ManagementObject service in services) + { + string pathName = service["PathName"]?.ToString(); + string binaryPath = MyUtils.GetExecutableFromPath(pathName ?? string.Empty); + if (string.IsNullOrEmpty(binaryPath) || !File.Exists(binaryPath)) + continue; + + if (!IsDotNetBinary(binaryPath)) + continue; + + yield return new SoapClientProxyCandidate + { + BinaryPath = binaryPath, + SourceType = "Service", + Name = service["Name"]?.ToString() ?? string.Empty, + Account = service["StartName"]?.ToString() ?? string.Empty, + Extra = service["DisplayName"]?.ToString() ?? string.Empty, + }; + } + } + } + catch (Exception ex) + { + Beaprint.GrayPrint("Error while enumerating services for SOAP client analysis: " + ex.Message); + } + } + + private static IEnumerable EnumerateProcessCandidates() + { + var results = new List(); + try + { + List> processes = ProcessesInfo.GetProcInfo(); + foreach (var proc in processes) + { + string path = proc.ContainsKey("ExecutablePath") ? proc["ExecutablePath"] : string.Empty; + if (string.IsNullOrEmpty(path) || !File.Exists(path)) + continue; + + if (!IsDotNetBinary(path)) + continue; + + string owner = proc.ContainsKey("Owner") ? proc["Owner"] : string.Empty; + if (!IsInterestingProcessOwner(owner)) + continue; + + results.Add(new SoapClientProxyCandidate + { + BinaryPath = path, + SourceType = "Process", + Name = proc.ContainsKey("Name") ? proc["Name"] : string.Empty, + Account = owner, + Extra = proc.ContainsKey("ProcessID") ? $"PID {proc["ProcessID"]}" : string.Empty, + }); + } + } + catch (Exception ex) + { + Beaprint.GrayPrint("Error while enumerating processes for SOAP client analysis: " + ex.Message); + } + + return results; + } + + private static bool IsInterestingProcessOwner(string owner) + { + if (string.IsNullOrEmpty(owner)) + return true; + + string normalizedOwner = owner; + if (owner.Contains("\\")) + { + normalizedOwner = owner.Split('\\').Last(); + } + + return !normalizedOwner.Equals(Environment.UserName, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsDotNetBinary(string path) + { + lock (DotNetCacheLock) + { + if (DotNetCache.TryGetValue(path, out bool cached)) + { + return cached; + } + + bool result = false; + try + { + result = MyUtils.CheckIfDotNet(path, true); + } + catch + { + } + + DotNetCache[path] = result; + return result; + } + } + + private static void ScanBinaryIndicators(SoapClientProxyFinding finding) + { + try + { + FileInfo fi = new FileInfo(finding.BinaryPath); + if (!fi.Exists || fi.Length == 0) + return; + + if (fi.Length > MaxBinaryScanSize) + { + finding.BinaryScanFailed = true; + return; + } + + foreach (var indicator in BinaryIndicatorStrings) + { + if (FileContainsString(finding.BinaryPath, indicator)) + { + finding.BinaryIndicators.Add(indicator); + } + } + } + catch + { + finding.BinaryScanFailed = true; + } + } + + private static void ScanConfigIndicators(SoapClientProxyFinding finding) + { + string configPath = GetConfigPath(finding.BinaryPath); + if (!string.IsNullOrEmpty(configPath) && File.Exists(configPath)) + { + finding.ConfigPath = configPath; + try + { + string content = File.ReadAllText(configPath); + foreach (var kvp in ConfigIndicatorMap) + { + if (content.IndexOf(kvp.Key, StringComparison.OrdinalIgnoreCase) >= 0) + { + finding.ConfigIndicators.Add(kvp.Value); + } + } + } + catch + { + finding.ConfigScanFailed = true; + } + } + + string directory = Path.GetDirectoryName(finding.BinaryPath); + if (!string.IsNullOrEmpty(directory)) + { + try + { + var wsdlFiles = Directory.GetFiles(directory, "*.wsdl", SearchOption.TopDirectoryOnly); + if (wsdlFiles.Length > 0) + { + finding.ConfigIndicators.Add($"Found {wsdlFiles.Length} WSDL file(s) next to binary"); + } + } + catch + { + // ignore + } + } + } + + private static string GetConfigPath(string binaryPath) + { + if (string.IsNullOrEmpty(binaryPath)) + return string.Empty; + + string candidate = binaryPath + ".config"; + return File.Exists(candidate) ? candidate : string.Empty; + } + + private static bool FileContainsString(string path, string value) + { + const int bufferSize = 64 * 1024; + byte[] pattern = Encoding.UTF8.GetBytes(value); + if (pattern.Length == 0) + return false; + + try + { + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) + { + byte[] buffer = new byte[bufferSize + pattern.Length]; + int bufferLen = 0; + int bytesRead; + while ((bytesRead = fs.Read(buffer, bufferLen, bufferSize)) > 0) + { + int total = bufferLen + bytesRead; + if (IndexOf(buffer, total, pattern) >= 0) + { + return true; + } + + if (pattern.Length > 1) + { + bufferLen = Math.Min(pattern.Length - 1, total); + Buffer.BlockCopy(buffer, total - bufferLen, buffer, 0, bufferLen); + } + else + { + bufferLen = 0; + } + } + } + } + catch + { + return false; + } + + return false; + } + + private static int IndexOf(byte[] buffer, int bufferLength, byte[] pattern) + { + int limit = bufferLength - pattern.Length; + if (limit < 0) + return -1; + + for (int i = 0; i <= limit; i++) + { + bool match = true; + for (int j = 0; j < pattern.Length; j++) + { + if (buffer[i + j] != pattern[j]) + { + match = false; + break; + } + } + + if (match) + return i; + } + + return -1; + } + } +} diff --git a/winPEAS/winPEASexe/winPEAS/winPEAS.csproj b/winPEAS/winPEASexe/winPEAS/winPEAS.csproj index 4259210..a09519f 100644 --- a/winPEAS/winPEASexe/winPEAS/winPEAS.csproj +++ b/winPEAS/winPEASexe/winPEAS/winPEAS.csproj @@ -1197,6 +1197,7 @@ + @@ -1223,6 +1224,7 @@ + From 74521345f62daddea361f3afc1e28dafe3bc75af Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Sat, 13 Dec 2025 18:41:50 +0000 Subject: [PATCH 05/26] Add linpeas privilege escalation checks from: HTB WhiteRabbit: n8n HMAC Forgery, SQL Injection, restic Abuse, and Time-Seeded --- linPEAS/README.md | 4 ++ .../6_users_information/19_Sudo_restic.sh | 71 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 linPEAS/builder/linpeas_parts/6_users_information/19_Sudo_restic.sh diff --git a/linPEAS/README.md b/linPEAS/README.md index 620608d..27de3de 100755 --- a/linPEAS/README.md +++ b/linPEAS/README.md @@ -98,6 +98,10 @@ The goal of this script is to search for possible **Privilege Escalation Paths** This script doesn't have any dependency. +### Recent updates + +- **Dec 2025**: Added detection for sudo configurations that expose restic's `--password-command` helper, a common privilege escalation vector observed in real environments. + It uses **/bin/sh** syntax, so can run in anything supporting `sh` (and the binaries and parameters used). By default, **linpeas won't write anything to disk and won't try to login as any other user using `su`**. diff --git a/linPEAS/builder/linpeas_parts/6_users_information/19_Sudo_restic.sh b/linPEAS/builder/linpeas_parts/6_users_information/19_Sudo_restic.sh new file mode 100644 index 0000000..e141db0 --- /dev/null +++ b/linPEAS/builder/linpeas_parts/6_users_information/19_Sudo_restic.sh @@ -0,0 +1,71 @@ +# Title: Users Information - Sudo restic password-command abuse +# ID: UG_Sudo_restic +# Author: HT Bot +# Last Update: 13-12-2025 +# Description: Detect sudo configurations that allow abusing restic --password-command for privilege escalation +# License: GNU GPL +# Version: 1.0 +# Functions Used: echo_not_found, print_2title, print_info +# Global Variables: $PASSWORD +# Initial Functions: +# Generated Global Variables: $restic_bin, $restic_sudo_found, $sudo_no_pw_output, $sudo_with_pw_output, $matches, $sudo_file, $block, $origin +# Fat linpeas: 0 +# Small linpeas: 1 + + +print_2title "Checking sudo restic --password-command exposure" +print_info "https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#sudo-and-suid" + +restic_bin="$(command -v restic 2>/dev/null)" +if [ -n "$restic_bin" ]; then + echo "restic binary found at: $restic_bin" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g" +else + echo "restic binary not found in PATH (still checking sudoers rules)" | sed -${E} "s,.*,${SED_YELLOW},g" +fi + +restic_sudo_found="" + +check_restic_entries() { + local block="$1" + local origin="$2" + + if [ -n "$block" ]; then + local matches + matches="$(printf '%s\n' "$block" | grep -i "restic" 2>/dev/null)" + if [ -n "$matches" ]; then + restic_sudo_found=1 + echo "$origin" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g" + printf '%s\n' "$matches" | sed -${E} "s,.*,${SED_RED_YELLOW},g" + fi + fi +} + +sudo_no_pw_output="$(sudo -n -l 2>/dev/null)" +check_restic_entries "$sudo_no_pw_output" "Matches in 'sudo -n -l'" + +if [ -n "$PASSWORD" ]; then + sudo_with_pw_output="$(echo "$PASSWORD" | timeout 1 sudo -S -l 2>/dev/null)" + check_restic_entries "$sudo_with_pw_output" "Matches in 'sudo -l' using provided password" +fi + +if [ -r "/etc/sudoers" ]; then + check_restic_entries "$(grep -v '^#' /etc/sudoers 2>/dev/null)" "Matches in /etc/sudoers" +fi + +if [ -d "/etc/sudoers.d" ]; then + for sudo_file in /etc/sudoers.d/*; do + [ -f "$sudo_file" ] || continue + check_restic_entries "$(grep -v '^#' "$sudo_file" 2>/dev/null)" "Matches in $sudo_file" + done +fi + +if [ -n "$restic_sudo_found" ]; then + echo "" + echo "restic's --password-command runs as the sudo target user (root)." | sed -${E} "s,.*,${SED_RED},g" + echo "Example: sudo restic check --password-command 'cp /bin/bash /tmp/restic-root && chmod 6777 /tmp/restic-root'" | sed -${E} "s,.*,${SED_RED_YELLOW},g" + echo "Then execute /tmp/restic-root -p for a root shell." | sed -${E} "s,.*,${SED_RED_YELLOW},g" +else + echo_not_found "sudo restic" +fi + +echo "" From 85aa98a84177b2ff28b5bc4056d995a938287724 Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Tue, 16 Dec 2025 19:11:20 +0000 Subject: [PATCH 06/26] Add winpeas privilege escalation checks from: Inside Ink Dragon: Revealing the Relay Network and Inner Workings of a Stealthy --- winPEAS/winPEASexe/README.md | 1 + winPEAS/winPEASexe/winPEAS/Checks/UserInfo.cs | 54 +++++++++++++- .../winPEAS/Info/UserInfo/UserInfoHelper.cs | 73 +++++++++++++++++++ 3 files changed, 125 insertions(+), 3 deletions(-) diff --git a/winPEAS/winPEASexe/README.md b/winPEAS/winPEASexe/README.md index 8dd211c..877b514 100755 --- a/winPEAS/winPEASexe/README.md +++ b/winPEAS/winPEASexe/README.md @@ -76,6 +76,7 @@ The goal of this project is to search for possible **Privilege Escalation Paths* 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. +- Highlight disconnected high-privilege RDP sessions so you can plan LSASS/token theft when admins leave idle sessions (Ink Dragon-style escalation). 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). diff --git a/winPEAS/winPEASexe/winPEAS/Checks/UserInfo.cs b/winPEAS/winPEASexe/winPEAS/Checks/UserInfo.cs index 350e525..0804ff4 100644 --- a/winPEAS/winPEASexe/winPEAS/Checks/UserInfo.cs +++ b/winPEAS/winPEASexe/winPEAS/Checks/UserInfo.cs @@ -156,15 +156,63 @@ namespace winPEAS.Checks try { Beaprint.MainPrint("RDP Sessions"); + Beaprint.LinkPrint("https://book.hacktricks.wiki/en/windows-hardening/windows-local-privilege-escalation/credentials-mgmt/rdp-sessions", "Disconnected high-privilege RDP sessions keep reusable tokens inside LSASS."); List> rdp_sessions = UserInfoHelper.GetRDPSessions(); if (rdp_sessions.Count > 0) { - string format = " {0,-10}{1,-15}{2,-15}{3,-25}{4,-10}{5}"; - string header = string.Format(format, "SessID", "pSessionName", "pUserName", "pDomainName", "State", "SourceIP"); + string format = " {0,-8}{1,-15}{2,-20}{3,-22}{4,-15}{5,-18}{6,-10}"; + string header = string.Format(format, "SessID", "Session", "User", "Domain", "State", "SourceIP", "HighPriv"); Beaprint.GrayPrint(header); + var colors = ColorsU(); + List> flaggedSessions = new List>(); foreach (Dictionary rdpSes in rdp_sessions) { - Beaprint.AnsiPrint(string.Format(format, rdpSes["SessionID"], rdpSes["pSessionName"], rdpSes["pUserName"], rdpSes["pDomainName"], rdpSes["State"], rdpSes["SourceIP"]), ColorsU()); + rdpSes.TryGetValue("SessionID", out string sessionId); + rdpSes.TryGetValue("pSessionName", out string sessionName); + rdpSes.TryGetValue("pUserName", out string userName); + rdpSes.TryGetValue("pDomainName", out string domainName); + rdpSes.TryGetValue("State", out string state); + rdpSes.TryGetValue("SourceIP", out string sourceIp); + + sessionId = sessionId ?? string.Empty; + sessionName = sessionName ?? string.Empty; + userName = userName ?? string.Empty; + domainName = domainName ?? string.Empty; + state = state ?? string.Empty; + sourceIp = sourceIp ?? string.Empty; + + bool isHighPriv = UserInfoHelper.IsHighPrivilegeAccount(userName, domainName); + string highPrivLabel = isHighPriv ? "Yes" : "No"; + rdpSes["HighPriv"] = highPrivLabel; + + if (isHighPriv && string.Equals(state, "Disconnected", StringComparison.OrdinalIgnoreCase)) + { + flaggedSessions.Add(rdpSes); + } + + Beaprint.AnsiPrint(string.Format(format, sessionId, sessionName, userName, domainName, state, sourceIp, highPrivLabel), colors); + } + + if (flaggedSessions.Count > 0) + { + Beaprint.BadPrint(" [!] Disconnected high-privilege RDP sessions detected. Their credentials/tokens stay in LSASS until the user signs out."); + foreach (Dictionary session in flaggedSessions) + { + session.TryGetValue("pDomainName", out string flaggedDomain); + session.TryGetValue("pUserName", out string flaggedUser); + session.TryGetValue("SessionID", out string flaggedSessionId); + session.TryGetValue("SourceIP", out string flaggedIp); + + flaggedDomain = flaggedDomain ?? string.Empty; + flaggedUser = flaggedUser ?? string.Empty; + flaggedSessionId = flaggedSessionId ?? string.Empty; + flaggedIp = flaggedIp ?? string.Empty; + + string userDisplay = string.Format("{0}\\{1}", flaggedDomain, flaggedUser).Trim('\\'); + string source = string.IsNullOrEmpty(flaggedIp) ? "local" : flaggedIp; + Beaprint.BadPrint(string.Format(" -> Session {0} ({1}) from {2}", flaggedSessionId, userDisplay, source)); + } + Beaprint.LinkPrint("https://book.hacktricks.wiki/en/windows-hardening/windows-local-privilege-escalation/credentials-mgmt/rdp-sessions", "Dump LSASS / steal tokens (e.g., comsvcs.dll, LsaLogonSessions, custom SSPs) to reuse those privileges."); } } else diff --git a/winPEAS/winPEASexe/winPEAS/Info/UserInfo/UserInfoHelper.cs b/winPEAS/winPEASexe/winPEAS/Info/UserInfo/UserInfoHelper.cs index 06de121..1a2e3bd 100644 --- a/winPEAS/winPEASexe/winPEAS/Info/UserInfo/UserInfoHelper.cs +++ b/winPEAS/winPEASexe/winPEAS/Info/UserInfo/UserInfoHelper.cs @@ -16,6 +16,20 @@ namespace winPEAS.Info.UserInfo { class UserInfoHelper { + private static readonly Dictionary _highPrivAccountCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly string[] _highPrivGroupIndicators = new string[] + { + "administrators", + "domain admins", + "enterprise admins", + "schema admins", + "server operators", + "account operators", + "backup operators", + "dnsadmins", + "hyper-v administrators" + }; + // https://stackoverflow.com/questions/5247798/get-list-of-local-computer-usernames-in-windows @@ -91,6 +105,65 @@ namespace winPEAS.Info.UserInfo return oPrincipalContext; } + public static bool IsHighPrivilegeAccount(string userName, string domain) + { + if (string.IsNullOrWhiteSpace(userName)) + { + return false; + } + + string cacheKey = ($"{domain}\\{userName}").Trim('\\'); + if (_highPrivAccountCache.TryGetValue(cacheKey, out bool cached)) + { + return cached; + } + + bool isHighPriv = false; + try + { + string resolvedDomain = string.IsNullOrWhiteSpace(domain) ? Checks.Checks.CurrentUserDomainName : domain; + List groups = User.GetUserGroups(userName, resolvedDomain); + foreach (string group in groups) + { + if (IsHighPrivilegeGroup(group)) + { + isHighPriv = true; + break; + } + } + } + catch (Exception ex) + { + Beaprint.GrayPrint(string.Format(" [-] Unable to resolve groups for {0}\\{1}: {2}", domain, userName, ex.Message)); + } + + if (!isHighPriv) + { + isHighPriv = string.Equals(userName, "administrator", StringComparison.OrdinalIgnoreCase) || userName.StartsWith("admin", StringComparison.OrdinalIgnoreCase); + } + + _highPrivAccountCache[cacheKey] = isHighPriv; + return isHighPriv; + } + + private static bool IsHighPrivilegeGroup(string groupName) + { + if (string.IsNullOrWhiteSpace(groupName)) + { + return false; + } + + foreach (string indicator in _highPrivGroupIndicators) + { + if (groupName.IndexOf(indicator, StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + } + + return false; + } + //From Seatbelt public enum WTS_CONNECTSTATE_CLASS { From 488d38883040162d84980ff231bb6f95fea801c2 Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Wed, 17 Dec 2025 01:34:41 +0000 Subject: [PATCH 07/26] Add winpeas privilege escalation checks from: Windows Exploitation Technique: Amplifying Race Windows via Slow Object Manager --- winPEAS/winPEASexe/README.md | 2 ++ .../winPEASexe/winPEAS/Checks/SystemInfo.cs | 26 ++++++++++++++ .../winPEAS/Helpers/ObjectManagerHelper.cs | 34 +++++++++++++++++++ winPEAS/winPEASexe/winPEAS/winPEAS.csproj | 1 + 4 files changed, 63 insertions(+) create mode 100644 winPEAS/winPEASexe/winPEAS/Helpers/ObjectManagerHelper.cs diff --git a/winPEAS/winPEASexe/README.md b/winPEAS/winPEASexe/README.md index 8dd211c..91f7ef5 100755 --- a/winPEAS/winPEASexe/README.md +++ b/winPEAS/winPEASexe/README.md @@ -77,6 +77,8 @@ The goal of this project is to search for possible **Privilege Escalation Paths* 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. +- Added Object Manager race-window amplification guidance (Project Zero 2025): winPEAS now checks if the current user can create named objects under \\BaseNamedObjects and reminds you how to build extremely long names/deep directory chains to stretch kernel race windows. + 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). diff --git a/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs b/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs index 344fd13..e72b31d 100644 --- a/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs +++ b/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs @@ -81,6 +81,7 @@ namespace winPEAS.Checks PrintKrbRelayUp, PrintInsideContainer, PrintAlwaysInstallElevated, + PrintObjectManagerRaceAmplification, PrintLSAInfo, PrintNtlmSettings, PrintLocalGroupPolicy, @@ -667,6 +668,31 @@ namespace winPEAS.Checks } } + static void PrintObjectManagerRaceAmplification() + { + try + { + Beaprint.MainPrint("Object Manager race-window amplification primitives"); + Beaprint.LinkPrint("https://projectzero.google/2025/12/windows-exploitation-techniques.html", "Project Zero write-up:"); + + if (ObjectManagerHelper.TryCreateSessionEvent(out var objectName, out var error)) + { + Beaprint.BadPrint($" Created a test named event ({objectName}) under \\BaseNamedObjects."); + Beaprint.InfoPrint(" -> Low-privileged users can slow NtOpen*/NtCreate* lookups using ~32k-character names or ~16k-level directory chains."); + Beaprint.InfoPrint(" -> Point attacker-controlled symbolic links to the slow path to stretch kernel race windows."); + Beaprint.InfoPrint(" -> Use this whenever a bug follows check -> NtOpenX -> privileged action patterns."); + } + else + { + Beaprint.InfoPrint($" Could not create a test event under \\BaseNamedObjects ({error}). The namespace might be locked down."); + } + } + catch (Exception ex) + { + Beaprint.PrintException(ex.Message); + } + } + private static void PrintNtlmSettings() { Beaprint.MainPrint($"Enumerating NTLM Settings"); diff --git a/winPEAS/winPEASexe/winPEAS/Helpers/ObjectManagerHelper.cs b/winPEAS/winPEASexe/winPEAS/Helpers/ObjectManagerHelper.cs new file mode 100644 index 0000000..fdc6df1 --- /dev/null +++ b/winPEAS/winPEASexe/winPEAS/Helpers/ObjectManagerHelper.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace winPEAS.Helpers +{ + internal static class ObjectManagerHelper + { + public static bool TryCreateSessionEvent(out string objectName, out string error) + { + objectName = $"PEAS_OMNS_{Process.GetCurrentProcess().Id}_{Guid.NewGuid():N}"; + error = string.Empty; + + try + { + using (var handle = new EventWaitHandle(initialState: false, EventResetMode.ManualReset, objectName, out var createdNew)) + { + if (!createdNew) + { + error = "A test event with the generated name already existed."; + return false; + } + } + + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + } +} diff --git a/winPEAS/winPEASexe/winPEAS/winPEAS.csproj b/winPEAS/winPEASexe/winPEAS/winPEAS.csproj index 4259210..a499eee 100644 --- a/winPEAS/winPEASexe/winPEAS/winPEAS.csproj +++ b/winPEAS/winPEASexe/winPEAS/winPEAS.csproj @@ -1359,6 +1359,7 @@ + From 1039cc2eff9c0597b02eeb97a9bd95aaaa451c95 Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Wed, 17 Dec 2025 02:19:32 +0000 Subject: [PATCH 08/26] Add linpeas privilege escalation checks from: From Chrome Renderer Code Execution to Linux Kernel RCE via AF_UNIX MSG_OOB (CVE --- .../1_system_information/18_CVE_2025_38236.sh | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 linPEAS/builder/linpeas_parts/1_system_information/18_CVE_2025_38236.sh diff --git a/linPEAS/builder/linpeas_parts/1_system_information/18_CVE_2025_38236.sh b/linPEAS/builder/linpeas_parts/1_system_information/18_CVE_2025_38236.sh new file mode 100644 index 0000000..1114249 --- /dev/null +++ b/linPEAS/builder/linpeas_parts/1_system_information/18_CVE_2025_38236.sh @@ -0,0 +1,126 @@ +# Title: System Information - CVE_2025_38236 +# ID: SY_CVE_2025_38236 +# Author: HT Bot +# Last Update: 17-12-2025 +# Description: Detect Linux kernels exposed to CVE-2025-38236 (AF_UNIX MSG_OOB UAF) that allow local privilege escalation: +# - Vulnerable scope: +# * Linux kernels 6.9+ before commit 32ca245464e1479bfea8592b9db227fdc1641705 +# * AF_UNIX stream sockets with MSG_OOB enabled (CONFIG_AF_UNIX_OOB or implicit support) +# - Exploitation summary: +# * send/recv MSG_OOB pattern leaves zero-length SKBs in the receive queue +# * manage_oob() skips cleanup, freeing the OOB SKB while u->oob_skb still points to it +# * Subsequent recv(MSG_OOB) dereferences the dangling pointer → kernel UAF → LPE +# - Mitigations: +# * Update to a kernel that includes commit 32ca245464e1479bfea8592b9db227fdc1641705 (or newer) +# * Disable CONFIG_AF_UNIX_OOB or block MSG_OOB in sandboxed processes +# * Backport vendor fixes or follow Chrome's MSG_OOB filtering approach +# License: GNU GPL +# Version: 1.0 +# Functions Used: print_2title, print_info +# Global Variables: $MACPEAS, $SED_RED_YELLOW, $SED_GREEN, $E +# Initial Functions: +# Generated Global Variables: $cve38236_kernel_release, $cve38236_kernel_version, $cve38236_oob_line, $cve38236_unix_line, $cve38236_oob_status, $CVE38236_CONFIG_SOURCE, $cve38236_conf_file, $cve38236_config_key, $cve38236_release, $cve38236_cfg, $cve38236_config_line +# Fat linpeas: 0 +# Small linpeas: 1 + +_cve38236_version_to_number() { + if [ -z "$1" ]; then + printf '0\n' + return + fi + echo "$1" | awk -F. '{ + major=$1+0 + if (NF>=2) minor=$2+0; else minor=0 + if (NF>=3) patch=$3+0; else patch=0 + printf "%d\n", (major*1000000)+(minor*1000)+patch + }' +} + +_cve38236_version_ge() { + local v1 v2 + v1=$(_cve38236_version_to_number "$1") + v2=$(_cve38236_version_to_number "$2") + [ "$v1" -ge "$v2" ] +} + +_cve38236_cat_config_file() { + local cve38236_conf_file="$1" + if [ -z "$cve38236_conf_file" ] || ! [ -r "$cve38236_conf_file" ]; then + return 1 + fi + if printf '%s' "$cve38236_conf_file" | grep -q '\\.gz$'; then + if command -v zcat >/dev/null 2>&1; then + zcat "$cve38236_conf_file" 2>/dev/null + elif command -v gzip >/dev/null 2>&1; then + gzip -dc "$cve38236_conf_file" 2>/dev/null + else + cat "$cve38236_conf_file" 2>/dev/null + fi + else + cat "$cve38236_conf_file" 2>/dev/null + fi +} + +_cve38236_read_config_line() { + local cve38236_config_key="$1" + local cve38236_release cve38236_config_line cve38236_cfg + cve38236_release="$(uname -r 2>/dev/null)" + for cve38236_cfg in /proc/config.gz \ + "/boot/config-${cve38236_release}" \ + "/usr/lib/modules/${cve38236_release}/build/.config" \ + "/lib/modules/${cve38236_release}/build/.config"; do + if [ -r "$cve38236_cfg" ]; then + cve38236_config_line=$(_cve38236_cat_config_file "$cve38236_cfg" | grep -E "^(${cve38236_config_key}=|# ${cve38236_config_key} is not set)" | head -n1) + if [ -n "$cve38236_config_line" ]; then + CVE38236_CONFIG_SOURCE="$cve38236_cfg" + printf '%s\n' "$cve38236_config_line" + return 0 + fi + fi + done + return 1 +} + + +if [ ! "$MACPEAS" ]; then + cve38236_kernel_release="$(uname -r 2>/dev/null)" + cve38236_kernel_version="$(printf '%s' "$cve38236_kernel_release" | sed 's/[^0-9.].*//')" + + if [ -n "$cve38236_kernel_version" ] && _cve38236_version_ge "$cve38236_kernel_version" "6.9.0"; then + print_2title "CVE-2025-38236 - AF_UNIX MSG_OOB UAF" + + cve38236_oob_line=$(_cve38236_read_config_line "CONFIG_AF_UNIX_OOB") + cve38236_oob_status="unknown" + + if printf '%s' "$cve38236_oob_line" | grep -q '=y\|=m'; then + cve38236_oob_status="enabled" + elif printf '%s' "$cve38236_oob_line" | grep -q 'not set'; then + cve38236_oob_status="disabled" + fi + + if [ "$cve38236_oob_status" = "unknown" ]; then + cve38236_unix_line=$(_cve38236_read_config_line "CONFIG_UNIX") + if printf '%s' "$cve38236_unix_line" | grep -q 'not set'; then + cve38236_oob_status="disabled" + elif printf '%s' "$cve38236_unix_line" | grep -q '=y\|=m'; then + cve38236_oob_status="enabled" + fi + fi + + if [ "$cve38236_oob_status" = "disabled" ]; then + printf 'Kernel %s >= 6.9 but MSG_OOB support is disabled (%s).\n' "$cve38236_kernel_release" "${cve38236_oob_line:-CONFIG_AF_UNIX disabled}" | sed -${E} "s,.*,${SED_GREEN}," + print_info "CVE-2025-38236 requires AF_UNIX MSG_OOB; disabling CONFIG_AF_UNIX_OOB/CONFIG_UNIX mitigates it." + else + printf 'Kernel %s (parsed %s) may be vulnerable to CVE-2025-38236 - AF_UNIX MSG_OOB UAF.\n' "$cve38236_kernel_release" "$cve38236_kernel_version" | sed -${E} "s,.*,${SED_RED_YELLOW}," + [ -n "$cve38236_oob_line" ] && print_info "Config hint: $cve38236_oob_line" + if [ "$cve38236_oob_status" = "unknown" ]; then + print_info "Could not read CONFIG_AF_UNIX_OOB directly; AF_UNIX appears enabled, so assume MSG_OOB reachable." + fi + print_info "Exploit chain: crafted MSG_OOB send/recv frees the OOB SKB while u->oob_skb still points to it, enabling kernel UAF → arbitrary read/write primitives (Project Zero 2025/08)." + print_info "Mitigations: update to a kernel containing commit 32ca245464e1479bfea8592b9db227fdc1641705, disable CONFIG_AF_UNIX_OOB, or filter MSG_OOB in sandbox policies." + print_info "Heuristic detection: based solely on uname -r and kernel config; vendor kernels with backported fixes should be verified manually." + fi + echo "" + fi + +fi From 0e52c2feea01b7bd17f4bc8ddcfbc0056a49ba20 Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Mon, 22 Dec 2025 13:20:16 +0000 Subject: [PATCH 09/26] =?UTF-8?q?Add=20linpeas=20privilege=20escalation=20?= =?UTF-8?q?checks=20from:=20CVE-2025-38352=20=E2=80=93=20In-the-wild=20And?= =?UTF-8?q?roid=20Kernel=20Vulnerability=20Analysis=20and=20PoC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linPEAS/README.md | 2 + .../1_system_information/18_CVE_2025_38352.sh | 183 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 linPEAS/builder/linpeas_parts/1_system_information/18_CVE_2025_38352.sh diff --git a/linPEAS/README.md b/linPEAS/README.md index 620608d..da820ac 100755 --- a/linPEAS/README.md +++ b/linPEAS/README.md @@ -6,6 +6,8 @@ Check the **Local Linux Privilege Escalation checklist** from **[book.hacktricks.wiki](https://book.hacktricks.wiki/en/linux-hardening/linux-privilege-escalation-checklist.html)**. +> **Dec 2025 update:** linpeas now inspects Linux kernels for CVE-2025-38352 (POSIX CPU timers race) by combining CONFIG_POSIX_CPU_TIMERS_TASK_WORK state with kernel build information, so you immediately know if publicly available PoCs might succeed. + [![asciicast](https://asciinema.org/a/250532.png)](https://asciinema.org/a/309566) ## MacPEAS diff --git a/linPEAS/builder/linpeas_parts/1_system_information/18_CVE_2025_38352.sh b/linPEAS/builder/linpeas_parts/1_system_information/18_CVE_2025_38352.sh new file mode 100644 index 0000000..f0a52b6 --- /dev/null +++ b/linPEAS/builder/linpeas_parts/1_system_information/18_CVE_2025_38352.sh @@ -0,0 +1,183 @@ +# Title: System Information - CVE_2025_38352 +# ID: SY_CVE_2025_38352 +# Author: HT Bot +# Last Update: 22-12-2025 +# Description: Detect Linux kernels that may still be vulnerable to CVE-2025-38352 (race-condition UAF in POSIX CPU timers) +# - Highlights kernels built without CONFIG_POSIX_CPU_TIMERS_TASK_WORK +# - Flags 6.12.x builds older than the fix commit f90fff1e152dedf52b932240ebbd670d83330eca (first shipped in 6.12.34) +# - Provides quick risk scoring so operators can decide whether to attempt the publicly available PoC +# - Core requirements for exploitation: +# * CONFIG_POSIX_CPU_TIMERS_TASK_WORK disabled (common on 32-bit Android / custom kernels) +# * Lack of the upstream exit_state guard in run_posix_cpu_timers() +# License: GNU GPL +# Version: 1.0 +# Functions Used: echo_not_found, print_2title, print_list +# Global Variables: $E, $SED_GREEN, $SED_RED, $SED_RED_YELLOW, $SED_YELLOW +# Initial Functions: +# Generated Global Variables: $cve38352_kernel_release, $cve38352_kernel_version_cmp, $cve38352_symbol, $cve38352_task_work_state, $cve38352_config_status, $cve38352_config_source, $cve38352_config_candidates, $cve38352_cfg, $cve38352_line, $cve38352_patch_state, $cve38352_patch_label, $cve38352_fix_tag, $cve38352_last_vuln_tag, $cve38352_risk_msg, $cve38352_risk_color, $cve38352_task_line, $cve38352_patch_line, $cve38352_risk_line +# Fat linpeas: 0 +# Small linpeas: 1 + +cve38352_version_lt(){ + awk -v v1="$1" -v v2="$2" ' + function cleannum(val) { + gsub(/[^0-9].*/, "", val) + if (val == "") { + val = 0 + } + return val + 0 + } + BEGIN { + n = split(v1, a, ".") + m = split(v2, b, ".") + max = (n > m ? n : m) + for (i = 1; i <= max; i++) { + av = (i <= n ? cleannum(a[i]) : 0) + bv = (i <= m ? cleannum(b[i]) : 0) + if (av < bv) { + exit 0 + } + if (av > bv) { + exit 1 + } + } + exit 1 + }' +} + +cve38352_sanitize_version(){ + printf "%s" "$1" | tr '-' '.' | sed 's/[^0-9.].*$//' | sed 's/\.\./\./g' | sed 's/^\.//' | sed 's/\.$//' +} + +print_2title "CVE-2025-38352 - POSIX CPU timers race" + +cve38352_kernel_release=$(uname -r 2>/dev/null) +if [ -z "$cve38352_kernel_release" ]; then + echo_not_found "uname -r" + echo "" +else + + cve38352_kernel_version_cmp=$(cve38352_sanitize_version "$cve38352_kernel_release") + if [ -z "$cve38352_kernel_version_cmp" ]; then + cve38352_kernel_version_cmp="unknown" + fi + + cve38352_symbol="CONFIG_POSIX_CPU_TIMERS_TASK_WORK" + cve38352_task_work_state="unknown" + cve38352_config_status="Unknown ($cve38352_symbol not found)" + cve38352_config_source="" + + cve38352_config_candidates="/boot/config-$cve38352_kernel_release /proc/config.gz /lib/modules/$cve38352_kernel_release/build/.config /usr/lib/modules/$cve38352_kernel_release/build/.config /usr/src/linux/.config" + for cve38352_cfg in $cve38352_config_candidates; do + [ -r "$cve38352_cfg" ] || continue + if printf "%s" "$cve38352_cfg" | grep -q '\\.gz$'; then + cve38352_line=$(gzip -dc "$cve38352_cfg" 2>/dev/null | grep -E "^(# )?$cve38352_symbol" | head -n1) + else + cve38352_line=$(grep -E "^(# )?$cve38352_symbol" "$cve38352_cfg" 2>/dev/null | head -n1) + fi + [ -z "$cve38352_line" ] && continue + cve38352_config_source="$cve38352_cfg" + case "$cve38352_line" in + "$cve38352_symbol=y") + cve38352_task_work_state="enabled" + cve38352_config_status="Enabled (y)" + ;; + "$cve38352_symbol=m") + cve38352_task_work_state="enabled" + cve38352_config_status="Built as module (m)" + ;; + "$cve38352_symbol=n") + cve38352_task_work_state="disabled" + cve38352_config_status="Disabled (n)" + ;; + "# $cve38352_symbol is not set") + cve38352_task_work_state="disabled" + cve38352_config_status="Not set" + ;; + *) + cve38352_config_status="Found: $cve38352_line" + ;; + esac + break + done + + cve38352_patch_state="unknown_branch" + cve38352_patch_label="Unable to determine kernel train" + cve38352_fix_tag="6.12.34" + cve38352_last_vuln_tag="6.12.33" + case "$cve38352_kernel_version_cmp" in + 6.12|6.12.*) + if cve38352_version_lt "$cve38352_kernel_version_cmp" "$cve38352_fix_tag"; then + cve38352_patch_state="pre_fix" + cve38352_patch_label="6.12.x build < $cve38352_fix_tag (last known vulnerable LTS: $cve38352_last_vuln_tag)" + else + cve38352_patch_state="post_fix" + cve38352_patch_label="6.12.x build >= $cve38352_fix_tag (should include fix f90fff1e152d)" + fi + ;; + unknown) + cve38352_patch_label="Kernel version string could not be parsed" + ;; + *) + cve38352_patch_label="Kernel train $cve38352_kernel_version_cmp (verify commit f90fff1e152dedf52b932240ebbd670d83330eca manually)" + ;; + esac + + cve38352_risk_msg="Unknown - missing configuration data" + cve38352_risk_color="" + if [ "$cve38352_task_work_state" = "enabled" ]; then + cve38352_risk_msg="Low - CONFIG_POSIX_CPU_TIMERS_TASK_WORK is enabled" + cve38352_risk_color="green" + elif [ "$cve38352_task_work_state" = "disabled" ]; then + if [ "$cve38352_patch_state" = "pre_fix" ]; then + cve38352_risk_msg="High - task_work disabled & kernel predates fix f90fff1e152d" + cve38352_risk_color="red" + else + cve38352_risk_msg="Review - task_work disabled, ensure fix f90fff1e152d is backported" + cve38352_risk_color="yellow" + fi + fi + + print_list "Kernel release ............... $cve38352_kernel_release\n" + print_list "Comparable version ........... $cve38352_kernel_version_cmp\n" + + cve38352_task_line="Task_work config ............. $cve38352_config_status" + if [ -n "$cve38352_config_source" ]; then + cve38352_task_line="$cve38352_task_line (from $cve38352_config_source)" + fi + cve38352_task_line="$cve38352_task_line\n" + if [ "$cve38352_task_work_state" = "disabled" ]; then + print_list "$cve38352_task_line" | sed -${E} "s,.*,${SED_RED}," + elif [ "$cve38352_task_work_state" = "enabled" ]; then + print_list "$cve38352_task_line" | sed -${E} "s,.*,${SED_GREEN}," + else + print_list "$cve38352_task_line" + fi + + cve38352_patch_line="Patch status ................. $cve38352_patch_label\n" + if [ "$cve38352_patch_state" = "pre_fix" ]; then + print_list "$cve38352_patch_line" | sed -${E} "s,.*,${SED_RED_YELLOW}," + elif [ "$cve38352_patch_state" = "post_fix" ]; then + print_list "$cve38352_patch_line" | sed -${E} "s,.*,${SED_GREEN}," + else + print_list "$cve38352_patch_line" | sed -${E} "s,.*,${SED_YELLOW}," + fi + + cve38352_risk_line="CVE-2025-38352 risk .......... $cve38352_risk_msg\n" + case "$cve38352_risk_color" in + red) + print_list "$cve38352_risk_line" | sed -${E} "s,.*,${SED_RED_YELLOW}," + ;; + green) + print_list "$cve38352_risk_line" | sed -${E} "s,.*,${SED_GREEN}," + ;; + yellow) + print_list "$cve38352_risk_line" | sed -${E} "s,.*,${SED_YELLOW}," + ;; + *) + print_list "$cve38352_risk_line" + ;; + esac + + echo "" +fi From 4559fd09eab796e6ed7c04108aef70069ea45cf5 Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Sat, 17 Jan 2026 15:25:23 +0100 Subject: [PATCH 24/26] Fix SOAP service enumeration yield in try/catch --- .../Info/ApplicationInfo/SoapClientProxyAnalyzer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/winPEAS/winPEASexe/winPEAS/Info/ApplicationInfo/SoapClientProxyAnalyzer.cs b/winPEAS/winPEASexe/winPEAS/Info/ApplicationInfo/SoapClientProxyAnalyzer.cs index 9a499a3..cb14fc6 100644 --- a/winPEAS/winPEASexe/winPEAS/Info/ApplicationInfo/SoapClientProxyAnalyzer.cs +++ b/winPEAS/winPEASexe/winPEAS/Info/ApplicationInfo/SoapClientProxyAnalyzer.cs @@ -112,6 +112,7 @@ namespace winPEAS.Info.ApplicationInfo private static IEnumerable EnumerateServiceCandidates() { + var results = new List(); try { using (var searcher = new ManagementObjectSearcher(@"root\\cimv2", "SELECT Name, DisplayName, PathName, StartName FROM Win32_Service")) @@ -127,14 +128,14 @@ namespace winPEAS.Info.ApplicationInfo if (!IsDotNetBinary(binaryPath)) continue; - yield return new SoapClientProxyCandidate + results.Add(new SoapClientProxyCandidate { BinaryPath = binaryPath, SourceType = "Service", Name = service["Name"]?.ToString() ?? string.Empty, Account = service["StartName"]?.ToString() ?? string.Empty, Extra = service["DisplayName"]?.ToString() ?? string.Empty, - }; + }); } } } @@ -142,6 +143,8 @@ namespace winPEAS.Info.ApplicationInfo { Beaprint.GrayPrint("Error while enumerating services for SOAP client analysis: " + ex.Message); } + + return results; } private static IEnumerable EnumerateProcessCandidates() From 2c6cbfa43dfad62af675551c4ca3fc34ca07ee2b Mon Sep 17 00:00:00 2001 From: SirBroccoli Date: Sat, 17 Jan 2026 15:32:29 +0100 Subject: [PATCH 25/26] Updating sudoB.sh with variables information --- linPEAS/builder/linpeas_parts/variables/sudoB.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/linPEAS/builder/linpeas_parts/variables/sudoB.sh b/linPEAS/builder/linpeas_parts/variables/sudoB.sh index bb2362c..39f17dd 100644 --- a/linPEAS/builder/linpeas_parts/variables/sudoB.sh +++ b/linPEAS/builder/linpeas_parts/variables/sudoB.sh @@ -12,5 +12,4 @@ # Fat linpeas: 0 # Small linpeas: 1 - -sudoB="$(whoami)|ALL:ALL|ALL : ALL|ALL|env_keep|NOPASSWD|SETENV|/apache2|/cryptsetup|/mount" \ No newline at end of file +sudoB="$(whoami)|ALL:ALL|ALL : ALL|ALL|env_keep|NOPASSWD|SETENV|/apache2|/cryptsetup|/mount|/restic|--password-command|--password-file|-o ProxyCommand|-o PreferredAuthentications" \ No newline at end of file From ff21b3dcb95209944346a5c6eca99c315318a437 Mon Sep 17 00:00:00 2001 From: SirBroccoli Date: Sat, 17 Jan 2026 15:34:34 +0100 Subject: [PATCH 26/26] Delete linPEAS/builder/linpeas_parts/6_users_information/19_Sudo_restic.sh --- .../6_users_information/19_Sudo_restic.sh | 71 ------------------- 1 file changed, 71 deletions(-) delete mode 100644 linPEAS/builder/linpeas_parts/6_users_information/19_Sudo_restic.sh diff --git a/linPEAS/builder/linpeas_parts/6_users_information/19_Sudo_restic.sh b/linPEAS/builder/linpeas_parts/6_users_information/19_Sudo_restic.sh deleted file mode 100644 index e141db0..0000000 --- a/linPEAS/builder/linpeas_parts/6_users_information/19_Sudo_restic.sh +++ /dev/null @@ -1,71 +0,0 @@ -# Title: Users Information - Sudo restic password-command abuse -# ID: UG_Sudo_restic -# Author: HT Bot -# Last Update: 13-12-2025 -# Description: Detect sudo configurations that allow abusing restic --password-command for privilege escalation -# License: GNU GPL -# Version: 1.0 -# Functions Used: echo_not_found, print_2title, print_info -# Global Variables: $PASSWORD -# Initial Functions: -# Generated Global Variables: $restic_bin, $restic_sudo_found, $sudo_no_pw_output, $sudo_with_pw_output, $matches, $sudo_file, $block, $origin -# Fat linpeas: 0 -# Small linpeas: 1 - - -print_2title "Checking sudo restic --password-command exposure" -print_info "https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#sudo-and-suid" - -restic_bin="$(command -v restic 2>/dev/null)" -if [ -n "$restic_bin" ]; then - echo "restic binary found at: $restic_bin" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g" -else - echo "restic binary not found in PATH (still checking sudoers rules)" | sed -${E} "s,.*,${SED_YELLOW},g" -fi - -restic_sudo_found="" - -check_restic_entries() { - local block="$1" - local origin="$2" - - if [ -n "$block" ]; then - local matches - matches="$(printf '%s\n' "$block" | grep -i "restic" 2>/dev/null)" - if [ -n "$matches" ]; then - restic_sudo_found=1 - echo "$origin" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g" - printf '%s\n' "$matches" | sed -${E} "s,.*,${SED_RED_YELLOW},g" - fi - fi -} - -sudo_no_pw_output="$(sudo -n -l 2>/dev/null)" -check_restic_entries "$sudo_no_pw_output" "Matches in 'sudo -n -l'" - -if [ -n "$PASSWORD" ]; then - sudo_with_pw_output="$(echo "$PASSWORD" | timeout 1 sudo -S -l 2>/dev/null)" - check_restic_entries "$sudo_with_pw_output" "Matches in 'sudo -l' using provided password" -fi - -if [ -r "/etc/sudoers" ]; then - check_restic_entries "$(grep -v '^#' /etc/sudoers 2>/dev/null)" "Matches in /etc/sudoers" -fi - -if [ -d "/etc/sudoers.d" ]; then - for sudo_file in /etc/sudoers.d/*; do - [ -f "$sudo_file" ] || continue - check_restic_entries "$(grep -v '^#' "$sudo_file" 2>/dev/null)" "Matches in $sudo_file" - done -fi - -if [ -n "$restic_sudo_found" ]; then - echo "" - echo "restic's --password-command runs as the sudo target user (root)." | sed -${E} "s,.*,${SED_RED},g" - echo "Example: sudo restic check --password-command 'cp /bin/bash /tmp/restic-root && chmod 6777 /tmp/restic-root'" | sed -${E} "s,.*,${SED_RED_YELLOW},g" - echo "Then execute /tmp/restic-root -p for a root shell." | sed -${E} "s,.*,${SED_RED_YELLOW},g" -else - echo_not_found "sudo restic" -fi - -echo ""