Merge branch 'master' into update_PEASS-winpeas-The_Windows_Registry_Adventure__Part_20251217_014635

This commit is contained in:
SirBroccoli
2026-01-17 15:45:17 +01:00
committed by GitHub
18 changed files with 2850 additions and 43 deletions
+6
View File
@@ -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
@@ -98,6 +100,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`**.
@@ -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
@@ -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
@@ -12,5 +12,4 @@
# Fat linpeas: 0
# Small linpeas: 1
sudoB="$(whoami)|ALL:ALL|ALL : ALL|ALL|env_keep|NOPASSWD|SETENV|/apache2|/cryptsetup|/mount"
sudoB="$(whoami)|ALL:ALL|ALL : ALL|ALL|env_keep|NOPASSWD|SETENV|/apache2|/cryptsetup|/mount|/restic|--password-command|--password-file|-o ProxyCommand|-o PreferredAuthentications"
+1 -13
View File
@@ -74,23 +74,10 @@ winpeas.exe -lolbas #Execute also additional LOLBAS search check
The goal of this project is to search for possible **Privilege Escalation Paths** in Windows environments.
New in this version:
- Detect potential GPO abuse by flagging writable SYSVOL paths for GPOs applied to the current host and by highlighting membership in the "Group Policy Creator Owners" group.
- Highlight HKLM/HKU registry keys that grant write access to low-privileged groups (including the cross-user TypingInsights key) so you can spot the hive corruption primitives documented by Project Zero.
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)**.
### New (AD-aware) checks
- 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 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.
## Where are my COLORS?!?!?!
@@ -154,6 +141,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
File diff suppressed because it is too large Load Diff
@@ -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()),
@@ -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<ServicesInfoHelper.KernelDriverInfo> 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<string> reasons = new List<string>();
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<string, object> 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<string, object> 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<string, object> 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";
}
}
}
@@ -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<SoapClientProxyFinding> 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);
}
}
}
}
@@ -82,6 +82,7 @@ namespace winPEAS.Checks
PrintKrbRelayUp,
PrintInsideContainer,
PrintAlwaysInstallElevated,
PrintObjectManagerRaceAmplification,
PrintLSAInfo,
PrintNtlmSettings,
PrintLocalGroupPolicy,
@@ -89,6 +90,7 @@ namespace winPEAS.Checks
AppLockerHelper.PrintAppLockerPolicy,
PrintPrintersWMIInfo,
PrintNamedPipes,
PrintNamedPipeAbuseCandidates,
PrintAMSIProviders,
PrintSysmon,
PrintDotNetVersions
@@ -733,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");
@@ -857,6 +884,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");
+51 -3
View File
@@ -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<Dictionary<string, string>> 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<Dictionary<string, string>> flaggedSessions = new List<Dictionary<string, string>>();
foreach (Dictionary<string, string> 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<string, string> 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
+39 -24
View File
@@ -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;
@@ -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;
}
}
}
}
@@ -0,0 +1,369 @@
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<SoapClientProxyInstance> Instances { get; } = new List<SoapClientProxyInstance>();
public HashSet<string> BinaryIndicators { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public HashSet<string> ConfigIndicators { get; } = new HashSet<string>(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<string, string> ConfigIndicatorMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "soap:address", "soap:address element present" },
{ "soap12:address", "soap12:address element present" },
{ "?wsdl", "?wsdl reference" },
{ "<wsdl:", "WSDL schema embedded in config" },
{ "servicedescriptionimporter", "ServiceDescriptionImporter referenced in config" },
{ "system.web.services.description", "System.Web.Services.Description namespace referenced" },
{ "new-webserviceproxy", "PowerShell New-WebServiceProxy referenced" },
{ "file://", "file:// scheme referenced" },
};
private const long MaxBinaryScanSize = 200 * 1024 * 1024; // 200MB
private static readonly object DotNetCacheLock = new object();
private static readonly Dictionary<string, bool> DotNetCache = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
public static List<SoapClientProxyFinding> CollectFindings()
{
var findings = new Dictionary<string, SoapClientProxyFinding>(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<SoapClientProxyCandidate> EnumerateServiceCandidates()
{
var results = new List<SoapClientProxyCandidate>();
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;
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,
});
}
}
}
catch (Exception ex)
{
Beaprint.GrayPrint("Error while enumerating services for SOAP client analysis: " + ex.Message);
}
return results;
}
private static IEnumerable<SoapClientProxyCandidate> EnumerateProcessCandidates()
{
var results = new List<SoapClientProxyCandidate>();
try
{
List<Dictionary<string, string>> 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;
}
}
}
@@ -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<KernelDriverInfo> GetKernelDriverInfos()
{
List<KernelDriverInfo> drivers = new List<KernelDriverInfo>();
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 /////////
//////////////////////////////////////
@@ -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<string> LowPrivSidSet = new HashSet<string>(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<string> LowPrivPrincipalKeywords = new HashSet<string>(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<string> PrivilegedSidSet = new HashSet<string>(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<NamedPipeSecurityIssue> GetNamedPipeAbuseCandidates()
{
var insecurePipes = DiscoverInsecurePipes();
if (!insecurePipes.Any())
{
return Enumerable.Empty<NamedPipeSecurityIssue>();
}
AttachProcesses(insecurePipes);
return insecurePipes.Values
.Where(issue => issue.LowPrivilegeAces.Any())
.OrderByDescending(issue => issue.HasPrivilegedServer)
.ThenBy(issue => issue.Name)
.ToList();
}
private static Dictionary<string, NamedPipeSecurityIssue> DiscoverInsecurePipes()
{
var result = new Dictionary<string, NamedPipeSecurityIssue>(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<string, NamedPipeSecurityIssue> insecurePipes)
{
if (!insecurePipes.Any())
return;
var lookup = BuildLookup(insecurePipes.Values);
if (!lookup.Any())
return;
List<HandlesHelper.SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX> handles;
try
{
handles = HandlesHelper.GetAllHandlers();
}
catch
{
return;
}
var currentProcess = Kernel32.GetCurrentProcess();
var processCache = new Dictionary<int, NamedPipeProcessInfo>();
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<string, List<NamedPipeSecurityIssue>> BuildLookup(IEnumerable<NamedPipeSecurityIssue> issues)
{
var lookup = new Dictionary<string, List<NamedPipeSecurityIssue>>(StringComparer.OrdinalIgnoreCase);
foreach (var issue in issues)
{
foreach (var key in GetCandidateKeys(issue.NormalizedName))
{
if (!lookup.TryGetValue(key, out var list))
{
list = new List<NamedPipeSecurityIssue>();
lookup[key] = list;
}
if (!list.Contains(issue))
{
list.Add(issue);
}
}
}
return lookup;
}
private static IEnumerable<string> GetCandidateKeys(string normalizedName)
{
if (string.IsNullOrEmpty(normalizedName))
return Array.Empty<string>();
var candidates = new HashSet<string>(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<string> DescribeRights(int accessMask)
{
var rights = (FileSystemRights)accessMask;
var descriptions = new List<string>();
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<string, NamedPipePrincipalAccess> _principalAccess = new Dictionary<string, NamedPipePrincipalAccess>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<int, NamedPipeProcessInfo> _processes = new Dictionary<int, NamedPipeProcessInfo>();
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<NamedPipePrincipalAccess> LowPrivilegeAces => _principalAccess.Values;
public IReadOnlyCollection<NamedPipeProcessInfo> Processes => _processes.Values;
public bool HasPrivilegedServer => _processes.Values.Any(process => process.IsHighPrivilege);
public void AddLowPrivPrincipal(string principal, string sid, IEnumerable<string> 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<string> _rights = new HashSet<string>(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<string> Rights => _rights;
public void AddRights(IEnumerable<string> 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; }
}
}
@@ -16,6 +16,20 @@ namespace winPEAS.Info.UserInfo
{
class UserInfoHelper
{
private static readonly Dictionary<string, bool> _highPrivAccountCache = new Dictionary<string, bool>(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<string> 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
{
@@ -1197,6 +1197,7 @@
<Compile Include="Checks\NetworkInfo.cs" />
<Compile Include="Checks\ProcessInfo.cs" />
<Compile Include="Checks\ServicesInfo.cs" />
<Compile Include="Checks\SoapClientInfo.cs" />
<Compile Include="Checks\SystemInfo.cs" />
<Compile Include="Checks\UserInfo.cs" />
<Compile Include="Checks\WindowsCreds.cs" />
@@ -1224,6 +1225,7 @@
<Compile Include="Info\ApplicationInfo\ApplicationInfoHelper.cs" />
<Compile Include="Info\ApplicationInfo\AutoRuns.cs" />
<Compile Include="Info\ApplicationInfo\DeviceDrivers.cs" />
<Compile Include="Info\ApplicationInfo\SoapClientProxyAnalyzer.cs" />
<Compile Include="Info\ApplicationInfo\InstalledApps.cs" />
<Compile Include="Helpers\Beaprint.cs" />
<Compile Include="Info\CloudInfo\AWSInfo.cs" />
@@ -1292,6 +1294,7 @@
<Compile Include="Info\SystemInfo\GroupPolicy\GroupPolicy.cs" />
<Compile Include="Info\SystemInfo\GroupPolicy\LocalGroupPolicyInfo.cs" />
<Compile Include="Info\SystemInfo\NamedPipes\NamedPipeInfo.cs" />
<Compile Include="Info\SystemInfo\NamedPipes\NamedPipeSecurityAnalyzer.cs" />
<Compile Include="Info\SystemInfo\NamedPipes\NamedPipes.cs" />
<Compile Include="Info\SystemInfo\Ntlm\Ntlm.cs" />
<Compile Include="Info\SystemInfo\Ntlm\NtlmSettingsInfo.cs" />
@@ -1360,6 +1363,7 @@
<Compile Include="KnownFileCreds\Vault\Structs\VAULT_ITEM_WIN8.cs" />
<Compile Include="KnownFileCreds\Vault\VaultCli.cs" />
<Compile Include="Helpers\MyUtils.cs" />
<Compile Include="Helpers\ObjectManagerHelper.cs" />
<Compile Include="Info\UserInfo\SAM\Enums.cs" />
<Compile Include="Info\UserInfo\SAM\SamServer.cs" />
<Compile Include="Info\UserInfo\SAM\Structs.cs" />