diff --git a/linPEAS/README.md b/linPEAS/README.md index 5ade213..a632ace 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/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 diff --git a/winPEAS/winPEASexe/README.md b/winPEAS/winPEASexe/README.md index d9866c7..76a6daf 100755 --- a/winPEAS/winPEASexe/README.md +++ b/winPEAS/winPEASexe/README.md @@ -74,7 +74,6 @@ winpeas.exe -lolbas #Execute also additional LOLBAS search check The goal of this project is to search for possible **Privilege Escalation Paths** in Windows environments. - 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). The tool is based on **[SeatBelt](https://github.com/GhostPack/Seatbelt)**. diff --git a/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs b/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs index a15ad23..94853fe 100644 --- a/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs +++ b/winPEAS/winPEASexe/winPEAS/Checks/SystemInfo.cs @@ -82,6 +82,7 @@ namespace winPEAS.Checks PrintKrbRelayUp, PrintInsideContainer, PrintAlwaysInstallElevated, + PrintObjectManagerRaceAmplification, PrintLSAInfo, PrintNtlmSettings, PrintLocalGroupPolicy, @@ -734,6 +735,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/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/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/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 { diff --git a/winPEAS/winPEASexe/winPEAS/winPEAS.csproj b/winPEAS/winPEASexe/winPEAS/winPEAS.csproj index 6f9263c..4d40321 100644 --- a/winPEAS/winPEASexe/winPEAS/winPEAS.csproj +++ b/winPEAS/winPEASexe/winPEAS/winPEAS.csproj @@ -1362,6 +1362,7 @@ +