Compare commits

...

8 Commits

Author SHA1 Message Date
Carlos Polop
c5a77725db Fix Systemd module generated vars metadata 2026-02-03 23:51:27 +01:00
Carlos Polop
04c0b8aab3 f 2026-02-03 18:11:07 +01:00
SirBroccoli
a6c0491438 Auto-merge PR #578 (Codex)
Co-authored-by: HackTricks PEASS Autoimprover <peass-autoimprover@hacktricks.xyz>
2026-01-31 12:54:24 +00:00
SirBroccoli
fce28d2b81 Auto-merge PR #579 (Codex)
* autoimprover: simplify linpeas checks

* Fix CI failures for PR #579

---------

Co-authored-by: HackTricks PEASS Autoimprover <peass-autoimprover@hacktricks.xyz>
Co-authored-by: codex-action <codex-action@users.noreply.github.com>
2026-01-31 12:54:18 +00:00
Carlos Polop
fcc78b919a Fix Codex workflow gh usage 2026-01-31 13:19:50 +01:00
Martin Frandel
29d350fa79 Fixed binding error (#577)
Co-authored-by: Martin <git@frandel.eu>
2026-01-29 00:55:42 +01:00
Carlos Polop
1473fedcbf Fix linPEAS module section path matching 2026-01-21 15:21:50 +01:00
Carlos Polop
f8f4250b81 Add stronger winPEAS/linPEAS tests 2026-01-21 15:14:08 +01:00
13 changed files with 155 additions and 75 deletions

View File

@@ -28,6 +28,7 @@ jobs:
- name: Resolve PR context
id: gate
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
pr_number="${{ github.event.workflow_run.pull_requests[0].number }}"

View File

@@ -90,7 +90,7 @@ jobs:
run: |
gh api -X POST -H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/issues/${PR_NUMBER}/labels \
-f labels='["codex-fix-attempted"]'
-f labels[]=codex-fix-attempted
- name: Checkout PR head
uses: actions/checkout@v5

View File

@@ -30,11 +30,33 @@
# Functions Used: echo_not_found, print_2title, print_list, warn_exec
# Global Variables:
# Initial Functions:
# Generated Global Variables: $ASLR, $hypervisorflag, $detectedvirt, $unpriv_userns_clone, $perf_event_paranoid, $mmap_min_addr, $ptrace_scope, $dmesg_restrict, $kptr_restrict, $unpriv_bpf_disabled, $protected_symlinks, $protected_hardlinks
# Generated Global Variables: $ASLR, $hypervisorflag, $detectedvirt, $unpriv_userns_clone, $perf_event_paranoid, $mmap_min_addr, $ptrace_scope, $dmesg_restrict, $kptr_restrict, $unpriv_bpf_disabled, $protected_symlinks, $protected_hardlinks, $label, $sysctl_path, $sysctl_var, $zero_color, $nonzero_color, $sysctl_value
# Fat linpeas: 0
# Small linpeas: 0
print_sysctl_eq_zero() {
local label="$1"
local sysctl_path="$2"
local sysctl_var="$3"
local zero_color="$4"
local nonzero_color="$5"
local sysctl_value
print_list "$label" "$NC"
sysctl_value=$(cat "$sysctl_path" 2>/dev/null)
eval "$sysctl_var=\$sysctl_value"
if [ -z "$sysctl_value" ]; then
echo_not_found "$sysctl_path"
else
if [ "$sysctl_value" -eq 0 ]; then
echo "0" | sed -${E} "s,0,${zero_color},"
else
echo "$sysctl_value" | sed -${E} "s,.*,${nonzero_color},g"
fi
fi
}
#-- SY) AppArmor
print_2title "Protections"
print_list "AppArmor enabled? .............. "$NC
@@ -81,67 +103,25 @@ print_list "User namespace? ................ "$NC
if [ "$(cat /proc/self/uid_map 2>/dev/null)" ]; then echo "enabled" | sed "s,enabled,${SED_GREEN},"; else echo "disabled" | sed "s,disabled,${SED_RED},"; fi
#-- SY) Unprivileged user namespaces
print_list "unpriv_userns_clone? ........... "$NC
unpriv_userns_clone=$(cat /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null)
if [ -z "$unpriv_userns_clone" ]; then
echo_not_found "/proc/sys/kernel/unprivileged_userns_clone"
else
if [ "$unpriv_userns_clone" -eq 0 ]; then echo "0" | sed -${E} "s,0,${SED_GREEN},"; else echo "$unpriv_userns_clone" | sed -${E} "s,.*,${SED_RED},g"; fi
fi
print_sysctl_eq_zero "unpriv_userns_clone? ........... " "/proc/sys/kernel/unprivileged_userns_clone" "unpriv_userns_clone" "$SED_GREEN" "$SED_RED"
#-- SY) Unprivileged eBPF
print_list "unpriv_bpf_disabled? ........... "$NC
unpriv_bpf_disabled=$(cat /proc/sys/kernel/unprivileged_bpf_disabled 2>/dev/null)
if [ -z "$unpriv_bpf_disabled" ]; then
echo_not_found "/proc/sys/kernel/unprivileged_bpf_disabled"
else
if [ "$unpriv_bpf_disabled" -eq 0 ]; then echo "0" | sed -${E} "s,0,${SED_RED},"; else echo "$unpriv_bpf_disabled" | sed -${E} "s,.*,${SED_GREEN},g"; fi
fi
print_sysctl_eq_zero "unpriv_bpf_disabled? ........... " "/proc/sys/kernel/unprivileged_bpf_disabled" "unpriv_bpf_disabled" "$SED_RED" "$SED_GREEN"
#-- SY) cgroup2
print_list "Cgroup2 enabled? ............... "$NC
([ "$(grep cgroup2 /proc/filesystems 2>/dev/null)" ] && echo "enabled" || echo "disabled") | sed "s,disabled,${SED_RED}," | sed "s,enabled,${SED_GREEN},"
#-- SY) Kernel hardening sysctls
print_list "kptr_restrict? ................. "$NC
kptr_restrict=$(cat /proc/sys/kernel/kptr_restrict 2>/dev/null)
if [ -z "$kptr_restrict" ]; then
echo_not_found "/proc/sys/kernel/kptr_restrict"
else
if [ "$kptr_restrict" -eq 0 ]; then echo "0" | sed -${E} "s,0,${SED_RED},"; else echo "$kptr_restrict" | sed -${E} "s,.*,${SED_GREEN},g"; fi
fi
print_sysctl_eq_zero "kptr_restrict? ................. " "/proc/sys/kernel/kptr_restrict" "kptr_restrict" "$SED_RED" "$SED_GREEN"
print_list "dmesg_restrict? ................ "$NC
dmesg_restrict=$(cat /proc/sys/kernel/dmesg_restrict 2>/dev/null)
if [ -z "$dmesg_restrict" ]; then
echo_not_found "/proc/sys/kernel/dmesg_restrict"
else
if [ "$dmesg_restrict" -eq 0 ]; then echo "0" | sed -${E} "s,0,${SED_RED},"; else echo "$dmesg_restrict" | sed -${E} "s,.*,${SED_GREEN},g"; fi
fi
print_sysctl_eq_zero "dmesg_restrict? ................ " "/proc/sys/kernel/dmesg_restrict" "dmesg_restrict" "$SED_RED" "$SED_GREEN"
print_list "ptrace_scope? .................. "$NC
ptrace_scope=$(cat /proc/sys/kernel/yama/ptrace_scope 2>/dev/null)
if [ -z "$ptrace_scope" ]; then
echo_not_found "/proc/sys/kernel/yama/ptrace_scope"
else
if [ "$ptrace_scope" -eq 0 ]; then echo "0" | sed -${E} "s,0,${SED_RED},"; else echo "$ptrace_scope" | sed -${E} "s,.*,${SED_GREEN},g"; fi
fi
print_sysctl_eq_zero "ptrace_scope? .................. " "/proc/sys/kernel/yama/ptrace_scope" "ptrace_scope" "$SED_RED" "$SED_GREEN"
print_list "protected_symlinks? ............ "$NC
protected_symlinks=$(cat /proc/sys/fs/protected_symlinks 2>/dev/null)
if [ -z "$protected_symlinks" ]; then
echo_not_found "/proc/sys/fs/protected_symlinks"
else
if [ "$protected_symlinks" -eq 0 ]; then echo "0" | sed -${E} "s,0,${SED_RED},"; else echo "$protected_symlinks" | sed -${E} "s,.*,${SED_GREEN},g"; fi
fi
print_sysctl_eq_zero "protected_symlinks? ............ " "/proc/sys/fs/protected_symlinks" "protected_symlinks" "$SED_RED" "$SED_GREEN"
print_list "protected_hardlinks? ........... "$NC
protected_hardlinks=$(cat /proc/sys/fs/protected_hardlinks 2>/dev/null)
if [ -z "$protected_hardlinks" ]; then
echo_not_found "/proc/sys/fs/protected_hardlinks"
else
if [ "$protected_hardlinks" -eq 0 ]; then echo "0" | sed -${E} "s,0,${SED_RED},"; else echo "$protected_hardlinks" | sed -${E} "s,.*,${SED_GREEN},g"; fi
fi
print_sysctl_eq_zero "protected_hardlinks? ........... " "/proc/sys/fs/protected_hardlinks" "protected_hardlinks" "$SED_RED" "$SED_GREEN"
print_list "perf_event_paranoid? ........... "$NC
perf_event_paranoid=$(cat /proc/sys/kernel/perf_event_paranoid 2>/dev/null)
@@ -151,13 +131,7 @@ else
if [ "$perf_event_paranoid" -le 1 ]; then echo "$perf_event_paranoid" | sed -${E} "s,.*,${SED_RED},g"; else echo "$perf_event_paranoid" | sed -${E} "s,.*,${SED_GREEN},g"; fi
fi
print_list "mmap_min_addr? ................. "$NC
mmap_min_addr=$(cat /proc/sys/vm/mmap_min_addr 2>/dev/null)
if [ -z "$mmap_min_addr" ]; then
echo_not_found "/proc/sys/vm/mmap_min_addr"
else
if [ "$mmap_min_addr" -eq 0 ]; then echo "0" | sed -${E} "s,0,${SED_RED},"; else echo "$mmap_min_addr" | sed -${E} "s,.*,${SED_GREEN},g"; fi
fi
print_sysctl_eq_zero "mmap_min_addr? ................. " "/proc/sys/vm/mmap_min_addr" "mmap_min_addr" "$SED_RED" "$SED_GREEN"
print_list "lockdown mode? ................. "$NC
if [ -f "/sys/kernel/security/lockdown" ]; then

View File

@@ -17,7 +17,7 @@
# Functions Used: print_2title, print_list, echo_not_found
# Global Variables: $SEARCH_IN_FOLDER, $Wfolders, $SED_RED, $SED_RED_YELLOW, $NC
# Initial Functions:
# Generated Global Variables: $WRITABLESYSTEMDPATH, $line, $service, $file, $version, $user, $caps, $path, $path_line, $service_file, $exec_line, $cmd
# Generated Global Variables: $WRITABLESYSTEMDPATH, $line, $service, $file, $version, $user, $caps, $path, $path_line, $service_file, $exec_line, $exec_value, $cmd, $cmd_path
# Fat linpeas: 0
# Small linpeas: 1
@@ -116,18 +116,20 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
# Check ExecStart paths
grep -E "ExecStart|ExecStartPre|ExecStartPost" "$service_file" 2>/dev/null |
while read -r exec_line; do
# Extract the first word after ExecStart* as the command
cmd=$(echo "$exec_line" | awk '{print $2}' | tr -d '"')
# Extract the rest as arguments
args=$(echo "$exec_line" | awk '{$1=$2=""; print $0}' | tr -d '"')
# Extract command from the right side of Exec*=, not from argv
exec_value="${exec_line#*=}"
exec_value=$(echo "$exec_value" | sed 's/^[[:space:]]*//')
cmd=$(echo "$exec_value" | awk '{print $1}' | tr -d '"')
# Strip systemd command prefixes (-, @, :, +, !) before path checks
cmd_path=$(echo "$cmd" | sed -E 's/^[-@:+!]+//')
# Only check the command path, not arguments
if [ -n "$cmd" ] && [ -w "$cmd" ]; then
echo "$service: $cmd (from $exec_line)" | sed -${E} "s,.*,${SED_RED},g"
if [ -n "$cmd_path" ] && [ -w "$cmd_path" ]; then
echo "$service: $cmd_path (from $exec_line)" | sed -${E} "s,.*,${SED_RED},g"
fi
# Check for relative paths only in the command, not arguments
if [ -n "$cmd" ] && [ "${cmd#/}" = "$cmd" ] && ! echo "$cmd" | grep -qE '^-|^--'; then
echo "$service: Uses relative path '$cmd' (from $exec_line)" | sed -${E} "s,.*,${SED_RED},g"
if [ -n "$cmd_path" ] && [ "${cmd_path#/}" = "$cmd_path" ] && [ "${cmd_path#\$}" = "$cmd_path" ]; then
echo "$service: Uses relative path '$cmd_path' (from $exec_line)" | sed -${E} "s,.*,${SED_RED},g"
fi
done
fi
@@ -153,4 +155,4 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
fi
echo ""
fi
fi

View File

@@ -8,6 +8,7 @@ from .yamlGlobals import (
class LinpeasModule:
def __init__(self, path):
self.path = path
real_path = os.path.realpath(path)
with open(path, 'r') as file:
self.module_text = file.read()
@@ -29,7 +30,7 @@ class LinpeasModule:
self.section_info = {}
if not (self.is_base or self.is_function or self.is_variable):
for module in LINPEAS_PARTS["modules"]:
if module["folder_path"] in path:
if os.path.realpath(module["folder_path"]) in real_path:
self.section_info = module
self.is_check = True
break

View File

@@ -0,0 +1,60 @@
import re
import sys
import unittest
from pathlib import Path
class LinpeasModulesMetadataTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.repo_root = Path(__file__).resolve().parents[2]
cls.linpeas_dir = cls.repo_root / "linPEAS"
cls.parts_dir = cls.linpeas_dir / "builder" / "linpeas_parts"
# Ensure `import builder.*` works when tests are run from repo root.
sys.path.insert(0, str(cls.linpeas_dir))
from builder.src.linpeasModule import LinpeasModule # pylint: disable=import-error
cls.LinpeasModule = LinpeasModule
def _iter_module_files(self):
return sorted(self.parts_dir.rglob("*.sh"))
def test_all_modules_parse(self):
module_files = self._iter_module_files()
self.assertGreater(len(module_files), 0, "No linPEAS module files were found.")
# Parsing a module validates its metadata and dependencies.
for path in module_files:
_ = self.LinpeasModule(str(path))
def test_check_module_id_matches_filename(self):
for path in self._iter_module_files():
module = self.LinpeasModule(str(path))
if not getattr(module, "is_check", False):
continue
# For checks, the filename (without numeric prefix) must match the module ID
# (either full ID or stripping section prefix like `SI_`).
file_base = re.sub(r"^[0-9]+_", "", path.stem)
module_id = getattr(module, "id", "")
module_id_tail = module_id[3:] if len(module_id) >= 3 else ""
self.assertIn(
file_base,
{module_id, module_id_tail},
f"Module ID mismatch in {path}: id={module_id} expected suffix={file_base}",
)
def test_module_ids_are_unique(self):
ids = []
for path in self._iter_module_files():
module = self.LinpeasModule(str(path))
ids.append(getattr(module, "id", ""))
duplicates = {x for x in ids if x and ids.count(x) > 1}
self.assertEqual(set(), duplicates, f"Duplicate module IDs found: {sorted(duplicates)}")
if __name__ == "__main__":
unittest.main()

View File

@@ -29,6 +29,7 @@ namespace winPEAS.Tests
Assert.IsFalse(InvokeIsNetworkTypeValid("-network="));
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=10.10.10.999"));
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=10.10.10.10/64"));
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=999.999.999.999/24"));
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=not-an-ip"));
}
}

View File

@@ -0,0 +1,37 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace winPEAS.Tests
{
[TestClass]
public class ChecksArgumentEdgeCasesTests
{
[TestMethod]
public void ShouldNotThrowOnEmptyLogFileArg()
{
// Should return early with a user-friendly error, not crash.
Program.Main(new[] { "log=" });
}
[TestMethod]
public void ShouldNotThrowOnPortsWithoutNetwork()
{
// Should warn and return early because -network was not provided.
Program.Main(new[] { "-ports=80,443" });
}
[TestMethod]
public void ShouldNotThrowOnInvalidNetworkArgument()
{
// Should warn and return early because the IP is invalid.
Program.Main(new[] { "-network=10.10.10.999" });
}
[TestMethod]
public void ShouldNotThrowOnEmptyNetworkArgument()
{
// Should warn and return early because the value is empty.
Program.Main(new[] { "-network=" });
}
}
}

View File

@@ -356,7 +356,7 @@ namespace winPEAS.Checks
{
var rangeParts = networkType.Split('/');
if (rangeParts.Length == 2 && int.TryParse(rangeParts[1], out int res) && res <= 32 && res >= 0)
if (rangeParts.Length == 2 && IPAddress.TryParse(rangeParts[0], out _) && int.TryParse(rangeParts[1], out int res) && res <= 32 && res >= 0)
{
return true;
}

View File

@@ -524,7 +524,7 @@ namespace winPEAS.Checks
{
Beaprint.MainPrint("Looking for documents --limit 100--");
List<string> docFiles = InterestingFiles.InterestingFiles.ListUsersDocs();
Beaprint.ListPrint(docFiles.GetRange(0, docFiles.Count <= 100 ? docFiles.Count : 100));
Beaprint.ListPrint(MyUtils.GetLimitedRange(docFiles, 100));
}
catch (Exception ex)
{
@@ -546,7 +546,7 @@ namespace winPEAS.Checks
if (recFiles.Count != 0)
{
foreach (Dictionary<string, string> recF in recFiles.GetRange(0, recFiles.Count <= 70 ? recFiles.Count : 70))
foreach (Dictionary<string, string> recF in MyUtils.GetLimitedRange(recFiles, 70))
{
Beaprint.AnsiPrint(" " + recF["Target"] + "(" + recF["Accessed"] + ")", colorF);
}

View File

@@ -348,8 +348,7 @@ namespace winPEAS.Checks
Beaprint.MainPrint("DNS cached --limit 70--");
Beaprint.GrayPrint(string.Format(" {0,-38}{1,-38}{2}", "Entry", "Name", "Data"));
List<Dictionary<string, string>> DNScache = NetworkInfoHelper.GetDNSCache();
foreach (Dictionary<string, string> entry in DNScache.GetRange(0,
DNScache.Count <= 70 ? DNScache.Count : 70))
foreach (Dictionary<string, string> entry in MyUtils.GetLimitedRange(DNScache, 70))
{
Console.WriteLine($" {entry["Entry"],-38}{entry["Name"],-38}{entry["Data"]}");
}

View File

@@ -21,6 +21,11 @@ namespace winPEAS.Helpers
""); //To get the default object you need to use an empty string
}
public static List<T> GetLimitedRange<T>(List<T> items, int limit)
{
return items.GetRange(0, Math.Min(items.Count, limit));
}
////////////////////////////////////
/////// MISC - Files & Paths ///////
////////////////////////////////////

View File

@@ -1677,7 +1677,7 @@ if ($TimeStamp) { TimeElapsed }
Write-Host -ForegroundColor Blue "=========|| WHOAMI INFO"
Write-Host ""
if ($TimeStamp) { TimeElapsed }
Write-Host -ForegroundColor Blue "=========|| Check Token access here: https://book.hacktricks.wiki/en/windows-hardening/windows-local-privilege-escalation/privilege-escalation-abusing-tokens.html#abusing-tokens" -ForegroundColor yellow
Write-Host -ForegroundColor Blue "=========|| Check Token access here: https://book.hacktricks.wiki/en/windows-hardening/windows-local-privilege-escalation/privilege-escalation-abusing-tokens.html#abusing-tokens"
Write-Host -ForegroundColor Blue "=========|| Check if you are inside the Administrators group or if you have enabled any token that can be use to escalate privileges like SeImpersonatePrivilege, SeAssignPrimaryPrivilege, SeTcbPrivilege, SeBackupPrivilege, SeRestorePrivilege, SeCreateTokenPrivilege, SeLoadDriverPrivilege, SeTakeOwnershipPrivilege, SeDebugPrivilege"
Write-Host "https://book.hacktricks.wiki/en/windows-hardening/windows-local-privilege-escalation/index.html#users--groups" -ForegroundColor Yellow
Start-Process whoami.exe -ArgumentList "/all" -Wait -NoNewWindow