mirror of
https://github.com/peass-ng/PEASS-ng.git
synced 2026-11-07 09:25:57 -10:00
Auto-merge PR #758 (Chack Agent)
* Improve bounded Unix escalation enumeration and speed * Expand bounded Unix escalation indicators * Apply Chack Agent review fixes for PR #758 * Add bounded Unix privilege boundary signals * Improve bounded Unix credential and service signals * Apply Chack Agent review fixes for PR #758 * Add bounded Linux privilege indicators and redacted config views * Expand bounded local privilege reviews * Fix portable awk quote matching * Improve bounded Unix privilege escalation review and catalog loading * Use portable output for generated file selectors * Expand bounded Unix configuration and privilege review * Expand bounded Unix privilege review and prune scans * Expand bounded Unix privilege review and catalog paths * Refine bounded Unix privilege candidates and portable output * Apply Chack Agent review fixes for PR #758 * Improve bounded Unix privilege escalation review and scan efficiency * Improve Linux privilege indicators and builder resilience * Extend bounded Unix sudo, service, and file path review * Improve bounded Unix privilege discovery and CLI parsing * Expand bounded Unix privilege review across existing checks * Improve bounded local configuration and process review * Refine bounded Unix listener, sudo, and file-path review * Make listener fixture portable across socket tools * Apply Chack Agent review fixes for PR #758 * Correct cached file discovery and configuration review * Apply Chack Agent review fixes for PR #758 * Fetch current GTFOBins data for each applicable build * Apply Chack Agent review fixes for PR #758 --------- Co-authored-by: Carlos Polop <[email protected]> Co-authored-by: chack-agent <[email protected]>
This commit is contained in:
269 files changed
+29033
-669
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,8 @@ You can **build you own linpeas which will contain only the checks you want**. T
|
||||
It's possible to indicate the params `--all`, `--all-no-fat` and `--small` to build the classic `linpeas_fat.sh`, `linpeas.sh` and `linpeas_small.sh` outputs:
|
||||
- When testing builder changes locally, prefer writing the output to `/tmp` so you don't overwrite tracked release artifacts by accident.
|
||||
|
||||
Builds that include GTFOBins-based checks download the current GTFOBins archive once. They require network access and stop if the archive cannot be downloaded or validated.
|
||||
|
||||
- **linpeas_fat.sh**: Contains all checks, even third party applications in base64 embedded.
|
||||
- **linpeas.sh**: Contains all checks, but only the third party application `linux exploit suggester` is embedded. This is the default `linpeas.sh`.
|
||||
- **linpeas_small.sh**: Contains only the most *important* checks making its size smaller.
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Title: System Information - Protections
|
||||
# ID: SY_Protections
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 08-07-2026
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Check for system security protections and their bypass possibilities:
|
||||
# - AppArmor/SELinux status and profiles
|
||||
# - ASLR status
|
||||
# - Seccomp filters
|
||||
# - NoNewPrivs for the current process tree
|
||||
# - Capabilities
|
||||
# - Fail2ban/CrowdSec intrusion prevention systems (affects su/ssh brute forcing)
|
||||
# - Pending reboot (running kernel/libraries may be outdated -> kernel exploits)
|
||||
@@ -28,12 +29,12 @@
|
||||
# - Capability exploitation
|
||||
# - Protection circumvention
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1518.001
|
||||
# Functions Used: echo_no, echo_not_found, print_2title, print_list, warn_exec
|
||||
# Global Variables:
|
||||
# Global Variables: $IAMROOT, $ROOT_FOLDER, $SEARCH_IN_FOLDER, $TIMEOUT
|
||||
# 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, $label, $sysctl_path, $sysctl_var, $zero_color, $nonzero_color, $sysctl_value, $f2b_jails
|
||||
# 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, $protected_regular, $label, $sysctl_path, $sysctl_var, $zero_color, $nonzero_color, $sysctl_value, $f2b_jails, $no_new_privs_status, $cups_job_timeout, $cups_job_ids, $cups_job_id, $cups_job_doc, $cups_job_file, $cups_job_header
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
@@ -60,6 +61,22 @@ print_sysctl_eq_zero() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Read only the current process status. A missing procfs or field is unknown,
|
||||
# which is normal on non-Linux systems; do not infer the setting from a unit file.
|
||||
read_no_new_privs_status() {
|
||||
[ -r "$1" ] || { printf 'unknown\n'; return; }
|
||||
awk '
|
||||
NR > 256 { exit }
|
||||
$1 == "NoNewPrivs:" {
|
||||
found = 1
|
||||
if (NF == 2 && ($2 == "0" || $2 == "1")) print $2
|
||||
else print "unknown"
|
||||
exit
|
||||
}
|
||||
END { if (!found) print "unknown" }
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
#-- SY) AppArmor
|
||||
print_2title "Protections" "T1518.001"
|
||||
print_list "AppArmor enabled? .............. "$NC
|
||||
@@ -101,6 +118,14 @@ print_list "SELinux enabled? ............... "$NC
|
||||
print_list "Seccomp enabled? ............... "$NC
|
||||
([ "$(grep Seccomp /proc/self/status 2>/dev/null | grep -v 0)" ] && echo "enabled" || echo "disabled") | sed "s,disabled,${SED_RED}," | sed "s,enabled,${SED_GREEN},"
|
||||
|
||||
print_list "NoNewPrivs (this process)? ..... "$NC
|
||||
no_new_privs_status=$(read_no_new_privs_status /proc/self/status)
|
||||
case "$no_new_privs_status" in
|
||||
1) echo "enabled (1): set-ID and file-cap gains blocked on exec for this process tree" ;;
|
||||
0) echo "disabled (0) for this process" ;;
|
||||
*) echo "unknown" ;;
|
||||
esac
|
||||
|
||||
#-- SY) AppArmor
|
||||
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
|
||||
@@ -126,6 +151,11 @@ print_sysctl_eq_zero "protected_symlinks? ............ " "/proc/sys/fs/protected
|
||||
|
||||
print_sysctl_eq_zero "protected_hardlinks? ........... " "/proc/sys/fs/protected_hardlinks" "protected_hardlinks" "$SED_RED" "$SED_GREEN"
|
||||
|
||||
# Linux uses 0 (disabled), 1 (world-writable sticky directories), and 2
|
||||
# (also group-writable sticky directories). A nonzero value is protective;
|
||||
# privileged wrappers still need to stop if a rewrite is denied.
|
||||
print_sysctl_eq_zero "protected_regular? ............ " "/proc/sys/fs/protected_regular" "protected_regular" "$SED_RED" "$SED_GREEN"
|
||||
|
||||
print_list "perf_event_paranoid? ........... "$NC
|
||||
perf_event_paranoid=$(cat /proc/sys/kernel/perf_event_paranoid 2>/dev/null)
|
||||
if [ -z "$perf_event_paranoid" ]; then
|
||||
@@ -154,6 +184,14 @@ else
|
||||
fi
|
||||
|
||||
#-- SY) Fail2ban (Intrusion Prevention System)
|
||||
fail2ban_action_dir_review() {
|
||||
# Exact, metadata-only check. A symlinked final component can point to an
|
||||
# unrelated or slow filesystem, so leave that case for manual review.
|
||||
if ! [ "$IAMROOT" ] && [ ! -L "$1" ] && [ -d "$1" ] && [ -w "$1" ] && [ -x "$1" ]; then
|
||||
printf ' Fail2ban action directory review candidate: %s is writable/searchable (parent paths, selected jail/action, service identity, effective restart policy, and event trigger unverified).\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
print_list "Fail2ban present? .............. "$NC
|
||||
if command -v fail2ban-client >/dev/null 2>&1 || [ -S "/var/run/fail2ban/fail2ban.sock" ] || pgrep -x fail2ban-server >/dev/null 2>&1; then
|
||||
f2b_jails=$(fail2ban-client status 2>/dev/null | grep "Jail list" | sed "s/.*Jail list:[[:space:]]*//")
|
||||
@@ -162,6 +200,7 @@ if command -v fail2ban-client >/dev/null 2>&1 || [ -S "/var/run/fail2ban/fail2ba
|
||||
else
|
||||
echo "Yes - installed (could not query jails, may need root)" | sed -${E} "s,.*,${SED_GREEN},"
|
||||
fi
|
||||
fail2ban_action_dir_review /etc/fail2ban/action.d
|
||||
else
|
||||
echo_not_found "fail2ban"
|
||||
fi
|
||||
@@ -227,6 +266,44 @@ fi
|
||||
print_list "Printer? ....................... "$NC
|
||||
(lpstat -a || system_profiler SPPrintersDataType || echo_no) 2>/dev/null
|
||||
|
||||
# A searchable but unlistable CUPS spool can still contain readable retained
|
||||
# documents. Use completed job IDs only to test exact paths; never print data.
|
||||
if [ -z "$SEARCH_IN_FOLDER" ] && { [ -z "$ROOT_FOLDER" ] || [ "$ROOT_FOLDER" = / ]; } &&
|
||||
[ -d /var/spool/cups ] && command -v lpstat >/dev/null 2>&1; then
|
||||
cups_job_timeout=${TIMEOUT:-$(command -v gtimeout 2>/dev/null)}
|
||||
if [ -n "$cups_job_timeout" ]; then
|
||||
cups_job_ids=$("$cups_job_timeout" 2 lpstat -h localhost -W completed -o 2>/dev/null |
|
||||
awk '
|
||||
NR > 128 { exit }
|
||||
$1 ~ /-[0-9]+$/ {
|
||||
id = $1
|
||||
sub(/^.*-/, "", id)
|
||||
sub(/^0+/, "", id)
|
||||
if (id == "" || length(id) > 9) next
|
||||
while (length(id) < 5) id = "0" id
|
||||
if (!(id in seen)) {
|
||||
seen[id] = 1
|
||||
print id
|
||||
if (++count == 20) exit
|
||||
}
|
||||
}
|
||||
')
|
||||
cups_job_header=0
|
||||
for cups_job_id in $cups_job_ids; do
|
||||
for cups_job_doc in 001 002 003; do
|
||||
cups_job_file="/var/spool/cups/d${cups_job_id}-${cups_job_doc}"
|
||||
if [ -f "$cups_job_file" ] && [ ! -L "$cups_job_file" ] && [ -r "$cups_job_file" ]; then
|
||||
if [ "$cups_job_header" -eq 0 ]; then
|
||||
print_2title "Readable retained CUPS print jobs (paths only)" "T1552.001"
|
||||
cups_job_header=1
|
||||
fi
|
||||
ls -ld "$cups_job_file" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
#-- SY) Running in a virtual environment
|
||||
print_list "Is this a virtual machine? ..... "$NC
|
||||
hypervisorflag=$(grep flags /proc/cpuinfo 2>/dev/null | grep hypervisor)
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1082
|
||||
# Functions Used: print_2title, print_info, warn_exec, linpeas_start_host_checker_lookup
|
||||
# Functions Used: checkGlibcTunablesCVE20234911, print_2title, print_info, warn_exec, linpeas_start_host_checker_lookup
|
||||
# Global Variables: $MACPEAS, $kernelDCW_Ubuntu_Precise_1, $kernelB, $kernelDCW_Ubuntu_Precise_2, $kernelDCW_Ubuntu_Precise_3, $kernelDCW_Ubuntu_Precise_4, $kernelDCW_Ubuntu_Precise_5, $kernelDCW_Ubuntu_Precise_6, $kernelDCW_Rhel5_1, $kernelDCW_Rhel5_2, $kernelDCW_Rhel5_3, $kernelDCW_Rhel6_1, $kernelDCW_Rhel6_2, $kernelDCW_Rhel6_3, $kernelDCW_Rhel6_4, $kernelDCW_Rhel7, $kernelDCW_Ubuntu_Trusty_1, $kernelDCW_Ubuntu_Trusty_2, $kernelDCW_Ubuntu_Trusty_3, $kernelDCW_Ubuntu_Trusty_4, $kernelDCW_Ubuntu_Xenial
|
||||
# Initial Functions: linpeas_start_host_checker_lookup
|
||||
# Generated Global Variables:
|
||||
@@ -37,6 +37,7 @@ print_2title "Operative system" "T1082"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/main-system-information/kernel-vulnerability-assessment.html"
|
||||
(cat /proc/version || uname -a ) 2>/dev/null | sed -${E} "s,$kernelDCW_Ubuntu_Precise_1,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Ubuntu_Precise_2,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Ubuntu_Precise_3,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Ubuntu_Precise_4,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Ubuntu_Precise_5,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Ubuntu_Precise_6,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Ubuntu_Trusty_1,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Ubuntu_Trusty_2,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Ubuntu_Trusty_3,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Ubuntu_Trusty_4,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Ubuntu_Xenial,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Rhel5_1,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Rhel5_2,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Rhel5_3,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Rhel6_1,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Rhel6_2,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Rhel6_3,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Rhel6_4,${SED_RED_YELLOW}," | sed -${E} "s,$kernelDCW_Rhel7,${SED_RED_YELLOW}," | sed -${E} "s,$kernelB,${SED_RED},"
|
||||
warn_exec lsb_release -a 2>/dev/null
|
||||
checkGlibcTunablesCVE20234911
|
||||
if [ "$MACPEAS" ]; then
|
||||
warn_exec system_profiler SPSoftwareDataType
|
||||
fi
|
||||
|
||||
@@ -1,37 +1,126 @@
|
||||
# Title: System Information - Sudo Version
|
||||
# ID: SY_Sudo_version
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 07-03-2024
|
||||
# Description: Check for sudo vulnerabilities and misconfigurations that could lead to privilege escalation:
|
||||
# - Vulnerable sudo versions with known exploits
|
||||
# - Common vulnerable versions and CVEs:
|
||||
# * CVE-2021-3156 (Baron Samedit): Heap overflow in sudo
|
||||
# * CVE-2021-23239: Potential privilege escalation
|
||||
# * CVE-2021-23240: Potential privilege escalation
|
||||
# * CVE-2021-23241: Potential privilege escalation
|
||||
# - Exploitation methods:
|
||||
# * Version exploits: Use known exploits for vulnerable sudo versions
|
||||
# * Common targets: sudo < 1.9.5p2 (Baron Samedit)
|
||||
# * Exploit techniques:
|
||||
# - Heap overflow exploitation
|
||||
# - Race conditions
|
||||
# - Memory corruption
|
||||
# - Command injection
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Inspect bounded, known sudo paths for upstream version candidates.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1548.003,T1068
|
||||
# Functions Used: echo_not_found, print_2title, print_info
|
||||
# Global Variables: $sudovB
|
||||
# Functions Used: echo_not_found, lp_trusted_version_path, print_2title, print_info
|
||||
# Global Variables: $PATH
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Generated Global Variables: $sudo_trusted_candidate, $sudo_seen, $sudo_found, $sudo_timeout, $sudo_path_left, $sudo_path_count, $sudo_dir, $sudo_last, $sudo_candidate, $sudo_identity, $sudo_line, $sudo_version, $sudo_parts, $sudo_major, $sudo_minor, $sudo_patch, $sudo_revision, $sudo_host_candidate, $sudo_chroot_candidate
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
# shellcheck shell=sh
|
||||
|
||||
|
||||
print_2title "Sudo version" "T1548.003,T1068"
|
||||
if [ "$(command -v sudo 2>/dev/null || echo -n '')" ]; then
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/linux-basics/linux-privilege-escalation/index.html#sudo-version"
|
||||
sudo -V 2>/dev/null | grep "Sudo ver" | sed -${E} "s,$sudovB,${SED_RED},"
|
||||
else echo_not_found "sudo"
|
||||
print_info "https://www.sudo.ws/security/advisories/"
|
||||
|
||||
# Only execute a root-owned setuid sudo target. A user-controlled PATH entry may
|
||||
# contain an arbitrary program named sudo, so do not run it just to get a version.
|
||||
sudo_version_check() {
|
||||
sudo_candidate=$1
|
||||
[ -f "$sudo_candidate" ] && [ -x "$sudo_candidate" ] || return 0
|
||||
sudo_identity=$(stat -Lc '%d:%i:%u' "$sudo_candidate" 2>/dev/null) ||
|
||||
sudo_identity=$(stat -L -f '%d:%i:%u' "$sudo_candidate" 2>/dev/null) || return 0
|
||||
case "
|
||||
$sudo_seen
|
||||
" in *"
|
||||
$sudo_identity
|
||||
"*) return 0 ;; esac
|
||||
sudo_seen="${sudo_seen}
|
||||
${sudo_identity}"
|
||||
sudo_found=1
|
||||
|
||||
case "$sudo_identity" in
|
||||
*:0) [ -u "$sudo_candidate" ] || {
|
||||
printf ' %s: root-owned, but not setuid; version query skipped\n' "$sudo_candidate"
|
||||
return 0
|
||||
} ;;
|
||||
*) printf ' %s: not root-owned setuid; version query skipped\n' "$sudo_candidate"
|
||||
return 0 ;;
|
||||
esac
|
||||
|
||||
if [ -z "$sudo_timeout" ]; then
|
||||
printf ' %s: root-owned setuid; version query skipped (timeout unavailable)\n' "$sudo_candidate"
|
||||
return 0
|
||||
fi
|
||||
sudo_trusted_candidate=$(lp_trusted_version_path "$sudo_candidate") || {
|
||||
printf ' %s: version query skipped (executable or ancestor permissions are untrusted)\n' "$sudo_candidate"
|
||||
return 0
|
||||
}
|
||||
sudo_line=$("$sudo_timeout" -k 1 2 "$sudo_trusted_candidate" -V 2>/dev/null | head -c 256 | head -n 1)
|
||||
sudo_version=$(printf '%s\n' "$sudo_line" | awk '$1 == "Sudo" && $2 == "version" { print $3 }')
|
||||
if [ -z "$sudo_version" ]; then
|
||||
printf ' %s: root-owned setuid; version unavailable or query timed out\n' "$sudo_candidate"
|
||||
return 0
|
||||
fi
|
||||
printf ' %s: Sudo version %s\n' "$sudo_candidate" "$sudo_version"
|
||||
sudo_parts=$(printf '%s\n' "$sudo_version" | sed -nE 's/^([0-9]+)\.([0-9]+)\.([0-9]+)(p([0-9]+))?$/\1 \2 \3 \5/p')
|
||||
if [ -z "$sudo_parts" ]; then
|
||||
printf ' Upstream candidate range unknown; check the installed package advisory.\n'
|
||||
return 0
|
||||
fi
|
||||
# Split only the four numeric fields produced by the anchored expression.
|
||||
set -- $sudo_parts
|
||||
sudo_major=$1 sudo_minor=$2 sudo_patch=$3 sudo_revision=${4:-0}
|
||||
sudo_host_candidate=0 sudo_chroot_candidate=0
|
||||
if [ "$sudo_major" -eq 1 ]; then
|
||||
if [ "$sudo_minor" -eq 8 ] && [ "$sudo_patch" -ge 8 ]; then
|
||||
sudo_host_candidate=1
|
||||
elif [ "$sudo_minor" -eq 9 ]; then
|
||||
if [ "$sudo_patch" -le 16 ] || { [ "$sudo_patch" -eq 17 ] && [ "$sudo_revision" -eq 0 ]; }; then
|
||||
sudo_host_candidate=1
|
||||
fi
|
||||
if { [ "$sudo_patch" -ge 14 ] && [ "$sudo_patch" -le 16 ]; } ||
|
||||
{ [ "$sudo_patch" -eq 17 ] && [ "$sudo_revision" -eq 0 ]; }; then
|
||||
sudo_chroot_candidate=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "$sudo_host_candidate" -eq 1 ]; then
|
||||
printf ' CVE-2025-32462 upstream candidate (1.8.8-1.9.17); requires a host-specific sudoers or LDAP rule.\n'
|
||||
fi
|
||||
if [ "$sudo_chroot_candidate" -eq 1 ]; then
|
||||
printf ' CVE-2025-32463 upstream candidate (1.9.14-1.9.17); chroot/NSS behavior is relevant.\n'
|
||||
fi
|
||||
if [ "$sudo_host_candidate" -eq 1 ] || [ "$sudo_chroot_candidate" -eq 1 ]; then
|
||||
printf ' Version alone cannot confirm exposure: vendors backport fixes; check the installed package advisory.\n'
|
||||
else
|
||||
printf ' No CVE-2025-32462/32463 upstream candidate by version.\n'
|
||||
fi
|
||||
}
|
||||
|
||||
sudo_seen=''
|
||||
sudo_found=''
|
||||
sudo_timeout=''
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
sudo_timeout=$(command -v timeout)
|
||||
elif command -v gtimeout >/dev/null 2>&1; then
|
||||
sudo_timeout=$(command -v gtimeout)
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Probe each PATH entry in order, then the bounded standard locations. Inode
|
||||
# identity deduplicates aliases (including symlinks) without traversing the disk.
|
||||
sudo_path_left=$PATH
|
||||
sudo_path_count=0
|
||||
while :; do
|
||||
sudo_path_count=$((sudo_path_count + 1))
|
||||
[ "$sudo_path_count" -le 24 ] || break
|
||||
sudo_last=
|
||||
case "$sudo_path_left" in
|
||||
*:*) sudo_dir=${sudo_path_left%%:*}; sudo_path_left=${sudo_path_left#*:} ;;
|
||||
*) sudo_dir=$sudo_path_left; sudo_last=1 ;;
|
||||
esac
|
||||
[ -n "$sudo_dir" ] || sudo_dir=.
|
||||
case "$sudo_dir" in /*) ;; *) sudo_dir="$(pwd -P)/$sudo_dir" ;; esac
|
||||
sudo_version_check "$sudo_dir/sudo"
|
||||
[ -n "$sudo_last" ] && break
|
||||
done
|
||||
for sudo_dir in /usr/bin /bin /usr/local/bin /usr/local/sbin; do
|
||||
sudo_version_check "$sudo_dir/sudo"
|
||||
done
|
||||
[ -n "$sudo_found" ] || echo_not_found "sudo"
|
||||
echo ""
|
||||
@@ -1,48 +1,78 @@
|
||||
# Title: System Information - USBCreator
|
||||
# ID: SY_USBCreator
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 07-03-2024
|
||||
# Description: Check for USBCreator vulnerabilities that could lead to privilege escalation:
|
||||
# - Vulnerable policykit-desktop-privileges versions
|
||||
# - Common vulnerable versions:
|
||||
# * policykit-desktop-privileges < 0.21
|
||||
# - Exploitation methods:
|
||||
# * D-Bus command injection through USBCreator
|
||||
# * Abuse of policykit privileges
|
||||
# * Common attack vectors:
|
||||
# - D-Bus method call injection
|
||||
# - PolicyKit authentication bypass
|
||||
# - Command execution through USB creation
|
||||
# * Exploit techniques:
|
||||
# - D-Bus method spoofing
|
||||
# - PolicyKit privilege escalation
|
||||
# - USB device creation abuse
|
||||
# Last Update: 2026-10-09
|
||||
# Description: Passive review of the USB imaging D-Bus service and legacy passwordless group policy
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1548.003,T1068
|
||||
# Functions Used: print_2title, print_info
|
||||
# Global Variables: $DEBUG
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $pc_version, $pc_length, $pc_major, $pc_minor
|
||||
# Generated Global Variables: $usbcreator_service, $usbcreator_policy, $usbcreator_size, $usbcreator_groups
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
# Read only the exact legacy vendor policy file, after a metadata size check.
|
||||
# A matching stanza is a review lead: later rules and the installed helper's
|
||||
# implementation can change the effective authorization and file-copy behavior.
|
||||
usbcreator_passwordless_group_policy() (
|
||||
usbcreator_policy=$1
|
||||
[ -f "$usbcreator_policy" ] && [ ! -L "$usbcreator_policy" ] &&
|
||||
[ -r "$usbcreator_policy" ] || exit 1
|
||||
usbcreator_size=$(LC_ALL=C ls -ldn "$usbcreator_policy" 2>/dev/null | awk 'NR == 1 { print $5 }')
|
||||
case "$usbcreator_size" in ''|*[!0-9]*) exit 1 ;; esac
|
||||
[ "$usbcreator_size" -le 16384 ] || exit 1
|
||||
awk '
|
||||
function match_stanza() { return identity && action && active }
|
||||
/^\[/ { if (match_stanza()) found = 1; identity = action = active = 0; next }
|
||||
NR > 200 { overflow = 1; exit }
|
||||
{
|
||||
line = $0
|
||||
sub(/^[[:space:]]*/, "", line)
|
||||
sub(/[[:space:]]*$/, "", line)
|
||||
if (line ~ /^Identity[[:space:]]*=/) {
|
||||
sub(/^[^=]*=[[:space:]]*/, "", line)
|
||||
identity = line ~ /(^|;)unix-group:(admin|sudo)(;|$)/
|
||||
} else if (line ~ /^Action[[:space:]]*=/) {
|
||||
sub(/^[^=]*=[[:space:]]*/, "", line)
|
||||
action = line ~ /(^|;)com[.]ubuntu[.]usbcreator[.]image(;|$)/
|
||||
} else if (line ~ /^ResultActive[[:space:]]*=/) {
|
||||
sub(/^[^=]*=[[:space:]]*/, "", line)
|
||||
active = line == "yes"
|
||||
}
|
||||
}
|
||||
END { if (overflow) exit 1; if (match_stanza()) found = 1; if (found) exit 0; exit 1 }
|
||||
' "$usbcreator_policy"
|
||||
)
|
||||
|
||||
if (busctl list 2>/dev/null | grep -q com.ubuntu.USBCreator) || [ "$DEBUG" ]; then
|
||||
usbcreator_service=''
|
||||
for usbcreator_policy in /etc/dbus-1/system-services/com.ubuntu.USBCreator.service \
|
||||
/run/dbus-1/system-services/com.ubuntu.USBCreator.service \
|
||||
/usr/local/share/dbus-1/system-services/com.ubuntu.USBCreator.service \
|
||||
/usr/share/dbus-1/system-services/com.ubuntu.USBCreator.service; do
|
||||
if [ -f "$usbcreator_policy" ] && [ ! -L "$usbcreator_policy" ]; then
|
||||
usbcreator_service=$usbcreator_policy
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$usbcreator_service" ] || [ "$DEBUG" ]; then
|
||||
print_2title "USBCreator" "T1548.003,T1068"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/processes-crontab-systemd-dbus/d-bus-enumeration-and-command-injection-privilege-escalation.html"
|
||||
|
||||
pc_version=$(dpkg -l 2>/dev/null | grep policykit-desktop-privileges | grep -oP "[0-9][0-9a-zA-Z\.]+")
|
||||
if [ -z "$pc_version" ]; then
|
||||
pc_version=$(apt-cache policy policykit-desktop-privileges 2>/dev/null | grep -oP "\*\*\*.*" | cut -d" " -f2)
|
||||
if [ -n "$usbcreator_service" ]; then
|
||||
printf 'USB imaging system-bus service descriptor: %s (installed/activatable candidate; effective service and patch state unverified)\n' "$usbcreator_service"
|
||||
fi
|
||||
if [ -n "$pc_version" ]; then
|
||||
pc_length=${#pc_version}
|
||||
pc_major=$(echo "$pc_version" | cut -d. -f1)
|
||||
pc_minor=$(echo "$pc_version" | cut -d. -f2)
|
||||
if [ "$pc_length" -eq 4 ] && [ "$pc_major" -eq 0 ] && [ "$pc_minor" -lt 21 ]; then
|
||||
echo "Vulnerable!!" | sed -${E} "s,.*,${SED_RED},"
|
||||
fi
|
||||
if [ -n "$usbcreator_service" ] && [ "$(id -u 2>/dev/null)" != 0 ]; then
|
||||
usbcreator_groups=$(id -Gn 2>/dev/null)
|
||||
case " $usbcreator_groups " in
|
||||
*' sudo '*|*' admin '*)
|
||||
usbcreator_policy=/var/lib/polkit-1/localauthority/10-vendor.d/com.ubuntu.desktop.pkla
|
||||
if usbcreator_passwordless_group_policy "$usbcreator_policy"; then
|
||||
printf 'USB imaging legacy group policy candidate: %s (image action permits an admin group without a prompt in an active session; effective rules and helper behavior unverified)\n' "$usbcreator_policy"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
echo ""
|
||||
@@ -1,8 +1,8 @@
|
||||
# Title: System Information - Mounts
|
||||
# ID: SY_Mounts
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 07-09-2026
|
||||
# Description: Check for mount point misconfigurations that could lead to privilege escalation, udisks2 CVE-2026-7867 exposure, and XFSTango CVE-2026-80530 exposure:
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Check mount point misconfigurations, bounded local NFS export policy, udisks2 CVE-2026-7867 exposure, and XFSTango CVE-2026-80530 exposure:
|
||||
# - Unmounted filesystems
|
||||
# - Mount point permissions
|
||||
# - Mount options
|
||||
@@ -26,19 +26,110 @@
|
||||
# License: GNU GPL
|
||||
# Version: 1.2
|
||||
# Mitre: T1068,T1082,T1120
|
||||
# Functions Used: checkUDisksCVE20267867, checkXFSTangoCVE202680530, print_2title, print_info
|
||||
# Functions Used: checkLibblockdevCVE20256019, checkUDisksCVE20267867, checkXFSTangoCVE202680530, print_2title, print_info
|
||||
# Global Variables: $DEBUG, $SEARCH_IN_FOLDER, $mountG, $mountpermsB, $mountpermsG, $notmounted, $Wfolders, $mounted
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Generated Global Variables: $nfs_exports_file, $nfs_exports_size, $nfs_exports_seen, $nfs_exports_output, $nfs_exports_result
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
fuse_allow_other_state() {
|
||||
[ -f "$1" ] && [ -r "$1" ] || { printf 'unknown\n'; return; }
|
||||
awk '
|
||||
NR > 256 { truncated = 1; exit }
|
||||
/^[[:space:]]*user_allow_other[[:space:]]*$/ { enabled = 1; exit }
|
||||
END {
|
||||
if (enabled) print "enabled"
|
||||
else if (truncated) print "unknown"
|
||||
else print "disabled"
|
||||
}
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Inspect only short, regular local policy files. The server's effective
|
||||
# exports, security flavor, clients, and numeric UID mapping remain unknown.
|
||||
nfs_export_file_candidates() {
|
||||
[ -f "$1" ] && [ -r "$1" ] && [ ! -L "$1" ] || return 0
|
||||
nfs_exports_size=$(dd if="$1" bs=16385 count=1 2>/dev/null | wc -c | tr -d '[:space:]') || :
|
||||
case "$nfs_exports_size" in ''|*[!0-9]*) return 0 ;; esac
|
||||
if [ "$nfs_exports_size" -gt 16384 ]; then
|
||||
printf 'NFS export review partial (16 KiB file limit): %s\n' "$1"
|
||||
return 0
|
||||
fi
|
||||
[ "$nfs_exports_size" -gt 0 ] || return 0
|
||||
dd if="$1" bs=16384 count=1 2>/dev/null | awk -v source="$1" '
|
||||
NR > 128 { print "NFS export review partial (128-line limit): " source; exit }
|
||||
length($0) > 512 {
|
||||
if (!long_line++) print "NFS export review partial (long line): " source
|
||||
next
|
||||
}
|
||||
{
|
||||
line = $0
|
||||
sub(/[[:space:]]+#.*/, "", line)
|
||||
if (line ~ /^[[:space:]]*#/) next
|
||||
candidate = 0
|
||||
while (match(line, /\([^)]*\)/)) {
|
||||
options = substr(line, RSTART + 1, RLENGTH - 2)
|
||||
gsub(/[[:space:]]/, "", options)
|
||||
if (options ~ /(^|,)rw(,|$)/ &&
|
||||
options !~ /(^|,)all_squash(,|$)/ &&
|
||||
(options !~ /(^|,)sec=/ || options ~ /(^|,)sec=([^,:]*:)*sys([,:]|$)/)) candidate = 1
|
||||
line = substr(line, RSTART + RLENGTH)
|
||||
}
|
||||
if (!candidate) next
|
||||
if (++matches <= 4) printf "NFS writable export identity review candidate: %s:%d\n", source, NR
|
||||
else if (matches == 5) { print "NFS export review partial (4-candidate file limit): " source; exit }
|
||||
}
|
||||
' 2>/dev/null
|
||||
}
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
checkLibblockdevCVE20256019
|
||||
checkUDisksCVE20267867
|
||||
checkXFSTangoCVE202680530
|
||||
fi
|
||||
|
||||
if [ -e /etc/fuse.conf ] || [ -L /etc/fuse.conf ]; then
|
||||
print_2title "FUSE cross-user mount policy" "T1082"
|
||||
case "$(fuse_allow_other_state /etc/fuse.conf)" in
|
||||
enabled) echo "user_allow_other enabled: non-root mounts may opt into access by other users, including root; review privileged writes into caller-controlled paths" ;;
|
||||
disabled) echo "user_allow_other disabled in /etc/fuse.conf" ;;
|
||||
*) echo "user_allow_other unknown (unreadable, non-regular, or beyond first 256 lines)" ;;
|
||||
esac
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Live-host policy must not be mixed into an extracted filesystem review.
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
nfs_exports_seen=0
|
||||
nfs_exports_output=""
|
||||
for nfs_exports_file in /etc/exports /etc/exports.d/*.exports; do
|
||||
case "$nfs_exports_file" in
|
||||
/etc/exports.d/*)
|
||||
[ -d /etc/exports.d ] && [ ! -L /etc/exports.d ] || continue
|
||||
nfs_exports_seen=$((nfs_exports_seen + 1))
|
||||
if [ "$nfs_exports_seen" -gt 8 ]; then
|
||||
nfs_exports_output="${nfs_exports_output}NFS export review partial (8 drop-in limit).
|
||||
"
|
||||
break
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
nfs_exports_result=$(nfs_export_file_candidates "$nfs_exports_file") || :
|
||||
if [ -n "$nfs_exports_result" ]; then
|
||||
nfs_exports_output="${nfs_exports_output}${nfs_exports_result}
|
||||
"
|
||||
fi
|
||||
done
|
||||
if [ -n "$nfs_exports_output" ]; then
|
||||
print_2title "NFS writable export identity mapping (passive review)" "T1082"
|
||||
printf '%s' "$nfs_exports_output"
|
||||
echo "Verify active export policy, allowed clients, AUTH_SYS, numeric UID/GID mapping, ACLs, and nosuid/SELinux before treating a writable export as cross-user execution."
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "/etc/fstab" ] || [ "$DEBUG" ]; then
|
||||
print_2title "Unmounted file-system?" "T1082,T1120"
|
||||
print_info "Check if you can mount umounted devices"
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1613,T1611
|
||||
# Functions Used: containerCheck, echo_no, enumerateDockerSockets, print_2title, print_list, warn_exec
|
||||
# Functions Used: checkDockerOverlayStorage, containerCheck, echo_no, enumerateDockerSockets, print_2title, print_list, warn_exec
|
||||
# Global Variables: $containerType
|
||||
# Initial Functions: containerCheck
|
||||
# Generated Global Variables: $containerCounts, $crictlcontainers, $ctrcontainers, $dockercontainers, $lxccontainers, $nerdctlcontainers, $podmancontainers, $rktcontainers
|
||||
# Generated Global Variables: $containerCounts, $crictlcontainers, $ctrcontainers, $dockercontainers, $docker_overlay_candidates, $lxccontainers, $minikube_log_checked, $minikube_log_home, $minikube_log_parent, $minikube_log_path, $minikube_log_version, $nerdctlcontainers, $podmancontainers, $rktcontainers
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -43,6 +43,32 @@ print_runtime_info() {
|
||||
fi
|
||||
}
|
||||
|
||||
minikube_crio_log_probe() {
|
||||
minikube_log_checked=0
|
||||
for minikube_log_home in "$@"; do
|
||||
minikube_log_checked=$((minikube_log_checked + 1))
|
||||
[ "$minikube_log_checked" -le 8 ] || break
|
||||
minikube_log_parent="${minikube_log_home%/*}"
|
||||
minikube_log_path="$minikube_log_home/.minikube/logs/lastStart.txt"
|
||||
# Avoid links into unrelated or remote trees; only inspect this exact log.
|
||||
[ -L "$minikube_log_parent" ] && continue
|
||||
[ -L "$minikube_log_home" ] && continue
|
||||
[ -L "$minikube_log_home/.minikube" ] && continue
|
||||
[ -L "$minikube_log_home/.minikube/logs" ] && continue
|
||||
[ -L "$minikube_log_path" ] && continue
|
||||
[ -f "$minikube_log_path" ] && [ -r "$minikube_log_path" ] || continue
|
||||
minikube_log_version="$(head -c 65536 "$minikube_log_path" 2>/dev/null |
|
||||
head -n 200 |
|
||||
sed -n 's/.*Preparing Kubernetes .* on CRI-O \([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\) .*/\1/p' |
|
||||
head -n 1)"
|
||||
[ "${#minikube_log_version}" -le 20 ] || continue
|
||||
case "$minikube_log_version" in
|
||||
''|*[!0-9.]*) continue ;;
|
||||
esac
|
||||
print_list "Minikube startup log CRI-O ....$NC $minikube_log_version (historical; verify live runtime, fixes, Pod creation rights, seccomp/userns)"
|
||||
done
|
||||
}
|
||||
|
||||
get_runtime_container_count() {
|
||||
if has_runtime_cli "$1"; then
|
||||
shift
|
||||
@@ -86,6 +112,7 @@ print_runtime_info podman "Podman info ................" podman info
|
||||
print_runtime_info lxc "LXC version ................" lxc version
|
||||
print_runtime_info lxc "LXC info ..................." lxc info
|
||||
print_runtime_info crio "CRI-O version ..............." crio --version
|
||||
minikube_crio_log_probe /root /home/*
|
||||
print_runtime_info runc "runc version ..............." runc --version
|
||||
print_runtime_info crun "crun version ..............." crun --version
|
||||
print_runtime_info nerdctl "nerdctl version ............" nerdctl version
|
||||
@@ -95,6 +122,13 @@ print_runtime_info ctr "ctr version ................" ctr version
|
||||
print_list "Interesting runtime sockets ... "$NC
|
||||
enumerateDockerSockets
|
||||
|
||||
docker_overlay_candidates="$(checkDockerOverlayStorage)"
|
||||
if [ "$docker_overlay_candidates" ]; then
|
||||
print_list "Traversable Docker overlay roots (review candidate; mount lacks nosuid/noexec):\n"$NC
|
||||
printf '%s\n' "$docker_overlay_candidates"
|
||||
print_list "Requires a root-owned SetUID file, compatible rootful/userns mapping, and effective execution policy; newer daemon versions may leave old container permissions until restart. No file search was run.\n"$NC
|
||||
fi
|
||||
|
||||
print_list "Any running containers? ........ "$NC
|
||||
# Get counts of running containers for each platform
|
||||
dockercontainers=0
|
||||
|
||||
@@ -23,9 +23,10 @@ if echo "$containerType" | grep -qi "docker"; then
|
||||
enumerateDockerSockets
|
||||
print_list "Docker version .................$NC$dockerVersion"
|
||||
checkDockerVersionExploits
|
||||
print_list "Vulnerable to CVE-2019-5736 ....$NC$VULN_CVE_2019_5736"$NC | sed -${E} "s,Yes,${SED_RED_YELLOW},"
|
||||
print_list "Vulnerable to CVE-2019-13139 ...$NC$VULN_CVE_2019_13139"$NC | sed -${E} "s,Yes,${SED_RED_YELLOW},"
|
||||
print_list "Vulnerable to CVE-2021-41091 ...$NC$VULN_CVE_2021_41091"$NC | sed -${E} "s,Yes,${SED_RED_YELLOW},"
|
||||
print_list "Docker version may predate bundled runc CVE-2019-5736 fix ...$NC$VULN_CVE_2019_5736"$NC | sed -${E} "s,Yes,${SED_RED_YELLOW},"
|
||||
print_list "Docker version may predate CVE-2019-13139 fix ...$NC$VULN_CVE_2019_13139"$NC | sed -${E} "s,Yes,${SED_RED_YELLOW},"
|
||||
print_list "Docker version may predate CVE-2021-41091 fix ...$NC$VULN_CVE_2021_41091"$NC | sed -${E} "s,Yes,${SED_RED_YELLOW},"
|
||||
print_list "A newer daemon version does not rule out stale permissions in containers that have not restarted.\n"$NC
|
||||
if [ "$inContainer" ]; then
|
||||
checkDockerRootless
|
||||
print_list "Rootless Docker? ............... $DOCKER_ROOTLESS\n"$NC | sed -${E} "s,No,${SED_RED}," | sed -${E} "s,Yes,${SED_GREEN},"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
# Functions Used: print_2title, print_3title
|
||||
# Global Variables: $containerType, $EXTRA_CHECKS
|
||||
# Initial Functions: containerCheck
|
||||
# Generated Global Variables: $k8s_aws_bucket, $k8s_cap_eff, $k8s_cap_low, $k8s_cfg, $k8s_cfg_env, $k8s_context_name, $k8s_count, $k8s_current_context, $k8s_dir, $k8s_direct_base, $k8s_direct_ca, $k8s_direct_host, $k8s_direct_port, $k8s_direct_token, $k8s_direct_token_file, $k8s_docker_host, $k8s_file, $k8s_gcs_bucket, $k8s_mount, $k8s_mount_options, $k8s_mount_root, $k8s_namespace, $k8s_node_address, $k8s_node_addresses, $k8s_node_name, $k8s_ns_one, $k8s_ns_self, $k8s_pid, $k8s_probe_host, $k8s_probe_port, $k8s_probe_scheme, $k8s_probe_status, $k8s_rc, $k8s_readable, $k8s_root, $k8s_root_count, $k8s_runc_version, $k8s_socket, $k8s_writable
|
||||
# Generated Global Variables: $k8s_access_exit, $k8s_access_http, $k8s_access_json, $k8s_access_response, $k8s_access_result, $k8s_aws_bucket, $k8s_cap_eff, $k8s_cap_low, $k8s_cfg, $k8s_cfg_env, $k8s_context_name, $k8s_count, $k8s_current_context, $k8s_dir, $k8s_direct_base, $k8s_direct_ca, $k8s_direct_host, $k8s_direct_port, $k8s_direct_token, $k8s_direct_token_file, $k8s_docker_host, $k8s_file, $k8s_gcs_bucket, $k8s_mount, $k8s_mount_options, $k8s_mount_root, $k8s_namespace, $k8s_node_address, $k8s_node_addresses, $k8s_node_name, $k8s_ns_one, $k8s_ns_self, $k8s_pid, $k8s_probe_host, $k8s_probe_port, $k8s_probe_scheme, $k8s_probe_status, $k8s_rc, $k8s_readable, $k8s_root, $k8s_root_count, $k8s_runc_version, $k8s_secret_response, $k8s_secret_exit, $k8s_secret_http, $k8s_secret_json, $k8s_secret_summary, $k8s_socket, $k8s_writable, $count, $keys, $continued, $k8s_curl_limit
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
@@ -371,7 +371,109 @@ k8s_sa_api_get() {
|
||||
"$k8s_direct_base$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Bound captured bytes even on curl versions whose --max-filesize only checks
|
||||
# Content-Length. Keep curl's exit status in-band because head owns the pipeline
|
||||
# status. A final dot preserves trailing newlines in command substitutions.
|
||||
# Callers reserve 15 bytes: HTTP status (4), exit trailer (up to 10), and
|
||||
# one over-limit byte. Reject that extra byte before parsing any response.
|
||||
k8s_bounded_curl() (
|
||||
k8s_curl_limit=$1
|
||||
shift
|
||||
{ curl "$@"; printf '\nexit=%s\n' "$?"; } | head -c "$k8s_curl_limit"
|
||||
printf '.'
|
||||
)
|
||||
|
||||
k8s_sa_kube_system_secret_access() {
|
||||
# SelfSubjectAccessReview evaluates the mounted token without reading Secrets.
|
||||
k8s_access_response="$(printf 'header = "Authorization: Bearer %s"\n' "$k8s_direct_token" |
|
||||
k8s_bounded_curl 4111 -q --config - -fsS --connect-timeout 2 --max-time 5 --max-filesize 4096 \
|
||||
--cacert "$k8s_direct_ca" -H 'Accept: application/json' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectAccessReview","spec":{"resourceAttributes":{"namespace":"kube-system","verb":"list","resource":"secrets"}}}' \
|
||||
-w '\n%{http_code}' \
|
||||
"$k8s_direct_base/apis/authorization.k8s.io/v1/selfsubjectaccessreviews" 2>/dev/null)"
|
||||
k8s_access_response=${k8s_access_response%.}
|
||||
if [ "$(printf '%s' "$k8s_access_response" | wc -c)" -gt 4110 ]; then
|
||||
echo unknown
|
||||
return
|
||||
fi
|
||||
k8s_access_exit="$(printf '%s' "$k8s_access_response" | tail -n 1)"
|
||||
k8s_access_exit=${k8s_access_exit#exit=}
|
||||
k8s_access_response=${k8s_access_response%exit=*}
|
||||
k8s_access_response=${k8s_access_response%?}
|
||||
k8s_access_http="$(printf '%s' "$k8s_access_response" | tail -c 3)"
|
||||
case "$k8s_access_exit:$k8s_access_http" in 0:200|0:201) ;; *) echo unknown; return ;; esac
|
||||
k8s_access_json="${k8s_access_response%????}"
|
||||
k8s_access_result="$(printf '%s' "$k8s_access_json" | jq -r '
|
||||
if (.status | type) != "object" or
|
||||
(.status.allowed | type) != "boolean" or
|
||||
((.status.evaluationError // "") != "") or
|
||||
(.status.allowed == true and .status.denied == true)
|
||||
then "unknown"
|
||||
elif .status.allowed then "allowed"
|
||||
else "denied"
|
||||
end
|
||||
' 2>/dev/null | head -n 1)"
|
||||
case "$k8s_access_result" in allowed|denied) echo "$k8s_access_result" ;; *) echo unknown ;; esac
|
||||
}
|
||||
|
||||
k8s_scan_sa_secrets() {
|
||||
# The API response contains values; keep it in memory and emit only selected metadata.
|
||||
# Bound the capture independently of Content-Length and curl version.
|
||||
k8s_secret_response="$(printf 'header = "Authorization: Bearer %s"\n' "$k8s_direct_token" |
|
||||
k8s_bounded_curl 1048591 -q --config - -fsS --connect-timeout 2 --max-time 5 --max-filesize 1048576 \
|
||||
--cacert "$k8s_direct_ca" -H 'Accept: application/json' -w '\n%{http_code}' \
|
||||
"$k8s_direct_base/api/v1/namespaces/$k8s_namespace/secrets?limit=40" 2>/dev/null)"
|
||||
k8s_secret_response=${k8s_secret_response%.}
|
||||
if [ "$(printf '%s' "$k8s_secret_response" | wc -c)" -gt 1048590 ]; then
|
||||
echo ' secrets: response exceeded 1 MiB; inventory skipped'
|
||||
return
|
||||
fi
|
||||
k8s_secret_exit="$(printf '%s' "$k8s_secret_response" | tail -n 1)"
|
||||
k8s_secret_exit=${k8s_secret_exit#exit=}
|
||||
k8s_secret_response=${k8s_secret_response%exit=*}
|
||||
k8s_secret_response=${k8s_secret_response%?}
|
||||
k8s_secret_http="$(printf '%s' "$k8s_secret_response" | tail -c 3)"
|
||||
case "$k8s_secret_exit:$k8s_secret_http" in
|
||||
0:200) ;;
|
||||
22:403) echo ' secrets: access denied (RBAC)' ; return ;;
|
||||
22:401) echo ' secrets: authentication denied' ; return ;;
|
||||
22:404) echo ' secrets: namespace unavailable' ; return ;;
|
||||
28:*) echo ' secrets: request timed out' ; return ;;
|
||||
63:*) echo ' secrets: response exceeded 1 MiB; inventory skipped' ; return ;;
|
||||
*) echo ' secrets: API request failed or returned an unexpected status' ; return ;;
|
||||
esac
|
||||
k8s_secret_json="${k8s_secret_response%????}"
|
||||
k8s_secret_summary="$(printf '%s' "$k8s_secret_json" | jq -r '
|
||||
if type != "object" or (.items | type) != "array" or
|
||||
any(.items[]; type != "object" or (.metadata.name | type) != "string" or
|
||||
((.type // "") | type) != "string" or
|
||||
((.data // {}) | type) != "object" or
|
||||
((.stringData // {}) | type) != "object")
|
||||
then error("invalid Secret list")
|
||||
else
|
||||
(.items | length) as $count |
|
||||
((.metadata["continue"] // "") != "") as $continued |
|
||||
if $count == 0 then " secrets: none (accessible list)"
|
||||
else
|
||||
.items[:40][] |
|
||||
((.data // {} | keys) + (.stringData // {} | keys) | unique) as $keys |
|
||||
" secrets: name=\(.metadata.name | @json) type=\((.type // "unknown") | @json) keys=\($keys[:20] | @json)\(if ($keys | length) > 20 then " (additional keys omitted)" else "" end)"
|
||||
end,
|
||||
if $count > 40 or $continued then " secrets: additional results omitted (40 shown; no pagination)" else empty end
|
||||
end
|
||||
' 2>/dev/null)" || {
|
||||
echo ' secrets: malformed API response; inventory unknown'
|
||||
return
|
||||
}
|
||||
printf '%s\n' "$k8s_secret_summary"
|
||||
}
|
||||
|
||||
k8s_scan_sa_api() {
|
||||
if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||
echo ' Kubernetes API discovery skipped (curl or jq unavailable)'
|
||||
return
|
||||
fi
|
||||
k8s_direct_api_ready "$@" || return
|
||||
echo ' Kubernetes API discovery with mounted service account (names and settings only):'
|
||||
k8s_sa_api_get '/api/v1/namespaces?limit=100' |
|
||||
@@ -382,11 +484,14 @@ k8s_scan_sa_api() {
|
||||
(.spec.containers[]? | " container \(.name) privileged=\(.securityContext.privileged // false) allowPE=\((.securityContext.allowPrivilegeEscalation | if . == null then "default" else . end)) caps=\((.securityContext.capabilities.add // []) | join(","))"),
|
||||
(.spec.volumes[]? | select(.hostPath.path != null) | " hostPath \(.hostPath.path)")
|
||||
)' 2>/dev/null | head -n 100 | sed 's/^/ pod: /'
|
||||
for k8s_file in services serviceaccounts secrets; do
|
||||
for k8s_file in services serviceaccounts; do
|
||||
k8s_sa_api_get "/api/v1/namespaces/$k8s_namespace/$k8s_file?limit=100" |
|
||||
jq -r '.items[]? | .metadata.name // empty' 2>/dev/null | head -n 40 |
|
||||
sed "s/^/ $k8s_file: /"
|
||||
done
|
||||
k8s_scan_sa_secrets
|
||||
printf ' kube-system secrets list authorization (mounted identity): %s\n' \
|
||||
"$(k8s_sa_kube_system_secret_access)"
|
||||
k8s_sa_api_get '/api/v1/nodes?limit=100' |
|
||||
jq -r '.items[]?.metadata.name // empty' 2>/dev/null | head -n 40 | sed 's/^/ node: /'
|
||||
k8s_direct_token=''
|
||||
@@ -421,6 +526,10 @@ k8s_scan_kubectl() {
|
||||
echo ' Current context authorization rules:'
|
||||
k8s_kubectl --request-timeout=5s auth can-i --list -n "$k8s_namespace" \
|
||||
2>/dev/null | head -n 80
|
||||
k8s_access_result="$(k8s_kubectl --request-timeout=5s auth can-i list secrets \
|
||||
-n kube-system 2>/dev/null | head -n 1)"
|
||||
case "$k8s_access_result" in yes) k8s_access_result=allowed ;; no) k8s_access_result=denied ;; *) k8s_access_result=unknown ;; esac
|
||||
printf ' kube-system secrets list authorization (current context): %s\n' "$k8s_access_result"
|
||||
echo ' Other configured context authorization rules (up to five):'
|
||||
k8s_kubectl config get-contexts -o name 2>/dev/null | head -n 5 |
|
||||
while IFS= read -r k8s_context_name; do
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# Title: Container - Writable bind mounts without nosuid (SUID risk)
|
||||
# Title: Container - Possible shared writable mounts (SUID conditions)
|
||||
# ID: CT_RW_bind_mounts_nosuid
|
||||
# Author: HT Bot
|
||||
# Last Update: 17-09-2025
|
||||
# Description: Detect writable bind-mounted paths inside containers that are not mounted with nosuid.
|
||||
# If the container user is root and the mount is a host bind mount without nosuid, an attacker may
|
||||
# be able to drop a SUID binary on the shared path and execute it from the host to escalate to root
|
||||
# (classic container-to-host breakout via writable bind mount).
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Show ordinary writable mounts that may be shared with a host.
|
||||
# Mountinfo does not identify bind mounts or prove that a path is host-backed.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1611
|
||||
# Functions Used: containerCheck, print_2title, print_list, print_info
|
||||
# Global Variables: $inContainer
|
||||
@@ -16,32 +14,101 @@
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
# Keep mountinfo's octal path escapes intact so spaces and tabs cannot split rows.
|
||||
# The two groups put non-root filesystem roots first, but root=/ is retained:
|
||||
# a bind of an entire filesystem has root=/ too. At most ten rows are printed.
|
||||
ct_rw_mountinfo_candidates() {
|
||||
awk '
|
||||
function hasopt(opts, opt) { return index("," opts ",", "," opt ",") != 0 }
|
||||
function ordinary(fs, target) {
|
||||
if (fs ~ /^(proc|sysfs|devtmpfs|devpts|tmpfs|cgroup|cgroup2|mqueue|pstore|securityfs|debugfs|tracefs|configfs|fusectl|hugetlbfs|autofs|rpc_pipefs|binfmt_misc|nsfs|bpf)$/) return 0
|
||||
if (target == "/" && fs == "overlay") return 0
|
||||
if (target ~ /^\/(proc|sys|dev)(\/|$)/) return 0
|
||||
if (target == "/etc/hosts" || target == "/etc/hostname" || target == "/etc/resolv.conf" || target ~ /^\/run\/secrets(\/|$)/) return 0
|
||||
return 1
|
||||
}
|
||||
{
|
||||
sep = 0
|
||||
for (i = 7; i <= NF; i++) if ($i == "-") { sep = i; break }
|
||||
if (!sep || sep + 3 > NF) next
|
||||
root = $4; target = $5; opts = $6
|
||||
fs = $(sep + 1); source = $(sep + 2); superopts = $(sep + 3)
|
||||
if (!ordinary(fs, target) || !hasopt(opts, "rw") || !hasopt(superopts, "rw")) next
|
||||
suid = hasopt(opts, "nosuid") ? "blocked-here" : "not-blocked-here"
|
||||
exec = hasopt(opts, "noexec") ? "blocked-here" : "not-blocked-here"
|
||||
group = root == "/" ? 2 : 1
|
||||
count[group]++
|
||||
if (count[group] <= 10) rows[group, count[group]] = " mount(raw)=" target " root(raw)=" root " fs=" fs " source(raw)=" source " opts=" opts " local-suid=" suid " local-exec=" exec
|
||||
}
|
||||
END {
|
||||
shown = 0
|
||||
for (group = 1; group <= 2; group++)
|
||||
for (i = 1; i <= count[group] && shown < 10; i++) { print rows[group, i]; shown++ }
|
||||
total = count[1] + count[2]
|
||||
if (total > shown) print " ... " total - shown " more ordinary writable mounts omitted"
|
||||
}
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# /proc/mounts has no filesystem-root field, so this is only a generic list.
|
||||
ct_rw_mounts_fallback() {
|
||||
awk '
|
||||
function hasopt(opts, opt) { return index("," opts ",", "," opt ",") != 0 }
|
||||
$3 !~ /^(proc|sysfs|devtmpfs|devpts|tmpfs|cgroup|cgroup2|mqueue|pstore|securityfs|debugfs|tracefs|configfs|fusectl|hugetlbfs|autofs|rpc_pipefs|binfmt_misc|nsfs|bpf)$/ &&
|
||||
$2 !~ /^\/(proc|sys|dev)(\/|$)/ && $2 != "/" &&
|
||||
$2 != "/etc/hosts" && $2 != "/etc/hostname" && $2 != "/etc/resolv.conf" &&
|
||||
hasopt($4, "rw") {
|
||||
count++
|
||||
if (count <= 10) print " mount(raw)=" $2 " fs=" $3 " source(raw)=" $1 " opts=" $4
|
||||
}
|
||||
END { if (count > 10) print " ... " count - 10 " more writable mounts omitted" }
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
ct_rw_uid_note() {
|
||||
if [ "$2" != "0" ]; then
|
||||
echo " Current effective UID is not 0; container-root access is an additional condition."
|
||||
elif [ ! -r "$1" ]; then
|
||||
echo " UID 0 mapping is unknown."
|
||||
else
|
||||
awk '
|
||||
$1 == 0 && $3 > 0 {
|
||||
if ($2 == 0) print " UID 0 maps to UID 0 in the parent user namespace; host-root mapping is unverified."
|
||||
else print " UID 0 is remapped in the parent user namespace; host-root ownership is less likely."
|
||||
found = 1; exit
|
||||
}
|
||||
END { if (!found) print " UID 0 mapping is unknown." }
|
||||
' "$1" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
containerCheck
|
||||
|
||||
if [ "$inContainer" ]; then
|
||||
echo ""
|
||||
print_2title "Container - Writable bind mounts w/o nosuid (SUID persistence risk)" "T1611"
|
||||
print_2title "Container - Possible shared writable mounts (SUID conditions)" "T1611"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/containers-namespaces/container-security/sensitive-host-mounts.html"
|
||||
|
||||
if [ -r /proc/self/mountinfo ]; then
|
||||
CT_RW_bind_mounts_matches=$(grep -E "(^| )bind( |$)" /proc/self/mountinfo 2>/dev/null | grep -E "(^|,)rw(,|$)" | grep -v "nosuid" || true)
|
||||
CT_RW_bind_mounts_matches=$(ct_rw_mountinfo_candidates /proc/self/mountinfo)
|
||||
print_list "Ordinary writable mounts (mountinfo) ......."
|
||||
elif [ -r /proc/mounts ]; then
|
||||
CT_RW_bind_mounts_matches=$(ct_rw_mounts_fallback /proc/mounts)
|
||||
print_list "Generic writable mounts (/proc/mounts) ...."
|
||||
echo " Mountinfo unavailable; filesystem-root and bind classification unavailable."
|
||||
else
|
||||
CT_RW_bind_mounts_matches=$(mount -l 2>/dev/null | grep -E "bind" | grep -E "(^|,)rw(,|$)" | grep -v "nosuid" || true)
|
||||
CT_RW_bind_mounts_matches=""
|
||||
print_list "Writable mount data ......................."
|
||||
echo " Unavailable"
|
||||
fi
|
||||
|
||||
if [ -z "$CT_RW_bind_mounts_matches" ]; then
|
||||
print_list "Writable bind mounts without nosuid ............ No"
|
||||
else
|
||||
print_list "Writable bind mounts without nosuid ............ Yes" | sed -${E} "s,Yes,${SED_RED},"
|
||||
echo "$CT_RW_bind_mounts_matches" | sed -${E} "s,/proc/self/mountinfo,${SED_GREEN},"
|
||||
echo ""
|
||||
if [ "$(id -u 2>/dev/null)" = "0" ]; then
|
||||
print_list "Note"; echo ": You are root inside a container and there are writable bind mounts without nosuid." | sed -${E} "s,.*,${SED_RED},"
|
||||
echo " If the path is shared with the host and executable there, you may plant a SUID binary (e.g., copy /bin/bash and chmod 6777)"
|
||||
echo " and execute it from the host to obtain root. Ensure proper authorization before testing."
|
||||
else
|
||||
print_list "Note"; echo ": Current user is not root; if you obtain container root, these mounts may enable host escalation via SUID planting." | sed -${E} "s,.*,${SED_RED},"
|
||||
fi
|
||||
if [ -n "$CT_RW_bind_mounts_matches" ]; then
|
||||
printf '%s\n' "$CT_RW_bind_mounts_matches"
|
||||
elif [ -r /proc/self/mountinfo ] || [ -r /proc/mounts ]; then
|
||||
echo " No ordinary writable mounts observed; this does not rule out host shares."
|
||||
fi
|
||||
ct_rw_uid_note /proc/self/uid_map "$(id -u 2>/dev/null)"
|
||||
echo " nosuid/noexec describe this mount view only; host mount options may differ."
|
||||
echo " Verify host path identity, ownership, mount options, security policy and host access separately."
|
||||
echo ""
|
||||
fi
|
||||
@@ -1,23 +1,70 @@
|
||||
# Title: Processes & Cron & Services & Timers - Services and Service Files
|
||||
# ID: PR_Services
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 16-09-2026
|
||||
# Description: Services and service files analysis with privilege escalation vectors, including replaceable root-service executables.
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Services and service files analysis with privilege escalation vectors, including replaceable executables, writable root-service drop-ins, and root TFTP upload scope.
|
||||
# License: GNU GPL
|
||||
# Version: 1.3
|
||||
# Version: 1.5
|
||||
# Mitre: T1543.002,T1574.010,T1007
|
||||
# Functions Used: checkSystemdWritableExecPaths, echo_not_found, print_2title, print_info, print_3title
|
||||
# Functions Used: checkSystemdWritableExecPaths, checkSystemdWritableDropins, echo_not_found, print_2title, print_info, print_3title
|
||||
# Global Variables: $EXTRA_CHECKS, $IAMROOT, $SEARCH_IN_FOLDER, $TIMEOUT, $WRITABLESYSTEMDPATH
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $service_unit, $service_path, $service_content, $finding, $findings, $service_file, $exec_path, $exec_paths, $service, $line, $target_file, $target_exec, $relpath1, $relpath2
|
||||
# Generated Global Variables: $service_unit, $service_path, $service_content, $finding, $findings, $service_file, $exec_path, $exec_paths, $service, $line, $target_file, $target_exec, $relpath1, $relpath2, $conf, $bytes
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
# Read only the small, conventional tftpd-hpa settings file. Configuration is a
|
||||
# review cue: the running daemon, network access and effective arguments may differ.
|
||||
check_tftpd_hpa_root_upload_scope() (
|
||||
conf="${1:-/etc/default/tftpd-hpa}"
|
||||
if [ "$conf" = /etc/default/tftpd-hpa ]; then
|
||||
[ ! -L /etc ] && [ ! -L /etc/default ] || return 0
|
||||
fi
|
||||
[ -f "$conf" ] && [ ! -L "$conf" ] && [ -r "$conf" ] || return 0
|
||||
bytes=$(stat -c %s "$conf" 2>/dev/null) ||
|
||||
bytes=$(stat -f %z "$conf" 2>/dev/null) || return 0
|
||||
case "$bytes" in ''|*[!0-9]*) return 0 ;; esac
|
||||
[ "$bytes" -le 4096 ] || return 0
|
||||
|
||||
if awk '
|
||||
function setting(line) {
|
||||
sub(/^[^=]*=[[:space:]]*/, "", line)
|
||||
sub(/[[:space:]]*$/, "", line)
|
||||
if (length(line) >= 2 &&
|
||||
((substr(line, 1, 1) == "\"" && substr(line, length(line), 1) == "\"") ||
|
||||
(substr(line, 1, 1) == "\047" && substr(line, length(line), 1) == "\047")))
|
||||
line = substr(line, 2, length(line) - 2)
|
||||
return line
|
||||
}
|
||||
/^[[:space:]]*(#|$)/ { next }
|
||||
/^[[:space:]]*TFTP_USERNAME[[:space:]]*=/ { user = setting($0); next }
|
||||
/^[[:space:]]*TFTP_DIRECTORY[[:space:]]*=/ { directory = setting($0); next }
|
||||
/^[[:space:]]*TFTP_OPTIONS[[:space:]]*=/ { options = setting($0); next }
|
||||
END {
|
||||
if (user != "root" || directory != "/") exit 1
|
||||
n = split(options, words, /[[:space:]]+/)
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (words[i] == "--create" || words[i] == "-c") create = 1
|
||||
if (words[i] == "--secure" || words[i] == "-s") secure = 1
|
||||
if (words[i] == "--read-only" || words[i] == "-r") readonly = 1
|
||||
}
|
||||
exit !(create && secure && !readonly)
|
||||
}
|
||||
' "$conf" 2>/dev/null; then
|
||||
print_3title "Root TFTP file-creation scope (review candidate)" "T1543.002"
|
||||
echo "Configured tftpd-hpa root user, filesystem-root secure directory and --create: $conf"
|
||||
echo "Verify active effective service, listener reachability, TFTP access rules, and whether a privileged job consumes newly created files (for example APT hooks)."
|
||||
fi
|
||||
)
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
print_2title "Services and Service Files" "T1543.002,T1007"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/processes-crontab-systemd-dbus/process-enumeration-and-service-paths.html#follow-the-service-execution-chain"
|
||||
|
||||
check_tftpd_hpa_root_upload_scope
|
||||
|
||||
checkSystemdWritableExecPaths
|
||||
checkSystemdWritableDropins
|
||||
|
||||
# Function to check service content for privilege escalation vectors
|
||||
check_service_content() {
|
||||
|
||||
@@ -1,27 +1,117 @@
|
||||
# Title: System Information - Systemd
|
||||
# ID: SY_Systemd
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 2024-03-19
|
||||
# Description: Check for systemd vulnerabilities and misconfigurations that could lead to privilege escalation:
|
||||
# - Systemd version vulnerabilities (CVE-2021-4034, CVE-2021-33910, etc.)
|
||||
# Last Update: 2026-10-09
|
||||
# Description: Check systemd version, service misconfigurations, and readable service environment files:
|
||||
# - Systemd release (distribution package and patch status require separate verification)
|
||||
# - Services running as root that could be exploited
|
||||
# - Services with dangerous capabilities that could be abused
|
||||
# - Services with writable paths that could be used to inject malicious code
|
||||
# - Exploitation methods:
|
||||
# * Version exploits: Use known exploits for vulnerable systemd versions
|
||||
# * Version review: Compare the installed distribution package with vendor fixes
|
||||
# * Root services: Abuse services running as root to execute commands
|
||||
# * Capabilities: Abuse services with dangerous capabilities (CAP_SYS_ADMIN, etc.)
|
||||
# * Writable paths: Replace executables in writable paths to get code execution
|
||||
# License: GNU GPL
|
||||
# Version: 1.1
|
||||
# Mitre: T1543.002
|
||||
# Version: 1.2
|
||||
# Mitre: T1543.002,T1552.001
|
||||
# Functions Used: print_2title, print_list, echo_not_found
|
||||
# Global Variables: $SEARCH_IN_FOLDER, $Wfolders, $SED_RED, $SED_RED_YELLOW, $NC
|
||||
# Global Variables: $SEARCH_IN_FOLDER, $IAMROOT, $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, $exec_value, $cmd, $cmd_path, $svc_path_entry, $svc_writable_path
|
||||
# Generated Global Variables: $WRITABLESYSTEMDPATH, $line, $service, $file, $version, $user, $caps, $path, $path_line, $service_file, $exec_line, $exec_value, $cmd, $cmd_path, $svc_path_entry, $svc_writable_path, $running_services, $env_file_findings, $env_file_path, $env_file_size, $env_key_names, $env_unit_size
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
# Inspect only literal EnvironmentFile paths in a service unit. This is an
|
||||
# indicator of readable credential-like assignments, not evidence that a key
|
||||
# is used for authentication or provides privilege escalation.
|
||||
systemd_envfile_candidates() {
|
||||
[ -f "$1" ] && [ -r "$1" ] || return
|
||||
local env_unit_size
|
||||
env_unit_size=$(stat -c %s "$1" 2>/dev/null) ||
|
||||
env_unit_size=$(stat -f %z "$1" 2>/dev/null) || return
|
||||
case "$env_unit_size" in ''|*[!0-9]*) return ;; esac
|
||||
[ "$env_unit_size" -le 65536 ] || return
|
||||
awk '
|
||||
function add_path(value, path, quoted, optional) {
|
||||
sub(/^[[:space:]]*/, "", value)
|
||||
sub(/[[:space:]]*$/, "", value)
|
||||
if (value == "") { count = 0; return }
|
||||
optional = 0
|
||||
if (substr(value, 1, 1) == "-") { value = substr(value, 2); optional = 1 }
|
||||
quoted = 0
|
||||
if ((substr(value, 1, 1) == "\"" && substr(value, length(value), 1) == "\"") ||
|
||||
(substr(value, 1, 1) == sprintf("%c", 39) && substr(value, length(value), 1) == sprintf("%c", 39))) {
|
||||
quoted = 1
|
||||
value = substr(value, 2, length(value) - 2)
|
||||
}
|
||||
if (!optional && substr(value, 1, 1) == "-") value = substr(value, 2)
|
||||
# Skip globs, escapes, unquoted whitespace, and relative paths.
|
||||
if (!quoted && value ~ /[[:space:]]/) return
|
||||
if (value !~ /^\// || value ~ /[*?\[\]{}\\%$"\047]/ || value ~ /[\r\n]/) return
|
||||
for (path = 1; path <= count; path++) if (paths[path] == value) return
|
||||
paths[++count] = value
|
||||
}
|
||||
/^[[:space:]]*\[/ {
|
||||
section = $0
|
||||
sub(/^[[:space:]]*/, "", section)
|
||||
sub(/[[:space:]]*$/, "", section)
|
||||
next
|
||||
}
|
||||
section == "[Service]" && /^[[:space:]]*EnvironmentFile[[:space:]]*=/ {
|
||||
value = $0
|
||||
sub(/^[[:space:]]*EnvironmentFile[[:space:]]*=/, "", value)
|
||||
add_path(value)
|
||||
}
|
||||
END { for (i = 1; i <= count && i <= 4; i++) print paths[i] }
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
systemd_envfile_key_names() {
|
||||
awk '
|
||||
/^[[:space:]]*[#;]/ { next }
|
||||
/^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=/ {
|
||||
line = $0
|
||||
sub(/=.*/, "", line)
|
||||
gsub(/[[:space:]]/, "", line)
|
||||
value = $0
|
||||
sub(/^[^=]*=[[:space:]]*/, "", value)
|
||||
if (value == "" || value == "\"\"" || value == sprintf("%c%c", 39, 39)) next
|
||||
upper = toupper(line)
|
||||
if (upper !~ /(^|_)(SECRET|TOKEN|PASSWORD|PASSWD|APIKEY|KEY)($|_)/) next
|
||||
if (seen[line]++) next
|
||||
if (found >= 20) exit
|
||||
if (found++) printf ","
|
||||
printf "%s", line
|
||||
}
|
||||
END { if (found) printf "\n" }
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
systemd_envfile_active_units() {
|
||||
awk '
|
||||
{
|
||||
unit = $1
|
||||
if (unit == "●") unit = $2
|
||||
if (unit !~ /^[A-Za-z0-9_.@-]+[.]service$/ || unit ~ /[.][.]/) next
|
||||
print unit
|
||||
if (++count >= 200) exit
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
systemd_envfile_findings_for_unit() {
|
||||
systemd_envfile_candidates "$1" | while IFS= read -r env_file_path; do
|
||||
[ -f "$env_file_path" ] && [ -r "$env_file_path" ] || continue
|
||||
env_file_size=$(stat -c %s "$env_file_path" 2>/dev/null) ||
|
||||
env_file_size=$(stat -f %z "$env_file_path" 2>/dev/null) || continue
|
||||
case "$env_file_size" in ''|*[!0-9]*) continue ;; esac
|
||||
[ "$env_file_size" -le 65536 ] || continue
|
||||
env_key_names=$(systemd_envfile_key_names "$env_file_path")
|
||||
[ "$env_key_names" ] && printf '%s: %s\n' "$env_file_path" "$env_key_names"
|
||||
done
|
||||
}
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
print_2title "Systemd Information" "T1543.002"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/linux-basics/linux-privilege-escalation/index.html#systemd-path---relative-paths"
|
||||
@@ -44,7 +134,7 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
get_service_file() {
|
||||
local service="$1"
|
||||
local file=""
|
||||
for path in "/etc/systemd/system/$service" "/lib/systemd/system/$service"; do
|
||||
for path in "/etc/systemd/system/$service" "/run/systemd/system/$service" "/usr/lib/systemd/system/$service" "/lib/systemd/system/$service"; do
|
||||
if [ -f "$path" ]; then
|
||||
file="$path"
|
||||
break
|
||||
@@ -60,28 +150,26 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
return $?
|
||||
}
|
||||
|
||||
# Check systemd version and known vulnerabilities
|
||||
print_list "Systemd version and vulnerabilities? .............. "$NC
|
||||
# Reuse one active-unit listing without changing the scope of older checks.
|
||||
running_services=""
|
||||
if check_systemctl; then
|
||||
version=$(systemctl --version | head -n 1 | grep -oE '([0-9]+(\.[0-9]+)+)')
|
||||
running_services=$(list_running_services)
|
||||
fi
|
||||
|
||||
# The upstream release alone does not identify distribution backports or
|
||||
# mitigations. PwnKit is a polkit/pkexec issue, not a systemd version issue.
|
||||
print_list "Systemd release (verify package fixes)? .......... "$NC
|
||||
if check_systemctl; then
|
||||
version=$(systemctl --version 2>/dev/null | awk 'NR == 1 && $1 == "systemd" && $2 ~ /^[0-9]+$/ { print $2 }')
|
||||
if [ -n "$version" ]; then
|
||||
echo "$version" | sed -${E} "s,([0-9]+(\.[0-9]+)+),${SED_RED},g"
|
||||
# Check for known vulnerable versions
|
||||
case "$version" in
|
||||
"2.3"[0-4]|"2.3"[0-4]"."*)
|
||||
echo " └─ Vulnerable to CVE-2021-4034 (Polkit)" | sed -${E} "s,.*,${SED_RED},g"
|
||||
;;
|
||||
"2.4"[0-9]|"2.4"[0-9]"."*)
|
||||
echo " └─ Vulnerable to CVE-2021-33910 (systemd-tmpfiles)" | sed -${E} "s,.*,${SED_RED},g"
|
||||
;;
|
||||
esac
|
||||
printf '%s\n' "$version"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for systemd services running as root
|
||||
print_list "Services running as root? ..... "$NC
|
||||
if check_systemctl; then
|
||||
list_running_services |
|
||||
printf '%s\n' "$running_services" |
|
||||
grep -E "root|0:0" |
|
||||
while read -r line; do
|
||||
service=$(echo "$line" | awk '{print $1}')
|
||||
@@ -96,7 +184,7 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
# Check for systemd services with dangerous capabilities
|
||||
print_list "Running services with dangerous capabilities? ... "$NC
|
||||
if check_systemctl; then
|
||||
list_running_services |
|
||||
printf '%s\n' "$running_services" |
|
||||
grep -E "\.service" |
|
||||
while read -r line; do
|
||||
service=$(echo "$line" | awk '{print $1}')
|
||||
@@ -113,7 +201,7 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
# Check for systemd services with writable paths
|
||||
print_list "Services with writable paths? . "$NC
|
||||
if check_systemctl; then
|
||||
list_running_services |
|
||||
printf '%s\n' "$running_services" |
|
||||
grep -E "\.service" |
|
||||
while read -r line; do
|
||||
service=$(echo "$line" | awk '{print $1}')
|
||||
@@ -162,6 +250,23 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
|
||||
echo ""
|
||||
|
||||
# Unit files may point at files outside the usual .env naming convention.
|
||||
# Keep this passive and print key names only, never environment values.
|
||||
if ! [ "$IAMROOT" ]; then
|
||||
env_file_findings=$(
|
||||
printf '%s\n' "$running_services" | systemd_envfile_active_units | while IFS= read -r service; do
|
||||
service_file=$(get_service_file "$service")
|
||||
[ "$service_file" ] || continue
|
||||
systemd_envfile_findings_for_unit "$service_file"
|
||||
done | sort -u
|
||||
)
|
||||
if [ "$env_file_findings" ]; then
|
||||
print_2title "Readable systemd EnvironmentFile credential-like keys (possible exposure)" "T1552.001"
|
||||
printf '%s\n' "$env_file_findings"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
print_2title "Systemd PATH" "T1543.002"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/linux-basics/linux-privilege-escalation/index.html#systemd-path---relative-paths"
|
||||
if check_systemctl; then
|
||||
|
||||
+92
-42
@@ -12,9 +12,9 @@
|
||||
# Version: 1.1
|
||||
# Mitre: T1571,T1049
|
||||
# Functions Used: print_2title, print_info
|
||||
# Global Variables: $EXTRA_CHECKS, $groupsB, $groupsVB, $IAMROOT, $idB, $knw_grps, $knw_usrs, $nosh_usrs, $SEARCH_IN_FOLDER, $sh_usrs, $USER, $SED_RED, $SED_GREEN, $SED_RED_YELLOW, $NC, $RED
|
||||
# Global Variables: $EXTRA_CHECKS, $groupsB, $groupsVB, $IAMROOT, $idB, $knw_grps, $knw_usrs, $nosh_usrs, $SEARCH_IN_FOLDER, $sh_usrs, $USER, $SED_RED, $SED_GREEN
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $unix_scks_list, $unix_scks_list2, $perms, $owner, $owner_info, $response, $socket, $cmd, $mode, $group
|
||||
# Generated Global Variables: $unix_scks_list, $unix_scks_list2, $ss_metadata, $socket_listing, $socket_info, $perms, $owner, $owner_info, $owner_uid, $response, $socket, $cmd, $mode, $group, $read_access, $write_access, $listener_info, $listener_pid, $listener_uid, $listener_is_active
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
@@ -27,15 +27,18 @@ if ! [ "$IAMROOT" ]; then
|
||||
# Function to get socket permissions
|
||||
get_socket_perms() {
|
||||
local socket="$1"
|
||||
local mode="$2"
|
||||
local read_access="$3"
|
||||
local write_access="$4"
|
||||
local perms=""
|
||||
|
||||
# Check read permission
|
||||
if [ -r "$socket" ]; then
|
||||
if [ "$read_access" = "yes" ]; then
|
||||
perms="Read "
|
||||
fi
|
||||
|
||||
# Check write permission
|
||||
if [ -w "$socket" ]; then
|
||||
if [ "$write_access" = "yes" ]; then
|
||||
perms="${perms}Write "
|
||||
fi
|
||||
|
||||
@@ -44,8 +47,6 @@ if ! [ "$IAMROOT" ]; then
|
||||
perms="${perms}Execute "
|
||||
fi
|
||||
|
||||
# Check socket mode
|
||||
local mode=$(stat -c "%a" "$socket" 2>/dev/null)
|
||||
if [ "$mode" = "777" ] || [ "$mode" = "666" ]; then
|
||||
perms="${perms}(Weak Permissions: $mode) "
|
||||
fi
|
||||
@@ -87,24 +88,33 @@ if ! [ "$IAMROOT" ]; then
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get socket owner and group
|
||||
get_socket_owner() {
|
||||
local socket="$1"
|
||||
local owner=""
|
||||
local group=""
|
||||
|
||||
if [ -e "$socket" ]; then
|
||||
owner=$(ls -l "$socket" 2>/dev/null | awk '{print $3}')
|
||||
group=$(ls -l "$socket" 2>/dev/null | awk '{print $4}')
|
||||
echo "$owner:$group"
|
||||
fi
|
||||
}
|
||||
|
||||
# Collect listening sockets using multiple methods
|
||||
unix_scks_list=""
|
||||
ss_metadata=""
|
||||
for cmd in "ss -xlp -H state listening" "ss -l -p -A 'unix'" "netstat -a -p --unix"; do
|
||||
if [ -z "$unix_scks_list" ]; then
|
||||
unix_scks_list=$($cmd 2>/dev/null | grep -Eo "/[a-zA-Z0-9\._/\-]+" | grep -v " " | sort -u)
|
||||
case "$cmd" in
|
||||
ss\ *)
|
||||
socket_listing=$($cmd 2>/dev/null)
|
||||
unix_scks_list=$(printf '%s\n' "$socket_listing" | grep -Eo "/[a-zA-Z0-9\._/\-]+" | grep -v " " | sort -u)
|
||||
if [ -n "$unix_scks_list" ]; then
|
||||
# Reuse the ss output already collected; never query processes per socket.
|
||||
ss_metadata=$(printf '%s\n' "$socket_listing" | awk '
|
||||
match($0, /\/[a-zA-Z0-9._\/-]+/) {
|
||||
path = substr($0, RSTART, RLENGTH)
|
||||
pid = ""; uid = ""
|
||||
if (match($0, /pid=[0-9]+/)) pid = substr($0, RSTART + 4, RLENGTH - 4)
|
||||
if (match($0, /uid[:=][0-9]+/)) uid = substr($0, RSTART + 4, RLENGTH - 4)
|
||||
print path "|" pid "|" uid
|
||||
}')
|
||||
ss_metadata="
|
||||
$ss_metadata"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
unix_scks_list=$($cmd 2>/dev/null | grep -Eo "/[a-zA-Z0-9\._/\-]+" | grep -v " " | sort -u)
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -124,29 +134,69 @@ if ! [ "$IAMROOT" ]; then
|
||||
(printf "%s\n" "$unix_scks_list" && printf "%s\n" "$unix_scks_list2") | sort -u | while read -r socket; do
|
||||
if [ -n "$socket" ] && [ -e "$socket" ]; then
|
||||
# Get socket information
|
||||
perms=$(get_socket_perms "$socket")
|
||||
perms=$(check_socket_connectivity "$socket" "$perms")
|
||||
owner_info=$(get_socket_owner "$socket")
|
||||
|
||||
# Print socket information
|
||||
if [ -z "$perms" ]; then
|
||||
echo "$socket" | sed -${E} "s,$socket,${SED_GREEN},g"
|
||||
socket_info=$(stat -c '%a|%U|%G|%u' "$socket" 2>/dev/null)
|
||||
[ -n "$socket_info" ] || socket_info=$(stat -f '%Lp|%Su|%Sg|%u' "$socket" 2>/dev/null)
|
||||
if [ -n "$socket_info" ]; then
|
||||
mode=${socket_info%%|*}
|
||||
socket_info=${socket_info#*|}
|
||||
owner=${socket_info%%|*}
|
||||
socket_info=${socket_info#*|}
|
||||
group=${socket_info%%|*}
|
||||
owner_uid=${socket_info#*|}
|
||||
else
|
||||
echo "$socket" | sed -${E} "s,$socket,${SED_RED},g"
|
||||
echo " └─(${RED}${perms}${NC})" | sed -${E} "s,Cannot Connect,${SED_GREEN},g"
|
||||
|
||||
# Analyze socket protocol if we can connect
|
||||
if echo "$perms" | grep -q "Can Connect"; then
|
||||
analyze_socket_protocol "$socket" "$owner_info"
|
||||
fi
|
||||
|
||||
# Highlight dangerous ownership
|
||||
if echo "$owner_info" | grep -q "root"; then
|
||||
echo " └─(${RED}Owned by root${NC})"
|
||||
if echo "$perms" | grep -q "Write"; then
|
||||
echo " └─High risk: root-owned and writable Unix socket" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
|
||||
fi
|
||||
fi
|
||||
mode="unknown"
|
||||
owner="unknown"
|
||||
group="unknown"
|
||||
owner_uid=""
|
||||
fi
|
||||
owner_info="$owner:$group"
|
||||
read_access="no"
|
||||
write_access="no"
|
||||
[ -r "$socket" ] && read_access="yes"
|
||||
[ -w "$socket" ] && write_access="yes"
|
||||
perms=$(get_socket_perms "$socket" "$mode" "$read_access" "$write_access")
|
||||
perms=$(check_socket_connectivity "$socket" "$perms")
|
||||
|
||||
echo "Path: $socket"
|
||||
echo " └─ Mode: $mode; Owner/Group: $owner_info"
|
||||
echo " └─ Current user access: read=$read_access, write=$write_access"
|
||||
if [ -n "$perms" ]; then
|
||||
echo " └─ $perms" | sed -${E} "s,Cannot Connect,${SED_GREEN},g"
|
||||
fi
|
||||
|
||||
# A discovered socket file can remain after its listener exits.
|
||||
listener_is_active=no
|
||||
case "
|
||||
$unix_scks_list
|
||||
" in
|
||||
*"
|
||||
$socket
|
||||
"*) listener_is_active=yes ;;
|
||||
esac
|
||||
|
||||
# Only display listener details that came with the existing ss listing.
|
||||
case "$ss_metadata" in
|
||||
*"
|
||||
$socket|"*)
|
||||
listener_info=${ss_metadata#*"
|
||||
$socket|"}
|
||||
listener_info=${listener_info%%"
|
||||
"*}
|
||||
listener_pid=${listener_info%%|*}
|
||||
listener_uid=${listener_info#*|}
|
||||
[ -n "$listener_pid" ] && echo " └─ ss listener PID: $listener_pid"
|
||||
[ -n "$listener_uid" ] && echo " └─ ss socket UID: $listener_uid"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Access alone does not establish what a service executes.
|
||||
if [ "$listener_is_active" = "yes" ] && [ "$owner_uid" = "0" ] && [ "$write_access" = "yes" ]; then
|
||||
echo " └─ Review lead: current user can write to a root-owned Unix socket; inspect service behavior and privileges."
|
||||
fi
|
||||
|
||||
# Preserve the optional protocol check when EXTRA_CHECKS found connectivity.
|
||||
if echo "$perms" | grep -q "Can Connect"; then
|
||||
analyze_socket_protocol "$socket" "$owner_info"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
+71
-18
@@ -10,10 +10,49 @@
|
||||
# Functions Used: print_2title, print_3title, echo_not_found
|
||||
# Global Variables:
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $rfile, $perms, $owner, $g, $o, $any_rhosts, $shown, $f, $p
|
||||
# Generated Global Variables: $rfile, $perms, $owner, $size, $metadata, $metadata_rest, $any_rhosts, $rhosts_scanned, $shown, $f, $p
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
rcommands_trust_metadata() {
|
||||
# GNU and BSD/OpenBSD stat use different format flags.
|
||||
stat -c '%a %U %s' "$1" 2>/dev/null || stat -f '%Lp %Su %z' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
rcommands_trust_group_other_writable() {
|
||||
case "$1" in
|
||||
[0-7][0-7][0-7]|[0-7][0-7][0-7][0-7])
|
||||
[ "$((0${1} & 0022))" -ne 0 ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
rcommands_trust_entries() {
|
||||
# Read at most 64 KiB and stop after one extra non-comment entry.
|
||||
dd if="$1" bs=65536 count=1 2>/dev/null | LC_ALL=C awk -v size="$2" '
|
||||
/^[[:space:]]*(#|$)/ { next }
|
||||
{
|
||||
entries++
|
||||
if (entries > 64) {
|
||||
print " [!] Trust entries truncated after 64 lines"
|
||||
exit
|
||||
}
|
||||
if (length($0) > 512) print " " substr($0, 1, 512) " ... [line shortened]"
|
||||
else print " " $0
|
||||
if ($1 !~ /^[-+@]/ && $1 !~ /:/ && $1 ~ /[[:alpha:]]/) hostname = 1
|
||||
if ($2 == "+") any_user = 1
|
||||
if ($1 == "+" || $2 == "+") wildcard = 1
|
||||
}
|
||||
END {
|
||||
if (size == "" || size + 0 > 65536) print " [!] Trust-file input limited to first 64 KiB"
|
||||
if (hostname) print " [!] Hostname-based trust candidate: review control of DNS and local name mappings"
|
||||
if (any_user) print " [!] Remote-user + may broaden trust (service and PAM policy still apply)"
|
||||
if (wildcard) print " [!] Wildcard '\''+'\'' trust found"
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
print_2title "Legacy r-commands (rsh/rlogin/rexec) and host-based trust" "T1021.004"
|
||||
echo ""
|
||||
@@ -73,33 +112,48 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
print_3title "/etc/hosts.equiv and /etc/shosts.equiv" "T1021.004"
|
||||
for f in /etc/hosts.equiv /etc/shosts.equiv; do
|
||||
if [ -f "$f" ]; then
|
||||
perms=$(stat -c %a "$f" 2>/dev/null)
|
||||
owner=$(stat -c %U "$f" 2>/dev/null)
|
||||
echo " $f (perm $perms, owner $owner)"
|
||||
# Print non-comment lines
|
||||
awk 'NF && $0 !~ /^\s*#/ {print " " $0}' "$f" 2>/dev/null
|
||||
if grep -qEv '^\s*#|^\s*$' "$f" 2>/dev/null; then
|
||||
if grep -qE '(^|\s)\+' "$f" 2>/dev/null; then
|
||||
echo " [!] Wildcard '+' trust found"
|
||||
fi
|
||||
metadata=$(rcommands_trust_metadata "$f")
|
||||
if [ -n "$metadata" ]; then
|
||||
perms=${metadata%% *}
|
||||
metadata_rest=${metadata#* }
|
||||
owner=${metadata_rest%% *}
|
||||
size=${metadata_rest##* }
|
||||
echo " $f (perm $perms, owner $owner)"
|
||||
else
|
||||
size=
|
||||
echo " $f (metadata unavailable)"
|
||||
fi
|
||||
rcommands_trust_entries "$f" "$size"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
print_3title "Per-user .rhosts files" "T1021.004"
|
||||
any_rhosts=false
|
||||
rhosts_scanned=0
|
||||
for rfile in /root/.rhosts /home/*/.rhosts; do
|
||||
if [ -f "$rfile" ]; then
|
||||
rhosts_scanned=$((rhosts_scanned + 1))
|
||||
if [ "$rhosts_scanned" -gt 64 ]; then
|
||||
echo " [!] .rhosts discovery limited to first 64 files; coverage is partial"
|
||||
break
|
||||
fi
|
||||
any_rhosts=true
|
||||
perms=$(stat -c %a "$rfile" 2>/dev/null)
|
||||
owner=$(stat -c %U "$rfile" 2>/dev/null)
|
||||
echo " $rfile (perm $perms, owner $owner)"
|
||||
awk 'NF && $0 !~ /^\s*#/ {print " " $0}' "$rfile" 2>/dev/null
|
||||
metadata=$(rcommands_trust_metadata "$rfile")
|
||||
if [ -n "$metadata" ]; then
|
||||
perms=${metadata%% *}
|
||||
metadata_rest=${metadata#* }
|
||||
owner=${metadata_rest%% *}
|
||||
size=${metadata_rest##* }
|
||||
echo " $rfile (perm $perms, owner $owner)"
|
||||
else
|
||||
perms=
|
||||
size=
|
||||
echo " $rfile (metadata unavailable)"
|
||||
fi
|
||||
rcommands_trust_entries "$rfile" "$size"
|
||||
# Warn on insecure perms (group/other write)
|
||||
g=$(printf "%s" "$perms" | cut -c2)
|
||||
o=$(printf "%s" "$perms" | cut -c3)
|
||||
if [ "${g:-0}" -ge 2 ] || [ "${o:-0}" -ge 2 ]; then
|
||||
if rcommands_trust_group_other_writable "$perms"; then
|
||||
echo " [!] Insecure permissions (group/other write)"
|
||||
fi
|
||||
fi
|
||||
@@ -136,4 +190,3 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
|
||||
echo ""
|
||||
fi
|
||||
|
||||
+125
-95
@@ -1,127 +1,157 @@
|
||||
# Title: Processes & Cron & Services & Timers - Crontab UI (root) Misconfiguration
|
||||
# ID: PR_Crontab_UI_misconfig
|
||||
# Author: HT Bot
|
||||
# Last Update: 2025-09-13
|
||||
# Description: Detect Crontab UI service and risky configurations that can lead to privesc:
|
||||
# - Root-run Crontab UI exposed on localhost
|
||||
# - Basic-Auth credentials in systemd Environment= (BASIC_AUTH_USER/PWD)
|
||||
# - Cron DB path (CRON_DB_PATH) and weak permissions / embedded secrets in jobs
|
||||
# Last Update: 2026-10-09
|
||||
# Description: Correlate Crontab UI service/process evidence with a bounded, redacted job-store check.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1053.003
|
||||
# Functions Used: print_2title, print_info, print_list, echo_not_found
|
||||
# Global Variables: $SEARCH_IN_FOLDER, $SED_RED, $SED_RED_YELLOW, $NC
|
||||
# Global Variables: $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $svc, $state, $user, $envvals, $port, $dbpath, $dbfile, $candidates, $procs, $perms, $basic_user, $basic_pwd, $uprint, $pprint, $dir, $found
|
||||
# Generated Global Variables: $candidates, $dir, $found, $unit_file, $svc, $state, $loadstate, $user, $envvals, $dbpath, $dbfile, $dbsize, $dbsummary, $jobcount, $credcount, $processes, $has_service, $has_process, $has_binary
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
print_2title "Crontab UI (root) misconfiguration checks" "T1053.003"
|
||||
print_2title "Crontab UI scheduler checks" "T1053.003"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/processes-crontab-systemd-dbus/cron-and-systemd-timers.html#enumerate-schedules"
|
||||
|
||||
# Collect candidate services referencing crontab-ui
|
||||
candidates=""
|
||||
has_service=""
|
||||
has_process=""
|
||||
has_binary=""
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
candidates=$(systemctl list-units --type=service --all 2>/dev/null | awk '{print $1}' | grep -Ei '^crontab-ui\.service$' 2>/dev/null)
|
||||
candidates=$(systemctl list-units --type=service --all --no-legend 2>/dev/null | awk '$1 ~ /[Cc]rontab[-_][Uu][Ii].*\.service$/ { print $1 }')
|
||||
found=$(systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '$1 ~ /[Cc]rontab[-_][Uu][Ii].*\.service$/ { print $1 }')
|
||||
candidates=$(printf '%s\n%s\n' "$candidates" "$found" | sed '/^$/d' | sort -u)
|
||||
fi
|
||||
|
||||
# Fallback: grep service files for ExecStart containing crontab-ui
|
||||
if [ -z "$candidates" ]; then
|
||||
for dir in /etc/systemd/system /lib/systemd/system; do
|
||||
[ -d "$dir" ] || continue
|
||||
found=$(grep -RIl "^Exec(Start|StartPre|StartPost)=.*crontab-ui" "$dir" 2>/dev/null | xargs -r -I{} basename {} 2>/dev/null)
|
||||
if [ -n "$found" ]; then
|
||||
candidates=$(printf "%s\n%s" "$candidates" "$found" | sort -u)
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# Search only service units; grep -E is required for the alternation below.
|
||||
for dir in /etc/systemd/system /lib/systemd/system /usr/lib/systemd/system; do
|
||||
[ -d "$dir" ] || continue
|
||||
found=$(find "$dir" -type f -name '*.service' -exec grep -Eil -e '^[[:space:]]*ExecStart(Pre|Post)?[[:space:]]*=[^#]*crontab-ui' {} + 2>/dev/null | while IFS= read -r unit_file; do basename "$unit_file"; done)
|
||||
candidates=$(printf '%s\n%s\n' "$candidates" "$found" | sed '/^$/d' | sort -u)
|
||||
done
|
||||
[ -n "$candidates" ] && has_service=1
|
||||
|
||||
# Also flag if the binary exists or a process seems to be running
|
||||
if command -v crontab-ui >/dev/null 2>&1; then
|
||||
print_list "crontab-ui binary found at: $(command -v crontab-ui)"$NC
|
||||
has_binary=1
|
||||
print_list "crontab-ui binary found"
|
||||
else
|
||||
echo_not_found "crontab-ui"
|
||||
fi
|
||||
|
||||
procs=$(ps aux 2>/dev/null | grep -E "(crontab-ui|node .*crontab-ui)" | grep -v grep)
|
||||
if [ -n "$procs" ]; then
|
||||
print_list "Processes matching crontab-ui? ..................... "$NC
|
||||
printf "%s\n" "$procs"
|
||||
echo ""
|
||||
# Use the executable name, not arbitrary arguments containing 'crontab-ui'.
|
||||
# Never print full process arguments: they may contain credentials.
|
||||
processes=$(ps -eo user=,comm=,args= 2>/dev/null | awk '
|
||||
{
|
||||
user=$1; comm=$2; exe=$3; argpath=$4; arg=$4
|
||||
sub(/^.*\//, "", comm)
|
||||
sub(/^.*\//, "", exe)
|
||||
sub(/^.*\//, "", arg)
|
||||
if (comm == "crontab-ui" || (comm == "node" && exe == "node" && (arg == "crontab-ui" || argpath ~ /(^|\/)crontab-ui(\/|$)/)))
|
||||
print user " crontab-ui"
|
||||
}' | sort -u)
|
||||
if [ -n "$processes" ]; then
|
||||
has_process=1
|
||||
printf 'Process evidence (owner and executable only): %s\n' "$processes"
|
||||
fi
|
||||
|
||||
# If no candidates detected, exit quietly
|
||||
if [ "$candidates" ]; then
|
||||
|
||||
# Iterate candidates and extract interesting data
|
||||
printf "%s\n" "$candidates" | while read -r svc; do
|
||||
if [ -n "$candidates" ]; then
|
||||
printf '%s\n' "$candidates" | head -n 20 | while IFS= read -r svc; do
|
||||
[ -n "$svc" ] || continue
|
||||
# Ensure suffix .service if missing
|
||||
case "$svc" in
|
||||
*.service) : ;;
|
||||
*) svc="$svc.service" ;;
|
||||
esac
|
||||
|
||||
state=""
|
||||
user=""
|
||||
state=unknown
|
||||
loadstate=unknown
|
||||
user=unknown
|
||||
envvals=""
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
state=$(systemctl is-active "$svc" 2>/dev/null)
|
||||
user=$(systemctl show "$svc" -p User 2>/dev/null | cut -d= -f2)
|
||||
state=$(systemctl is-active "$svc" 2>/dev/null) || state=unknown
|
||||
loadstate=$(systemctl show "$svc" -p LoadState 2>/dev/null | cut -d= -f2-)
|
||||
user=$(systemctl show "$svc" -p User 2>/dev/null | cut -d= -f2-)
|
||||
envvals=$(systemctl show "$svc" -p Environment 2>/dev/null | cut -d= -f2-)
|
||||
fi
|
||||
|
||||
[ -z "$state" ] && state="unknown"
|
||||
[ -z "$user" ] && user="unknown"
|
||||
|
||||
echo "Service: $svc (state: $state, User: $user)" | sed -${E} "s,root,${SED_RED},g"
|
||||
|
||||
# Read Environment from systemd (works even if file unreadable in many setups)
|
||||
envvals=$(systemctl show "$svc" -p Environment 2>/dev/null | cut -d= -f2-)
|
||||
if [ -n "$envvals" ]; then
|
||||
basic_user=$(printf "%s\n" "$envvals" | tr ' ' '\n' | grep -E '^BASIC_AUTH_USER=' | head -n1 | cut -d= -f2-)
|
||||
basic_pwd=$(printf "%s\n" "$envvals" | tr ' ' '\n' | grep -E '^BASIC_AUTH_PWD=' | head -n1 | cut -d= -f2-)
|
||||
dbpath=$(printf "%s\n" "$envvals" | tr ' ' '\n' | grep -E '^CRON_DB_PATH=' | head -n1 | cut -d= -f2-)
|
||||
port=$(printf "%s\n" "$envvals" | tr ' ' '\n' | grep -E '^PORT=' | head -n1 | cut -d= -f2-)
|
||||
|
||||
if [ -n "$basic_user" ] || [ -n "$basic_pwd" ]; then
|
||||
uprint="$basic_user"
|
||||
pprint="$basic_pwd"
|
||||
[ -n "$basic_pwd" ] && pprint="$basic_pwd"
|
||||
echo " └─ Basic-Auth credentials in Environment: user='${uprint}' pwd='${pprint}'" | sed -${E} "s,pwd='[^']*',${SED_RED_YELLOW},g"
|
||||
fi
|
||||
|
||||
if [ -n "$dbpath" ]; then
|
||||
echo " └─ CRON_DB_PATH: $dbpath"
|
||||
fi
|
||||
|
||||
# Check listener bound to localhost
|
||||
[ -z "$port" ] && port=8000
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
if ss -ltn 2>/dev/null | grep -qE "127\.0\.0\.1:${port}[[:space:]]"; then
|
||||
echo " └─ Listener detected on 127.0.0.1:${port} (likely Crontab UI)."
|
||||
fi
|
||||
else
|
||||
if netstat -tnl 2>/dev/null | grep -qE "127\.0\.0\.1:${port}[[:space:]]"; then
|
||||
echo " └─ Listener detected on 127.0.0.1:${port} (likely Crontab UI)."
|
||||
fi
|
||||
fi
|
||||
|
||||
# If we know DB path, try to read crontab.db for obvious secrets and check perms
|
||||
if [ -n "$dbpath" ] && [ -d "$dbpath" ] && [ -r "$dbpath" ]; then
|
||||
dbfile="$dbpath/crontab.db"
|
||||
if [ -f "$dbfile" ]; then
|
||||
perms=$(ls -ld "$dbpath" 2>/dev/null | awk '{print $1, $3, $4}')
|
||||
echo " └─ DB dir perms: $perms"
|
||||
if [ -w "$dbpath" ] || [ -w "$dbfile" ]; then
|
||||
echo " └─ Writable by current user -> potential job injection!" | sed -${E} "s,.*,${SED_RED},g"
|
||||
fi
|
||||
echo " └─ Inspecting $dbfile for embedded secrets in commands (zip -P / --password / pass/token/secret)..."
|
||||
grep -E "-P[[:space:]]+\S+|--password[[:space:]]+\S+|[Pp]ass(word)?|[Tt]oken|[Ss]ecret" "$dbfile" 2>/dev/null | head -n 20 | sed -${E} "s,(${SED_RED_YELLOW}),\1,g"
|
||||
fi
|
||||
fi
|
||||
[ -n "$state" ] || state=unknown
|
||||
if [ "$loadstate" = loaded ] && [ -z "$user" ]; then
|
||||
user='root (systemd default; configuration evidence)'
|
||||
elif [ -z "$user" ]; then
|
||||
user=unknown
|
||||
fi
|
||||
echo ""
|
||||
printf 'Service evidence: %s (state: %s, User: %s)\n' "$svc" "$state" "$user"
|
||||
if printf '%s\n' "$envvals" | grep -Eq '(^|[[:space:]])BASIC_AUTH_USER='; then
|
||||
printf ' BASIC_AUTH_USER is set (value redacted)\n'
|
||||
fi
|
||||
if printf '%s\n' "$envvals" | grep -Eq '(^|[[:space:]])BASIC_AUTH_PWD='; then
|
||||
printf ' BASIC_AUTH_PWD is set (value redacted)\n'
|
||||
fi
|
||||
printf '\n'
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# systemctl Environment may be hidden; the exact conventional store is still
|
||||
# useful when independent service, binary, or process evidence exists.
|
||||
if [ -n "$has_service$has_process$has_binary" ]; then
|
||||
dbpath=""
|
||||
if [ -n "$candidates" ] && command -v systemctl >/dev/null 2>&1; then
|
||||
svc=$(printf '%s\n' "$candidates" | head -n 1)
|
||||
envvals=$(systemctl show "$svc" -p Environment 2>/dev/null | cut -d= -f2-)
|
||||
dbpath=$(printf '%s\n' "$envvals" | tr ' ' '\n' | sed -n 's/^CRON_DB_PATH=//p' | head -n 1)
|
||||
fi
|
||||
if [ -n "$dbpath" ]; then
|
||||
case "$dbpath" in
|
||||
*/crontab.db) dbfile=$dbpath ;;
|
||||
*) dbfile=${dbpath%/}/crontab.db ;;
|
||||
esac
|
||||
else
|
||||
dbfile=/opt/crontabs/crontab.db
|
||||
fi
|
||||
|
||||
if [ -f "$dbfile" ]; then
|
||||
if [ ! -r "$dbfile" ]; then
|
||||
printf 'DB candidate: %s (unreadable; contents unknown)\n' "$dbfile"
|
||||
else
|
||||
dbsize=$(stat -L -c %s "$dbfile" 2>/dev/null) || dbsize=$(stat -L -f %z "$dbfile" 2>/dev/null)
|
||||
case "$dbsize" in
|
||||
''|*[!0-9]*) printf 'DB candidate: %s (size unknown; contents not inspected)\n' "$dbfile" ;;
|
||||
*)
|
||||
if [ "$dbsize" -gt 1048576 ]; then
|
||||
printf 'DB candidate: %s (readable, oversized: %s bytes; contents not inspected)\n' "$dbfile" "$dbsize"
|
||||
else
|
||||
printf 'DB candidate: %s (readable, %s bytes; writable by current user: ' "$dbfile" "$dbsize"
|
||||
if [ -w "$dbfile" ]; then printf 'yes)\n'; else printf 'no)\n'; fi
|
||||
dbsummary=$(LC_ALL=C awk '
|
||||
/^[[:space:]]*\{/ && /"command"[[:space:]]*:[[:space:]]*"/ {
|
||||
jobs++
|
||||
command=$0
|
||||
sub(/^.*"command"[[:space:]]*:[[:space:]]*"/, "", command)
|
||||
sub(/".*$/, "", command)
|
||||
if (command ~ /(^|[[:space:]])zip[[:space:]]+[^"]*[[:space:]]-P[[:space:]]+[^[:space:]"\\]+/)
|
||||
credentials++
|
||||
}
|
||||
END { printf "%d %d", jobs, credentials }
|
||||
' "$dbfile" 2>/dev/null)
|
||||
if [ -n "$dbsummary" ]; then
|
||||
jobcount=${dbsummary%% *}
|
||||
credcount=${dbsummary#* }
|
||||
if [ "$jobcount" -eq 0 ]; then
|
||||
printf ' Job store shape unrecognized; contents unknown\n'
|
||||
else
|
||||
printf ' Jobs with command fields: %s; inline zip -P credential candidates: %s (values redacted)\n' "$jobcount" "$credcount"
|
||||
fi
|
||||
else
|
||||
printf ' Job store inspection failed; contents unknown\n'
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
elif [ -e "$dbfile" ]; then
|
||||
printf 'DB candidate: %s (not a regular file; contents unknown)\n' "$dbfile"
|
||||
else
|
||||
printf 'DB candidate: %s (not found; contents unknown)\n' "$dbfile"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$has_service$has_process" ]; then
|
||||
printf 'Review existing Open ports output for a loopback listener; no service binding or credential reuse was verified.\n'
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,94 @@
|
||||
# Title: Processes & Cron & Services & Timers - Cross-user Xvfb framebuffer files
|
||||
# ID: PR_Xvfb_fbdir
|
||||
# Author: PEASS-ng contributors
|
||||
# Last Update: 08-10-2026
|
||||
# Description: Report readable framebuffer files from another user's running Xvfb without reading their contents
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1552.001
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $MACPEAS, $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $comm, $current_uid, $cwd, $fbdir, $found, $metadata, $mode, $owner_uid, $pid, $printed, $proc, $process_uid, $proc_root, $screen, $screens
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
lp_xvfb_fbdir_from_cmdline() {
|
||||
tr '\000' '\n' < "$1" 2>/dev/null | awk '
|
||||
NR == 1 {
|
||||
name = $0
|
||||
sub(/^.*\//, "", name)
|
||||
if (name != "Xvfb") exit
|
||||
}
|
||||
want_dir { if ($0 != "") print; exit }
|
||||
$0 == "-fbdir" { want_dir = 1 }
|
||||
'
|
||||
}
|
||||
|
||||
lp_xvfb_candidate_pids() {
|
||||
local proc_root="$1" proc comm found=0
|
||||
if [ "$proc_root" = /proc ] && command -v pgrep >/dev/null 2>&1; then
|
||||
pgrep -x Xvfb 2>/dev/null | head -n 8
|
||||
return
|
||||
fi
|
||||
# Fallback for systems without pgrep. Inspect only comm for other processes;
|
||||
# stop after eight actual Xvfb candidates, regardless of PID ordering.
|
||||
for proc in "$proc_root"/[0-9]*; do
|
||||
[ -r "$proc/comm" ] || continue
|
||||
IFS= read -r comm < "$proc/comm" || continue
|
||||
[ "$comm" = Xvfb ] || continue
|
||||
printf '%s\n' "${proc##*/}"
|
||||
found=$((found + 1))
|
||||
[ "$found" -lt 8 ] || break
|
||||
done
|
||||
}
|
||||
|
||||
lp_check_xvfb_fbdir() {
|
||||
local proc_root="${1:-/proc}" current_uid="${2:-$(id -u)}"
|
||||
local proc pid fbdir comm process_uid metadata owner_uid mode screen screens cwd printed=0
|
||||
|
||||
[ -d "$proc_root" ] || return 0
|
||||
# Root can normally read every user's framebuffer, so this is useful for
|
||||
# unprivileged cross-user access only.
|
||||
[ "$current_uid" = 0 ] && return 0
|
||||
for pid in $(lp_xvfb_candidate_pids "$proc_root"); do
|
||||
proc="$proc_root/$pid"
|
||||
[ -r "$proc/comm" ] && [ -r "$proc/cmdline" ] && [ -r "$proc/status" ] || continue
|
||||
IFS= read -r comm < "$proc/comm" || continue
|
||||
[ "$comm" = Xvfb ] || continue
|
||||
process_uid=$(awk '$1 == "Uid:" { print $3; exit }' "$proc/status" 2>/dev/null)
|
||||
case "$process_uid" in ''|*[!0-9]*) continue ;; esac
|
||||
[ "$process_uid" != "$current_uid" ] || continue
|
||||
fbdir=$(lp_xvfb_fbdir_from_cmdline "$proc/cmdline")
|
||||
[ -n "$fbdir" ] || continue
|
||||
case "$fbdir" in
|
||||
/*) ;;
|
||||
*) cwd=$(readlink "$proc/cwd" 2>/dev/null) || continue
|
||||
[ -n "$cwd" ] || continue
|
||||
fbdir="$cwd/$fbdir" ;;
|
||||
esac
|
||||
[ -d "$fbdir" ] || continue
|
||||
screens=$(find "$fbdir" -maxdepth 1 -type f -name 'Xvfb_screen*' -print 2>/dev/null | head -n 8)
|
||||
[ -n "$screens" ] || continue
|
||||
while IFS= read -r screen; do
|
||||
[ -f "$screen" ] && [ -r "$screen" ] || continue
|
||||
metadata=$(stat -c '%u %a' -- "$screen" 2>/dev/null) || continue
|
||||
owner_uid=${metadata%% *}
|
||||
mode=${metadata#* }
|
||||
[ "$owner_uid" = "$process_uid" ] || continue
|
||||
[ "$owner_uid" != "$current_uid" ] || continue
|
||||
if [ "$printed" -eq 0 ]; then
|
||||
print_2title "Readable cross-user Xvfb framebuffer files" "T1552.001"
|
||||
printed=1
|
||||
fi
|
||||
printf 'Review: running Xvfb PID %s exposes readable framebuffer: %s (owner UID %s, mode 0%s)\n' "$pid" "$screen" "$owner_uid" "$mode"
|
||||
done <<EOF
|
||||
$screens
|
||||
EOF
|
||||
done
|
||||
[ "$printed" -eq 0 ] || echo ""
|
||||
}
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ] && ! [ "$MACPEAS" ] && [ -d /proc ]; then
|
||||
lp_check_xvfb_fbdir
|
||||
fi
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
# Title: Processes & Cron & Services & Timers - Erlang SSH daemons
|
||||
# ID: PR_Erlang_ssh_daemons
|
||||
# Author: PEASS-ng contributors
|
||||
# Last Update: 2026-10-09
|
||||
# Description: Correlate running Erlang source files with OTP SSH markers without disclosing authentication values
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1552.001
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $snapshot, $proc_root, $ps_rows, $pid, $uid, $command, $source, $physical_dir, $canonical, $size, $markers, $auth, $shell, $candidates, $files, $printed, $unknowns, $seen_files
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
lp_erlang_ssh_source_from_ps() {
|
||||
printf '%s\n' "$1" | awk '
|
||||
{
|
||||
n = split($0, a, /[[:space:]]+/)
|
||||
executable = a[1]
|
||||
sub(/^.*\//, "", executable)
|
||||
if (executable != "escript") exit
|
||||
for (i = 2; i <= n; i++)
|
||||
if (a[i] ~ /^\/[^[:space:]]+\.(escript|erl)$/) { print a[i]; exit }
|
||||
}'
|
||||
}
|
||||
|
||||
lp_erlang_ssh_source_from_proc() {
|
||||
[ -r "$1" ] || return 1
|
||||
tr '\000' '\n' < "$1" 2>/dev/null | awk '
|
||||
NR == 1 {
|
||||
executable = $0
|
||||
sub(/^.*\//, "", executable)
|
||||
if (executable != "escript") exit
|
||||
next
|
||||
}
|
||||
/^\/.*\.(escript|erl)$/ { print; exit }'
|
||||
}
|
||||
|
||||
lp_erlang_ssh_markers() {
|
||||
awk '
|
||||
NR > 4096 { exit }
|
||||
{
|
||||
# Ignore comments and quoted strings, including markers placed in examples.
|
||||
line = $0 "\n"
|
||||
for (i = 1; i <= length(line); i++) {
|
||||
c = substr(line, i, 1)
|
||||
if (comment) { if (c == "\n") comment = 0; continue }
|
||||
if (quoted) {
|
||||
if (escape) { escape = 0; continue }
|
||||
if (c == "\\") { escape = 1; continue }
|
||||
if (c == quote) quoted = 0
|
||||
continue
|
||||
}
|
||||
if (c == "%") { comment = 1; continue }
|
||||
if (c == "\"" || c == "\047") { quoted = 1; quote = c; continue }
|
||||
code = code c
|
||||
}
|
||||
}
|
||||
END {
|
||||
gsub(/[[:space:]]+/, " ", code)
|
||||
if (code !~ /ssh[[:space:]]*:[[:space:]]*daemon[[:space:]]*\(/) exit
|
||||
auth = "none"
|
||||
if (code ~ /[{][[:space:]]*user_passwords[[:space:]]*,[[:space:]]*\[/) auth = "literal_user_passwords"
|
||||
else if (code ~ /[{][[:space:]]*(user_passwords|password|pwdfun|user_dir_fun|publickey|public_key)[[:space:]]*,/) auth = "auth_option"
|
||||
shell = "unknown"
|
||||
if (code ~ /[{][[:space:]]*shell[[:space:]]*,[[:space:]]*disabled/ || code ~ /[{][[:space:]]*ssh_cli[[:space:]]*,[[:space:]]*no_cli/) shell = "disabled_marker"
|
||||
else if (code ~ /[{][[:space:]]*(shell|ssh_cli)[[:space:]]*,/) shell = "explicit_option"
|
||||
print auth, shell
|
||||
}' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
lp_check_erlang_ssh_daemons() {
|
||||
# Optional arguments let focused fixtures supply a ps snapshot and proc root.
|
||||
local snapshot="$1" proc_root="${2:-/proc}" ps_rows pid uid command source physical_dir canonical size markers auth shell
|
||||
local candidates=0 files=0 printed=0 unknowns=0 seen_files='|'
|
||||
if [ -n "$snapshot" ]; then
|
||||
[ -r "$snapshot" ] || return 0
|
||||
ps_rows=$(cat "$snapshot" 2>/dev/null)
|
||||
else
|
||||
ps_rows=$(ps -eo pid=,uid=,args= 2>/dev/null) || ps_rows=$(ps -axo pid=,uid=,command= 2>/dev/null) || return 0
|
||||
fi
|
||||
[ -n "$ps_rows" ] || return 0
|
||||
while read -r pid uid command; do
|
||||
case "$pid:$uid" in *[!0-9:]*|:*|*:) continue ;; esac
|
||||
case "$command" in
|
||||
erl|erlang|escript|beam|beam.smp|*/erl|*/erlang|*/escript|*/beam|*/beam.smp|erl\ *|erlang\ *|escript\ *|beam\ *|beam.smp\ *|*/erl\ *|*/erlang\ *|*/escript\ *|*/beam\ *|*/beam.smp\ *) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
candidates=$((candidates + 1))
|
||||
[ "$candidates" -le 64 ] || break
|
||||
source=''
|
||||
if [ -r "$proc_root/$pid/cmdline" ]; then
|
||||
source=$(lp_erlang_ssh_source_from_proc "$proc_root/$pid/cmdline")
|
||||
fi
|
||||
[ -n "$source" ] || source=$(lp_erlang_ssh_source_from_ps "$command")
|
||||
if [ -z "$source" ]; then
|
||||
if [ "$unknowns" -lt 8 ]; then
|
||||
[ "$printed" -eq 1 ] || { print_2title 'Running Erlang SSH source candidates' 'T1552.001'; printed=1; }
|
||||
printf 'Candidate: Erlang PID %s UID %s; absolute launched source unavailable (SSH status unknown).\n' "$pid" "$uid"
|
||||
unknowns=$((unknowns + 1))
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
# Resolve parent directories physically; refuse a symlink at the final component.
|
||||
case "$source" in /*.escript|/*.erl) ;; *) continue ;; esac
|
||||
if [ -L "$source" ]; then
|
||||
canonical=''
|
||||
else
|
||||
physical_dir=$(cd -P "${source%/*}" 2>/dev/null && pwd -P) || physical_dir=''
|
||||
canonical="$physical_dir/${source##*/}"
|
||||
fi
|
||||
if [ -z "$physical_dir" ] || [ ! -f "$canonical" ] || [ ! -r "$canonical" ]; then
|
||||
if [ "$unknowns" -lt 8 ]; then
|
||||
[ "$printed" -eq 1 ] || { print_2title 'Running Erlang SSH source candidates' 'T1552.001'; printed=1; }
|
||||
printf 'Candidate: Erlang PID %s UID %s; launched source inaccessible or symlinked: %s (SSH status unknown).\n' "$pid" "$uid" "$source"
|
||||
unknowns=$((unknowns + 1))
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
case "$seen_files" in
|
||||
*"|$canonical|"*) ;;
|
||||
*) seen_files="$seen_files$canonical|"
|
||||
files=$((files + 1))
|
||||
[ "$files" -le 32 ] || break ;;
|
||||
esac
|
||||
size=$(wc -c < "$canonical" 2>/dev/null) || continue
|
||||
case "$size" in ''|*[!0-9]*) continue ;; esac
|
||||
if [ "$size" -gt 131072 ]; then
|
||||
if [ "$unknowns" -lt 8 ]; then
|
||||
[ "$printed" -eq 1 ] || { print_2title 'Running Erlang SSH source candidates' 'T1552.001'; printed=1; }
|
||||
printf 'Candidate: Erlang PID %s UID %s; launched source exceeds 128 KiB: %s (SSH status unknown).\n' "$pid" "$uid" "$canonical"
|
||||
unknowns=$((unknowns + 1))
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
markers=$(lp_erlang_ssh_markers "$canonical")
|
||||
[ -n "$markers" ] || continue
|
||||
auth=${markers%% *}
|
||||
shell=${markers#* }
|
||||
[ "$auth" != none ] || [ "$shell" != unknown ] || continue
|
||||
[ "$printed" -eq 1 ] || { print_2title 'Running Erlang SSH source candidates' 'T1552.001'; printed=1; }
|
||||
printf 'Candidate: Erlang SSH PID %s UID %s source %s; auth=%s, shell=%s.\n' "$pid" "$uid" "$canonical" "$auth" "$shell"
|
||||
done <<EOF
|
||||
$ps_rows
|
||||
EOF
|
||||
if [ "$printed" -eq 1 ]; then
|
||||
printf '%s\n' 'Review the live daemon options and OTP version: readable source markers do not prove accepted credentials or an Erlang shell. No authentication was attempted.'
|
||||
echo ''
|
||||
fi
|
||||
}
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
lp_check_erlang_ssh_daemons
|
||||
fi
|
||||
+86
-2
@@ -6,10 +6,10 @@
|
||||
# License: GNU GPL
|
||||
# Version: 1.4
|
||||
# Mitre: T1057
|
||||
# Functions Used: print_2title, print_info, print_ps
|
||||
# Functions Used: print_2title, print_info, print_ps, checkRootWritableProcessPaths
|
||||
# Global Variables: $capsB, $knw_usrs, $nosh_usrs, $NOUSEPS, $processesB, $processesDump, $processesVB, $rootcommon, $SEARCH_IN_FOLDER, $sh_usrs, $USER, $Wfolders
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $pslist, $cpid, $caphex, $psline, $pid, $selinux_ctx, $current_env_vars, $env_findings, $apparmor_profile, $mount, $mount_findings, $fd_findings, $proc_cmd, $proc_user, $mount_point, $current_mounts, $fd_target, $var, $findings, $sec_findings, $proc_env_vars, $fd_count, $proc_mounts, $$escaped_var
|
||||
# Generated Global Variables: $pslist, $cpid, $caphex, $psline, $pid, $selinux_ctx, $current_env_vars, $env_findings, $apparmor_profile, $mount, $mount_findings, $fd_findings, $proc_cmd, $proc_user, $mount_point, $current_mounts, $fd_target, $var, $findings, $sec_findings, $proc_env_vars, $fd_count, $proc_mounts, $office_uno_sockets, $spice_console_endpoints, $spice_exec_field, $$escaped_var
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -215,6 +215,74 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
fi
|
||||
}
|
||||
|
||||
# Inspect the already collected process list; do not connect to UNO or probe ports.
|
||||
check_privileged_office_uno_sockets() {
|
||||
[ -n "$pslist" ] || return
|
||||
printf '%s\n' "$pslist" | awk '
|
||||
NR > 5000 { exit }
|
||||
length($0) > 4096 { next }
|
||||
$1 == "root" &&
|
||||
/(^|[[:space:]\/])(soffice(\.bin)?|libreoffice|openoffice)([[:space:]]|$)/ &&
|
||||
/--accept=/ {
|
||||
if (!match($0, /--accept=[^[:space:]]+/)) next
|
||||
accept = substr($0, RSTART, RLENGTH)
|
||||
sub(/^--accept=/, "", accept)
|
||||
sub(/^"/, "", accept)
|
||||
sub(/"$/, "", accept)
|
||||
if (accept !~ /^socket,host=/ || accept !~ /;urp(;|$)/) next
|
||||
if (!match(accept, /host=[^,;[:space:]]+/)) next
|
||||
host = substr(accept, RSTART + 5, RLENGTH - 5)
|
||||
if (!match(accept, /port=[0-9]+/)) next
|
||||
port = substr(accept, RSTART + 5, RLENGTH - 5)
|
||||
if (port + 0 < 1 || port + 0 > 65535) next
|
||||
endpoint = host ":" port
|
||||
if (!seen[endpoint]++) {
|
||||
print endpoint
|
||||
if (++count >= 5) exit
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
# Reuse the process snapshot; a loopback SPICE option is a guest-console
|
||||
# lead, not evidence of a live listener or privilege on the host.
|
||||
check_unauthenticated_spice_console() {
|
||||
[ -n "$pslist" ] || return
|
||||
# The two cached snapshots have different executable columns. Select the
|
||||
# actual producer format so argument text cannot impersonate an executable.
|
||||
if [ -n "$NOUSEPS" ]; then spice_exec_field=3; else spice_exec_field=11; fi
|
||||
printf '%s\n' "$pslist" | awk -v exec_field="$spice_exec_field" '
|
||||
NR > 5000 { exit }
|
||||
length($0) > 8192 || NF <= exec_field { next }
|
||||
{
|
||||
if ($(exec_field) !~ /(^|\/)qemu-system-[A-Za-z0-9_]+$/) next
|
||||
first_arg = exec_field + 1
|
||||
for (i = first_arg; i < NF && i < first_arg + 116; i++) {
|
||||
if ($i != "-spice") continue
|
||||
spec = $(i + 1)
|
||||
gsub(/^"|"$/, "", spec)
|
||||
if (length(spec) > 512) continue
|
||||
n = split(spec, fields, ",")
|
||||
if (n > 32) continue
|
||||
addr = port = ""
|
||||
noauth = conflicting_auth = 0
|
||||
for (j = 1; j <= n; j++) {
|
||||
if (fields[j] == "disable-ticketing" || fields[j] == "disable-ticketing=on") noauth = 1
|
||||
else if (fields[j] ~ /^disable-ticketing=/ || fields[j] == "sasl=on" ||
|
||||
fields[j] ~ /^password-secret=/ || fields[j] ~ /^x509-/) conflicting_auth = 1
|
||||
else if (fields[j] ~ /^addr=/) addr = substr(fields[j], 6)
|
||||
else if (fields[j] ~ /^port=/) port = substr(fields[j], 6)
|
||||
}
|
||||
if (!noauth || conflicting_auth || (addr != "127.0.0.1" && addr != "::1")) continue
|
||||
if (port !~ /^[0-9]+$/ || port + 0 < 1 || port + 0 > 65535) continue
|
||||
endpoint = (addr == "::1" ? "[::1]" : addr) ":" (port + 0)
|
||||
if (!seen[endpoint]++) {
|
||||
print endpoint
|
||||
if (++count >= 5) exit
|
||||
}
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
if [ "$NOUSEPS" ]; then
|
||||
print_ps | grep -v 'sed-Es' | sed -${E} "s,$Wfolders,${SED_RED},g" | sed -${E} "s,$sh_usrs,${SED_LIGHT_CYAN}," | sed -${E} "s,$nosh_usrs,${SED_BLUE}," | sed -${E} "s,$rootcommon,${SED_GREEN}," | sed -${E} "s,$knw_usrs,${SED_GREEN}," | sed "s,$USER,${SED_LIGHT_MAGENTA}," | sed "s,root,${SED_RED}," | sed -${E} "s,$processesVB,${SED_RED_YELLOW},g" | sed "s,$processesB,${SED_RED}," | sed -${E} "s,$processesDump,${SED_RED},"
|
||||
pslist=$(print_ps)
|
||||
@@ -233,6 +301,22 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
office_uno_sockets=$(check_privileged_office_uno_sockets)
|
||||
if [ -n "$office_uno_sockets" ]; then
|
||||
print_2title "Privileged office automation sockets" "T1068"
|
||||
printf 'Root-owned process command advertises a LibreOffice/OpenOffice UNO acceptor at:\n%s\n' "$office_uno_sockets"
|
||||
print_info "Confirm the listener and local access; a reachable UNO API can act with the office process identity: https://book.hacktricks.wiki/en/linux-hardening/processes-crontab-systemd-dbus/process-enumeration-and-service-paths.html#privileged-office-automation-sockets"
|
||||
fi
|
||||
|
||||
spice_console_endpoints=$(check_unauthenticated_spice_console)
|
||||
if [ -n "$spice_console_endpoints" ]; then
|
||||
print_2title "Loopback guest-console review candidates" "T1057"
|
||||
printf 'QEMU command line advertises SPICE without ticket authentication at:\n%s\n' "$spice_console_endpoints"
|
||||
print_info "Verify the live listener, local access, and guest login/boot policy. Guest-console control is not host-root access: https://book.hacktricks.wiki/en/linux-hardening/processes-crontab-systemd-dbus/process-enumeration-and-service-paths.html"
|
||||
fi
|
||||
|
||||
checkRootWritableProcessPaths
|
||||
|
||||
# Additional checks for each process
|
||||
print_2title "Processes with unusual configurations" "T1057"
|
||||
for pid in $(find /proc -maxdepth 1 -regex '/proc/[0-9]+' -printf "%f\n" 2>/dev/null); do
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# Title: Processes & Cron & Services & Timers - System V Shared Memory
|
||||
# ID: PR_Sysv_shared_memory
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 2026-10-09
|
||||
# Description: Report root-owned, other-writable System V shared-memory segments for passive review
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1559
|
||||
# Functions Used: print_2title, print_info
|
||||
# Global Variables: $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $sysv_shm_report
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
# This is a single snapshot. Short-lived segments can disappear before it runs;
|
||||
# absence is not evidence that a privileged IPC consumer is safe. Never attach
|
||||
# to a segment or launch a privileged helper to make one appear.
|
||||
if [ ! "$SEARCH_IN_FOLDER" ] && command -v ipcs >/dev/null 2>&1; then
|
||||
sysv_shm_report=$(LC_ALL=C ipcs -m 2>/dev/null | awk '
|
||||
NR > 512 { limited = 1; exit }
|
||||
{
|
||||
if (tolower($1) == "key" && tolower($2) == "shmid" &&
|
||||
tolower($3) == "owner" && tolower($4) == "perms") {
|
||||
format = "linux"
|
||||
next
|
||||
}
|
||||
if (toupper($1) == "T" && toupper($2) == "ID" &&
|
||||
toupper($3) == "KEY" && toupper($4) == "MODE" &&
|
||||
toupper($5) == "OWNER") {
|
||||
format = "bsd"
|
||||
next
|
||||
}
|
||||
if (format == "linux" && NF >= 5) {
|
||||
id = $2; key = $1; owner = $3; perms = $4; size = $5
|
||||
} else if (format == "bsd" && NF >= 6 && $1 == "m") {
|
||||
id = $2; key = $3; perms = $4; owner = $5; size = "unknown"
|
||||
} else next
|
||||
if (owner != "root" && owner != "0") next
|
||||
other_writable = 0
|
||||
if (perms ~ /^[0-7]+$/ && length(perms) >= 3) {
|
||||
last = substr(perms, length(perms), 1)
|
||||
if (last ~ /[2367]/) other_writable = 1
|
||||
} else if (perms ~ /^[-a-zA-Z][-a-zA-Z][r-][w-][xstST-][r-][w-][xstST-][r-][w-][xstST-]$/) {
|
||||
if (substr(perms, length(perms) - 1, 1) == "w") other_writable = 1
|
||||
}
|
||||
if (!other_writable) next
|
||||
if (shown < 20) {
|
||||
printf "Root-owned System V shared memory writable by others: id=%s key=%s mode=%s size=%s\n", id, key, perms, size
|
||||
shown++
|
||||
} else limited = 1
|
||||
}
|
||||
END { if (limited) print "System V shared-memory review incomplete (512-line/20-candidate limit)" }
|
||||
')
|
||||
if [ "$sysv_shm_report" ]; then
|
||||
print_2title "Writable privileged System V shared memory (review candidates)" "T1559"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/processes-crontab-systemd-dbus/process-enumeration-and-service-paths.html#system-v-shared-memory-consumed-by-privileged-processes"
|
||||
printf '%s\n' "$sysv_shm_report"
|
||||
printf '%s\n' "A writable segment needs a privileged consumer that trusts it to be exploitable; this snapshot can miss transient segments."
|
||||
fi
|
||||
fi
|
||||
@@ -6,13 +6,651 @@
|
||||
# License: GNU GPL
|
||||
# Version: 1.2
|
||||
# Mitre: T1053.003
|
||||
# Functions Used: echo_not_found, print_2title, print_info
|
||||
# Global Variables: $cronjobsG, $nosh_usrs, $SEARCH_IN_FOLDER, $sh_usrs, $USER, $Wfolders, $cronjobsB, $PATH
|
||||
# Functions Used: check_pg_basebackup_boundary, echo_not_found, print_2title, print_3title, print_info
|
||||
# Global Variables: $cronjobsG, $nosh_usrs, $SEARCH_IN_FOLDER, $sh_usrs, $USER, $Wfolders, $cronjobsB, $PATH, $PG_BASEBACKUP_DESTS
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $cmd, $VAR, $file, $path, $user_crontab, $username, $job_id, $cron_dir, $crontab, $findings, $line, $finding, $bin
|
||||
# Generated Global Variables: $cmd, $VAR, $file, $path, $user_crontab, $username, $job_id, $cron_dir, $crontab, $findings, $line, $finding, $bin, $cron_log_timeout, $cron_log_status, $files, $cron_file, $prefix, $spool, $bash, $script, $log, $parent, $safe, $candidate, $rest, $part, $route, $mode, $sticky, $cron_tar_timeout, $cron_tar_status, $current_uid, $schedule, $spool_owner, $runas, $helper, $schedule_line, $owner_uid, $helper_text, $dir, $tar_cmd, $helper_line, $version, $cron_process_timeout, $cron_process_status, $magick_cwd, $magick_bin, $magick_cd_line, $magick_exec_line, $magick_marker, $cron_replace_status, $cron_replace_timeout, $cron_ansible_status, $cron_ansible_timeout, $glob, $depth, $kind, $wrapper_text, $chkrootkit_file, $chkrootkit_size, $chkrootkit_match, $file_port
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
# Inspect only conventional installed script paths for the old slapper loop
|
||||
# assignment defect. Never run the script or inspect its temporary-file inputs.
|
||||
cron_chkrootkit_static_probe() (
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set -- /usr/sbin/chkrootkit /usr/bin/chkrootkit
|
||||
fi
|
||||
for chkrootkit_file do
|
||||
[ ! -L "$chkrootkit_file" ] && [ -f "$chkrootkit_file" ] && [ -r "$chkrootkit_file" ] || continue
|
||||
chkrootkit_size=$(dd if="$chkrootkit_file" bs=65537 count=1 2>/dev/null | wc -c | tr -d '[:space:]') || continue
|
||||
case "$chkrootkit_size" in ''|*[!0-9]*) continue ;; esac
|
||||
[ "$chkrootkit_size" -gt 0 ] && [ "$chkrootkit_size" -le 65536 ] || continue
|
||||
chkrootkit_match=$(dd if="$chkrootkit_file" bs=65536 count=1 2>/dev/null | LC_ALL=C awk '
|
||||
NR > 256 { exit }
|
||||
/^[[:space:]]*slapper[[:space:]]*\([[:space:]]*\)[[:space:]]*\{/ {
|
||||
inside = 1; start = NR; files = loop = guard_line = 0; next
|
||||
}
|
||||
inside && NR - start > 80 { inside = 0 }
|
||||
inside {
|
||||
if ($0 ~ /^[[:space:]]*}[[:space:]]*$/) inside = 0
|
||||
if (loop && $0 ~ /^[[:space:]]*done([[:space:]]|$)/) { loop = guard_line = 0 }
|
||||
if ($0 ~ /^[[:space:]]*SLAPPER_FILES=/ && $0 ~ /tmp\//) files = 1
|
||||
if ($0 ~ /^[[:space:]]*for[[:space:]]+i[[:space:]]+in[[:space:]]+\$\{SLAPPER_FILES\};[[:space:]]*do/) loop = 1
|
||||
if (loop && $0 ~ /^[[:space:]]*if[[:space:]]+\[[[:space:]]+-f[[:space:]]+\$\{i\}[[:space:]]+\][[:space:]]*;?[[:space:]]*then/) guard_line = NR
|
||||
if (files && loop && guard_line && NR - guard_line <= 3 &&
|
||||
$0 ~ /^[[:space:]]*file_port=\$file_port[[:space:]]+\$i[[:space:]]*(#.*)?$/) {
|
||||
print "yes"; exit
|
||||
}
|
||||
}
|
||||
' 2>/dev/null) || continue
|
||||
[ "$chkrootkit_match" = yes ] || continue
|
||||
printf 'Checker script review candidate: %s has an unquoted slapper-loop assignment; verify root scheduling, writable executable temporary path, mount policy and installed patch state.\n' "$chkrootkit_file"
|
||||
break
|
||||
done
|
||||
)
|
||||
|
||||
# Correlate a literal root cron playbook glob with a directory the current
|
||||
# user can populate. Never expand the glob, inspect playbooks, or run Ansible.
|
||||
cron_ansible_glob_probe() {
|
||||
cron_ansible_timeout=$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null)
|
||||
[ -n "$cron_ansible_timeout" ] || return 0
|
||||
"$cron_ansible_timeout" 3 sh -c '
|
||||
LC_ALL=C; export LC_ALL
|
||||
[ "$(id -u 2>/dev/null)" != 0 ] || exit 0
|
||||
files=0
|
||||
for cron_file do
|
||||
files=$((files + 1))
|
||||
if [ "$files" -gt 12 ]; then
|
||||
echo "Cron Ansible playbook correlation: incomplete (12-file limit)."
|
||||
break
|
||||
fi
|
||||
[ -f "$cron_file" ] && [ -r "$cron_file" ] && [ ! -L "$cron_file" ] || continue
|
||||
schedule=$(dd if="$cron_file" bs=8193 count=1 2>/dev/null) || continue
|
||||
if [ "${#schedule}" -ge 8192 ]; then
|
||||
echo "Cron Ansible playbook correlation: incomplete (8 KiB schedule limit): $cron_file"
|
||||
continue
|
||||
fi
|
||||
case "$schedule" in *ansible-parallel*) ;; *) continue ;; esac
|
||||
case "$cron_file" in */spool/cron/root|*/spool/cron/crontabs/root) spool=1 ;; *) spool=0 ;; esac
|
||||
printf "%s\n" "$schedule" | awk -v spool="$spool" '\''
|
||||
NR > 32 || length($0) > 512 { partial = 1; next }
|
||||
/^[[:space:]]*(#|$)/ { next }
|
||||
{
|
||||
for (i = 1; i <= 5; i++) if ($i !~ /^[0-9*,\/-]+$/) next
|
||||
if (spool) {
|
||||
if (NF != 7) next
|
||||
helper = $6; glob = $7
|
||||
} else {
|
||||
if (NF != 8 || $6 != "root") next
|
||||
helper = $7; glob = $8
|
||||
}
|
||||
if (helper !~ /^\/[A-Za-z0-9_.\/+\-]+\/ansible-parallel$/ ||
|
||||
glob !~ /^\/[A-Za-z0-9_.\/+\-]+\/\*\.(yml|yaml)$/) next
|
||||
print glob "|" NR
|
||||
}
|
||||
END { if (partial) print "#PARTIAL" }
|
||||
'\'' | while IFS="|" read -r glob schedule_line; do
|
||||
if [ "$glob" = "#PARTIAL" ]; then
|
||||
echo "Cron Ansible playbook correlation: incomplete (line/column limit): $cron_file"
|
||||
continue
|
||||
fi
|
||||
case "$glob" in *"/../"*|*"/./"*|*"//"*) continue ;; esac
|
||||
dir=${glob%/*}
|
||||
[ -d "$dir" ] && [ -w "$dir" ] && [ -x "$dir" ] || continue
|
||||
path=; rest=${dir#/}; safe=1; depth=0
|
||||
while [ -n "$rest" ]; do
|
||||
depth=$((depth + 1))
|
||||
[ "$depth" -le 16 ] || { safe=0; break; }
|
||||
part=${rest%%/*}; path=$path/$part
|
||||
if [ -L "$path" ]; then safe=0; break; fi
|
||||
case "$rest" in */*) rest=${rest#*/} ;; *) rest= ;; esac
|
||||
done
|
||||
[ "$safe" -eq 1 ] || continue
|
||||
mode=$(ls -ld "$dir" 2>/dev/null) || continue
|
||||
mode=${mode%% *}
|
||||
case $(printf "%s" "$mode" | cut -c 10) in t|T) continue ;; esac
|
||||
echo "Cron Ansible playbook review candidate: $cron_file:$schedule_line (root cron passes $glob to helper; current user can create entries in $dir; verify helper behavior, ACLs, mount policy, and scheduler state)"
|
||||
done
|
||||
done
|
||||
' sh "$@"
|
||||
cron_ansible_status=$?
|
||||
case "$cron_ansible_status" in
|
||||
124|137) echo "Cron Ansible playbook correlation: incomplete (3-second timeout)." ;;
|
||||
0) ;;
|
||||
*) echo "Cron Ansible playbook correlation: incomplete (metadata error)." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# A read-only cron script may still be replaceable through its parent. Inspect
|
||||
# only literal script paths in a few visible cron files; no job is run.
|
||||
cron_replaceable_script_probe() {
|
||||
cron_replace_timeout=$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null)
|
||||
[ -n "$cron_replace_timeout" ] || return 0
|
||||
"$cron_replace_timeout" 3 sh -c '
|
||||
LC_ALL=C; export LC_ALL
|
||||
current_uid=$(id -u 2>/dev/null) || exit 1
|
||||
files=0
|
||||
for cron_file do
|
||||
files=$((files + 1))
|
||||
if [ "$files" -gt 12 ]; then
|
||||
echo "Cron script replacement review incomplete (12-file limit)."
|
||||
break
|
||||
fi
|
||||
[ -f "$cron_file" ] && [ -r "$cron_file" ] && [ ! -L "$cron_file" ] || continue
|
||||
schedule=$(dd if="$cron_file" bs=8193 count=1 2>/dev/null) || continue
|
||||
if [ "${#schedule}" -ge 8192 ]; then
|
||||
echo "Cron script replacement review incomplete (8 KiB schedule limit): $cron_file"
|
||||
continue
|
||||
fi
|
||||
spool_owner=
|
||||
case "$cron_file" in */spool/cron/root|*/spool/cron/crontabs/root) spool_owner=root ;; esac
|
||||
printf "%s\n" "$schedule" | awk -v owner="$spool_owner" '\''
|
||||
NR > 32 || length($0) > 512 { partial = 1; next }
|
||||
/^[[:space:]]*(#|$)/ { next }
|
||||
{
|
||||
for (i = 1; i <= 5; i++) if ($i !~ /^[0-9*,\/-]+$/) next
|
||||
n = NF
|
||||
if (n >= 10 && $(n-2) == ">>" && $(n-1) == "/dev/null" && $n == "2>&1") n -= 3
|
||||
if (owner == "") {
|
||||
runas = $6; shell = $7; script = $8
|
||||
kind = (NF == 8 ? "shell" : (NF == 7 ? "php" : ""))
|
||||
if (kind == "php") script = $7
|
||||
if ((shell == "php" || shell == "/usr/bin/php" || shell == "/bin/php") &&
|
||||
(n == 8 || n == 9) && (n == 8 || $9 ~ /^[A-Za-z0-9_.:-]+$/)) {
|
||||
kind = "php_cli"; script = $8
|
||||
}
|
||||
} else {
|
||||
runas = owner; shell = $6; script = $7
|
||||
kind = (NF == 7 ? "shell" : (NF == 6 ? "php" : ""))
|
||||
if (kind == "php") script = $6
|
||||
if ((shell == "php" || shell == "/usr/bin/php" || shell == "/bin/php") &&
|
||||
(n == 7 || n == 8) && (n == 7 || $8 ~ /^[A-Za-z0-9_.:-]+$/)) {
|
||||
kind = "php_cli"; script = $7
|
||||
}
|
||||
}
|
||||
if (runas !~ /^[A-Za-z_][A-Za-z0-9_-]*$/ || kind == "" ||
|
||||
(kind == "shell" && shell != "/bin/sh" && shell != "/usr/bin/sh" &&
|
||||
shell != "/bin/bash" && shell != "/usr/bin/bash") ||
|
||||
script !~ /^\/[A-Za-z0-9_\/.+-]+$/) next
|
||||
print runas "|" script "|" NR "|" kind
|
||||
}
|
||||
END { if (partial) print "#PARTIAL" }
|
||||
'\'' | while IFS="|" read -r runas script schedule_line kind; do
|
||||
if [ "$runas" = "#PARTIAL" ]; then
|
||||
echo "Cron script replacement review incomplete (line/column limit): $cron_file"
|
||||
continue
|
||||
fi
|
||||
case "$script" in *"/../"*|*"/.."|*"/./"*|*"/."|*"//"*) continue ;; esac
|
||||
owner_uid=$(id -u "$runas" 2>/dev/null) || continue
|
||||
[ "$owner_uid" != "$current_uid" ] || continue
|
||||
if [ "$kind" = php_cli ]; then
|
||||
[ "$owner_uid" = 0 ] && [ -f "$script" ] && [ -r "$script" ] &&
|
||||
[ -w "$script" ] && [ ! -L "$script" ] || continue
|
||||
path=; rest=${script#/}; safe=1
|
||||
while [ -n "$rest" ]; do
|
||||
part=${rest%%/*}; path=$path/$part
|
||||
if [ -L "$path" ]; then safe=0; break; fi
|
||||
case "$rest" in */*) rest=${rest#*/} ;; *) rest= ;; esac
|
||||
done
|
||||
[ "$safe" -eq 1 ] || continue
|
||||
echo "Cron PHP script review candidate: $cron_file:$schedule_line (root PHP runs current-user-writable $script; verify scheduler state, script identity, ACLs, and mount policy)"
|
||||
continue
|
||||
fi
|
||||
if [ "$kind" = php ]; then
|
||||
[ "$owner_uid" = 0 ] && [ -f "$script" ] && [ -r "$script" ] && [ ! -L "$script" ] || continue
|
||||
path=; rest=${script#/}; safe=1
|
||||
while [ -n "$rest" ]; do
|
||||
part=${rest%%/*}; path=$path/$part
|
||||
if [ -L "$path" ]; then safe=0; break; fi
|
||||
case "$rest" in */*) rest=${rest#*/} ;; *) rest= ;; esac
|
||||
done
|
||||
[ "$safe" -eq 1 ] || continue
|
||||
wrapper_text=$(dd if="$script" bs=4097 count=1 2>/dev/null) || continue
|
||||
[ "${#wrapper_text}" -lt 4096 ] || continue
|
||||
printf "%s\n" "$wrapper_text" | awk "NR == 1 { exit !(/^#!\\/(usr\\/bin|bin)\\/php([0-9.]*)?([[:space:]]|$)/) }" || continue
|
||||
printf "%s\n" "$wrapper_text" | awk '\''
|
||||
NR > 32 || length($0) > 512 { next }
|
||||
{
|
||||
line = $0
|
||||
sub(/^[ \t]*/, "", line)
|
||||
if (line !~ /^exec[ \t]*\(/) next
|
||||
sub(/^exec[ \t]*\([ \t]*/, "", line)
|
||||
quote = substr(line, 1, 1)
|
||||
if (quote != sprintf("%c", 39) && quote != "\"") next
|
||||
line = substr(line, 2)
|
||||
if (match(line, /^\/[A-Za-z0-9_\/.+-]+/) == 0) next
|
||||
helper = substr(line, 1, RLENGTH)
|
||||
tail = substr(line, RLENGTH + 1)
|
||||
if (substr(tail, 1, 1) != quote) next
|
||||
tail = substr(tail, 2)
|
||||
if (tail !~ /^[ \t]*\)[ \t]*;[ \t]*(\/\/.*)?$/) next
|
||||
if (++found > 2) exit
|
||||
print helper
|
||||
}
|
||||
'\'' | while IFS= read -r helper; do
|
||||
case "$helper" in *"/../"*|*"/.."|*"/./"*|*"/."|*"//"*) continue ;; esac
|
||||
[ ! -e "$helper" ] && [ ! -L "$helper" ] || continue
|
||||
parent=${helper%/*}; [ -n "$parent" ] || parent=/
|
||||
[ -d "$parent" ] && [ -w "$parent" ] && [ -x "$parent" ] || continue
|
||||
path=; rest=${helper#/}; safe=1
|
||||
while [ -n "$rest" ]; do
|
||||
part=${rest%%/*}; path=$path/$part
|
||||
if [ -L "$path" ]; then safe=0; break; fi
|
||||
case "$rest" in */*) rest=${rest#*/} ;; *) rest= ;; esac
|
||||
done
|
||||
[ "$safe" -eq 1 ] || continue
|
||||
mode=$(ls -ld "$parent" 2>/dev/null) || continue
|
||||
mode=${mode%% *}
|
||||
case $(printf "%s" "$mode" | cut -c 10) in t|T) continue ;; esac
|
||||
echo "Cron PHP helper review candidate: $cron_file:$schedule_line (root script $script calls absent $helper in current-user-writable $parent; verify scheduler state, PHP policy, ACLs, and mount policy)"
|
||||
done
|
||||
continue
|
||||
fi
|
||||
[ -f "$script" ] && [ ! -L "$script" ] && [ ! -w "$script" ] || continue
|
||||
parent=${script%/*}; [ -n "$parent" ] || parent=/
|
||||
[ -d "$parent" ] && [ -w "$parent" ] && [ -x "$parent" ] || continue
|
||||
path=; rest=${script#/}; safe=1
|
||||
while [ -n "$rest" ]; do
|
||||
part=${rest%%/*}; path=$path/$part
|
||||
if [ -L "$path" ]; then safe=0; break; fi
|
||||
case "$rest" in */*) rest=${rest#*/} ;; *) rest= ;; esac
|
||||
done
|
||||
[ "$safe" -eq 1 ] || continue
|
||||
mode=$(ls -ld "$parent" 2>/dev/null) || continue
|
||||
mode=${mode%% *}
|
||||
sticky=$(printf "%s" "$mode" | cut -c 10)
|
||||
case "$sticky" in t|T) continue ;; esac
|
||||
echo "Cron script replacement review candidate: $cron_file:$schedule_line ($runas runs $script; current user can replace its directory entry in $parent; verify ACLs, mount policy, and scheduler state)"
|
||||
done
|
||||
done
|
||||
' sh "$@"
|
||||
cron_replace_status=$?
|
||||
case "$cron_replace_status" in
|
||||
124|137) echo 'Cron script replacement review incomplete (3-second timeout).' ;;
|
||||
0) ;;
|
||||
*) echo 'Cron script replacement review incomplete (metadata error).' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Inspect only literal, visible root cron commands. The whole probe has a wall-clock
|
||||
# limit; without timeout, filesystem metadata on an unresponsive mount is unbounded.
|
||||
cron_log_input_probe() {
|
||||
cron_log_timeout=$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null)
|
||||
if [ -z "$cron_log_timeout" ]; then
|
||||
echo "Cron log input correlation: unknown (timeout unavailable)."
|
||||
return
|
||||
fi
|
||||
"$cron_log_timeout" 3 sh -c '
|
||||
LC_ALL=C; export LC_ALL
|
||||
files=0
|
||||
for cron_file do
|
||||
files=$((files + 1))
|
||||
if [ "$files" -gt 8 ]; then
|
||||
echo "Cron log input correlation: truncated at 8 cron files."
|
||||
break
|
||||
fi
|
||||
if [ -L "$cron_file" ] || [ ! -f "$cron_file" ] || [ ! -r "$cron_file" ]; then
|
||||
continue
|
||||
fi
|
||||
prefix=$(dd if="$cron_file" bs=8193 count=1 2>/dev/null) || continue
|
||||
if [ "${#prefix}" -ge 8192 ]; then
|
||||
echo "Cron log input correlation: skipped oversized cron file: $cron_file"
|
||||
continue
|
||||
fi
|
||||
case "$cron_file" in
|
||||
*/root) spool=1 ;;
|
||||
*) spool=0 ;;
|
||||
esac
|
||||
printf "%s\n" "$prefix" | awk -v spool="$spool" '\''NR <= 16 && length($0) <= 512 && $0 !~ /^[[:space:]]*(#|$)/ {
|
||||
for (i=1; i<=5; i++) if ($i !~ /^[0-9*,\/-]+$/) next
|
||||
if (spool == 0) {
|
||||
if (NF != 9 || $6 != "root") next
|
||||
bash=$7; script=$8; input=$9
|
||||
} else {
|
||||
if (NF != 8) next
|
||||
bash=$6; script=$7; input=$8
|
||||
}
|
||||
if (bash != "/bin/bash" && bash != "/usr/bin/bash") next
|
||||
if (script !~ /^\/[A-Za-z0-9_.\/-]+$/ || input !~ /^\/[A-Za-z0-9_.\/-]+\.log$/) next
|
||||
print bash "|" script "|" input
|
||||
}
|
||||
END { if (NR > 16) print "#TRUNCATED" }'\'' | while IFS="|" read -r bash script log; do
|
||||
if [ "$bash" = "#TRUNCATED" ]; then
|
||||
echo "Cron log input correlation: truncated at 16 lines: $cron_file"
|
||||
continue
|
||||
fi
|
||||
case "$script:$log" in *"/../"*|*"/..:"*|*"/./"*|*"//"*) continue ;; esac
|
||||
[ -L "$script" ] && continue
|
||||
[ -f "$script" ] && [ -r "$script" ] || continue
|
||||
[ -L "$log" ] && continue
|
||||
[ -f "$log" ] || continue
|
||||
parent=${log%/*}; [ -n "$parent" ] || parent=/
|
||||
[ -d "$parent" ] || continue
|
||||
# Reject symlinks in every ancestor; metadata otherwise describes a
|
||||
# different path than the one the privileged command would resolve.
|
||||
safe=1
|
||||
for candidate in "$script" "$log"; do
|
||||
path=; rest=${candidate#/}
|
||||
while [ -n "$rest" ]; do
|
||||
part=${rest%%/*}; path=$path/$part
|
||||
if [ -L "$path" ]; then safe=0; break; fi
|
||||
case "$rest" in */*) rest=${rest#*/} ;; *) rest= ;; esac
|
||||
done
|
||||
[ "$safe" -eq 1 ] || break
|
||||
done
|
||||
[ "$safe" -eq 1 ] || continue
|
||||
route=
|
||||
if [ -w "$log" ]; then route="current user can write input file"; fi
|
||||
if [ -w "$parent" ] && [ -x "$parent" ]; then
|
||||
mode=$(ls -ld "$parent" 2>/dev/null) || continue
|
||||
mode=${mode%% *}
|
||||
sticky=$(printf "%s" "$mode" | cut -c 10)
|
||||
case "$sticky" in t|T) ;; *) route="current user can replace directory entry" ;; esac
|
||||
fi
|
||||
[ -n "$route" ] || continue
|
||||
echo "Cron log input review candidate: $cron_file"
|
||||
echo " Root command: $bash $script $log"
|
||||
echo " Input: $log ($route)"
|
||||
ls -ld "$parent" "$log" 2>/dev/null
|
||||
echo " Metadata only; parser behavior and ACL/mount policy need review."
|
||||
done
|
||||
done
|
||||
' sh "$@"
|
||||
cron_log_status=$?
|
||||
case "$cron_log_status" in
|
||||
124|137) echo "Cron log input correlation: timed out; visibility unknown." ;;
|
||||
0) ;;
|
||||
*) echo "Cron log input correlation: incomplete; visibility unknown." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Correlate a visible cron identity with one literal helper and a GNU tar glob.
|
||||
# This intentionally recognizes only simple, adjacent `cd DIR` and `tar -cf
|
||||
# ARCHIVE *` lines. Shell expansion, other archivers, and nested helpers need
|
||||
# manual review; no scheduled command or archive operation is run here.
|
||||
cron_tar_wildcard_probe() {
|
||||
cron_tar_timeout=$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null)
|
||||
if [ -z "$cron_tar_timeout" ]; then
|
||||
echo "Cron tar wildcard correlation: unknown (timeout unavailable)."
|
||||
return
|
||||
fi
|
||||
"$cron_tar_timeout" 5 sh -c '
|
||||
LC_ALL=C; export LC_ALL
|
||||
current_uid=$(id -u 2>/dev/null) || exit 1
|
||||
[ "$current_uid" -ne 0 ] || exit 0
|
||||
files=0
|
||||
for cron_file do
|
||||
files=$((files + 1))
|
||||
if [ "$files" -gt 12 ]; then
|
||||
echo "Cron tar wildcard correlation: unknown beyond 12 schedule files."
|
||||
break
|
||||
fi
|
||||
[ -f "$cron_file" ] && [ -r "$cron_file" ] && [ ! -L "$cron_file" ] || continue
|
||||
schedule=$(dd if="$cron_file" bs=8193 count=1 2>/dev/null) || continue
|
||||
if [ "${#schedule}" -ge 8192 ]; then
|
||||
echo "Cron tar wildcard correlation: unknown (oversized schedule: $cron_file)."
|
||||
continue
|
||||
fi
|
||||
spool_owner=
|
||||
case "$cron_file" in */spool/cron/*) spool_owner=${cron_file##*/} ;; esac
|
||||
printf "%s\n" "$schedule" | awk -v owner="$spool_owner" '\''
|
||||
NR > 32 { truncated=1; next }
|
||||
length($0) > 512 { truncated=1; next }
|
||||
/^[[:space:]]*(#|$)/ { next }
|
||||
{
|
||||
for (i=1; i<=5; i++) if ($i !~ /^[0-9*,\/-]+$/) next
|
||||
if (owner == "") {
|
||||
if (NF != 8) next
|
||||
runas=$6; interpreter=$7; helper=$8
|
||||
} else {
|
||||
if (NF != 7) next
|
||||
runas=owner; interpreter=$6; helper=$7
|
||||
}
|
||||
if (interpreter != "/bin/sh" && interpreter != "/usr/bin/sh" &&
|
||||
interpreter != "/bin/bash" && interpreter != "/usr/bin/bash") next
|
||||
if (helper ~ /^\/[A-Za-z0-9_.\/-]+$/) print runas "|" helper "|" NR
|
||||
}
|
||||
END { if (truncated) print "#TRUNCATED" }
|
||||
'\'' | while IFS="|" read -r runas helper schedule_line; do
|
||||
if [ "$runas" = "#TRUNCATED" ]; then
|
||||
echo "Cron tar wildcard correlation: unknown beyond schedule line/length cap: $cron_file."
|
||||
continue
|
||||
fi
|
||||
case "$runas" in ""|*[!A-Za-z0-9_-]*) continue ;; esac
|
||||
case "$helper" in *"/../"*|*"/.."|*"/./"*|*"/."|*"//"*) continue ;; esac
|
||||
owner_uid=$(id -u "$runas" 2>/dev/null) || continue
|
||||
[ "$owner_uid" != "$current_uid" ] || continue
|
||||
[ -f "$helper" ] && [ -r "$helper" ] && [ ! -L "$helper" ] || continue
|
||||
helper_text=$(dd if="$helper" bs=4097 count=1 2>/dev/null) || continue
|
||||
if [ "${#helper_text}" -ge 4096 ]; then
|
||||
echo "Cron tar wildcard correlation: unknown (oversized helper: $helper)."
|
||||
continue
|
||||
fi
|
||||
printf "%s\n" "$helper_text" | awk '\''
|
||||
NR > 24 { truncated=1; next }
|
||||
length($0) > 512 { truncated=1; next }
|
||||
/^[[:space:]]*(#|$)/ { next }
|
||||
{
|
||||
if (NF == 2 && $1 == "cd" && $2 ~ /^\/[A-Za-z0-9_.\/-]+$/) {
|
||||
dir=$2; next
|
||||
}
|
||||
if (dir != "" && NF == 4 &&
|
||||
($1 == "tar" || $1 == "/usr/bin/tar" || $1 == "/bin/tar" || $1 == "gtar") &&
|
||||
$2 ~ /^-[A-Za-z]+$/ && $2 ~ /c/ && $2 ~ /f/ &&
|
||||
$3 ~ /^[A-Za-z0-9_.\/-]+$/ && $4 == "*") {
|
||||
print dir "|" $1 "|" NR
|
||||
}
|
||||
dir=""
|
||||
}
|
||||
END { if (truncated) print "#TRUNCATED" }
|
||||
'\'' | while IFS="|" read -r dir tar_cmd helper_line; do
|
||||
if [ "$dir" = "#TRUNCATED" ]; then
|
||||
echo "Cron tar wildcard correlation: unknown beyond helper line/length cap: $helper."
|
||||
continue
|
||||
fi
|
||||
case "$dir" in *"/../"*|*"/.."|*"/./"*|*"/."|*"//"*) continue ;; esac
|
||||
[ -d "$dir" ] && [ -w "$dir" ] && [ -x "$dir" ] && [ ! -L "$dir" ] || continue
|
||||
version=$($tar_cmd --version 2>/dev/null | awk '\''NR == 1 { print; exit }'\'')
|
||||
case "$version" in *"GNU tar"*) ;; *) continue ;; esac
|
||||
echo "Cron GNU tar wildcard review candidate: $cron_file:$schedule_line"
|
||||
echo " Schedule owner: $runas (uid $owner_uid); helper: $helper:$helper_line"
|
||||
echo " Working directory: $dir (current user can write and enter)"
|
||||
echo " Command: $tar_cmd archive * (unquoted glob, no --); GNU tar version observed"
|
||||
echo " Candidate only; execution context and file ownership need review."
|
||||
done
|
||||
done
|
||||
done
|
||||
' sh "$@"
|
||||
cron_tar_status=$?
|
||||
case "$cron_tar_status" in
|
||||
124|137) echo "Cron tar wildcard correlation: timed out; visibility unknown." ;;
|
||||
0) ;;
|
||||
*) echo "Cron tar wildcard correlation: incomplete; visibility unknown." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Read only literal shell helpers in visible root cron entries. This is a
|
||||
# conservative text correlation, not a shell parser or proof of exploitability.
|
||||
cron_process_args_probe() {
|
||||
cron_process_timeout=$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null)
|
||||
if [ -z "$cron_process_timeout" ]; then
|
||||
echo "Cron process arguments correlation: unknown (timeout unavailable)."
|
||||
return
|
||||
fi
|
||||
"$cron_process_timeout" 5 sh -c '
|
||||
LC_ALL=C; export LC_ALL
|
||||
files=0
|
||||
for cron_file do
|
||||
files=$((files + 1))
|
||||
if [ "$files" -gt 6 ]; then
|
||||
echo "Cron process arguments correlation: unknown beyond 6 schedule files."
|
||||
break
|
||||
fi
|
||||
[ -e "$cron_file" ] || continue
|
||||
if [ -L "$cron_file" ] || [ ! -f "$cron_file" ] || [ ! -r "$cron_file" ]; then
|
||||
echo "Cron process arguments correlation: unknown (unreadable schedule: $cron_file)."
|
||||
continue
|
||||
fi
|
||||
schedule=$(dd if="$cron_file" bs=8193 count=1 2>/dev/null) || {
|
||||
echo "Cron process arguments correlation: unknown (unreadable schedule: $cron_file)."
|
||||
continue
|
||||
}
|
||||
if [ "${#schedule}" -ge 8192 ]; then
|
||||
echo "Cron process arguments correlation: unknown (oversized schedule: $cron_file)."
|
||||
continue
|
||||
fi
|
||||
spool_owner=
|
||||
case "$cron_file" in */spool/cron/root|*/spool/cron/crontabs/root) spool_owner=root ;; esac
|
||||
printf "%s\n" "$schedule" | awk -v owner="$spool_owner" '\''
|
||||
NR > 32 { truncated=1; next }
|
||||
length($0) > 512 { truncated=1; next }
|
||||
/^[[:space:]]*(#|$)/ { next }
|
||||
{
|
||||
for (i=1; i<=5; i++) if ($i !~ /^[0-9*,\/-]+$/) next
|
||||
if (owner == "") {
|
||||
if (NF != 8 || $6 != "root") next
|
||||
interpreter=$7; helper=$8
|
||||
} else {
|
||||
if (NF != 7) next
|
||||
interpreter=$6; helper=$7
|
||||
}
|
||||
if (interpreter != "/bin/sh" && interpreter != "/usr/bin/sh" &&
|
||||
interpreter != "/bin/bash" && interpreter != "/usr/bin/bash") next
|
||||
if (helper !~ /^\/[A-Za-z0-9_.\/-]+$/ ||
|
||||
helper ~ /\/\.\.?($|\/)/ || helper ~ /\/\//) next
|
||||
if (++helpers > 2) { capped=1; next }
|
||||
print helper "|" NR
|
||||
}
|
||||
END {
|
||||
if (truncated) print "#LINES"
|
||||
if (capped) print "#HELPERS"
|
||||
}
|
||||
'\'' | while IFS="|" read -r helper schedule_line; do
|
||||
case "$helper" in
|
||||
"#LINES") echo "Cron process arguments correlation: unknown beyond schedule line/length cap: $cron_file."; continue ;;
|
||||
"#HELPERS") echo "Cron process arguments correlation: unknown beyond 2 helpers: $cron_file."; continue ;;
|
||||
esac
|
||||
if [ -L "$helper" ] || [ ! -f "$helper" ] || [ ! -r "$helper" ]; then
|
||||
echo "Cron process arguments correlation: unknown (unreadable helper: $helper)."
|
||||
continue
|
||||
fi
|
||||
helper_text=$(dd if="$helper" bs=4097 count=1 2>/dev/null) || {
|
||||
echo "Cron process arguments correlation: unknown (unreadable helper: $helper)."
|
||||
continue
|
||||
}
|
||||
if [ "${#helper_text}" -ge 4096 ]; then
|
||||
echo "Cron process arguments correlation: unknown (oversized helper: $helper)."
|
||||
continue
|
||||
fi
|
||||
printf "%s\n" "$helper_text" | awk -v cron="$cron_file" -v schedule_line="$schedule_line" -v helper="$helper" '\''
|
||||
NR > 32 { truncated=1; next }
|
||||
length($0) > 512 { truncated=1; next }
|
||||
/^[[:space:]]*(#|$)/ { next }
|
||||
{
|
||||
line=$0
|
||||
if (line ~ /pgrep[[:space:]]+(-[A-Za-z]*f[A-Za-z]*|--full)([[:space:]]|$)/ &&
|
||||
line ~ /[[:space:]]read[[:space:]]/) {
|
||||
capture=line
|
||||
sub(/^.*[[:space:]]read[[:space:]]+/, "", capture)
|
||||
sub(/^-r[[:space:]]+/, "", capture)
|
||||
sub(/[;|].*$/, "", capture)
|
||||
n=split(capture, fields, /[[:space:]]+/)
|
||||
if (fields[n] ~ /^[A-Za-z_][A-Za-z0-9_]*$/) {
|
||||
input=fields[n]; select_line=NR
|
||||
derived=""; derive_line=0; execute_line=0; apache=0; config=0
|
||||
}
|
||||
}
|
||||
if (input != "" && line ~ /^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=/ &&
|
||||
index(line, "$" input) && line ~ /sed|\/\//) {
|
||||
derived=line
|
||||
sub(/^[[:space:]]*/, "", derived)
|
||||
sub(/=.*/, "", derived)
|
||||
if (derived ~ /^[A-Za-z_][A-Za-z0-9_]*$/) {
|
||||
derive_line=NR
|
||||
if (line ~ /apache2ctl|apachectl|httpd/) apache=1
|
||||
if (line ~ /[[:space:]]-t([^A-Za-z0-9_-]|$)/) config=1
|
||||
}
|
||||
}
|
||||
if (derived != "" && index(line, "$" derived) &&
|
||||
line ~ /[[:space:]]-t([^A-Za-z0-9_-]|$)/) config=1
|
||||
if (derived != "" && line ~ /^[[:space:]]*\$[A-Za-z_][A-Za-z0-9_]*/ && NR > derive_line) {
|
||||
executed=line
|
||||
sub(/^[[:space:]]*\$/, "", executed)
|
||||
sub(/[^A-Za-z0-9_].*$/, "", executed)
|
||||
if (executed == derived) execute_line=NR
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (truncated) {
|
||||
print "Cron process arguments correlation: unknown beyond helper line/length cap: " helper "."
|
||||
} else if (select_line && derive_line && execute_line) {
|
||||
print "Cron process arguments review candidate: " cron ":" schedule_line
|
||||
print " Schedule owner: root; helper: " helper
|
||||
print " Full-args pgrep/read: helper line " select_line "; derived command: line " derive_line "; execution: line " execute_line
|
||||
if (apache && config) print " Apache config-test option flow observed; review process identity and config inputs."
|
||||
print " Candidate only; process arguments are unauthenticated input."
|
||||
}
|
||||
}
|
||||
'\''
|
||||
# Reuse the same bounded helper text for the Linux AppImage search-path
|
||||
# lead. No image tool is executed: a crafted CWD could load code on run.
|
||||
if [ "$(uname -s 2>/dev/null)" = Linux ]; then
|
||||
printf "%s\n" "$helper_text" | awk '\''
|
||||
NR > 32 || length($0) > 512 { exit }
|
||||
/^[[:space:]]*(#|$)/ { next }
|
||||
{
|
||||
line=$0
|
||||
sub(/^[[:space:]]+/, "", line)
|
||||
sub(/[[:space:]]+#.*$/, "", line)
|
||||
if (line ~ /^cd[[:space:]]+/) {
|
||||
cwd=line
|
||||
sub(/^cd[[:space:]]+/, "", cwd)
|
||||
sub(/[[:space:]]+$/, "", cwd)
|
||||
if (cwd ~ /^\/[A-Za-z0-9_.\/-]+$/ &&
|
||||
cwd !~ /\/\.\.?($|\/)/ && cwd !~ /\/\//) {
|
||||
cd_line=NR
|
||||
} else { cwd=""; cd_line=0 }
|
||||
next
|
||||
}
|
||||
if (!cwd) next
|
||||
n=split(line, words, /[[:space:]]+/)
|
||||
for (i=1; i<n; i++) {
|
||||
if (words[i] ~ /^\/[A-Za-z0-9_.\/-]+\/magick$/ &&
|
||||
words[i+1] == "identify" &&
|
||||
(i == 1 || words[i-1] == "xargs")) {
|
||||
print cwd "|" words[i] "|" cd_line "|" NR
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
'\'' | while IFS="|" read -r magick_cwd magick_bin magick_cd_line magick_exec_line; do
|
||||
[ -d "$magick_cwd" ] && [ -w "$magick_cwd" ] && [ -x "$magick_cwd" ] || continue
|
||||
[ -f "$magick_bin" ] && [ -x "$magick_bin" ] || continue
|
||||
magick_marker=""
|
||||
if [ -r "$magick_bin" ] && command -v od >/dev/null 2>&1; then
|
||||
magick_marker=$(dd if="$magick_bin" bs=1 skip=8 count=3 2>/dev/null | od -An -tx1 2>/dev/null | tr -d "[:space:]")
|
||||
fi
|
||||
echo "Cron ImageMagick working-directory review candidate: $cron_file:$schedule_line"
|
||||
echo " Schedule owner: root; helper: $helper (cd line $magick_cd_line; identify line $magick_exec_line)"
|
||||
echo " Caller-writable working directory: $magick_cwd; executable: $magick_bin"
|
||||
if [ "$magick_marker" = 414902 ]; then
|
||||
echo " AppImage type-2 marker found; verify affected ImageMagick AppRun search paths and version."
|
||||
else
|
||||
echo " AppImage format not confirmed; native builds and safe search paths may be unaffected."
|
||||
fi
|
||||
echo " Passive correlation only; cron execution, loader paths, and mount/ACL policy need review."
|
||||
done
|
||||
fi
|
||||
done
|
||||
done
|
||||
' sh "$@"
|
||||
cron_process_status=$?
|
||||
case "$cron_process_status" in
|
||||
124|137) echo "Cron process arguments correlation: timed out; visibility unknown." ;;
|
||||
0) ;;
|
||||
*) echo "Cron process arguments correlation: incomplete; visibility unknown." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
print_2title "Check for vulnerable cron jobs" "T1053.003"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/processes-crontab-systemd-dbus/cron-and-systemd-timers.html#enumerate-schedules"
|
||||
@@ -26,6 +664,20 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
cat /etc/cron* /etc/at* /etc/anacrontab /var/spool/cron/crontabs/* /etc/incron.d/* /var/spool/incron/* 2>/dev/null | tr -d "\r" | grep -v "^#" | sed -${E} "s,$Wfolders,${SED_RED_YELLOW},g" | sed -${E} "s,$sh_usrs,${SED_LIGHT_CYAN}," | sed "s,$USER,${SED_LIGHT_MAGENTA}," | sed -${E} "s,$nosh_usrs,${SED_BLUE}," | sed "s,root,${SED_RED},"
|
||||
grep -Hn '^PATH=' /etc/crontab /etc/cron.d/* 2>/dev/null | sed -${E} "s,$Wfolders,${SED_RED_YELLOW},g"
|
||||
grep -RInE 'pg_basebackup|run-parts|crontab-ui' /etc/crontab /etc/cron.d /etc/anacrontab /var/spool/cron/crontabs /etc/incron.d /var/spool/incron 2>/dev/null | sed -${E} "s,$cronjobsB,${SED_RED},g" | sed -${E} "s,$Wfolders,${SED_RED_YELLOW},g"
|
||||
print_3title "Root cron literal log inputs (passive review)" "T1053.003"
|
||||
cron_log_input_probe /etc/crontab /etc/cron.d/* /var/spool/cron/crontabs/root /var/spool/cron/root
|
||||
cron_replaceable_script_probe /etc/crontab /etc/cron.d/* /var/spool/cron/crontabs/root /var/spool/cron/root
|
||||
cron_ansible_glob_probe /etc/crontab /etc/cron.d/* /var/spool/cron/crontabs/root /var/spool/cron/root
|
||||
echo "Only readable root entries were inspected; private crontabs and other schedules may be invisible."
|
||||
print_3title "GNU tar cron wildcard inputs (passive review)" "T1053.003"
|
||||
cron_tar_wildcard_probe /etc/crontab /etc/cron.d/* /var/spool/cron/crontabs/* /var/spool/cron/*
|
||||
echo "Only readable cron schedules were inspected; private tasks remain unknown."
|
||||
print_3title "Root cron process arguments (passive review)" "T1053.003"
|
||||
cron_process_args_probe /etc/crontab /etc/cron.d/* /var/spool/cron/crontabs/root /var/spool/cron/root
|
||||
echo "Only literal, readable root cron helpers were inspected; private tasks remain unknown."
|
||||
cron_chkrootkit_static_probe
|
||||
PG_BASEBACKUP_DESTS=
|
||||
check_pg_basebackup_boundary
|
||||
crontab -l -u "$USER" 2>/dev/null | tr -d "\r"
|
||||
ls -lR /usr/lib/cron/tabs/ /private/var/at/jobs /var/at/tabs/ /etc/periodic/ 2>/dev/null | sed -${E} "s,$cronjobsG,${SED_GREEN},g" | sed "s,$cronjobsB,${SED_RED},g" #MacOS paths
|
||||
atq 2>/dev/null
|
||||
|
||||
+164
-27
@@ -9,7 +9,7 @@
|
||||
# Functions Used: echo_not_found, print_2title, print_info, print_3title
|
||||
# Global Variables: $SEARCH_IN_FOLDER, $timersG
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $timer_unit, $timer_path, $timer_content, $exec_path, $timer_file, $line, $findings, $unit_path, $finding, $service_unit, $timer, $target_unit, $target_file
|
||||
# Generated Global Variables: $timer_unit, $timer_path, $exec_path, $timer_file, $line, $findings, $unit_path, $finding, $service_unit, $timer, $target_unit, $target_file, $timer_count, $timer_unit_property, $service_properties, $service_user, $service_user_seen, $dynamic_user, $exec_start, $property, $timer_restart_timeout, $timer_restart_queries, $timer_restart_targets, $restart_target, $restart_properties, $restart_user, $restart_user_seen, $restart_dynamic_user, $restart_root_dir, $restart_root_dir_seen, $restart_root_image, $restart_root_image_seen, $restart_exec, $restart_exec_path, $restart_script
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -21,17 +21,47 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
check_timer_content() {
|
||||
local timer="$1"
|
||||
local findings=""
|
||||
local restart_target restart_properties restart_user restart_user_seen
|
||||
local restart_dynamic_user restart_root_dir restart_root_dir_seen
|
||||
local restart_root_image restart_root_image_seen restart_exec
|
||||
local restart_exec_path restart_script
|
||||
|
||||
# Get the service unit this timer activates
|
||||
local service_unit=$(systemctl show "$timer" -p Unit 2>/dev/null | cut -d= -f2)
|
||||
local timer_unit_property=$(systemctl show "$timer" -p Unit 2>/dev/null)
|
||||
case "$timer_unit_property" in Unit=*) ;; *) return ;; esac
|
||||
local service_unit=${timer_unit_property#Unit=}
|
||||
if [ -n "$service_unit" ]; then
|
||||
# Read the service properties once per timer to keep the check cheap.
|
||||
local service_properties=$(systemctl show "$service_unit" -p User -p DynamicUser -p ExecStart 2>/dev/null)
|
||||
local service_user="" service_user_seen="" dynamic_user="" exec_start="" property
|
||||
while IFS= read -r property; do
|
||||
case "$property" in
|
||||
User=*) service_user=${property#User=}; service_user_seen=1 ;;
|
||||
DynamicUser=*) dynamic_user=${property#DynamicUser=} ;;
|
||||
ExecStart=*) exec_start=${property#ExecStart=} ;;
|
||||
esac
|
||||
done <<EOF
|
||||
$service_properties
|
||||
EOF
|
||||
|
||||
# Check if the service runs with elevated privileges
|
||||
if systemctl show "$service_unit" -p User 2>/dev/null | grep -q "root"; then
|
||||
findings="${findings}RUNS_AS_ROOT: Service runs as root\n"
|
||||
if [ -n "$service_user_seen" ]; then
|
||||
case "$service_user" in
|
||||
""|root|0)
|
||||
if [ -n "$service_user" ] || [ "$dynamic_user" != "yes" ]; then
|
||||
findings="${findings}RUNS_AS_ROOT: Service runs as root\n"
|
||||
fi ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Get the executable path
|
||||
local exec_path=$(systemctl show "$service_unit" -p ExecStart 2>/dev/null | cut -d= -f2 | cut -d' ' -f1)
|
||||
# systemctl show commonly formats ExecStart as { path=... ; argv[]=... }.
|
||||
local exec_path
|
||||
case "$exec_start" in
|
||||
*path=*) exec_path=${exec_start#*path=}
|
||||
exec_path=${exec_path%%[[:space:];]*}
|
||||
exec_path=${exec_path%\}} ;;
|
||||
*) exec_path=${exec_start%% *} ;;
|
||||
esac
|
||||
if [ -n "$exec_path" ]; then
|
||||
if [ -w "$exec_path" ]; then
|
||||
findings="${findings}WRITABLE_EXEC: Executable is writable: $exec_path\n"
|
||||
@@ -44,7 +74,7 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
fi
|
||||
|
||||
# Check for unsafe configurations
|
||||
if systemctl show "$service_unit" -p ExecStart 2>/dev/null | grep -qE '(chmod|chown|mount|sudo|su)'; then
|
||||
if printf '%s\n' "$exec_start" | grep -qE '(chmod|chown|mount|sudo|su)'; then
|
||||
findings="${findings}UNSAFE_CMD: Uses potentially dangerous commands\n"
|
||||
fi
|
||||
|
||||
@@ -52,6 +82,98 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
if [ -e "$exec_path" ] && [ "$(stat -c %a "$exec_path" 2>/dev/null)" = "777" ]; then
|
||||
findings="${findings}WEAK_PERMS: Executable has 777 permissions\n"
|
||||
fi
|
||||
|
||||
# Follow only a literal root timer service -> systemctl restart -> root
|
||||
# shell-script edge. Reuse the direct service properties above; query no
|
||||
# more than four distinct restart targets, with a one-second deadline.
|
||||
if [ -n "$timer_restart_timeout" ] && [ "$timer_restart_queries" -lt 4 ] &&
|
||||
[ -n "$service_user_seen" ]; then
|
||||
case "$service_user" in
|
||||
''|root|0)
|
||||
if [ -n "$service_user" ] || [ "$dynamic_user" != yes ]; then
|
||||
case "$exec_path" in
|
||||
/bin/systemctl|/usr/bin/systemctl)
|
||||
restart_target=$(printf '%s\n' "$exec_start" | awk -v exe="$exec_path" '
|
||||
{
|
||||
i = index($0, "argv[]=")
|
||||
if (!i) exit
|
||||
args = substr($0, i + 7)
|
||||
if (args !~ /[[:space:]];[[:space:]]/) exit
|
||||
sub(/[[:space:]];[[:space:]].*/, "", args)
|
||||
if (index(args, ";")) exit
|
||||
sub(/^[[:space:]]+/, "", args)
|
||||
sub(/[[:space:]]+$/, "", args)
|
||||
n = split(args, word, /[[:space:]]+/)
|
||||
if (n == 3 && word[1] == exe && word[2] == "restart" &&
|
||||
word[3] ~ /^[A-Za-z0-9_.@-]+\.service$/) print word[3]
|
||||
}
|
||||
')
|
||||
if [ -n "$restart_target" ]; then
|
||||
case " $timer_restart_targets" in
|
||||
*" $restart_target "*) ;;
|
||||
*)
|
||||
timer_restart_targets="$timer_restart_targets$restart_target "
|
||||
timer_restart_queries=$((timer_restart_queries + 1))
|
||||
if restart_properties=$("$timer_restart_timeout" 1 systemctl show "$restart_target" \
|
||||
-p User -p DynamicUser -p RootDirectory -p RootImage -p ExecStart 2>/dev/null); then
|
||||
restart_user='' restart_user_seen='' restart_dynamic_user=''
|
||||
restart_root_dir='' restart_root_dir_seen=''
|
||||
restart_root_image='' restart_root_image_seen='' restart_exec=''
|
||||
while IFS= read -r property; do
|
||||
case "$property" in
|
||||
User=*) restart_user=${property#User=}; restart_user_seen=1 ;;
|
||||
DynamicUser=*) restart_dynamic_user=${property#DynamicUser=} ;;
|
||||
RootDirectory=*) restart_root_dir=${property#RootDirectory=}; restart_root_dir_seen=1 ;;
|
||||
RootImage=*) restart_root_image=${property#RootImage=}; restart_root_image_seen=1 ;;
|
||||
ExecStart=*) restart_exec=${property#ExecStart=} ;;
|
||||
esac
|
||||
done <<EOF
|
||||
$restart_properties
|
||||
EOF
|
||||
if [ -n "$restart_user_seen" ] && [ -n "$restart_root_dir_seen" ] &&
|
||||
[ -n "$restart_root_image_seen" ] && [ "$restart_dynamic_user" != yes ] &&
|
||||
[ -z "$restart_root_image" ] &&
|
||||
{ [ -z "$restart_root_dir" ] || [ "$restart_root_dir" = / ]; }; then
|
||||
case "$restart_user" in
|
||||
''|root|0)
|
||||
restart_exec_path=${restart_exec#*path=}
|
||||
restart_exec_path=${restart_exec_path%%[[:space:];]*}
|
||||
case "$restart_exec_path" in
|
||||
/bin/bash|/usr/bin/bash|/bin/sh|/usr/bin/sh)
|
||||
restart_script=$(printf '%s\n' "$restart_exec" | awk -v exe="$restart_exec_path" '
|
||||
{
|
||||
i = index($0, "argv[]=")
|
||||
if (!i) exit
|
||||
args = substr($0, i + 7)
|
||||
if (args !~ /[[:space:]];[[:space:]]/) exit
|
||||
sub(/[[:space:]];[[:space:]].*/, "", args)
|
||||
if (index(args, ";")) exit
|
||||
sub(/^[[:space:]]+/, "", args)
|
||||
sub(/[[:space:]]+$/, "", args)
|
||||
n = split(args, word, /[[:space:]]+/)
|
||||
if (n == 2 && word[1] == exe &&
|
||||
word[2] ~ /^\/[A-Za-z0-9_.\/-]+$/) print word[2]
|
||||
}
|
||||
')
|
||||
if [ -n "$restart_script" ] && [ -f "$restart_script" ] &&
|
||||
[ ! -L "$restart_script" ] && [ -w "$restart_script" ]; then
|
||||
findings="${findings}WRITABLE_TIMER_SCRIPT: $restart_target invokes $restart_script through $restart_exec_path (root review candidate)\n"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
# If any findings, print them
|
||||
@@ -90,7 +212,7 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
fi
|
||||
|
||||
# Check for writable executables in Unit directive (following symlinks)
|
||||
local unit_path=$(grep -Po '^Unit=*(.*?$)' "$timer_file" 2>/dev/null | cut -d '=' -f2)
|
||||
local unit_path=$(sed -n 's/^Unit=//p' "$timer_file" 2>/dev/null | head -n 1)
|
||||
if [ -n "$unit_path" ]; then
|
||||
if [ -L "$unit_path" ]; then
|
||||
local target_unit=$(readlink -f "$unit_path")
|
||||
@@ -113,32 +235,47 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
|
||||
# List all timers and check for privilege escalation vectors
|
||||
print_3title "Active timers:" "T1053.003"
|
||||
systemctl list-timers --all 2>/dev/null | grep -Ev "(^$|timers listed)" | while read -r line; do
|
||||
# Extract timer unit name
|
||||
timer_unit=$(echo "$line" | awk '{print $1}')
|
||||
timer_count=0
|
||||
timer_restart_queries=0
|
||||
timer_restart_targets=''
|
||||
timer_restart_timeout=''
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timer_restart_timeout=$(command -v timeout)
|
||||
elif command -v gtimeout >/dev/null 2>&1; then
|
||||
timer_restart_timeout=$(command -v gtimeout)
|
||||
fi
|
||||
systemctl list-timers --all --no-pager --full 2>/dev/null | grep -Ev "(^$|timers listed)" | while IFS= read -r line; do
|
||||
# UNIT is the next-to-last column; NEXT can contain several date fields.
|
||||
timer_unit=$(printf '%s\n' "$line" | awk '$(NF-1) ~ /\.timer$/ { print $(NF-1) }')
|
||||
if [ -n "$timer_unit" ]; then
|
||||
# Check if timer file is writable
|
||||
timer_path=$(systemctl show "$timer_unit" -p FragmentPath 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$timer_path" ]; then
|
||||
check_timer_file "$timer_path"
|
||||
timer_count=$(( ${timer_count:-0} + 1 ))
|
||||
if [ "$timer_count" -le 200 ]; then
|
||||
# Check if timer file is writable
|
||||
timer_path=$(systemctl show "$timer_unit" -p FragmentPath 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$timer_path" ]; then
|
||||
check_timer_file "$timer_path"
|
||||
fi
|
||||
|
||||
# Check timer content for privilege escalation vectors
|
||||
check_timer_content "$timer_unit"
|
||||
fi
|
||||
|
||||
# Check timer content for privilege escalation vectors
|
||||
check_timer_content "$timer_unit"
|
||||
|
||||
# Print the timer line with highlighting
|
||||
echo "$line" | sed -${E} "s,$timersG,${SED_GREEN},"
|
||||
fi
|
||||
# Keep the list output, including its heading, in the existing format.
|
||||
echo "$line" | sed -${E} "s,$timersG,${SED_GREEN},"
|
||||
done || echo_not_found
|
||||
|
||||
# Check for disabled but available timers
|
||||
print_3title "Disabled timers:" "T1053.003"
|
||||
systemctl list-unit-files --type=timer --state=disabled 2>/dev/null | grep -v "UNIT FILE" | while read -r line; do
|
||||
timer_unit=$(echo "$line" | awk '{print $1}')
|
||||
timer_count=0
|
||||
systemctl list-unit-files --type=timer --state=disabled 2>/dev/null | grep -v "UNIT FILE" | while IFS= read -r line; do
|
||||
timer_unit=$(printf '%s\n' "$line" | awk '$1 ~ /\.timer$/ { print $1 }')
|
||||
if [ -n "$timer_unit" ]; then
|
||||
timer_path=$(systemctl show "$timer_unit" -p FragmentPath 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$timer_path" ]; then
|
||||
check_timer_file "$timer_path"
|
||||
timer_count=$(( ${timer_count:-0} + 1 ))
|
||||
if [ "$timer_count" -le 200 ]; then
|
||||
timer_path=$(systemctl show "$timer_unit" -p FragmentPath 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$timer_path" ]; then
|
||||
check_timer_file "$timer_path"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done || echo_not_found
|
||||
@@ -154,4 +291,4 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
fi
|
||||
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
@@ -9,7 +9,7 @@
|
||||
# Functions Used: print_2title, print_3title
|
||||
# Global Variables: $E, $SED_RED_YELLOW
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $iface, $state, $mac, $ip_file, $line
|
||||
# Generated Global Variables: $iface, $state, $mac, $ip_file, $line, $wireless_modes
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -74,6 +74,15 @@ else
|
||||
parse_network_interfaces
|
||||
fi
|
||||
|
||||
# A local nl80211 query shows AP/managed/monitor modes without scanning the air.
|
||||
if [ -d /sys/class/ieee80211 ] && command -v iw >/dev/null 2>&1; then
|
||||
wireless_modes="$(iw dev 2>/dev/null | head -n 100)"
|
||||
if [ -n "$wireless_modes" ]; then
|
||||
print_3title "Wireless interface modes (local inventory)" "T1016"
|
||||
printf '%s\n' "$wireless_modes"
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v ip >/dev/null 2>&1; then
|
||||
print_3title "Routing & policy quick view" "T1016"
|
||||
ip route 2>/dev/null
|
||||
|
||||
@@ -36,6 +36,9 @@ get_hostname_info() {
|
||||
get_hosts_info() {
|
||||
print_3title "Hosts File Information" "T1016,T1018"
|
||||
if [ -f "/etc/hosts" ]; then
|
||||
if test -w /etc/hosts; then
|
||||
echo "Writable /etc/hosts review candidate: current user can change local hostname resolution; verify a higher-privileged job resolves an affected hostname and executes or trusts the fetched result."
|
||||
fi
|
||||
echo "Contents of /etc/hosts:"
|
||||
grep -v "^#" /etc/hosts 2>/dev/null | grep -v "^$" | while read -r line; do
|
||||
echo " $line"
|
||||
@@ -103,4 +106,4 @@ print_2title "Hostname, hosts and DNS" "T1016,T1018"
|
||||
# Execute all information gathering functions
|
||||
get_hostname_info
|
||||
get_hosts_info
|
||||
get_dns_info
|
||||
get_dns_info
|
||||
@@ -1,15 +1,15 @@
|
||||
# Title: Network Information - Open ports
|
||||
# ID: NT_Open_ports
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Enumerate open ports
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1049
|
||||
# Functions Used: print_2title, print_3title, print_info
|
||||
# Global Variables: $E, $SED_RED, $SED_RED_YELLOW
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $pid_dir, $tx_queue, $pid, $rem_port, $proc_file, $rem_ip, $local_ip, $rx_queue, $proto, $rem_addr, $program, $state, $header_sep, $proc_info, $inode, $header, $line, $local_addr, $local_port
|
||||
# Generated Global Variables: $pid_dir, $tx_queue, $pid, $rem_port, $proc_file, $rem_ip, $local_ip, $rx_queue, $proto, $rem_addr, $program, $state, $header_sep, $proc_info, $inode, $header, $line, $local_addr, $local_port, $column, $lp_freebsd_netstat, $lp_freebsd_listeners
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -105,13 +105,49 @@ parse_proc_net_ports() {
|
||||
echo ""
|
||||
}
|
||||
|
||||
lp_loopback_listeners() {
|
||||
awk -v column="$1" '
|
||||
{
|
||||
address = tolower($column)
|
||||
if (address ~ /^127\.[0-9]+\.[0-9]+\.[0-9]+[.:][0-9]+$/ ||
|
||||
address ~ /^\[?::1\]?[.:][0-9]+$/ ||
|
||||
address ~ /^\[?::ffff:127\.[0-9]+\.[0-9]+\.[0-9]+\]?[.:][0-9]+$/)
|
||||
print
|
||||
}'
|
||||
}
|
||||
|
||||
# FreeBSD netstat uses a dot before the port and does not accept Linux's
|
||||
# -punta option set. Keep only TCP LISTEN records and flag partial output.
|
||||
lp_freebsd_tcp_listeners() {
|
||||
netstat -an -p tcp 2>/dev/null | awk '
|
||||
tolower($1) ~ /^tcp(4|6)?$/ && toupper($NF) == "LISTEN" {
|
||||
if (++seen <= 256) print
|
||||
else if (seen == 257) {
|
||||
print "__LP_FREEBSD_PARTIAL__"
|
||||
exit
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
# Function to get open ports information
|
||||
get_open_ports() {
|
||||
print_2title "Active Ports" "T1049"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/network-information/local-network-and-socket-triage.html#loopback-and-local-service-enumeration"
|
||||
|
||||
lp_freebsd_netstat=
|
||||
if command -v netstat >/dev/null 2>&1 && [ "$(uname -s 2>/dev/null)" = FreeBSD ]; then
|
||||
lp_freebsd_netstat=1
|
||||
lp_freebsd_listeners=$(lp_freebsd_tcp_listeners)
|
||||
fi
|
||||
|
||||
# Try standard tools first
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
if [ "$lp_freebsd_netstat" ]; then
|
||||
print_3title "Active Ports (FreeBSD netstat, TCP)" "T1049"
|
||||
printf '%s\n' "$lp_freebsd_listeners" | grep -v '^__LP_FREEBSD_PARTIAL__$' | sed -${E} "s,127\.[0-9]+\.[0-9]+\.[0-9]+|::1|0\.0\.0\.0,${SED_RED},g"
|
||||
case "$lp_freebsd_listeners" in
|
||||
*"__LP_FREEBSD_PARTIAL__"*) echo "FreeBSD TCP listener inventory incomplete (256-entry cap)." ;;
|
||||
esac
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
print_3title "Active Ports (netstat)" "T1049"
|
||||
netstat -punta 2>/dev/null | grep -i listen | sed -${E} "s,127.0.[0-9]+.[0-9]+|:::|::1:|0\.0\.0\.0,${SED_RED},g"
|
||||
elif command -v ss >/dev/null 2>&1; then
|
||||
@@ -125,14 +161,22 @@ get_open_ports() {
|
||||
|
||||
# Focused local service exposure view
|
||||
print_3title "Local-only listeners (loopback)" "T1049"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -nltpu 2>/dev/null | grep -E "127\.0\.0\.1:|::1:" | sed -${E} "s,127\.0\.0\.1:|::1:,${SED_RED},g"
|
||||
if [ "$lp_freebsd_netstat" ]; then
|
||||
printf '%s\n' "$lp_freebsd_listeners" | lp_loopback_listeners 4 | sed -${E} "s,127\.[0-9]+\.[0-9]+\.[0-9]+|::1,${SED_RED},g"
|
||||
elif command -v ss >/dev/null 2>&1; then
|
||||
ss -nltpu 2>/dev/null | lp_loopback_listeners 5 | sed -${E} "s,127\.[0-9]+\.[0-9]+\.[0-9]+|::1,${SED_RED},g"
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
netstat -punta 2>/dev/null | grep -i listen | grep -E "127\.0\.0\.1:|::1:" | sed -${E} "s,127\.0\.0\.1:|::1:,${SED_RED},g"
|
||||
netstat -punta 2>/dev/null | grep -i listen | lp_loopback_listeners 4 | sed -${E} "s,127\.[0-9]+\.[0-9]+\.[0-9]+|::1,${SED_RED},g"
|
||||
fi
|
||||
|
||||
print_3title "Unique listener bind addresses" "T1049"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
if [ "$lp_freebsd_netstat" ]; then
|
||||
printf '%s\n' "$lp_freebsd_listeners" | awk 'toupper($NF) == "LISTEN" {
|
||||
a=$4
|
||||
sub(/[.][0-9]+$/, "", a)
|
||||
if (a != "") print a
|
||||
}' | sort -u | sed -${E} "s,127\.0\.0\.1|::1,${SED_RED},g"
|
||||
elif command -v ss >/dev/null 2>&1; then
|
||||
ss -nltpuH 2>/dev/null | awk '{
|
||||
a=$5
|
||||
if (a ~ /^\[/) {
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1049
|
||||
# Functions Used: print_2title, print_3title, warn_exec, echo_not_found
|
||||
# Functions Used: print_2title, print_3title, warn_exec, echo_not_found, lp_trusted_version_path
|
||||
# Global Variables: $EXTRA_CHECKS, $E, $SED_RED, $SED_GREEN, $SED_YELLOW
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $inetd_service, $log_file, $cmd, $service_name, $conf_file, $service_dir, $service_file, $inetd_file
|
||||
# Generated Global Variables: $inetd_service, $log_file, $cmd, $service_name, $conf_file, $service_dir, $service_file, $file, $inetd_command, $inetd_telnet_probe_count, $service, $socket, $protocol, $user, $server, $checked_binary, $trusted_server, $version_output, $version, $major, $minor, $patch, $rest
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
@@ -24,23 +24,95 @@ check_command() {
|
||||
return 1
|
||||
}
|
||||
|
||||
inetd_telnet_upstream_affected() {
|
||||
local major=$1 minor=$2 patch=$3
|
||||
if [ "$major" -eq 1 ]; then
|
||||
[ "$minor" -gt 9 ] || { [ "$minor" -eq 9 ] && [ "$patch" -ge 3 ]; }
|
||||
elif [ "$major" -eq 2 ]; then
|
||||
[ "$minor" -lt 7 ] || { [ "$minor" -eq 7 ] && [ "$patch" -eq 0 ]; }
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Inspect only configured root telnet stanzas. A version is an upstream hint,
|
||||
# not proof that a downstream package is unpatched or that a listener is active.
|
||||
inetd_telnet_cve_hint() {
|
||||
local conf_file=$1 service socket protocol wait_mode user server arguments
|
||||
local checked_binary='' version_output='' version='' major minor patch rest trusted_server
|
||||
|
||||
[ -f "$conf_file" ] || return 0
|
||||
while read -r service socket protocol wait_mode user server arguments || [ -n "$service" ]; do
|
||||
[ "$service" = telnet ] && [ "$socket" = stream ] || continue
|
||||
case "$protocol" in tcp*) ;; *) continue ;; esac
|
||||
case "$user" in root|root:*|root.*|0|0:*|0.*) ;; *) continue ;; esac
|
||||
[ -n "$server" ] || continue
|
||||
|
||||
if [ "$server" != "$checked_binary" ]; then
|
||||
checked_binary=$server
|
||||
version_output=''
|
||||
# A non-GNU daemon may not support --version or honor SIGTERM.
|
||||
# Bound both the process lifetime and captured output.
|
||||
case "$server" in
|
||||
/*) if [ -x "$server" ] && [ "${inetd_telnet_probe_count:-0}" -lt 4 ] && command -v timeout >/dev/null 2>&1; then
|
||||
inetd_telnet_probe_count=$((inetd_telnet_probe_count + 1))
|
||||
if trusted_server=$(lp_trusted_version_path "$server"); then
|
||||
version_output=$(timeout -k 1 1 "$trusted_server" --version 2>&1 | head -c 4096)
|
||||
fi
|
||||
fi ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if printf '%s\n' "$version_output" | grep -qi 'GNU inetutils'; then
|
||||
version=$(printf '%s\n' "$version_output" | sed -n 's/.*[Ii]netutils[^0-9]*\([0-9][0-9.]*\).*/\1/p' | head -n 1)
|
||||
case "$version" in *.*) ;; *) version='' ;; esac
|
||||
case "$version" in *[!0-9.]*) version='' ;; esac
|
||||
if [ -n "$version" ]; then
|
||||
major=${version%%.*}
|
||||
rest=${version#*.}
|
||||
minor=${rest%%.*}
|
||||
patch=0
|
||||
[ "$rest" = "$minor" ] || patch=${rest#*.}
|
||||
case "$major:$minor:$patch" in *[!0-9:]*|*::*|:*|*:) version='' ;; esac
|
||||
fi
|
||||
if [ -n "$version" ] && inetd_telnet_upstream_affected "$major" "$minor" "$patch"; then
|
||||
echo "CVE-2026-24061: GNU Inetutils telnetd $version in upstream affected range (1.9.3-2.7), root inetd stanza in $conf_file; confirm package patches and active listener."
|
||||
elif [ -n "$version" ]; then
|
||||
echo "CVE-2026-24061: GNU Inetutils telnetd $version outside upstream affected range, root inetd stanza in $conf_file."
|
||||
else
|
||||
echo "CVE-2026-24061: GNU Inetutils telnetd version unknown, root inetd stanza in $conf_file; inspect package and listener."
|
||||
fi
|
||||
elif printf '%s\n' "$version_output" | grep -qiE 'BusyBox|NetKit|OpenBSD|FreeBSD|BSD telnetd'; then
|
||||
echo "CVE-2026-24061: configured telnetd appears non-GNU, root inetd stanza in $conf_file."
|
||||
else
|
||||
echo "CVE-2026-24061: telnetd implementation/version unknown, root inetd stanza in $conf_file; inspect package and listener."
|
||||
fi
|
||||
done < "$conf_file"
|
||||
}
|
||||
|
||||
# Function to analyze inetd services
|
||||
analyze_inetd() {
|
||||
local inetd_command
|
||||
inetd_telnet_probe_count=0
|
||||
echo ""
|
||||
print_3title "Inetd Services" "T1049"
|
||||
# Check if inetd is installed
|
||||
if ! check_command inetd; then
|
||||
if check_command inetd; then
|
||||
inetd_command=inetd
|
||||
elif check_command inetutils-inetd; then
|
||||
inetd_command=inetutils-inetd
|
||||
else
|
||||
echo_not_found "inetd"
|
||||
return
|
||||
fi
|
||||
|
||||
# Check if inetd is running
|
||||
if ! pgrep -x inetd >/dev/null 2>&1; then
|
||||
if ! pgrep -x inetd >/dev/null 2>&1 && ! pgrep -x inetutils-inetd >/dev/null 2>&1; then
|
||||
echo "inetd is not running" | sed -${E} "s,.*,${SED_YELLOW},g"
|
||||
fi
|
||||
|
||||
# Get inetd version
|
||||
warn_exec inetd -v 2>/dev/null
|
||||
warn_exec "$inetd_command" -v 2>/dev/null
|
||||
|
||||
# Check main configuration file
|
||||
if [ -f "/etc/inetd.conf" ]; then
|
||||
@@ -62,6 +134,11 @@ analyze_inetd() {
|
||||
warn_exec cat "$conf_file" | grep -v "^$" | grep -Ev "\W+\#|^#" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# Keep the existing configuration output and add a local, passive CVE hint.
|
||||
for conf_file in /etc/inetd.conf /etc/inetd.d/* /etc/inet/*.conf; do
|
||||
inetd_telnet_cve_hint "$conf_file"
|
||||
done
|
||||
}
|
||||
|
||||
# Function to analyze xinetd services
|
||||
@@ -174,9 +251,9 @@ analyze_inetd_services() {
|
||||
# Check for inetd/xinetd related files
|
||||
echo -e "\nChecking for related files..."
|
||||
for file in /etc/init.d/inetd /etc/init.d/xinetd /etc/default/inetd /etc/default/xinetd; do
|
||||
if [ -f "$inetd_file" ]; then
|
||||
echo "Found file: $inetd_file" | sed -${E} "s,.*,${SED_GREEN},g"
|
||||
warn_exec cat "$inetd_file" | grep -v "^$" | grep -Ev "\W+\#|^#" 2>/dev/null
|
||||
if [ -f "$file" ]; then
|
||||
echo "Found file: $file" | sed -${E} "s,.*,${SED_GREEN},g"
|
||||
warn_exec cat "$file" | grep -v "^$" | grep -Ev "\W+\#|^#" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
fi
|
||||
@@ -185,4 +262,4 @@ analyze_inetd_services() {
|
||||
}
|
||||
|
||||
# Run the main function
|
||||
analyze_inetd_services
|
||||
analyze_inetd_services
|
||||
@@ -1,15 +1,15 @@
|
||||
# Title: Users Information - Pkexec
|
||||
# ID: UG_Pkexec
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Check Pkexec policy and related files for privilege escalation
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1548.003,T1548.004,T1068
|
||||
# Functions Used: print_2title, print_info
|
||||
# Global Variables: $Groups, $groupsB, $groupsVB, $nosh_usrs, $sh_usrs, $USER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $pkexec_bin, $pkexec_version, $policy_dir, $policy_file
|
||||
# Generated Global Variables: $pkexec_bin, $pkexec_owner, $pkexec_version_output, $pkexec_version, $pkexec_major, $pkexec_minor, $policy_dir, $policy_file
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -27,14 +27,24 @@ if [ -n "$pkexec_bin" ]; then
|
||||
echo "Pkexec binary has SUID bit set!" | sed -${E} "s,.*,${SED_RED},g"
|
||||
fi
|
||||
ls -l "$pkexec_bin" 2>/dev/null
|
||||
|
||||
# Check polkit version for known vulnerabilities
|
||||
if command -v pkexec >/dev/null 2>&1; then
|
||||
pkexec --version 2>/dev/null
|
||||
pkexec_version="$(pkexec --version 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+)+')"
|
||||
if [ "$pkexec_version" ] && [ "$(printf '%s\n' "$pkexec_version" "0.120" | sort -V | head -n1)" = "$pkexec_version" ] && [ "$pkexec_version" != "0.120" ]; then
|
||||
echo "Potentially vulnerable to CVE-2021-4034 (PwnKit) - check distro patches" | sed -${E} "s,.*,${SED_RED_YELLOW},"
|
||||
fi
|
||||
|
||||
# The local escalation requires an executable root-owned SUID pkexec.
|
||||
pkexec_owner=$(LC_ALL=C ls -ldnL "$pkexec_bin" 2>/dev/null | awk 'NR == 1 { print $3 }')
|
||||
if [ -f "$pkexec_bin" ] && [ -x "$pkexec_bin" ] && [ -u "$pkexec_bin" ] && [ "$pkexec_owner" = 0 ]; then
|
||||
pkexec_version_output=$(pkexec --version 2>/dev/null)
|
||||
if [ -n "$pkexec_version_output" ]; then printf '%s\n' "$pkexec_version_output"; fi
|
||||
pkexec_version=$(printf '%s\n' "$pkexec_version_output" | awk 'match($0, /[0-9]+([.][0-9]+)+/) { print substr($0, RSTART, RLENGTH); exit }')
|
||||
pkexec_major=${pkexec_version%%.*}
|
||||
pkexec_minor=${pkexec_version#*.}
|
||||
pkexec_minor=${pkexec_minor%%.*}
|
||||
case "$pkexec_major:$pkexec_minor" in
|
||||
*[!0-9:]*|:|*:) ;;
|
||||
*) if [ "$pkexec_major" -eq 0 ] && [ "$pkexec_minor" -lt 120 ]; then
|
||||
echo "CVE-2021-4034 candidate: root-owned SUID pkexec with older upstream version; verify distro package fixes and mitigations" | sed -${E} "s,.*,${SED_RED_YELLOW},"
|
||||
fi ;;
|
||||
esac
|
||||
else
|
||||
echo "Pkexec is not an executable root-owned SUID binary; local SUID exploit candidate suppressed"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,32 +1,62 @@
|
||||
# Title: Users Information - Superusers
|
||||
# ID: UG_Superusers
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Check for superusers and users with UID 0
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Check UID 0 accounts and bounded shared local UIDs
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1087.001
|
||||
# Functions Used: print_2title, print_info
|
||||
# Global Variables: $knw_usrs, $nosh_usrs, $sh_usrs, $USER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $group
|
||||
# Generated Global Variables: $group, $group_entry, $passwd_identity_rows, $shared_uid_rows
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
# Collect the existing UID 0 inventory and a bounded local duplicate-UID cue
|
||||
# in one read of /etc/passwd. A shared UID is a review lead, not root access.
|
||||
summarize_local_passwd() {
|
||||
awk -F: '
|
||||
$3 ~ /^[0-9]+$/ && $3 + 0 == 0 { print "R\t" $0 }
|
||||
NR == 4097 { print "P" }
|
||||
NR <= 4096 && $1 != "" && $3 ~ /^[0-9]+$/ && $3 + 0 > 0 {
|
||||
uid = $3 + 0
|
||||
if (uid in first) {
|
||||
if (++duplicates <= 32) print "D\t" uid "\t" first[uid] "\t" $1
|
||||
else if (duplicates == 33) print "L"
|
||||
} else first[uid] = $1
|
||||
}
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
print_2title "Superusers and UID 0 Users" "T1087.001"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/user-information/interesting-groups-linux-pe/index.html"
|
||||
passwd_identity_rows=$(summarize_local_passwd /etc/passwd)
|
||||
|
||||
# Check /etc/passwd for UID 0 users
|
||||
echo ""
|
||||
print_3title "Users with UID 0 in /etc/passwd" "T1087.001"
|
||||
awk -F: '($3 == "0") {print}' /etc/passwd 2>/dev/null | sed -${E} "s,$sh_usrs,${SED_LIGHT_CYAN},g" | sed -${E} "s,$nosh_usrs,${SED_BLUE},g" | sed -${E} "s,$knw_usrs,${SED_GREEN},g" | sed "s,$USER,${SED_RED_YELLOW},g" | sed "s,root,${SED_RED},g"
|
||||
printf '%s\n' "$passwd_identity_rows" | awk -F '\t' '$1 == "R" { sub(/^R\t/, ""); print }' | sed -${E} "s,$sh_usrs,${SED_LIGHT_CYAN},g" | sed -${E} "s,$nosh_usrs,${SED_BLUE},g" | sed -${E} "s,$knw_usrs,${SED_GREEN},g" | sed "s,$USER,${SED_RED_YELLOW},g" | sed "s,root,${SED_RED},g"
|
||||
|
||||
if [ command -v getent >/dev/null 2>&1 ]; then
|
||||
shared_uid_rows=$(printf '%s\n' "$passwd_identity_rows" | awk -F '\t' '
|
||||
$1 == "D" { printf "UID %s: %s, %s\n", $2, $3, $4 }
|
||||
$1 == "L" { print "Partial: first 32 shared-UID pairs shown." }
|
||||
')
|
||||
if [ -n "$shared_uid_rows" ]; then
|
||||
echo ""
|
||||
print_3title "Accounts sharing nonzero local UIDs (review candidate)" "T1087.001"
|
||||
printf '%s\n' "$shared_uid_rows"
|
||||
echo "Shared UIDs can be intentional; verify account ownership, authentication, and local policy."
|
||||
fi
|
||||
if printf '%s\n' "$passwd_identity_rows" | grep -q '^P$'; then
|
||||
echo "Shared-UID comparison partial: only the first 4096 local accounts were compared."
|
||||
fi
|
||||
|
||||
if command -v getent >/dev/null 2>&1; then
|
||||
for group in sudo wheel adm docker lxd lxc root shadow disk video; do
|
||||
if getent group "$group" >/dev/null 2>&1; then
|
||||
if group_entry=$(getent group "$group" 2>/dev/null) && [ -n "$group_entry" ]; then
|
||||
echo "- Users in group '$group':"
|
||||
getent group "$group" 2>/dev/null | sed -${E} "s,$sh_usrs,${SED_LIGHT_CYAN},g" | sed -${E} "s,$nosh_usrs,${SED_BLUE},g" | sed -${E} "s,$knw_usrs,${SED_GREEN},g" | sed "s,$USER,${SED_RED},g" | sed "s,root,${SED_RED},g"
|
||||
printf '%s\n' "$group_entry" | sed -${E} "s,$sh_usrs,${SED_LIGHT_CYAN},g" | sed -${E} "s,$nosh_usrs,${SED_BLUE},g" | sed -${E} "s,$knw_usrs,${SED_GREEN},g" | sed "s,$USER,${SED_RED},g" | sed "s,root,${SED_RED},g"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
# Version: 1.0
|
||||
# Mitre: T1069.001
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $groupsVB, $groupsB, $Groups
|
||||
# Global Variables: $groupsVB, $groupsB
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $ActualGroup, $groupname, $gid, $result
|
||||
# Generated Global Variables: $ActualGroup, $groupname, $gid, $result, $actual_group_timeout, $actual_group_deadline, $actual_group_gids
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -20,15 +20,35 @@ print_2title "Actual Group Memberships via newgrp" "T1069.001"
|
||||
if [ "${IAMROOT:-0}" != "1" ]; then
|
||||
ActualGroup="|"
|
||||
|
||||
while IFS=: read -r groupname _ gid _; do
|
||||
result=$(timeout 1 sh -c "echo id | newgrp \"$groupname\"" 2>/dev/null)
|
||||
if echo "$result" | grep -q "uid="; then
|
||||
if ! echo "${Groups}|" | grep -Fq "|${groupname}|"; then
|
||||
# A password prompt can read /dev/tty rather than stdin. Never run newgrp
|
||||
# without a watchdog; newer OpenBSD and GNU provide compatible timeout -k.
|
||||
actual_group_timeout=""
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
actual_group_timeout=timeout
|
||||
elif command -v gtimeout >/dev/null 2>&1; then
|
||||
actual_group_timeout=gtimeout
|
||||
fi
|
||||
|
||||
if [ -n "$actual_group_timeout" ] && command -v newgrp >/dev/null 2>&1; then
|
||||
actual_group_gids=" $(id -G 2>/dev/null) "
|
||||
actual_group_deadline=$(($(date +%s) + 15))
|
||||
|
||||
while IFS=: read -r groupname _ gid _; do
|
||||
[ "$(date +%s)" -lt "$actual_group_deadline" ] || break
|
||||
case "$gid" in ''|*[!0-9]*) continue ;; esac
|
||||
case "$actual_group_gids" in *" $gid "*) continue ;; esac
|
||||
|
||||
# Check the effective GID after newgrp: it can start a shell even
|
||||
# when the group switch failed. Pass the name as data, not shell code.
|
||||
result=$(printf 'id -g\n' | "$actual_group_timeout" -k 1 1 newgrp "$groupname" 2>/dev/null)
|
||||
if [ "$result" = "$gid" ]; then
|
||||
ActualGroup="${ActualGroup}${groupname}|"
|
||||
echo "Accessible group not shown in id: $groupname (gid=$gid)" | sed -${E} "s,$groupsVB,${SED_RED_YELLOW},g" | sed -${E} "s,$groupsB,${SED_RED},g"
|
||||
# groupsVB/groupsB are populated by the builder's variables module.
|
||||
# shellcheck disable=SC2154
|
||||
echo "Accessible group not shown in id: $groupname (gid=$gid)" | sed -"${E}" "s,$groupsVB,${SED_RED_YELLOW},g" | sed -"${E}" "s,$groupsB,${SED_RED},g"
|
||||
fi
|
||||
fi
|
||||
done < /etc/group
|
||||
done < /etc/group
|
||||
fi
|
||||
|
||||
echo ""
|
||||
fi
|
||||
@@ -9,7 +9,7 @@
|
||||
# Functions Used: echo_not_found, print_2title, print_info
|
||||
# Global Variables: $HOME
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $pgp_file
|
||||
# Generated Global Variables: $pgp_file, $pgp_walk, $pgp_key_count, $pgp_home_count, $pgp_home, $pgp_gnupg, $pgp_keydir, $pgp_nearby, $pgp_ciphertext, $pgp_key
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -17,6 +17,62 @@
|
||||
print_2title "PGP Keys and Related Files" "T1552.004"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/user-information/user-and-session-triage.html#review-user-artifacts"
|
||||
|
||||
# Review direct GnuPG private-key paths in login homes. Never open key material.
|
||||
# Input is a newline-separated list of home paths; the caller caps that list.
|
||||
pgp_path_has_no_symlinks() {
|
||||
pgp_walk=$1
|
||||
while [ "$pgp_walk" != / ]; do
|
||||
[ ! -L "$pgp_walk" ] || return 1
|
||||
pgp_walk=${pgp_walk%/*}
|
||||
[ -n "$pgp_walk" ] || pgp_walk=/
|
||||
done
|
||||
}
|
||||
|
||||
pgp_private_key_candidates() {
|
||||
pgp_key_count=0
|
||||
pgp_home_count=0
|
||||
while IFS= read -r pgp_home; do
|
||||
[ "$pgp_key_count" -lt 24 ] && [ "$pgp_home_count" -lt 64 ] || break
|
||||
pgp_home_count=$((pgp_home_count + 1))
|
||||
case "$pgp_home" in
|
||||
/*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
case "$pgp_home/" in
|
||||
*'/../'*|*'/./'*|*'//'*) continue ;;
|
||||
esac
|
||||
[ "$pgp_home" != "$HOME" ] || continue
|
||||
pgp_gnupg="$pgp_home/.gnupg"
|
||||
pgp_keydir="$pgp_gnupg/private-keys-v1.d"
|
||||
# Reject symlinks at each path component we intentionally enter.
|
||||
[ -d "$pgp_home" ] && [ -x "$pgp_home" ] && pgp_path_has_no_symlinks "$pgp_home" || continue
|
||||
[ -d "$pgp_gnupg" ] && [ -x "$pgp_gnupg" ] && [ ! -L "$pgp_gnupg" ] || continue
|
||||
[ -d "$pgp_keydir" ] && [ -x "$pgp_keydir" ] && [ ! -L "$pgp_keydir" ] || continue
|
||||
|
||||
pgp_nearby="no"
|
||||
for pgp_ciphertext in "$pgp_home"/*.gpg "$pgp_home"/backup/*.gpg; do
|
||||
case "$pgp_ciphertext" in
|
||||
"$pgp_home"/backup/*)
|
||||
[ -d "$pgp_home/backup" ] && [ -x "$pgp_home/backup" ] && [ ! -L "$pgp_home/backup" ] || continue ;;
|
||||
esac
|
||||
if [ -f "$pgp_ciphertext" ] && [ -r "$pgp_ciphertext" ] && [ ! -L "$pgp_ciphertext" ]; then
|
||||
pgp_nearby="yes: $pgp_ciphertext"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Only direct children, with a global output cap; do not traverse homes.
|
||||
for pgp_key in "$pgp_keydir"/*.key; do
|
||||
[ "$pgp_key_count" -lt 24 ] || break
|
||||
[ -f "$pgp_key" ] && [ -r "$pgp_key" ] && [ ! -L "$pgp_key" ] || continue
|
||||
printf 'Readable GnuPG private key (review candidate): %s\n' "$pgp_key"
|
||||
ls -ld "$pgp_key" 2>/dev/null
|
||||
printf ' Readable nearby .gpg ciphertext: %s\n' "$pgp_nearby"
|
||||
pgp_key_count=$((pgp_key_count + 1))
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# Check for GPG
|
||||
echo "GPG:" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g"
|
||||
if command -v gpg >/dev/null 2>&1; then
|
||||
@@ -47,4 +103,9 @@ for pgp_file in "$HOME/.gnupg" "$HOME/.pgp" "$HOME/.openpgp" "$HOME/.ssh/gpg-age
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
# Only login homes from the account database; at most 64 distinct homes.
|
||||
echo "Cross-user GnuPG private-key paths (metadata only, limit 24):" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g"
|
||||
awk -F: '$6 ~ /^\// && $7 !~ /(nologin|false)$/ && $6 != "/" && $6 != "/nonexistent" && !seen[$6]++ { print $6; if (++n == 64) exit }' /etc/passwd 2>/dev/null | pgp_private_key_candidates
|
||||
echo ""
|
||||
File diff suppressed because it is too large.
Load diff
@@ -19,6 +19,7 @@ print_info "https://book.hacktricks.wiki/en/linux-hardening/linux-basics/linux-p
|
||||
ptrace_scope="$(cat /proc/sys/kernel/yama/ptrace_scope 2>/dev/null)"
|
||||
if [ "$ptrace_scope" ] && [ "$ptrace_scope" -eq 0 ]; then
|
||||
echo "ptrace protection is disabled (0), so sudo tokens could be abused" | sed "s,is disabled,${SED_RED},g";
|
||||
echo "Same-UID process input or memory may also expose vault passwords or reused credentials if the target is dumpable and other access controls permit tracing"
|
||||
|
||||
if [ "$(command -v gdb 2>/dev/null || echo -n '')" ]; then
|
||||
echo "gdb was found in PATH" | sed -${E} "s,.*,${SED_RED},g";
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Title: Users Information - Doas
|
||||
# ID: UG_Doas
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 11-08-2026
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Check doas/OpenDoas configuration, effective rules, binary permissions, and known vulnerable versions.
|
||||
# Detects unrestricted and nopass root rules, GTFOBins-capable commands (including the HTB Soccer dstat path), dangerous environment preservation, writable configuration paths, and applicable CVEs.
|
||||
# Detects unrestricted and nopass root rules, GTFOBins-capable commands, writable dstat plugin directories, dangerous environment preservation, writable configuration paths, and applicable CVEs.
|
||||
# License: GNU GPL
|
||||
# Version: 1.1
|
||||
# Version: 1.2
|
||||
# Mitre: T1548.003
|
||||
# Functions Used: doas_check_command, doas_command_is_dangerous, doas_config_syntax_valid, doas_extract_upstream_version, doas_get_package_details, doas_read_rules, doas_rule_applies_to_current_user, doas_rule_command, doas_rule_has_dangerous_environment, doas_rule_has_option, doas_rule_targets_root, doas_version_ge, doas_version_le, doas_version_lt, echo_not_found, print_2title, print_3title, print_info
|
||||
# Global Variables: $doas_package_full_version, $doas_package_homepage, $doas_package_implementation, $doas_package_manager, $doas_package_name
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $conf_file, $doas_active_rules, $doas_bin, $doas_bin_mode, $doas_bin_owner, $doas_bin_trusted, $doas_conf_candidates, $doas_conf_dir, $doas_conf_dir_mode, $doas_conf_found, $doas_conf_mode, $doas_conf_owner, $doas_conf_trusted, $doas_current_gids, $doas_current_groups, $doas_current_uid, $doas_current_user, $doas_package_label, $doas_rule_applies, $doas_rule_cmd_value, $doas_rule_dangerous, $doas_rule_env, $doas_rule_line, $doas_rule_nopass, $doas_rule_number, $doas_rule_root, $doas_rule_unrestricted, $doas_seen_configs, $doas_seen_test_commands, $doas_strings_conf, $doas_test_cmd, $doas_test_commands, $doas_check_output, $doas_tiocsti, $doas_upstream_version
|
||||
# Generated Global Variables: $conf_file, $doas_active_rules, $doas_bin, $doas_bin_mode, $doas_bin_owner, $doas_bin_trusted, $doas_conf_candidates, $doas_conf_dir, $doas_conf_dir_mode, $doas_conf_found, $doas_conf_mode, $doas_conf_owner, $doas_conf_trusted, $doas_current_gids, $doas_current_groups, $doas_current_uid, $doas_current_user, $doas_package_label, $doas_rule_applies, $doas_rule_cmd_value, $doas_rule_dangerous, $doas_rule_env, $doas_rule_line, $doas_rule_nopass, $doas_rule_number, $doas_rule_root, $doas_rule_unrestricted, $doas_seen_configs, $doas_seen_test_commands, $doas_strings_conf, $doas_test_cmd, $doas_test_commands, $doas_check_output, $doas_tiocsti, $doas_upstream_version, $doas_dstat_plugin_dir, $doas_dstat_checked
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -21,6 +21,21 @@ doas_current_uid="$(id -u 2>/dev/null)"
|
||||
doas_current_groups="$(id -Gn 2>/dev/null)"
|
||||
doas_current_gids="$(id -G 2>/dev/null)"
|
||||
doas_bin_trusted="no"
|
||||
doas_dstat_checked=""
|
||||
|
||||
# Dstat implementations differ: this checks only two documented global
|
||||
# plugin locations, and only current-user write/search rights. It does not
|
||||
# inspect plugin content or invoke the privileged program.
|
||||
doas_dstat_plugin_review() {
|
||||
[ "$doas_current_uid" != "0" ] || return 0
|
||||
for doas_dstat_plugin_dir in "$@"; do
|
||||
[ -d "$doas_dstat_plugin_dir" ] &&
|
||||
[ -w "$doas_dstat_plugin_dir" ] &&
|
||||
[ -x "$doas_dstat_plugin_dir" ] || continue
|
||||
printf 'Writable dstat plugin directory candidate: %s (review installed plugin search path, doas policy, and whether a selected plugin is loaded)\n' "$doas_dstat_plugin_dir"
|
||||
ls -ld "$doas_dstat_plugin_dir" 2>/dev/null || :
|
||||
done
|
||||
}
|
||||
|
||||
doas_conf_candidates="/etc/doas.conf
|
||||
/usr/local/etc/doas.conf
|
||||
@@ -180,9 +195,6 @@ if [ -n "$doas_bin" ] || [ "$doas_conf_found" = "yes" ]; then
|
||||
echo "POTENTIAL: a matching permit rule allows arbitrary root commands after authentication; a later rule may override it" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
|
||||
elif [ "$doas_rule_dangerous" = "yes" ] && [ "$doas_rule_nopass" = "yes" ]; then
|
||||
echo "POTENTIAL: a matching permit rule allows GTFOBins-capable command $doas_rule_cmd_value as root without a password; a later rule may override it" | sed -${E} "s,.*,${SED_RED},g"
|
||||
if [ "${doas_rule_cmd_value##*/}" = "dstat" ]; then
|
||||
echo "This is the HTB Soccer privilege-escalation pattern: a user-controlled dstat plugin can execute as root" | sed -${E} "s,.*,${SED_RED},g"
|
||||
fi
|
||||
elif [ "$doas_rule_dangerous" = "yes" ]; then
|
||||
echo "POTENTIAL: a matching permit rule allows GTFOBins-capable command $doas_rule_cmd_value as root after authentication; a later rule may override it" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
|
||||
elif [ "$doas_rule_nopass" = "yes" ]; then
|
||||
@@ -191,6 +203,11 @@ if [ -n "$doas_bin" ] || [ "$doas_conf_found" = "yes" ]; then
|
||||
if [ "$doas_rule_env" = "yes" ]; then
|
||||
echo "Dangerous environment preservation is enabled for an applicable root rule (keepenv or sensitive setenv variable)" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
|
||||
fi
|
||||
if [ "${doas_rule_cmd_value##*/}" = "dstat" ] && [ -z "$doas_dstat_checked" ]; then
|
||||
doas_dstat_checked=1
|
||||
doas_dstat_plugin_review /usr/local/share/dstat /usr/share/dstat |
|
||||
sed -"${E}" "s,.*,${SED_RED_YELLOW},"
|
||||
fi
|
||||
elif [ "$doas_rule_root" = "yes" ] && { [ "$doas_rule_nopass" = "yes" ] || [ "$doas_rule_unrestricted" = "yes" ] || [ "$doas_rule_dangerous" = "yes" ]; }; then
|
||||
echo " $conf_file:$doas_rule_number $doas_rule_line" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g"
|
||||
else
|
||||
|
||||
@@ -7,11 +7,154 @@
|
||||
# Version: 1.0
|
||||
# Mitre: T1552.001
|
||||
# Functions Used: print_3title, warn_exec
|
||||
# Global Variables: $NGINX_KNOWN_MODULES
|
||||
# Global Variables: $NGINX_KNOWN_MODULES, $TIMEOUT, $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Generated Global Variables: $lp_apache_checked, $lp_apache_deadline, $lp_apache_dir, $lp_apache_file, $lp_apache_found, $lp_apache_lines, $lp_apache_name, $lp_remco_checked, $lp_remco_config, $lp_remco_deadline, $lp_remco_raw, $lp_remco_root, $lp_remco_size, $lp_remco_source, $lp_remco_sources, $lp_remco_template_size, $lp_remco_templates
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
# The ordinary Apache file inventory below already displays virtual-host
|
||||
# configuration. This extra view names piped-log directives without echoing
|
||||
# their commands (which may contain credentials or attacker-controlled text).
|
||||
lp_apache_piped_log_check() (
|
||||
if [ "$#" -eq 0 ]; then
|
||||
if [ "$SEARCH_IN_FOLDER" ]; then
|
||||
set -- "$SEARCH_IN_FOLDER" "$SEARCH_IN_FOLDER/sites-enabled"
|
||||
else
|
||||
set -- /etc/apache2/sites-enabled /etc/apache2/sites-available /etc/httpd/conf.d /usr/local/etc/apache24/Includes
|
||||
fi
|
||||
fi
|
||||
if [ -z "$TIMEOUT" ] || [ ! -x "$TIMEOUT" ]; then
|
||||
for lp_apache_dir do
|
||||
if [ -d "$lp_apache_dir" ]; then
|
||||
printf '%s\n' 'Apache piped-log inspection unavailable (timeout missing); configuration output withholds piped-log commands.'
|
||||
break
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
lp_apache_deadline=$(($(date +%s 2>/dev/null || printf 0) + 4))
|
||||
lp_apache_checked=0
|
||||
lp_apache_found=0
|
||||
for lp_apache_dir do
|
||||
[ -d "$lp_apache_dir" ] && [ -r "$lp_apache_dir" ] || continue
|
||||
for lp_apache_file in "$lp_apache_dir"/*; do
|
||||
[ "$lp_apache_checked" -lt 64 ] || break 2
|
||||
[ "$(date +%s 2>/dev/null || printf 0)" -lt "$lp_apache_deadline" ] || break 2
|
||||
[ -f "$lp_apache_file" ] && [ -r "$lp_apache_file" ] || continue
|
||||
lp_apache_checked=$((lp_apache_checked + 1))
|
||||
lp_apache_lines=$("$TIMEOUT" 1 dd if="$lp_apache_file" bs=4096 count=16 2>/dev/null | awk '
|
||||
{
|
||||
line = $0
|
||||
sub(/^[[:space:]]*/, "", line)
|
||||
if (line ~ /^#/ || line !~ /^[[:alpha:]]+[[:space:]]+/) next
|
||||
directive = line
|
||||
sub(/[[:space:]].*$/, "", directive)
|
||||
if (tolower(directive) != "customlog" && tolower(directive) != "errorlog" &&
|
||||
tolower(directive) != "transferlog") next
|
||||
sub(/^[^[:space:]]+[[:space:]]+/, "", line)
|
||||
if (substr(line, 1, 1) == "\"" || substr(line, 1, 1) == "\047") line = substr(line, 2)
|
||||
if (substr(line, 1, 2) == "|$") mode = "shell"
|
||||
else if (substr(line, 1, 1) == "|") mode = "direct"
|
||||
else next
|
||||
if (shown++ < 2) print directive ": " mode " pipeline (command redacted)"
|
||||
}
|
||||
' 2>/dev/null)
|
||||
[ -n "$lp_apache_lines" ] || continue
|
||||
if [ "$lp_apache_found" -eq 0 ]; then
|
||||
printf '%s\n' 'Apache piped-log directives (bounded, read-only):'
|
||||
lp_apache_found=1
|
||||
fi
|
||||
# Limit the displayed basename; never print the directive's arguments.
|
||||
lp_apache_name=$(basename "$lp_apache_file" | tr -cd '[:print:]' | cut -c 1-80)
|
||||
printf ' %s: %s\n' "$lp_apache_name" "$lp_apache_lines"
|
||||
done
|
||||
done
|
||||
[ "$lp_apache_found" -eq 0 ] || printf '%s\n' 'A piped logger is a review cue; its command and privilege depend on the Apache parent.'
|
||||
if [ "$lp_apache_checked" -ge 64 ] || [ "$(date +%s 2>/dev/null || printf 0)" -ge "$lp_apache_deadline" ]; then
|
||||
printf '%s\n' 'Apache piped-log inspection was partial (file or time cap reached).'
|
||||
fi
|
||||
)
|
||||
|
||||
peass{Apache-Nginx}
|
||||
# remco can render key/value data into privileged service configuration. This
|
||||
# checks only fixed local configuration and templates; it never contacts the
|
||||
# backend or invokes the target service.
|
||||
lp_remco_apache_template_check() (
|
||||
[ "$SEARCH_IN_FOLDER" ] && return 0
|
||||
lp_remco_config=${1:-/etc/remco/config}
|
||||
lp_remco_templates=${2:-/etc/remco/templates}
|
||||
[ -e "$lp_remco_config" ] || return 0
|
||||
if [ ! -f "$lp_remco_config" ] || [ ! -r "$lp_remco_config" ] || [ -L "$lp_remco_config" ]; then
|
||||
printf '%s\n' 'remco configuration exists; static linkage unknown (unreadable or non-regular file).'
|
||||
return 0
|
||||
fi
|
||||
if [ -z "$TIMEOUT" ] || [ ! -x "$TIMEOUT" ]; then
|
||||
printf '%s\n' 'remco configuration exists; bounded inspection unavailable (timeout missing).'
|
||||
return 0
|
||||
fi
|
||||
lp_remco_deadline=$(($(date +%s 2>/dev/null || printf 0) + 4))
|
||||
lp_remco_size=$("$TIMEOUT" 1 dd if="$lp_remco_config" bs=65537 count=1 2>/dev/null | wc -c | tr -d '[:space:]')
|
||||
case "$lp_remco_size" in ''|*[!0-9]*) lp_remco_size=0 ;; esac
|
||||
if [ "$lp_remco_size" -eq 0 ] || [ "$lp_remco_size" -gt 65536 ]; then
|
||||
printf '%s\n' 'remco configuration exists; static linkage unknown (empty, unreadable, or over 64 KiB).'
|
||||
return 0
|
||||
fi
|
||||
lp_remco_root=${3:-}
|
||||
if [ -z "$lp_remco_root" ]; then
|
||||
lp_remco_root=$("$TIMEOUT" 1 ps -eo user=,args= 2>/dev/null | awk '
|
||||
NR > 2048 { exit }
|
||||
$1 == "root" && $2 ~ /(^|\/)remco$/ && $0 !~ /[[:space:]]-config([=[:space:]]|$)/ { print "root"; exit }
|
||||
')
|
||||
fi
|
||||
[ "$lp_remco_root" = root ] || lp_remco_root=unknown
|
||||
lp_remco_sources=$("$TIMEOUT" 1 dd if="$lp_remco_config" bs=65536 count=1 2>/dev/null | awk -v root="$lp_remco_templates" '
|
||||
function quoted(line, fields, n) {
|
||||
n = split(line, fields, "\"")
|
||||
return n >= 3 ? fields[2] : ""
|
||||
}
|
||||
function finish() {
|
||||
if (src != "" && index(src, root "/") == 1 && src !~ /\.\./ &&
|
||||
src ~ /^[[:alnum:]_.\/-]+$/ &&
|
||||
dst ~ /^\/etc\/(apache2|httpd)\// &&
|
||||
reload ~ /(apache2|httpd)/ && reload ~ /(restart|reload|graceful)/ &&
|
||||
backend && watch && keys && emitted++ < 8) print src
|
||||
}
|
||||
/^[[:space:]]*\[\[resource(\.template)?\]\][[:space:]]*$/ {
|
||||
finish(); src = dst = reload = ""; backend = watch = keys = 0; next
|
||||
}
|
||||
/^[[:space:]]*#/ { next }
|
||||
/^[[:space:]]*src[[:space:]]*=/ { src = quoted($0) }
|
||||
/^[[:space:]]*dst[[:space:]]*=/ { dst = quoted($0) }
|
||||
/^[[:space:]]*reload_cmd[[:space:]]*=/ { reload = tolower(quoted($0)) }
|
||||
/^[[:space:]]*\[resource.backend.etcd\]/ { backend = 1 }
|
||||
/^[[:space:]]*watch[[:space:]]*=[[:space:]]*true([[:space:]#]|$)/ { watch = 1 }
|
||||
/^[[:space:]]*keys[[:space:]]*=[[:space:]]*\[[[:space:]]*"/ { keys = 1 }
|
||||
END { finish() }
|
||||
' 2>/dev/null)
|
||||
[ -n "$lp_remco_sources" ] || return 0
|
||||
lp_remco_checked=0
|
||||
printf '%s\n' 'remco watches a backend and reloads Apache from a template; template contents and backend write permission require review.'
|
||||
printf '%s\n' "$lp_remco_sources" | while IFS= read -r lp_remco_source; do
|
||||
[ "$lp_remco_checked" -lt 8 ] || break
|
||||
[ "$(date +%s 2>/dev/null || printf 0)" -lt "$lp_remco_deadline" ] || break
|
||||
lp_remco_checked=$((lp_remco_checked + 1))
|
||||
[ -f "$lp_remco_source" ] && [ -r "$lp_remco_source" ] && [ ! -L "$lp_remco_source" ] || continue
|
||||
lp_remco_template_size=$("$TIMEOUT" 1 dd if="$lp_remco_source" bs=32769 count=1 2>/dev/null | wc -c | tr -d '[:space:]')
|
||||
case "$lp_remco_template_size" in ''|*[!0-9]*) lp_remco_template_size=0 ;; esac
|
||||
[ "$lp_remco_template_size" -gt 0 ] && [ "$lp_remco_template_size" -le 32768 ] || continue
|
||||
lp_remco_raw=$("$TIMEOUT" 1 dd if="$lp_remco_source" bs=32768 count=1 2>/dev/null | awk '
|
||||
/^[[:space:]]*(ServerName|ServerAlias|DocumentRoot|CustomLog|ErrorLog|TransferLog|Include|LoadModule)[[:space:]]+/ &&
|
||||
/\{\{[[:space:]]*getv[[:space:]]*\([^}]*\)[[:space:]]*\}\}/ { print "yes"; exit }
|
||||
' 2>/dev/null)
|
||||
[ "$lp_remco_raw" = yes ] || continue
|
||||
if [ "$lp_remco_root" = root ]; then
|
||||
printf '%s\n' 'Candidate: root remco process and watched backend value rendered directly into Apache configuration (backend write permission unknown).'
|
||||
else
|
||||
printf '%s\n' 'Candidate: watched backend value rendered directly into Apache configuration (remco process privilege and backend write permission unknown).'
|
||||
fi
|
||||
break
|
||||
done
|
||||
)
|
||||
|
||||
|
||||
peass{Apache-Nginx}
|
||||
@@ -0,0 +1,82 @@
|
||||
# Title: Software Information - Below log directory
|
||||
# ID: SI_Below_Log_Directory
|
||||
# Author: PEASS-ng contributors
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Passive triage of a writable Below log directory with a privileged execution path
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1068
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $MACPEAS, $SEARCH_IN_FOLDER, $sudo_l_cached_output, $sudo_l_password_output, $sudo_l_output
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $lp_below_binary, $lp_below_candidate, $lp_below_dir_mode, $lp_below_privileged_path
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
lp_below_effective_access() {
|
||||
[ -w "$1" ] && [ -x "$1" ]
|
||||
}
|
||||
|
||||
lp_below_is_symlink() {
|
||||
[ -L "$1" ]
|
||||
}
|
||||
|
||||
lp_below_check_log_directory() {
|
||||
# The arguments permit metadata-only fixtures; the live call uses fixed paths.
|
||||
[ -f "$1" ] && [ -x "$1" ] || return 0
|
||||
[ -d "$2" ] && ! lp_below_is_symlink "$2" && lp_below_effective_access "$2" || return 0
|
||||
|
||||
# Only the mode token for the fixed Below directory is used.
|
||||
# shellcheck disable=SC2012
|
||||
lp_below_dir_mode=$(ls -ld "$2" 2>/dev/null | awk 'NR == 1 { print $1 }')
|
||||
case "$lp_below_dir_mode" in
|
||||
d????????t*|d????????T*) return 0 ;;
|
||||
d????????[x-]*) ;;
|
||||
*) return 0 ;; # Unknown metadata must not become a positive finding.
|
||||
esac
|
||||
|
||||
lp_below_privileged_path=""
|
||||
# These values are collected by the preceding sudo module when available.
|
||||
# shellcheck disable=SC2154
|
||||
if printf '%s\n%s\n%s\n' "$sudo_l_cached_output" "$sudo_l_password_output" "$sudo_l_output" |
|
||||
awk -v path="$1" '
|
||||
/^[[:space:]]*\((root|ALL)([[:space:]]*:[[:space:]]*ALL)?\)/ && index($0, "!") == 0 {
|
||||
at = index($0, path)
|
||||
if (at && (at == 1 || substr($0, at - 1, 1) ~ /[[:space:],:]/) &&
|
||||
(substr($0, at + length(path), 1) ~ /[[:space:]]/ || at + length(path) > length($0)))
|
||||
found = 1
|
||||
}
|
||||
END { exit !found }
|
||||
'; then
|
||||
lp_below_privileged_path="sudo rule lists this Below executable for root"
|
||||
elif command -v ps >/dev/null 2>&1 && ps -eo user=,comm= 2>/dev/null |
|
||||
awk '$1 == "root" && $2 == "below" { found = 1 } END { exit !found }'; then
|
||||
lp_below_privileged_path="root-owned Below process observed"
|
||||
fi
|
||||
[ -n "$lp_below_privileged_path" ] || return 0
|
||||
|
||||
print_2title "Below log directory: potential privileged log-file exposure" "T1068"
|
||||
printf 'Installed Below executable: %s\n' "$1"
|
||||
printf 'Log directory: %s (effective write/search access; nonsticky mode %s)\n' "$2" "$lp_below_dir_mode"
|
||||
printf 'Privileged execution evidence: %s\n' "$lp_below_privileged_path"
|
||||
if lp_below_is_symlink "$3"; then
|
||||
printf 'Log entry: %s is a symlink (target not read)\n' "$3"
|
||||
elif [ -e "$3" ]; then
|
||||
printf 'Log entry: %s exists\n' "$3"
|
||||
else
|
||||
printf 'Log entry: %s absent\n' "$3"
|
||||
fi
|
||||
printf '%s\n' 'Patch status: unknown; package version and backports were not verified.'
|
||||
printf '%s\n\n' 'Review ACLs, filesystem behavior, and the actual privileged launch before treating this as CVE-2025-27591 exposure.'
|
||||
}
|
||||
|
||||
if [ -z "$SEARCH_IN_FOLDER" ] && [ -z "$MACPEAS" ] && [ "$(id -u 2>/dev/null)" != 0 ]; then
|
||||
lp_below_binary=""
|
||||
for lp_below_candidate in /usr/bin/below /usr/local/bin/below; do
|
||||
if [ -f "$lp_below_candidate" ] && [ -x "$lp_below_candidate" ]; then
|
||||
lp_below_binary="$lp_below_candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ -n "$lp_below_binary" ] && lp_below_check_log_directory "$lp_below_binary" /var/log/below /var/log/below/error_root.log
|
||||
fi
|
||||
@@ -9,7 +9,7 @@
|
||||
# Functions Used: print_2title, print_3title, print_info
|
||||
# Global Variables: $HOMESEARCH, $SED_RED
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $h, $firefox_ini, $chrome_base, $profiles
|
||||
# Generated Global Variables: $h, $firefox_ini, $chrome_base, $profiles, $profile, $profile_count, $extension_settings, $passbolt_store
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -57,6 +57,16 @@ for h in $HOMESEARCH; do
|
||||
if [ "$profiles" ]; then
|
||||
print_3title "Chromium profiles ($chrome_base)" "T1539,T1217"
|
||||
printf "%s\n" "$profiles" | sed -${E} "s,.*,${SED_RED},"
|
||||
profile_count=0
|
||||
printf "%s\n" "$profiles" | while IFS= read -r profile; do
|
||||
profile_count=$((profile_count + 1))
|
||||
[ "$profile_count" -le 8 ] || break
|
||||
extension_settings="$profile/Local Extension Settings"
|
||||
passbolt_store="$extension_settings/didegimhafipceonhjepacocaffmoppf"
|
||||
[ -d "$extension_settings" ] && [ ! -L "$extension_settings" ] || continue
|
||||
[ -d "$passbolt_store" ] && [ ! -L "$passbolt_store" ] && [ -r "$passbolt_store" ] && [ -x "$passbolt_store" ] || continue
|
||||
printf ' Passbolt extension local storage (path only; key availability unknown): %s\n' "$passbolt_store"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,22 +1,40 @@
|
||||
# Title: Software Information - Cached AD Hashes
|
||||
# ID: SI_Cached_AD_hashes
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Cached AD Hashes
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Passive metadata for local domain credential-cache candidates
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1003.003
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $DEBUG
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $adhashes
|
||||
# Generated Global Variables: $adhashes, $adhash, $lp_ad_cache_count
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
adhashes=$(ls "/var/lib/samba/private/secrets.tdb" "/var/lib/samba/passdb.tdb" "/var/opt/quest/vas/authcache/vas_auth.vdb" "/var/lib/sss/db/cache_*" 2>/dev/null)
|
||||
if [ "$adhashes" ] || [ "$DEBUG" ]; then
|
||||
print_2title "Searching AD cached hashes" "T1003.003"
|
||||
ls -l "/var/lib/samba/private/secrets.tdb" "/var/lib/samba/passdb.tdb" "/var/opt/quest/vas/authcache/vas_auth.vdb" "/var/lib/sss/db/cache_*" 2>/dev/null
|
||||
echo ""
|
||||
fi
|
||||
adhashes=""
|
||||
lp_ad_cache_count=0
|
||||
for adhash in /var/lib/samba/private/secrets.tdb /var/lib/samba/passdb.tdb /var/opt/quest/vas/authcache/vas_auth.vdb /var/lib/sss/db/cache_*.ldb; do
|
||||
[ -f "$adhash" ] || continue
|
||||
if [ -z "$adhashes" ]; then
|
||||
print_2title "Local domain credential-cache candidates" "T1003.003"
|
||||
adhashes=found
|
||||
fi
|
||||
ls -ld "$adhash" 2>/dev/null
|
||||
if [ -r "$adhash" ]; then
|
||||
printf ' readable by current user (contents not inspected)\n'
|
||||
else
|
||||
printf ' not readable by current user\n'
|
||||
fi
|
||||
lp_ad_cache_count=$((lp_ad_cache_count + 1))
|
||||
if [ "$lp_ad_cache_count" -ge 32 ]; then
|
||||
printf ' additional cache files omitted after 32 entries\n'
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$adhashes" ] && [ "$DEBUG" ]; then
|
||||
print_2title "Local domain credential-cache candidates" "T1003.003"
|
||||
fi
|
||||
[ -z "$adhashes" ] || printf '%s\n\n' 'Cache-file presence alone does not prove cached passwords; check domain cache_credentials and prior logins.'
|
||||
@@ -0,0 +1,152 @@
|
||||
# Title: Software Information - Consul agent script checks
|
||||
# ID: SO_Consul
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 2026-10-09
|
||||
# Description: Passive review of script checks and loaded configuration directories for root-run Consul agents
|
||||
# License: GNU GPL
|
||||
# Version: 1.1
|
||||
# Mitre: T1548.003
|
||||
# Functions Used: print_2title, print_info
|
||||
# Global Variables: $SEARCH_IN_FOLDER, $IAMROOT
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $consul_root_agents, $consul_agent_args, $consul_cli_enabled, $consul_config_paths, $consul_config_path, $consul_config_file, $consul_file_count, $consul_file_result, $consul_enabled_file, $consul_disabled_file, $consul_writable_dirs, $consul_writable_dir, $consul_partial
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
# Only inspect the current process list and readable, bounded configuration.
|
||||
# Never call the agent API or register a health check during enumeration.
|
||||
if [ ! "$SEARCH_IN_FOLDER" ] && command -v ps >/dev/null 2>&1; then
|
||||
consul_root_agents=$(LC_ALL=C ps -eo user=,args= 2>/dev/null | awk '
|
||||
NR > 4096 { exit }
|
||||
length($0) > 2048 { next }
|
||||
$0 ~ /^[[:space:]]*root[[:space:]]+([^[:space:]]*\/)?consul[[:space:]]+agent([[:space:]]|$)/ {
|
||||
sub(/^[[:space:]]*root[[:space:]]+/, "")
|
||||
if (seen++ < 4) print
|
||||
}
|
||||
')
|
||||
if [ "$consul_root_agents" ]; then
|
||||
printf '%s\n' "$consul_root_agents" | while IFS= read -r consul_agent_args; do
|
||||
consul_cli_enabled=
|
||||
case " $consul_agent_args " in
|
||||
*' -enable-script-checks '*|*' -enable-script-checks=true '*) consul_cli_enabled=1 ;;
|
||||
esac
|
||||
# Inspect only paths named by this running agent. An unused default
|
||||
# directory could contain stale configuration and create a false alarm.
|
||||
consul_config_paths=$(printf '%s\n' "$consul_agent_args" | awk '
|
||||
{
|
||||
count = split($0, args, /[[:space:]]+/)
|
||||
for (i = 1; i <= count && found < 8; i++) {
|
||||
if (args[i] == "-config-dir" || args[i] == "-config-file") {
|
||||
if (i < count) { print args[++i]; found++ }
|
||||
} else if (args[i] ~ /^-config-(dir|file)=/) {
|
||||
sub(/^-config-(dir|file)=/, "", args[i])
|
||||
print args[i]
|
||||
found++
|
||||
}
|
||||
}
|
||||
}
|
||||
')
|
||||
consul_file_count=0
|
||||
consul_enabled_file=
|
||||
consul_disabled_file=
|
||||
consul_writable_dirs=
|
||||
consul_partial=
|
||||
while IFS= read -r consul_config_path; do
|
||||
case "$consul_config_path" in
|
||||
/*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
case "$consul_config_path" in
|
||||
*'//'*|*'/./'*|*'/../'*|*'/.'|*'/..'|*'"'*|*"'"*) continue ;;
|
||||
esac
|
||||
if [ -d "$consul_config_path" ] && [ ! -L "$consul_config_path" ]; then
|
||||
if [ ! "$IAMROOT" ] && [ -w "$consul_config_path" ] && [ -x "$consul_config_path" ]; then
|
||||
if [ "$consul_writable_dirs" ]; then
|
||||
consul_writable_dirs="$consul_writable_dirs
|
||||
$consul_config_path"
|
||||
else
|
||||
consul_writable_dirs=$consul_config_path
|
||||
fi
|
||||
fi
|
||||
set -- "$consul_config_path"/*.json "$consul_config_path"/*.hcl
|
||||
else
|
||||
set -- "$consul_config_path"
|
||||
fi
|
||||
for consul_config_file do
|
||||
case "$consul_config_file" in
|
||||
*.json|*.hcl) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
[ -f "$consul_config_file" ] && [ -r "$consul_config_file" ] &&
|
||||
[ ! -L "$consul_config_file" ] || continue
|
||||
consul_file_count=$((consul_file_count + 1))
|
||||
if [ "$consul_file_count" -gt 24 ]; then
|
||||
consul_partial=1
|
||||
break
|
||||
fi
|
||||
if [ ! "$(find "$consul_config_file" -prune -type f -size -65537c -print 2>/dev/null)" ]; then
|
||||
consul_partial=1
|
||||
continue
|
||||
fi
|
||||
consul_file_result=$(LC_ALL=C awk '
|
||||
NR > 400 { partial = 1; exit }
|
||||
length($0) > 2048 { partial = 1; next }
|
||||
{
|
||||
line = $0
|
||||
sub(/^[[:space:]]*/, "", line)
|
||||
if (line ~ /^(#|\/\/)/) next
|
||||
if (line ~ /(^|[{,])[[:space:]]*"?enable_script_checks"?[[:space:]]*[:=][[:space:]]*true([,[:space:]}]|$)/) enabled = 1
|
||||
if (line ~ /(^|[{,])[[:space:]]*"?enable_script_checks"?[[:space:]]*[:=][[:space:]]*false([,[:space:]}]|$)/) disabled = 1
|
||||
}
|
||||
END {
|
||||
if (enabled) print "enabled"
|
||||
if (disabled) print "disabled"
|
||||
if (partial) print "partial"
|
||||
}
|
||||
' "$consul_config_file" 2>/dev/null)
|
||||
case "$consul_file_result" in
|
||||
*enabled*) consul_enabled_file=$consul_config_file ;;
|
||||
esac
|
||||
case "$consul_file_result" in
|
||||
*disabled*) consul_disabled_file=$consul_config_file ;;
|
||||
esac
|
||||
case "$consul_file_result" in
|
||||
*partial*) consul_partial=1 ;;
|
||||
esac
|
||||
done
|
||||
[ "$consul_file_count" -gt 24 ] && break
|
||||
done <<_PEAS_CONSUL_CONFIG_PATHS_
|
||||
$consul_config_paths
|
||||
_PEAS_CONSUL_CONFIG_PATHS_
|
||||
if [ "$consul_enabled_file" ] || [ "$consul_cli_enabled" ] || [ "$consul_writable_dirs" ]; then
|
||||
if [ "$consul_enabled_file" ] || [ "$consul_cli_enabled" ]; then
|
||||
print_2title "Root-run Consul script-check review candidate" "T1548.003"
|
||||
else
|
||||
print_2title "Root-run Consul config-directory write review candidate" "T1548.003"
|
||||
fi
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/processes-crontab-systemd-dbus/process-enumeration-and-service-paths.html#consul-agent-script-checks"
|
||||
if [ "$consul_enabled_file" ]; then
|
||||
printf 'Root-run Consul agent has enable_script_checks=true in %s\n' "$consul_enabled_file"
|
||||
fi
|
||||
if [ "$consul_cli_enabled" ]; then
|
||||
printf '%s\n' 'Root-run Consul agent enables script checks on its command line'
|
||||
fi
|
||||
if [ "$consul_disabled_file" ]; then
|
||||
printf 'Conflicting enable_script_checks=false in %s; effective setting unknown\n' "$consul_disabled_file"
|
||||
fi
|
||||
if [ "$consul_partial" ]; then
|
||||
printf '%s\n' 'Consul config review incomplete (file size, line length, line count, or file count limit)'
|
||||
fi
|
||||
if [ "$consul_writable_dirs" ]; then
|
||||
printf '%s\n' "$consul_writable_dirs" | while IFS= read -r consul_writable_dir; do
|
||||
printf 'Loaded Consul config-dir writable/searchable by current user: %s\n' "$consul_writable_dir"
|
||||
done
|
||||
printf '%s\n' 'Config-file execution requires a loaded directory, effective script-check policy, and an authorized reload or restart; verify parent path/symlink and ACL policy. No configuration was written or reloaded.'
|
||||
fi
|
||||
if [ "$consul_enabled_file" ] || [ "$consul_cli_enabled" ]; then
|
||||
printf '%s\n' 'API script execution requires an authorized service registration and the agent running as privileged user; static config does not prove either API access or effective ACL policy.'
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
@@ -9,7 +9,7 @@
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $DEBUG
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Generated Global Variables: $gitlab_bytes
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -30,18 +30,37 @@ if [ "$(command -v gitlab-rails || echo -n '')" ] || [ "$(command -v gitlab-back
|
||||
echo ""
|
||||
fi
|
||||
#Check gitlab files
|
||||
printf "%s\n" "$PSTORAGE_GITLAB" | sort | uniq | while read f; do
|
||||
if echo $f | grep -q secrets.yml; then
|
||||
printf "%s\n" "$PSTORAGE_GITLAB" | sort | uniq | while IFS= read -r f; do
|
||||
if [ "${f##*/}" = "secrets.yml" ]; then
|
||||
echo "Found $f" | sed "s,$f,${SED_RED},"
|
||||
cat "$f" 2>/dev/null | grep -Iv "^$" | grep -v "^#"
|
||||
elif echo $f | grep -q gitlab.yml; then
|
||||
elif [ "${f##*/}" = "gitlab.yml" ]; then
|
||||
echo "Found $f" | sed "s,$f,${SED_RED},"
|
||||
cat "" | grep -A 4 "repositories:"
|
||||
elif echo $f | grep -q gitlab.rb; then
|
||||
if [ ! -f "$f" ] || [ -L "$f" ] || [ ! -r "$f" ]; then
|
||||
echo "Repository preview skipped (unreadable, non-regular, or symlink); inspect path manually"
|
||||
else
|
||||
gitlab_bytes=$(dd if="$f" bs=262145 count=1 2>/dev/null | wc -c | tr -d '[:space:]')
|
||||
if [ "$gitlab_bytes" -le 262144 ] 2>/dev/null; then
|
||||
dd if="$f" bs=262144 count=1 2>/dev/null |
|
||||
grep -A 4 '^[[:space:]]*repositories:' | head -n 20 | cut -c 1-256
|
||||
else
|
||||
echo "Repository preview skipped (larger than 256 KiB); inspect path manually"
|
||||
fi
|
||||
fi
|
||||
elif [ "${f##*/}" = "gitlab.rb" ]; then
|
||||
echo "Found $f" | sed "s,$f,${SED_RED},"
|
||||
cat "$f" | grep -Iv "^$" | grep -v "^#" | sed -${E} "s,email|user|password,${SED_RED},"
|
||||
if [ ! -f "$f" ] || [ -L "$f" ] || [ ! -r "$f" ]; then
|
||||
echo "Config preview skipped (unreadable, non-regular, or symlink); inspect path manually"
|
||||
else
|
||||
gitlab_bytes=$(dd if="$f" bs=262145 count=1 2>/dev/null | wc -c | tr -d '[:space:]')
|
||||
if [ "$gitlab_bytes" -le 262144 ] 2>/dev/null; then
|
||||
dd if="$f" bs=262144 count=1 2>/dev/null | grep -Eiv '^[[:space:]]*(#|$)' | grep -Ei 'smtp_|password|user|token|secret|database|redis|external_url' | head -n 40 | cut -c 1-256 | sed -${E} "s,email|user|password,${SED_RED},"
|
||||
else
|
||||
echo "Config preview skipped (larger than 256 KiB); inspect path manually"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
@@ -41,7 +41,7 @@ if ! [ "$SEARCH_IN_FOLDER" ] && ! [ "$MACPEAS" ]; then
|
||||
|
||||
if [ "$gogs_processes" ] || [ "$gogs_bin" ] || [ -r /etc/gogs/conf/app.ini ] || [ -r /etc/gogs/app.ini ] || [ "$DEBUG" ]; then
|
||||
print_2title "Gogs privileged file-write checks" "T1068"
|
||||
print_info "Gogs <= 0.13.3 is vulnerable to authenticated symlink-based file writes through the PutContents API (CVE-2025-8110): https://github.com/gogs/gogs/security/advisories/GHSA-gg64-xxr9-qhjp"
|
||||
print_info "Gogs <= 0.13.3 is vulnerable to authenticated symlink-based file writes through the PutContents API (CVE-2025-8110): https://github.com/advisories/GHSA-mq8m-42gh-wq7r"
|
||||
|
||||
gogs_root_process="$(printf "%s\n" "$gogs_processes" | awk '$1 == "root" { print "yes"; exit }')"
|
||||
if [ "$gogs_processes" ]; then
|
||||
|
||||
@@ -31,9 +31,12 @@ if [ "$kadmin_exists" ] || [ "$klist_exists" ] || [ "$kinit_exists" ] || [ "$PST
|
||||
|
||||
(env || printenv) 2>/dev/null | grep -E "^KRB5" | sed -${E} "s,KRB5,${SED_RED},g"
|
||||
|
||||
printf "%s\n" "$PSTORAGE_KERBEROS" | while read f; do
|
||||
if [ -r "$f" ]; then
|
||||
if echo "$f" | grep -q .k5login; then
|
||||
printf "%s\n" "$PSTORAGE_KERBEROS" | while IFS= read -r f; do
|
||||
if [ "${f##*/}" = ".k5users" ] && [ -f "$f" ]; then
|
||||
echo ".k5users authorization file (principal and command rights require review)"
|
||||
ls -ld "$f" 2>/dev/null
|
||||
elif [ -r "$f" ]; then
|
||||
if [ "${f##*/}" = ".k5login" ]; then
|
||||
echo ".k5login file (users with access to the user who has this file in his home)"
|
||||
cat "$f" 2>/dev/null | sed -${E} "s,.*,${SED_RED},g"
|
||||
elif echo "$f" | grep -q keytab; then
|
||||
@@ -69,4 +72,4 @@ if [ "$kadmin_exists" ] || [ "$klist_exists" ] || [ "$kinit_exists" ] || [ "$PST
|
||||
klist 2>/dev/null || echo_not_found "klist"
|
||||
echo ""
|
||||
|
||||
fi
|
||||
fi
|
||||
@@ -1,29 +1,61 @@
|
||||
# Title: Software Information - Logstash
|
||||
# ID: SI_Logstash
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Searching logstash files
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Review output/filter directives and bounded input-path leads in known Logstash directories
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1552.001
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $DEBUG, $knw_usrs, $nosh_usrs, $sh_usrs, $USER
|
||||
# Global Variables: $DEBUG, $E, $SED_BLUE, $SED_GREEN, $SED_LIGHT_CYAN, $SED_LIGHT_MAGENTA, $SED_RED, $knw_usrs, $nosh_usrs, $sh_usrs, $USER
|
||||
# Cached storage inputs (builder): $PSTORAGE_LOGSTASH
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Generated Global Variables: $d, $ls_config, $ls_count, $ls_lines, $ls_root_count
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
if [ "$PSTORAGE_LOGSTASH" ] || [ "$DEBUG" ]; then
|
||||
print_2title "Searching logstash files" "T1552.001"
|
||||
printf "$PSTORAGE_LOGSTASH"
|
||||
printf "%s\n" "$PSTORAGE_LOGSTASH" | while read d; do
|
||||
printf '%s\n' "$PSTORAGE_LOGSTASH"
|
||||
ls_root_count=0
|
||||
printf '%s\n' "$PSTORAGE_LOGSTASH" | while IFS= read -r d; do
|
||||
[ -n "$d" ] && [ -d "$d" ] || continue
|
||||
ls_root_count=$((ls_root_count + 1))
|
||||
if [ -r "$d/startup.options" ]; then
|
||||
echo "Logstash is running as user:"
|
||||
cat "$d/startup.options" 2>/dev/null | grep "LS_USER\|LS_GROUP" | sed -${E} "s,$sh_usrs,${SED_LIGHT_CYAN}," | sed -${E} "s,$nosh_usrs,${SED_BLUE}," | sed -${E} "s,$knw_usrs,${SED_GREEN}," | sed -${E} "s,$USER,${SED_LIGHT_MAGENTA}," | sed -${E} "s,root,${SED_RED},"
|
||||
echo "Logstash startup.options account setting (verify running identity):"
|
||||
cat "$d/startup.options" 2>/dev/null | grep "LS_USER\|LS_GROUP" | sed -${E} "s,$sh_usrs,${SED_LIGHT_CYAN}," | sed -${E} "s,$nosh_usrs,${SED_BLUE}," | sed -${E} "s,$knw_usrs,${SED_GREEN}," | sed -${E} "s,${USER:-^$},${SED_LIGHT_MAGENTA}," | sed -${E} "s,root,${SED_RED},"
|
||||
fi
|
||||
# Preserve full-file output/filter coverage, including extensionless names.
|
||||
# Keep the directory component quoted while allowing the filename glob.
|
||||
for ls_config in "$d"/conf.d/out*; do
|
||||
[ -f "$ls_config" ] && [ -r "$ls_config" ] || continue
|
||||
grep -E 'exec[[:space:]]*\{|command[[:space:]]*=>' "$ls_config" 2>/dev/null |
|
||||
sed -${E} "s,exec\W*\{|command\W*=>,${SED_RED},"
|
||||
done
|
||||
for ls_config in "$d"/conf.d/filt*; do
|
||||
[ -f "$ls_config" ] && [ -r "$ls_config" ] || continue
|
||||
grep -E 'path[[:space:]]*=>|code[[:space:]]*=>|match[[:space:]]*=>|ruby[[:space:]]*\{' "$ls_config" 2>/dev/null |
|
||||
sed -${E} "s,path\W*=>|code\W*=>|match\W*=>|ruby\W*\{,${SED_RED},"
|
||||
done
|
||||
# Input-path cues are a separate bounded pass over already-known dirs.
|
||||
if [ "$ls_root_count" -le 8 ]; then
|
||||
ls_count=0
|
||||
for ls_config in "$d"/conf.d/in*; do
|
||||
[ -f "$ls_config" ] && [ -r "$ls_config" ] && [ ! -L "$ls_config" ] || continue
|
||||
ls_count=$((ls_count + 1))
|
||||
if [ "$ls_count" -gt 16 ]; then
|
||||
echo ' Additional input-named Logstash files not inspected (16-file cap).'
|
||||
break
|
||||
fi
|
||||
ls_lines=$(dd if="$ls_config" bs=8192 count=1 2>/dev/null |
|
||||
grep -E '^[[:space:]]*path[[:space:]]*=>' | head -n 16)
|
||||
if [ -n "$ls_lines" ]; then
|
||||
printf ' Input path directive lead in %s (verify file input and permissions):\n' "$ls_config"
|
||||
printf '%s\n' "$ls_lines"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
cat "$d/conf.d/out*" | grep "exec\s*{\|command\s*=>" | sed -${E} "s,exec\W*\{|command\W*=>,${SED_RED},"
|
||||
cat "$d/conf.d/filt*" | grep "path\s*=>\|code\s*=>\|ruby\s*{" | sed -${E} "s,path\W*=>|code\W*=>|ruby\W*\{,${SED_RED},"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
echo ""
|
||||
@@ -0,0 +1,97 @@
|
||||
# Title: Software Information - Motion configuration
|
||||
# ID: SI_Motion_Configuration
|
||||
# Author: PEASS-ng contributors
|
||||
# Last Update: 08-10-2026
|
||||
# Description: Read-only inventory of local motionEye and Motion control settings without printing credentials
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1552.001,T1068
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $MACPEAS, $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $lp_motion_conf_dir, $lp_motion_custom_dir, $motion_config, $motion_dir, $motion_camera, $motion_camera_count, $motion_hook, $motion_filename, $motion_camera_unreadable, $motion_camera_flags
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
lp_motion_readable() {
|
||||
[ -r "$1" ]
|
||||
}
|
||||
|
||||
lp_motion_check_config() {
|
||||
local motion_config="$1" motion_dir motion_camera motion_camera_count=0
|
||||
local motion_hook=0 motion_filename=0 motion_camera_unreadable=0 motion_camera_flags
|
||||
|
||||
[ -e "$motion_config" ] || return 0
|
||||
print_2title "Motion configuration: $motion_config" "T1552.001"
|
||||
if [ ! -f "$motion_config" ] || ! lp_motion_readable "$motion_config"; then
|
||||
echo "Configuration exists but is not readable by the current user"
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Values are reduced to fixed labels or validated numbers. In particular,
|
||||
# motionEye stores the admin hash in a comment, which must never be printed.
|
||||
dd if="$motion_config" bs=65536 count=1 2>/dev/null | awk '
|
||||
/^[[:space:]]*#[[:space:]]*@admin_password([[:space:]]|$)/ {
|
||||
admin_seen = 1
|
||||
admin_present = ($3 != "")
|
||||
next
|
||||
}
|
||||
/^[[:space:]]*#/ { next }
|
||||
$1 == "webcontrol_port" { port = ($2 ~ /^[0-9]+$/ && $2 + 0 <= 65535 ? $2 : "invalid") }
|
||||
$1 == "webcontrol_parms" { parms = ($2 ~ /^[0-3]$/ ? $2 : "invalid") }
|
||||
$1 == "webcontrol_auth_method" {
|
||||
method = tolower($2)
|
||||
if (method == "0" || method == "none") auth = "disabled"
|
||||
else if (method == "1" || method == "2" || method == "basic" || method == "digest") auth = "enabled"
|
||||
else auth = "unknown"
|
||||
}
|
||||
$1 == "webcontrol_localhost" { local_only = ($2 == "on" || $2 == "off" ? $2 : "invalid") }
|
||||
END {
|
||||
if (admin_seen) print "motionEye admin hash: " (admin_present ? "present (redacted)" : "empty")
|
||||
print "Motion webcontrol port: " (port == "" ? "not configured" : port)
|
||||
print "Motion webcontrol parameter level: " (parms == "" ? "default (0)" : parms)
|
||||
print "Motion webcontrol authentication: " (auth == "" ? "disabled/default" : auth)
|
||||
print "Motion webcontrol localhost restriction: " (local_only == "" ? "default (on)" : local_only)
|
||||
if (port ~ /^[0-9]+$/ && port + 0 > 0 && parms ~ /^[2-3]$/ && (auth == "" || auth == "disabled"))
|
||||
print "Review: Motion webcontrol may expose advanced settings without authentication to local users"
|
||||
}
|
||||
' 2>/dev/null
|
||||
|
||||
# motionEye normally places per-camera settings beside motion.conf. Bound
|
||||
# this local glob so an unusual directory cannot turn the check into a scan.
|
||||
motion_dir="${motion_config%/*}"
|
||||
for motion_camera in "$motion_dir"/camera-*.conf; do
|
||||
[ -e "$motion_camera" ] || continue
|
||||
motion_camera_count=$((motion_camera_count + 1))
|
||||
[ "$motion_camera_count" -le 10 ] || break
|
||||
if [ ! -f "$motion_camera" ] || ! lp_motion_readable "$motion_camera"; then
|
||||
motion_camera_unreadable=1
|
||||
continue
|
||||
fi
|
||||
motion_camera_flags="$(dd if="$motion_camera" bs=65536 count=1 2>/dev/null | awk '
|
||||
/^[[:space:]]*#/ { next }
|
||||
$1 == "on_picture_save" && NF > 1 { hook = 1 }
|
||||
$1 == "picture_filename" && NF > 1 { filename = 1 }
|
||||
END { print hook + 0, filename + 0 }
|
||||
' 2>/dev/null)"
|
||||
case "$motion_camera_flags" in "1 "*) motion_hook=1 ;; esac
|
||||
case "$motion_camera_flags" in *" 1") motion_filename=1 ;; esac
|
||||
done
|
||||
[ "$motion_hook" -eq 1 ] && echo "Camera picture-save event hook: configured (value redacted)"
|
||||
[ "$motion_filename" -eq 1 ] && echo "Camera picture filename: configured (value redacted)"
|
||||
[ "$motion_camera_unreadable" -eq 1 ] && echo "Some camera configuration files are not readable"
|
||||
echo "Configuration alone does not establish that Motion is running or its service user"
|
||||
echo ""
|
||||
}
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ] && ! [ "$MACPEAS" ]; then
|
||||
lp_motion_conf_dir="/etc/motioneye"
|
||||
if lp_motion_readable /etc/motioneye/motioneye.conf; then
|
||||
lp_motion_custom_dir="$(awk '$1 == "conf_path" { path = $2 } END { print path }' /etc/motioneye/motioneye.conf 2>/dev/null)"
|
||||
case "$lp_motion_custom_dir" in /*) lp_motion_conf_dir="$lp_motion_custom_dir" ;; esac
|
||||
fi
|
||||
lp_motion_check_config "$lp_motion_conf_dir/motion.conf"
|
||||
lp_motion_check_config /etc/motion/motion.conf
|
||||
lp_motion_check_config /usr/local/etc/motion/motion.conf
|
||||
fi
|
||||
@@ -9,7 +9,7 @@
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $DEBUG, $knw_usrs, $nosh_usrs, $sh_usrs, $DEBUG, $USER, $STRINGS
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $mysqluser, $mysqlexec, $mysqlconnect, $mysqlconnectnopass, $mysqluser, $version_output, $major_version, $version, $process_info
|
||||
# Generated Global Variables: $mysqluser, $mysqlexec, $mysqlconnect, $mysqlconnectnopass, $mysqluser, $version_output, $version, $process_info, $mysql_unit_dir, $mysql_unit_parent, $mysql_unit_file, $mysql_unit_count, $mysql_unit_size, $mysql_unit_signal
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -107,13 +107,69 @@ if [ "$(command -v mysql || echo -n '')" ] || [ "$(command -v mysqladmin || echo
|
||||
echo ""
|
||||
fi
|
||||
|
||||
### This section checks if MySQL (mysqld) is running as root and if its version is 4.x or 5.x to refer a known local privilege escalation exploit! ###
|
||||
### Review the MySQL/MariaDB service identity without assuming a version is exploitable. ###
|
||||
|
||||
# Find the mysqld process
|
||||
process_info=$(ps aux | grep '[m]ysqld' | head -n1)
|
||||
process_info=$(ps aux 2>/dev/null | grep -E '[m]ysqld|[m]ariadbd' | head -n1)
|
||||
|
||||
if [ -z "$process_info" ]; then
|
||||
echo "MySQL process not found." | sed -${E} "s,.*,${SED_GREEN},"
|
||||
echo "MySQL/MariaDB process not visible to this user (it may be stopped or /proc may hide other users)."
|
||||
|
||||
# A fixed, small unit-file review can still expose an explicitly configured
|
||||
# root context when hidepid prevents process enumeration. Never run the unit.
|
||||
if [ -d /run/systemd/system ]; then
|
||||
mysql_unit_count=0
|
||||
for mysql_unit_dir in /etc/systemd/system /usr/lib/systemd/system /lib/systemd/system; do
|
||||
[ -d "$mysql_unit_dir" ] || continue
|
||||
mysql_unit_parent=$mysql_unit_dir
|
||||
while [ "$mysql_unit_parent" != / ] && [ ! -L "$mysql_unit_parent" ]; do
|
||||
mysql_unit_parent=${mysql_unit_parent%/*}
|
||||
[ "$mysql_unit_parent" ] || mysql_unit_parent=/
|
||||
done
|
||||
[ "$mysql_unit_parent" = / ] || continue
|
||||
for mysql_unit_file in "$mysql_unit_dir"/*mysql*.service "$mysql_unit_dir"/*mariadb*.service; do
|
||||
[ -f "$mysql_unit_file" ] && [ -r "$mysql_unit_file" ] && [ ! -L "$mysql_unit_file" ] || continue
|
||||
mysql_unit_count=$((mysql_unit_count + 1))
|
||||
if [ "$mysql_unit_count" -gt 12 ]; then
|
||||
echo "MySQL/MariaDB unit review incomplete (12-file limit)."
|
||||
break 2
|
||||
fi
|
||||
mysql_unit_size=$(stat -c %s "$mysql_unit_file" 2>/dev/null) ||
|
||||
mysql_unit_size=$(stat -f %z "$mysql_unit_file" 2>/dev/null) || continue
|
||||
case "$mysql_unit_size" in ''|*[!0-9]*) continue ;; esac
|
||||
[ "${#mysql_unit_size}" -le 6 ] && [ "$mysql_unit_size" -le 65536 ] || continue
|
||||
mysql_unit_signal=$(head -c 65537 "$mysql_unit_file" 2>/dev/null | awk '
|
||||
/^\[Service\][[:space:]]*$/ { service = 1; next }
|
||||
/^\[/ { service = 0; next }
|
||||
!service || /^[[:space:]]*[#;]/ { next }
|
||||
{
|
||||
line = $0
|
||||
sub(/^[[:space:]]*/, "", line)
|
||||
if (line ~ /^User[[:space:]]*=/) {
|
||||
sub(/^User[[:space:]]*=[[:space:]]*/, "", line)
|
||||
sub(/[[:space:]]*$/, "", line)
|
||||
user = line
|
||||
} else if (line ~ /^DynamicUser[[:space:]]*=/) {
|
||||
sub(/^DynamicUser[[:space:]]*=[[:space:]]*/, "", line)
|
||||
sub(/[[:space:]]*$/, "", line)
|
||||
dynamic = tolower(line)
|
||||
} else if (line ~ /^ExecStart[[:space:]]*=/) {
|
||||
sub(/^ExecStart[[:space:]]*=[[:space:]]*[-+!@]*/, "", line)
|
||||
direct = (line ~ /^\/[^[:space:]]+\/(mysqld|mariadbd)([[:space:]]|$)/ &&
|
||||
line !~ /(^|[[:space:]])--user([=[:space:]]|$)/)
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (direct && (user == "" || user == "root") && dynamic !~ /^(yes|true|1)$/)
|
||||
print (user == "root" ? "explicit root" : "default root")
|
||||
}
|
||||
' 2>/dev/null)
|
||||
if [ "$mysql_unit_signal" ]; then
|
||||
printf 'MySQL/MariaDB unit root-context review candidate: %s (%s; process state, drop-ins, SQL FILE/UDF rights and plugin directory unverified)\n' "$mysql_unit_file" "$mysql_unit_signal"
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
else
|
||||
|
||||
# Extract the process user
|
||||
@@ -125,22 +181,9 @@ else
|
||||
# Extract the version number (expects format like X.Y.Z)
|
||||
version=$(echo "$version_output" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
echo "Unable to determine MySQL version." | sed -${E} "s,.*,${SED_GREEN},"
|
||||
if [ "$mysqluser" = "root" ]; then
|
||||
printf 'MySQL/MariaDB process runs as root%s; review SQL access, FILE/UDF rights, plugin directory, and effective service policy.\n' "${version:+ (binary version $version)}" | sed -${E} "s,.*,${SED_RED},"
|
||||
else
|
||||
|
||||
# Extract the major version number (X from X.Y.Z)
|
||||
major_version=$(echo "$version" | cut -d. -f1)
|
||||
|
||||
# Check if MySQL is running as root and if the version is either 4.x or 5.x
|
||||
if [ "$mysqluser" = "root" ] && { [ "$major_version" -eq 4 ] || [ "$major_version" -eq 5 ]; }; then
|
||||
echo "MySQL is running as root with version $version. This is a potential local privilege escalation vulnerability!" | sed -${E} "s,.*,${SED_RED},"
|
||||
echo "\tRefer to: https://www.exploit-db.com/exploits/1518" | sed -${E} "s,.*,${SED_YELLOW},"
|
||||
echo "\tRefer to: https://medium.com/r3d-buck3t/privilege-escalation-with-mysql-user-defined-functions-996ef7d5ceaf" | sed -${E} "s,.*,${SED_YELLOW},"
|
||||
else
|
||||
echo "MySQL is running as user '$mysqluser' with version $version." | sed -${E} "s,.*,${SED_GREEN},"
|
||||
fi
|
||||
### ------------------------------------------------------------------------------------------------------------------------------------------------ ###
|
||||
|
||||
printf "MySQL/MariaDB process runs as user '%s'%s.\n" "$mysqluser" "${version:+ (binary version $version)}" | sed -${E} "s,.*,${SED_GREEN},"
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,84 @@
|
||||
# Title: Software Information - OMI management listener
|
||||
# ID: SI_Omi
|
||||
# Author: PEASS-ng contributors
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Passively correlate a standard OMI installation, process identity, and local WS-Management listener.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1068
|
||||
# Functions Used: print_3title, print_info
|
||||
# Global Variables:
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $omi_proc, $omi_port, $omi_pkg, $omi_timeout
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
omi_bounded() {
|
||||
if [ -n "$omi_timeout" ]; then
|
||||
"$omi_timeout" 2 "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
omi_process_user() {
|
||||
LC_ALL=C awk '
|
||||
NR > 4096 { print "#limit"; exit }
|
||||
$2 == "omiengine" { print $1; exit }
|
||||
'
|
||||
}
|
||||
|
||||
omi_listener() {
|
||||
LC_ALL=C awk '
|
||||
NR > 4096 { print "#limit"; exit }
|
||||
/LISTEN/ {
|
||||
local_address = $4
|
||||
if (local_address ~ /[.:]598[56]$/) { print local_address; exit }
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
omi_report_evidence() {
|
||||
printf 'OMI package version: %s (vendor backports/build provenance unverified).\n' "${3:-unknown}"
|
||||
case "$1" in
|
||||
'#limit') echo 'OMI process context: unknown (4,096-line process limit).' ;;
|
||||
'') echo 'OMI process context: not visible; service may be stopped or hidden.' ;;
|
||||
*) printf 'OMI process owner: %s (effective service identity needs confirmation).\n' "$1" ;;
|
||||
esac
|
||||
case "$2" in
|
||||
'#limit') echo 'WS-Management listener: unknown (4,096-line socket limit).' ;;
|
||||
'') echo 'WS-Management listener on 5985/5986: not visible; custom ports or Unix socket remain possible.' ;;
|
||||
*) printf 'WS-Management listener co-observed: %s (socket ownership unproven).\n' "$2" ;;
|
||||
esac
|
||||
if [ "$1" = root ] && [ -n "$2" ] && [ "$2" != '#limit' ]; then
|
||||
echo 'OMI review candidate: root-owned omiengine and a 5985/5986 listener are visible; verify socket ownership, installed security fix/backports, access policy, and actual SSRF/network reachability.'
|
||||
fi
|
||||
}
|
||||
|
||||
check_omi_exposure() {
|
||||
# Fixed standard paths gate all commands; no directory walk or service request.
|
||||
[ -e /opt/omi/bin/omiengine ] || [ -e /etc/opt/omi/conf/omiserver.conf ] || return 0
|
||||
print_3title 'OMI management listener review' 'T1068'
|
||||
print_info 'https://github.com/microsoft/omi/security/advisories/GHSA-fmh7-p6gp-8xpj'
|
||||
print_info 'https://book.hacktricks.wiki/en/network-services-pentesting/5985-5986-pentesting-omi.html'
|
||||
|
||||
omi_timeout=$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null)
|
||||
omi_proc=$(omi_bounded ps -eo user=,comm= 2>/dev/null | omi_process_user)
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
omi_port=$(omi_bounded ss -ltn 2>/dev/null | omi_listener)
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
omi_port=$(omi_bounded netstat -an 2>/dev/null | omi_listener)
|
||||
else
|
||||
omi_port=
|
||||
fi
|
||||
omi_pkg=
|
||||
if command -v dpkg-query >/dev/null 2>&1; then
|
||||
omi_pkg=$(omi_bounded dpkg-query -W -f='$''{Version}\n' omi 2>/dev/null | LC_ALL=C head -n1 | cut -c1-64)
|
||||
elif command -v rpm >/dev/null 2>&1; then
|
||||
omi_pkg=$(omi_bounded rpm -q --qf '%{VERSION}-%{RELEASE}\n' omi 2>/dev/null | LC_ALL=C head -n1 | cut -c1-64)
|
||||
fi
|
||||
case "$omi_pkg" in [0-9]* ) ;; * ) omi_pkg= ;; esac
|
||||
omi_report_evidence "$omi_proc" "$omi_port" "$omi_pkg"
|
||||
}
|
||||
|
||||
check_omi_exposure
|
||||
@@ -1,22 +1,100 @@
|
||||
# Title: Software Information - Pam.d
|
||||
# ID: SI_Pamd
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Passwords inside pam.d
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Password strings, nonstandard modules, and writable pam_exec helpers in PAM policy
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1556.003
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $DEBUG
|
||||
# Functions Used: checkPamAllowActiveCVE20256018, print_2title
|
||||
# Global Variables: $DEBUG, $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $pamdpass
|
||||
# Generated Global Variables: $pamdpass, $pamd_files, $pamd_hits, $pamd_module_hits, $pamd_module_limited, $pamd_config, $pamd_size, $pamd_helpers, $pamd_helper, $pamd_target, $pamd_module, $pamd_line
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
pamdpass=$(grep -Ri "passwd" ${ROOT_FOLDER}etc/pam.d/ 2>/dev/null | grep -v ":#")
|
||||
pamdpass=$(grep -Ri "passwd" "${ROOT_FOLDER}etc/pam.d/" 2>/dev/null | grep -v ":#")
|
||||
if [ "$pamdpass" ] || [ "$DEBUG" ]; then
|
||||
print_2title "Passwords inside pam.d" "T1556.003"
|
||||
grep -Ri "passwd" ${ROOT_FOLDER}etc/pam.d/ 2>/dev/null | grep -v ":#" | sed "s,passwd,${SED_RED},"
|
||||
if [ "$pamdpass" ]; then
|
||||
printf '%s\n' "$pamdpass" | sed "s,passwd,${SED_RED},"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Inspect only small local PAM policy files; do not execute or read helper contents.
|
||||
(
|
||||
pamd_files=0
|
||||
pamd_hits=0
|
||||
pamd_module_hits=0
|
||||
pamd_module_limited=0
|
||||
for pamd_config in "${ROOT_FOLDER}etc/pam.d/"* "${ROOT_FOLDER}etc/pam.conf"; do
|
||||
if [ "$pamd_files" -ge 128 ] || [ "$pamd_hits" -ge 24 ]; then break; fi
|
||||
[ -f "$pamd_config" ] && [ ! -L "$pamd_config" ] && [ -r "$pamd_config" ] || continue
|
||||
pamd_files=$((pamd_files + 1))
|
||||
pamd_size=$(wc -c < "$pamd_config" 2>/dev/null) || continue
|
||||
[ "$pamd_size" -le 8192 ] || continue
|
||||
pamd_helpers=$(awk '
|
||||
/^[[:space:]]*#/ { next }
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if (substr($i, 1, 1) == "#") break
|
||||
if ($i ~ /(^|\/)pam_exec[.]so$/) {
|
||||
for (j = i + 1; j <= NF; j++) {
|
||||
if (substr($j, 1, 1) == "#") break
|
||||
if ($j ~ /^\/[A-Za-z0-9._+\/-]+$/) { print "E:" $j; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
module_start = 0
|
||||
if ($1 ~ /^-?(auth|account|password|session)$/) module_start = 3
|
||||
else if ($2 ~ /^-?(auth|account|password|session)$/) module_start = 4
|
||||
if (module_start) {
|
||||
for (i = module_start; i <= NF; i++) {
|
||||
if (substr($i, 1, 1) == "#") break
|
||||
if ($i ~ /^[A-Za-z0-9._+\/-]+[.]so$/) {
|
||||
module = $i
|
||||
sub(/^.*\//, "", module)
|
||||
if (tolower(module) !~ /^pam_/) print "M:" NR ":" $i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
' "$pamd_config" 2>/dev/null)
|
||||
[ -n "$pamd_helpers" ] || continue
|
||||
while IFS= read -r pamd_helper; do
|
||||
case "$pamd_helper" in
|
||||
M:*)
|
||||
if [ "$pamd_module_hits" -ge 16 ]; then
|
||||
pamd_module_limited=1
|
||||
continue
|
||||
fi
|
||||
pamd_line=${pamd_helper#M:}
|
||||
pamd_module=${pamd_line#*:}
|
||||
pamd_line=${pamd_line%%:*}
|
||||
pamd_module_hits=$((pamd_module_hits + 1))
|
||||
printf 'PAM nonstandard module review candidate: %s:%s -> %s (confirm installed module, service reachability, and control flow)\n' "$pamd_config" "$pamd_line" "$pamd_module"
|
||||
continue
|
||||
;;
|
||||
E:*) pamd_helper=${pamd_helper#E:} ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
[ "$pamd_hits" -ge 24 ] && continue
|
||||
pamd_target="${ROOT_FOLDER%/}${pamd_helper}"
|
||||
[ -f "$pamd_target" ] && [ ! -L "$pamd_target" ] && [ -x "$pamd_target" ] && [ -w "$pamd_target" ] || continue
|
||||
pamd_hits=$((pamd_hits + 1))
|
||||
printf 'PAM exec writable helper candidate: %s -> %s (confirm PAM event, effective UID, parent paths, and ACLs)\n' "$pamd_config" "$pamd_target"
|
||||
ls -ld "$pamd_target" 2>/dev/null
|
||||
if command -v lsattr >/dev/null 2>&1; then lsattr -d "$pamd_target" 2>/dev/null; fi
|
||||
done <<EOF
|
||||
$pamd_helpers
|
||||
EOF
|
||||
done
|
||||
[ "$pamd_module_limited" -eq 1 ] && printf 'PAM nonstandard module review capped at 16 findings; more policy lines may need review.\n'
|
||||
)
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
checkPamAllowActiveCVE20256018
|
||||
fi
|
||||
@@ -9,10 +9,214 @@
|
||||
# Functions Used: print_2title, print_3title
|
||||
# Global Variables: $HOME, $HOMESEARCH, $ROOT_FOLDER, $SEARCH_IN_FOLDER, $TIMEOUT, $USER, $wgroups
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $certsb4_grep, $hostsallow, $hostsdenied, $sshconfig, $writable_agents, $agent_sockets, $privatekeyfilesetc, $privatekeyfileshome, $privatekeyfilesroot, $privatekeyfilesmnt,
|
||||
# Generated Global Variables: $certsb4_grep, $hostsallow, $hostsdenied, $sshconfig, $writable_agents, $agent_sockets, $privatekeyfilesetc, $privatekeyfileshome, $privatekeyfilesroot, $privatekeyfilesmnt, $sc_number, $sc_depth, $sc_pub, $ssh_ca_files, $sc_file, $sc_line, $sc_meta, $sc_candidate, $ssh_ca_root_login_seen, $sc_mapped, $sc_key, $ssh_ca_context, $sc_header, $sc_value, $sc_pattern, $ssh_ca_principals_seen, $sc_keyword, $sc_config, $sc_output, $sc_root, $sc_count, $sc_size, $mux_home, $mux_path, $mux_count
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
# A multiplexed SSH master can leave a Unix socket below the current home.
|
||||
# Inspect only .ssh and one direct child level; do not connect to candidates.
|
||||
ssh_control_socket_candidates() {
|
||||
mux_home=$1
|
||||
mux_count=0
|
||||
[ "$SEARCH_IN_FOLDER" ] && return 0
|
||||
[ -n "$mux_home" ] && [ -d "$mux_home/.ssh" ] || return 0
|
||||
for mux_path in "$mux_home"/.ssh/* "$mux_home"/.ssh/*/*; do
|
||||
[ -S "$mux_path" ] && [ ! -L "$mux_path" ] || continue
|
||||
if [ "$mux_count" -eq 0 ]; then
|
||||
print_3title "Unix sockets under current SSH home (ControlMaster candidates):" "T1021.004"
|
||||
fi
|
||||
LC_ALL=C ls -ld "$mux_path" 2>/dev/null
|
||||
mux_count=$((mux_count + 1))
|
||||
if [ "$mux_count" -ge 20 ]; then
|
||||
printf '%s\n' ' [*] Output capped at 20 sockets; others may be unseen.'
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$mux_count" -gt 0 ]; then
|
||||
printf '%s\n' ' [?] Path, owner and mode only. Config, access and live SSH master remain unverified; an agent socket is different.'
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# SSH user CA correlation. This is a bounded config read, not sshd's effective
|
||||
# configuration: Match criteria, command-line options and unvisited Includes may
|
||||
# change the values for a particular connection.
|
||||
ssh_ca_scan_config() {
|
||||
local sc_file="$1" sc_depth="$2" sc_line sc_number=0 sc_keyword sc_value
|
||||
local sc_pattern sc_mapped sc_candidate sc_pub sc_key sc_header sc_meta
|
||||
[ "$sc_depth" -le 3 ] && [ "$ssh_ca_files" -lt 32 ] || return 0
|
||||
[ -f "$sc_file" ] && [ -r "$sc_file" ] || return 0
|
||||
ssh_ca_files=$((ssh_ca_files + 1))
|
||||
while IFS= read -r sc_line || [ -n "$sc_line" ]; do
|
||||
sc_number=$((sc_number + 1))
|
||||
[ "$sc_number" -le 256 ] || break
|
||||
# Only simple, unquoted directives are interpreted. Never evaluate config text.
|
||||
sc_line=${sc_line%%#*}
|
||||
case "$sc_line" in *\"*|*\'* ) continue ;; esac
|
||||
set -f
|
||||
# Intentional whitespace tokenization with globbing disabled.
|
||||
# shellcheck disable=SC2086
|
||||
set -- $sc_line
|
||||
set +f
|
||||
[ "$#" -gt 0 ] || continue
|
||||
sc_keyword=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
|
||||
shift
|
||||
case "$sc_keyword" in
|
||||
match)
|
||||
ssh_ca_context='Match (conditional)'
|
||||
;;
|
||||
include)
|
||||
[ "$#" -le 8 ] || continue
|
||||
for sc_pattern do
|
||||
# Restrict to plain paths and a final-component '*' glob. This avoids
|
||||
# eval, broad directory walks, and ambiguous quoted/token paths.
|
||||
case "$sc_pattern" in
|
||||
*[!A-Za-z0-9_./*-]*|*/*/*/*/*/*/*/*|*'**'*) continue ;;
|
||||
esac
|
||||
case "$sc_pattern" in
|
||||
/*) ;;
|
||||
*) sc_pattern="/etc/ssh/$sc_pattern" ;;
|
||||
esac
|
||||
case "${sc_pattern%/*}" in *'*'*) continue ;; esac
|
||||
sc_mapped="$sc_pattern"
|
||||
[ "$SEARCH_IN_FOLDER" ] && sc_mapped="${ROOT_FOLDER%/}$sc_pattern"
|
||||
for sc_candidate in $sc_mapped; do
|
||||
[ "$ssh_ca_files" -lt 32 ] || break
|
||||
case "$sc_candidate" in *[!A-Za-z0-9_./-]* ) continue ;; esac
|
||||
ssh_ca_scan_config "$sc_candidate" $((sc_depth + 1))
|
||||
done
|
||||
done
|
||||
;;
|
||||
trustedusercakeys|authorizedprincipalsfile|authorizedprincipalscommand|permitrootlogin)
|
||||
[ "$#" -gt 0 ] || continue
|
||||
sc_value="$1"
|
||||
case "$sc_keyword" in
|
||||
trustedusercakeys)
|
||||
[ "$#" -eq 1 ] || continue
|
||||
case "$sc_value" in /*) ;; *) continue ;; esac
|
||||
case "$sc_value" in *[!A-Za-z0-9_./-]* ) continue ;; esac
|
||||
printf 'TrustedUserCAKeys %s (%s:%s; %s)\n' "$sc_value" "$sc_file" "$sc_number" "$ssh_ca_context"
|
||||
case "$sc_value" in *.pub) ;; *) continue ;; esac
|
||||
sc_pub="$sc_value"
|
||||
[ "$SEARCH_IN_FOLDER" ] && sc_pub="${ROOT_FOLDER%/}$sc_value"
|
||||
[ -f "$sc_pub" ] && [ -r "$sc_pub" ] || continue
|
||||
sc_key=${sc_pub%.pub}
|
||||
[ -f "$sc_key" ] && [ -r "$sc_key" ] && [ ! -L "$sc_key" ] || continue
|
||||
sc_header=$(dd if="$sc_key" bs=64 count=1 2>/dev/null) || continue
|
||||
case "$sc_header" in
|
||||
'-----BEGIN OPENSSH PRIVATE KEY-----'*|'-----BEGIN RSA PRIVATE KEY-----'*|\
|
||||
'-----BEGIN EC PRIVATE KEY-----'*|'-----BEGIN DSA PRIVATE KEY-----'*|\
|
||||
'-----BEGIN PRIVATE KEY-----'*|'-----BEGIN ENCRYPTED PRIVATE KEY-----'*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
sc_meta=$(stat -c '%U %a' "$sc_key" 2>/dev/null) ||
|
||||
sc_meta=$(stat -f '%Su %Lp' "$sc_key" 2>/dev/null) || continue
|
||||
printf 'Readable CA private-key sibling: %s (owner mode: %s; header only checked)\n' "$sc_key" "$sc_meta"
|
||||
;;
|
||||
authorizedprincipalsfile|authorizedprincipalscommand)
|
||||
ssh_ca_principals_seen=1
|
||||
printf '%s %s (%s:%s; %s)\n' "$sc_keyword" "$sc_value" "$sc_file" "$sc_number" "$ssh_ca_context"
|
||||
;;
|
||||
permitrootlogin)
|
||||
ssh_ca_root_login_seen=1
|
||||
printf 'PermitRootLogin %s (%s:%s; %s)\n' "$sc_value" "$sc_file" "$sc_number" "$ssh_ca_context"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done < "$sc_file"
|
||||
}
|
||||
|
||||
ssh_ca_trust_correlation() {
|
||||
local sc_config='/etc/ssh/sshd_config' sc_output
|
||||
[ "$SEARCH_IN_FOLDER" ] && sc_config="${ROOT_FOLDER%/}$sc_config"
|
||||
[ -f "$sc_config" ] && [ -r "$sc_config" ] || return 0
|
||||
sc_output=$(
|
||||
ssh_ca_files=0
|
||||
ssh_ca_context='before Match in inspected files'
|
||||
ssh_ca_principals_seen=0
|
||||
ssh_ca_root_login_seen=0
|
||||
ssh_ca_scan_config "$sc_config" 0
|
||||
[ "$ssh_ca_principals_seen" -eq 0 ] &&
|
||||
printf 'No AuthorizedPrincipalsFile/Command observed in inspected files.\n'
|
||||
[ "$ssh_ca_root_login_seen" -eq 0 ] &&
|
||||
printf 'No PermitRootLogin observed in inspected files.\n'
|
||||
)
|
||||
case "$sc_output" in *'TrustedUserCAKeys '*) ;; *) return 0 ;; esac
|
||||
print_3title 'SSH user CA trust (partial config inspection)' 'T1552.004,T1021.004'
|
||||
printf '%s\n' "$sc_output"
|
||||
printf '%s\n' 'If both principal sources are absent from the effective config, certificate principals must match account names.'
|
||||
printf '%s\n' 'Earlier directives, Match, unvisited Includes, and runtime options may change these values; key headers do not prove a CA match or root login.'
|
||||
echo ''
|
||||
}
|
||||
|
||||
# Show only a bounded portion of the readable server config. Match headings
|
||||
# retain the scope of subsequent directives; this text is not sshd's policy.
|
||||
ssh_forwarding_config_review() {
|
||||
print_3title 'SSH server directives (review effective SSH policy)' 'T1021.004'
|
||||
if [ ! -f "$1" ] || [ ! -r "$1" ]; then
|
||||
printf '%s\n' 'SSH server config unreadable; forwarding policy unknown.'
|
||||
return 0
|
||||
fi
|
||||
awk '
|
||||
NR > 512 { exit }
|
||||
{
|
||||
line = substr($0, 1, 1024)
|
||||
sub(/[[:space:]]*#.*/, "", line)
|
||||
sub(/^[[:space:]]*/, "", line)
|
||||
sub(/[[:space:]]*$/, "", line)
|
||||
if (line == "") next
|
||||
split(line, fields, /[[:space:]]+/)
|
||||
key = tolower(fields[1])
|
||||
if (key == "match") {
|
||||
context = line
|
||||
printf "%d: %s\n", NR, line
|
||||
} else if (key == "include" || key == "permitrootlogin" ||
|
||||
key == "challengeresponseauthentication" ||
|
||||
key == "passwordauthentication" || key == "usepam" ||
|
||||
key == "port" || key == "permitemptypasswords" ||
|
||||
key == "pubkeyauthentication" || key == "listenaddress" ||
|
||||
key == "forwardagent" || key == "allowagentforwarding" ||
|
||||
key == "authorizedkeysfile" || key == "authorizedkeyscommand" ||
|
||||
key == "authorizedkeyscommanduser" || key == "allowtcpforwarding" ||
|
||||
key == "disableforwarding" || key == "permitopen" ||
|
||||
key == "forcecommand" || key == "chrootdirectory") {
|
||||
printf "%d [%s]: %s\n", NR, context, line
|
||||
}
|
||||
}
|
||||
BEGIN { context = "global" }
|
||||
' "$1"
|
||||
printf '%s\n' 'Text review only (first 512 lines, 1024 characters per line). Include targets, Match applicability, command-line overrides, and authorized-key restrictions are unknown here; key-helper directives do not prove execution or vulnerability.'
|
||||
echo ''
|
||||
}
|
||||
|
||||
# Without timeout, inspect only direct files in /root. Cap the number and size
|
||||
# before reading a header; report metadata, never key material.
|
||||
ssh_root_key_headers() {
|
||||
local sc_root="$1" sc_file sc_count=0 sc_size sc_header sc_meta
|
||||
[ "$(id -u 2>/dev/null)" = 0 ] || return 0
|
||||
[ -d "$sc_root" ] && [ -r "$sc_root" ] && [ -x "$sc_root" ] || return 0
|
||||
for sc_file in "$sc_root"/* "$sc_root"/.[!.]* "$sc_root"/..?*; do
|
||||
[ -f "$sc_file" ] && [ -r "$sc_file" ] && [ ! -L "$sc_file" ] || continue
|
||||
sc_count=$((sc_count + 1))
|
||||
[ "$sc_count" -le 64 ] || break
|
||||
sc_size=$(stat -c '%s' "$sc_file" 2>/dev/null) ||
|
||||
sc_size=$(stat -f '%z' "$sc_file" 2>/dev/null) || continue
|
||||
case "$sc_size" in ''|*[!0-9]*) continue ;; esac
|
||||
[ "$sc_size" -le 1048576 ] || continue
|
||||
sc_header=$(dd if="$sc_file" bs=64 count=1 2>/dev/null) || continue
|
||||
case "$sc_header" in
|
||||
'-----BEGIN OPENSSH PRIVATE KEY-----'*|'-----BEGIN RSA PRIVATE KEY-----'*|\
|
||||
'-----BEGIN EC PRIVATE KEY-----'*|'-----BEGIN DSA PRIVATE KEY-----'*|\
|
||||
'-----BEGIN PRIVATE KEY-----'*|'-----BEGIN ENCRYPTED PRIVATE KEY-----'*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
sc_meta=$(stat -c '%U %a' "$sc_file" 2>/dev/null) ||
|
||||
sc_meta=$(stat -f '%Su %Lp' "$sc_file" 2>/dev/null) || continue
|
||||
printf '%s (owner mode: %s; header only checked)\n' "$sc_file" "$sc_meta"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
print_2title "Searching ssl/ssh files" "T1552.004,T1021.004"
|
||||
if [ "$PSTORAGE_CERTSB4" ]; then certsb4_grep=$(grep -L "\"\|'\|(" $PSTORAGE_CERTSB4 2>/dev/null); fi
|
||||
@@ -36,7 +240,12 @@ fi
|
||||
|
||||
peass{SSH}
|
||||
|
||||
grep "PermitRootLogin \|ChallengeResponseAuthentication \|PasswordAuthentication \|UsePAM \|Port\|PermitEmptyPasswords\|PubkeyAuthentication\|ListenAddress\|ForwardAgent\|AllowAgentForwarding\|AuthorizedKeysFile" /etc/ssh/sshd_config 2>/dev/null | grep -v "#" | sed -${E} "s,PermitRootLogin.*es|PermitEmptyPasswords.*es|ChallengeResponseAuthentication.*es|FordwardAgent.*es,${SED_RED},"
|
||||
if [ "$SEARCH_IN_FOLDER" ]; then
|
||||
ssh_forwarding_config_review "${ROOT_FOLDER%/}/etc/ssh/sshd_config"
|
||||
else
|
||||
ssh_forwarding_config_review '/etc/ssh/sshd_config'
|
||||
fi
|
||||
ssh_ca_trust_correlation
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
if [ "$TIMEOUT" ]; then
|
||||
@@ -47,6 +256,7 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
else
|
||||
privatekeyfilesetc=$(grep -rl '\-\-\-\-\-BEGIN .* PRIVATE KEY\-\-\-\-\-' /etc 2>/dev/null) #If there is tons of files linpeas gets frozen here without a timeout
|
||||
privatekeyfileshome=$(grep -rl '\-\-\-\-\-BEGIN .* PRIVATE KEY\-\-\-\-\-' $HOME/.ssh 2>/dev/null)
|
||||
privatekeyfilesroot=$(ssh_root_key_headers /root)
|
||||
fi
|
||||
else
|
||||
# If $SEARCH_IN_FOLDER lets just search for private keys in the whole firmware
|
||||
@@ -83,6 +293,7 @@ if [ "$agent_sockets" ]; then
|
||||
printf "%s\n" "$agent_sockets" | sed -${E} "s,.*,${SED_RED},"
|
||||
echo ""
|
||||
fi
|
||||
ssh_control_socket_candidates "$HOME"
|
||||
if ssh-add -l 2>/dev/null | grep -qv 'no identities'; then
|
||||
print_3title "Listing SSH Agents" "T1552.004,T1021.004"
|
||||
ssh-add -l
|
||||
|
||||
@@ -7,24 +7,84 @@
|
||||
# Version: 1.0
|
||||
# Mitre: T1563
|
||||
# Functions Used: print_2title, print_info
|
||||
# Global Variables: $DEBUG, $SEARCH_IN_FOLDER, $wgroups
|
||||
# Global Variables: $DEBUG, $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $tmuxdefsess, $tmuxnondefsess, $tmuxsess2
|
||||
# Generated Global Variables: $tmuxdefsess, $tmuxnondefsess, $tmuxsess2, $tmux_scan_output, $tmux_socket_candidates, $tmux_scan_partial, $tmux_root, $tmux_root_seen, $tmux_dir, $tmux_socket, $tmux_dir_count, $tmux_entry_count, $tmux_partial, $tmux_uid, $tmux_groups, $tmux_meta, $tmux_owner_uid, $tmux_owner_gid, $tmux_rest, $tmux_mode, $tmux_mode_writable, $TMUX_TMPDIR
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
# The standard socket lives directly below tmux-UID. Inspect only that shape,
|
||||
# never recurse through /tmp or attach to a session. The second root is used
|
||||
# only when TMUX_TMPDIR names a real absolute directory.
|
||||
tmux_socket_review() (
|
||||
tmux_uid=$(id -u 2>/dev/null) || exit 0
|
||||
tmux_groups=$(id -G 2>/dev/null) || exit 0
|
||||
tmux_dir_count=0
|
||||
tmux_entry_count=0
|
||||
tmux_partial=
|
||||
tmux_root_seen=0
|
||||
for tmux_root in "$1" "$2"; do
|
||||
[ -n "$tmux_root" ] || continue
|
||||
case "$tmux_root" in /*) ;; *) continue ;; esac
|
||||
[ -d "$tmux_root" ] && [ ! -L "$tmux_root" ] || continue
|
||||
if [ "$tmux_root_seen" -gt 0 ] && [ "$tmux_root" = "$1" ]; then continue; fi
|
||||
tmux_root_seen=$((tmux_root_seen + 1))
|
||||
for tmux_dir in "$tmux_root"/tmux-*; do
|
||||
[ -d "$tmux_dir" ] && [ ! -L "$tmux_dir" ] || continue
|
||||
if [ "$tmux_dir_count" -ge 32 ]; then tmux_partial=1; break 2; fi
|
||||
tmux_dir_count=$((tmux_dir_count + 1))
|
||||
printf 'DIR %s\n' "$tmux_dir"
|
||||
for tmux_socket in "$tmux_dir"/*; do
|
||||
[ -e "$tmux_socket" ] || [ -L "$tmux_socket" ] || continue
|
||||
if [ "$tmux_entry_count" -ge 64 ]; then tmux_partial=1; break 3; fi
|
||||
tmux_entry_count=$((tmux_entry_count + 1))
|
||||
[ ! -L "$tmux_socket" ] && [ -S "$tmux_socket" ] || continue
|
||||
tmux_meta=$(stat -c '%u:%g:%A' "$tmux_socket" 2>/dev/null) ||
|
||||
tmux_meta=$(stat -f '%u:%g:%Sp' "$tmux_socket" 2>/dev/null) || continue
|
||||
case "$tmux_meta" in *:*:*) ;; *) continue ;; esac
|
||||
tmux_owner_uid=${tmux_meta%%:*}
|
||||
tmux_rest=${tmux_meta#*:}
|
||||
tmux_owner_gid=${tmux_rest%%:*}
|
||||
tmux_mode=${tmux_rest#*:}
|
||||
[ "$tmux_owner_uid" != "$tmux_uid" ] || continue
|
||||
tmux_mode_writable=
|
||||
case "$tmux_mode" in ????????w?) tmux_mode_writable=1 ;; esac
|
||||
case "$tmux_mode" in
|
||||
?????w????)
|
||||
case " $tmux_groups " in *" $tmux_owner_gid "*) tmux_mode_writable=1 ;; esac
|
||||
;;
|
||||
esac
|
||||
[ "$tmux_mode_writable" ] && [ -w "$tmux_socket" ] || continue
|
||||
printf 'SOCKET %s\n' "$tmux_socket"
|
||||
done
|
||||
done
|
||||
done
|
||||
[ "$tmux_partial" ] && printf 'PARTIAL\n'
|
||||
return 0
|
||||
)
|
||||
|
||||
tmuxdefsess=$(tmux ls 2>/dev/null)
|
||||
tmuxnondefsess=$(ps auxwww | grep "tmux " | grep -v grep)
|
||||
tmuxsess2=$(find /tmp -type d -path "/tmp/tmux-*" 2>/dev/null)
|
||||
if ([ "$tmuxdefsess" ] || [ "$tmuxnondefsess" ] || [ "$tmuxsess2" ] || [ "$DEBUG" ]) && ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
TMUX_TMPDIR=${TMUX_TMPDIR:-}
|
||||
tmux_scan_output=
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
tmux_scan_output=$(tmux_socket_review /tmp "$TMUX_TMPDIR")
|
||||
fi
|
||||
tmuxsess2=$(printf '%s\n' "$tmux_scan_output" | sed -n 's/^DIR //p')
|
||||
tmux_socket_candidates=$(printf '%s\n' "$tmux_scan_output" | sed -n 's/^SOCKET //p')
|
||||
tmux_scan_partial=$(printf '%s\n' "$tmux_scan_output" | sed -n '/^PARTIAL$/p')
|
||||
if ([ "$tmuxdefsess" ] || [ "$tmuxnondefsess" ] || [ "$tmuxsess2" ] || [ "$tmux_scan_partial" ] || [ "$DEBUG" ]) && ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
print_2title "Searching tmux sessions"$N
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/linux-basics/linux-privilege-escalation/index.html#open-shell-sessions"
|
||||
tmux -V
|
||||
printf "$tmuxdefsess\n$tmuxnondefsess\n$tmuxsess2" | sed -${E} "s,.*,${SED_RED}," | sed -${E} "s,no server running on.*,${C}[32m&${C}[0m,"
|
||||
printf '%s\n%s\n%s\n' "$tmuxdefsess" "$tmuxnondefsess" "$tmuxsess2" | sed -${E} "s,.*,${SED_RED}," | sed -${E} "s,no server running on.*,${C}[32m&${C}[0m,"
|
||||
|
||||
find /tmp -type s -path "/tmp/tmux*" -not -user $USER '(' '(' -perm -o=w ')' -or '(' -perm -g=w -and '(' $wgroups ')' ')' ')' 2>/dev/null | while read f; do
|
||||
echo "Other user tmux socket is writable: $f" | sed "s,$f,${SED_RED_YELLOW},"
|
||||
done
|
||||
if [ "$tmux_socket_candidates" ]; then
|
||||
printf '%s\n' "$tmux_socket_candidates" | sed "s,.*,Other user tmux socket is writable: ${SED_RED_YELLOW}&,"
|
||||
fi
|
||||
if [ "$tmux_scan_partial" ]; then
|
||||
echo "Tmux socket review partial: stopped after 32 directories or 64 direct entries."
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
@@ -1,25 +1,32 @@
|
||||
# Title: Software Information - Vault-ssh
|
||||
# ID: SI_Vault_ssh
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Searching Vault-ssh files
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Report cached Vault CLI token paths independently of SSH helper configuration
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1552.004
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $DEBUG
|
||||
# Global Variables: $DEBUG, $E, $SED_RED
|
||||
# Cached storage inputs (builder): $PSTORAGE_VAULT_SSH_HELPER, $PSTORAGE_VAULT_SSH_TOKEN
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
if [ "$PSTORAGE_VAULT_SSH_TOKEN" ]; then
|
||||
print_2title "Vault CLI token file paths" "T1552.004"
|
||||
printf '%s\n' "$PSTORAGE_VAULT_SSH_TOKEN" | awk 'NR <= 10 { print } END { if (NR > 10) print "Additional token paths omitted (partial inventory)" }' | sed -${E} "1,10s,.*,${SED_RED},"
|
||||
echo "Paths only; token policy, SSH role, and login reachability require separate review."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ "$PSTORAGE_VAULT_SSH_HELPER" ] || [ "$DEBUG" ]; then
|
||||
print_2title "Searching Vault-ssh files" "T1552.004"
|
||||
printf "$PSTORAGE_VAULT_SSH_HELPER\n"
|
||||
printf "%s\n" "$PSTORAGE_VAULT_SSH_HELPER" | while read f; do cat "$f" 2>/dev/null; vault-ssh-helper -verify-only -config "$f" 2>/dev/null; done
|
||||
echo ""
|
||||
vault secrets list 2>/dev/null
|
||||
printf "%s\n" "$PSTORAGE_VAULT_SSH_TOKEN" | sed -${E} "s,.*,${SED_RED}," 2>/dev/null
|
||||
fi
|
||||
echo ""
|
||||
echo ""
|
||||
+13
-4
@@ -1,10 +1,10 @@
|
||||
# Title: Interesting Permissions Files - Writable files by ownership or all
|
||||
# ID: IP_Writable_files_owner_all
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Interesting writable files owned by me or writable by everyone (not in Home)
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1574.009,T1574.010
|
||||
# Functions Used: print_2title, print_info
|
||||
# Global Variables: $HOME, $IAMROOT, $ITALIC, $notExtensions, $ROOT_FOLDER, $USER, $writeVB, $writeB
|
||||
@@ -18,7 +18,16 @@ if ! [ "$IAMROOT" ]; then
|
||||
print_2title "Interesting writable files owned by me or writable by everyone (not in Home) (max 200)" "T1574.009,T1574.010"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/linux-basics/linux-privilege-escalation/index.html#writable-files"
|
||||
#In the next file, you need to specify type "d" and "f" to avoid fake link files apparently writable by all
|
||||
obmowbe=$(find $ROOT_FOLDER '(' -type f -or -type d ')' '(' '(' -user $USER ')' -or '(' -perm -o=w ')' ')' ! -path "/proc/*" ! -path "/sys/*" ! -path "/dev/*" ! -path "/snap/*" ! -path "$HOME/*" 2>/dev/null | grep -Ev "$notExtensions" | sort | uniq | awk -F/ '{line_init=$0; if (!cont){ cont=0 }; $NF=""; act=$0; if (act == pre){(cont += 1)} else {cont=0}; if (cont < 5){ print line_init; } if (cont == "5"){print "#)You_can_write_even_more_files_inside_last_directory\n"}; pre=act }' | head -n 200)
|
||||
# These trees were already excluded from results. Prune them before walking
|
||||
# their descendants so a large home or virtual filesystem cannot dominate
|
||||
# this scan.
|
||||
obmowbe=$(find $ROOT_FOLDER \
|
||||
'(' -path /proc -o -path /sys -o -path /dev -o -path /snap ')' -prune \
|
||||
'(' -type f -o -type d ')' '(' -user "$USER" -o -perm -o=w ')' -print -o \
|
||||
'(' -path "$HOME" -prune '(' -type f -o -type d ')' '(' -user "$USER" -o -perm -o=w ')' -print ')' -o \
|
||||
'(' '(' -type f -o -type d ')' '(' -user "$USER" -o -perm -o=w ')' \
|
||||
! -path "/proc/*" ! -path "/sys/*" ! -path "/dev/*" ! -path "/snap/*" ! -path "$HOME/*" -print ')' \
|
||||
2>/dev/null | grep -Ev "$notExtensions" | sort | uniq | awk -F/ '{line_init=$0; if (!cont){ cont=0 }; $NF=""; act=$0; if (act == pre){(cont += 1)} else {cont=0}; if (cont < 5){ print line_init; } if (cont == "5"){print "#)You_can_write_even_more_files_inside_last_directory\n"}; pre=act }' | head -n 200)
|
||||
printf "%s\n" "$obmowbe" | while read l; do
|
||||
if echo "$l" | grep -q "You_can_write_even_more_files_inside_last_directory"; then printf $ITALIC"$l\n"$NC;
|
||||
elif echo "$l" | grep -qE "$writeVB"; then
|
||||
@@ -28,4 +37,4 @@ if ! [ "$IAMROOT" ]; then
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
@@ -1,10 +1,10 @@
|
||||
# Title: Interesting Permissions Files - Interesting writable files by group
|
||||
# ID: IP_Writable_files_group
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Interesting GROUP writable files (not in Home)
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1574.009,T1574.010
|
||||
# Functions Used: print_2title, print_info
|
||||
# Global Variables: $DEBUG, $HOME, $IAMROOT, $notExtensions, $ROOT_FOLDER, $writeVB, $writeB
|
||||
@@ -18,7 +18,13 @@ if ! [ "$IAMROOT" ]; then
|
||||
print_2title "Interesting GROUP writable files (not in Home) (max 200)" "T1574.009,T1574.010"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/linux-basics/linux-privilege-escalation/index.html#writable-files"
|
||||
for g in $(groups); do
|
||||
iwfbg=$(find $ROOT_FOLDER '(' -type f -or -type d ')' -group $g -perm -g=w ! -path "/proc/*" ! -path "/sys/*" ! -path "$HOME/*" 2>/dev/null | grep -Ev "$notExtensions" | awk -F/ '{line_init=$0; if (!cont){ cont=0 }; $NF=""; act=$0; if (act == pre){(cont += 1)} else {cont=0}; if (cont < 5){ print line_init; } if (cont == "5"){print "#)You_can_write_even_more_files_inside_last_directory\n"}; pre=act }' | head -n 200)
|
||||
iwfbg=$(find $ROOT_FOLDER \
|
||||
'(' -path /proc -o -path /sys ')' -prune \
|
||||
'(' -type f -o -type d ')' -group "$g" -perm -g=w -print -o \
|
||||
'(' -path "$HOME" -prune '(' -type f -o -type d ')' -group "$g" -perm -g=w -print ')' -o \
|
||||
'(' '(' -type f -o -type d ')' -group "$g" -perm -g=w \
|
||||
! -path "/proc/*" ! -path "/sys/*" ! -path "$HOME/*" -print ')' \
|
||||
2>/dev/null | grep -Ev "$notExtensions" | awk -F/ '{line_init=$0; if (!cont){ cont=0 }; $NF=""; act=$0; if (act == pre){(cont += 1)} else {cont=0}; if (cont < 5){ print line_init; } if (cont == "5"){print "#)You_can_write_even_more_files_inside_last_directory\n"}; pre=act }' | head -n 200)
|
||||
if [ "$iwfbg" ] || [ "$DEBUG" ]; then
|
||||
printf " Group $GREEN$g:\n$NC";
|
||||
printf "%s\n" "$iwfbg" | while read l; do
|
||||
@@ -32,4 +38,4 @@ if ! [ "$IAMROOT" ]; then
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
@@ -1,15 +1,15 @@
|
||||
# Title: Interesting Permissions Files - SUID
|
||||
# ID: IP_SUID
|
||||
# Author: Carlos Polop, HT Bot
|
||||
# Last Update: 30-09-2026
|
||||
# Description: SUID - Check easy privesc, exploits, write perms, and risky file placement
|
||||
# Last Update: 09-10-2026
|
||||
# Description: SUID - Check easy privesc, exploits, write perms, risky file placement, and identity-helper candidates
|
||||
# License: GNU GPL
|
||||
# Version: 1.2
|
||||
# Version: 1.5
|
||||
# Mitre: T1548.001
|
||||
# Functions Used: check_privileged_file_location, echo_not_found, print_2title, print_info
|
||||
# Global Variables: $IAMROOT, $LDD, $ROOT_FOLDER, $READELF, $sidB, $sidG1, $sidG2, $sidG3, $sidG4, $sidVB, $sidVB2, $STRACE, $STRINGS, $TIMEOUT, $Wfolders, $cfuncs
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $suids_files, $sfile, $sname, $sowner, $sline_first, $sline, $OLD_LD_LIBRARY_PATH, $LD_LIBRARY_PATH
|
||||
# Generated Global Variables: $suids_files, $sfile, $sname, $sowner, $sline_first, $sline, $OLD_LD_LIBRARY_PATH, $LD_LIBRARY_PATH, $ndsudo_uid, $ndsudo_nnp, $ndsudo_mount_options, $ndsudo_uncertainty, $pinns_uid, $pinns_nnp, $pinns_mount_options, $pinns_uncertainty, $jjs_nnp, $jjs_mount_options, $gosu_nnp, $gosu_mount_options
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -32,12 +32,84 @@ printf "%s\n" "$suids_files" | while IFS= read -r sfile; do
|
||||
# Keep the path returned by find: parsing it from ls output breaks on whitespace.
|
||||
sname="$sfile"
|
||||
sowner="$(echo "$s" | awk '{print $3}')"
|
||||
if [ "${sname##*/}" = "pinns" ] && [ "$(uname -s 2>/dev/null)" = "Linux" ]; then
|
||||
# The privileged CRI-O helper is a lead only; never invoke it to test.
|
||||
pinns_uid=$(stat -c '%u' "$sname" 2>/dev/null || stat -f '%u' "$sname" 2>/dev/null)
|
||||
if [ "$IAMROOT" ] || [ "$pinns_uid" != "0" ] || ! [ -u "$sname" ] || ! [ -x "$sname" ]; then
|
||||
echo " pinns: no local privilege-escalation candidate for this user (requires root ownership, SUID, and execute access)."
|
||||
else
|
||||
pinns_nnp=$(awk '/^NoNewPrivs:/ {print $2; exit}' /proc/self/status 2>/dev/null)
|
||||
pinns_mount_options=$(findmnt -no OPTIONS -T "$sname" 2>/dev/null | head -n 1)
|
||||
if [ "$pinns_nnp" = "1" ]; then
|
||||
echo " pinns: SUID transition blocked by NoNewPrivs for this process tree."
|
||||
elif echo ",$pinns_mount_options," | grep -q ',nosuid,'; then
|
||||
echo " pinns: SUID transition blocked by nosuid mount options."
|
||||
else
|
||||
pinns_uncertainty=""
|
||||
[ "$pinns_nnp" = "0" ] || pinns_uncertainty="NoNewPrivs unknown; "
|
||||
[ -n "$pinns_mount_options" ] || pinns_uncertainty="${pinns_uncertainty}mount SUID policy unknown; "
|
||||
echo " pinns review candidate (CVE-2022-0811): root-owned SUID executable by current user; NoNewPrivs=${pinns_nnp:-unknown}; mount options=${pinns_mount_options:-unknown}."
|
||||
echo " ${pinns_uncertainty}Installed helper build and vendor fixes unknown; verify the exact binary and host sysctl policy. No helper execution performed."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "$sname" = "." ] || [ "$sname" = ".." ]; then
|
||||
true #Don't do nothing
|
||||
elif echo "$sname" | grep -qE '/netdata/plugins[.]d/ndsudo$'; then
|
||||
# Keep this branch passive: even the generic non-FAST SUID probe must not run ndsudo.
|
||||
echo "$s"
|
||||
ndsudo_uid=$(stat -c '%u' "$sname" 2>/dev/null || stat -f '%u' "$sname" 2>/dev/null)
|
||||
if [ "$ndsudo_uid" != "0" ] || ! [ -u "$sname" ] || ! [ -x "$sname" ]; then
|
||||
echo " ndsudo: no privilege-escalation candidate for this user (requires root ownership, SUID, and execute access)."
|
||||
else
|
||||
ndsudo_nnp=$(awk '/^NoNewPrivs:/ {print $2; exit}' /proc/self/status 2>/dev/null)
|
||||
ndsudo_mount_options=$(findmnt -no OPTIONS -T "$sname" 2>/dev/null | head -n 1)
|
||||
if [ "$ndsudo_nnp" = "1" ]; then
|
||||
echo " ndsudo: SUID transition blocked by NoNewPrivs for this process tree."
|
||||
elif echo ",$ndsudo_mount_options," | grep -q ',nosuid,'; then
|
||||
echo " ndsudo: SUID transition blocked by nosuid mount options."
|
||||
else
|
||||
ndsudo_uncertainty=""
|
||||
[ "$ndsudo_nnp" = "0" ] || ndsudo_uncertainty="NoNewPrivs unknown; "
|
||||
[ -n "$ndsudo_mount_options" ] || ndsudo_uncertainty="${ndsudo_uncertainty}mount SUID policy unknown; "
|
||||
echo " ndsudo candidate (CVE-2024-32019): root-owned SUID executable by current user; NoNewPrivs=${ndsudo_nnp:-unknown}; mount options=${ndsudo_mount_options:-unknown}."
|
||||
echo " ${ndsudo_uncertainty}Installed build and vendor backports unknown; confirm affected version and PATH lookup manually. Vendor fixes: v1.45.3 and v1.45.0-169. No helper execution performed."
|
||||
fi
|
||||
fi
|
||||
elif ! [ "$IAMROOT" ] && [ -O "$sname" ]; then
|
||||
echo "You own the SUID file: $sname" | sed -${E} "s,.*,${SED_RED},"
|
||||
elif ! [ "$IAMROOT" ] && [ -w "$sname" ]; then #If write permision, win found (no check exploits)
|
||||
echo "You can write SUID file: $sname" | sed -${E} "s,.*,${SED_RED_YELLOW},"
|
||||
elif [ "${sname##*/}" = jjs ] && [ ! "$IAMROOT" ] &&
|
||||
[ -u "$sname" ] && [ -x "$sname" ] &&
|
||||
[ "$(stat -c '%u' "$sname" 2>/dev/null || stat -f '%u' "$sname" 2>/dev/null)" = 0 ]; then
|
||||
# Nashorn's file APIs can retain the SUID identity even if a spawned shell
|
||||
# drops it. Check only the exact executable; never run the JVM here.
|
||||
echo "$s"
|
||||
jjs_nnp=$(awk '/^NoNewPrivs:/ {print $2; exit}' /proc/self/status 2>/dev/null)
|
||||
jjs_mount_options=$(findmnt -no OPTIONS -T "$sname" 2>/dev/null | head -n 1)
|
||||
if [ "$jjs_nnp" = 1 ]; then
|
||||
echo " jjs: SUID transition blocked by NoNewPrivs for this process tree."
|
||||
elif echo ",$jjs_mount_options," | grep -q ',nosuid,'; then
|
||||
echo " jjs: SUID transition blocked by nosuid mount options."
|
||||
else
|
||||
echo " jjs SUID file-access review candidate: root-owned and caller-executable; verify effective UID, mount/NoNewPrivs policy, and installed JVM behavior without running it."
|
||||
fi
|
||||
elif [ "${sname##*/}" = gosu ] && [ ! "$IAMROOT" ] &&
|
||||
[ -u "$sname" ] && [ -x "$sname" ] &&
|
||||
[ "$(stat -c '%u' "$sname" 2>/dev/null || stat -f '%u' "$sname" 2>/dev/null)" = 0 ]; then
|
||||
# A SUID identity-switching helper is a lead; never invoke it to test.
|
||||
echo "$s"
|
||||
gosu_nnp=$(awk '/^NoNewPrivs:/ {print $2; exit}' /proc/self/status 2>/dev/null)
|
||||
gosu_mount_options=$(findmnt -no OPTIONS -T "$sname" 2>/dev/null | head -n 1)
|
||||
if [ "$gosu_nnp" = 1 ]; then
|
||||
echo " gosu: SUID transition blocked by NoNewPrivs for this process tree."
|
||||
elif echo ",$gosu_mount_options," | grep -q ',nosuid,'; then
|
||||
echo " gosu: SUID transition blocked by nosuid mount options."
|
||||
else
|
||||
echo " gosu SUID review candidate: root-owned and caller-executable; NoNewPrivs=${gosu_nnp:-unknown}; mount options=${gosu_mount_options:-unknown}."
|
||||
echo " Verify effective identity, user-namespace mapping, and installed helper behavior; this does not establish host-root access. No helper execution performed."
|
||||
fi
|
||||
else
|
||||
c="a"
|
||||
for b in $sidB; do
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
# ID: IP_Capabilities
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 14-08-2026
|
||||
# Description: Capabilities, including set-capabilities snap-confine exposure to CVE-2026-8933
|
||||
# Description: Capabilities, including snap-confine race prerequisites and CVE-2026-8933
|
||||
# License: GNU GPL
|
||||
# Version: 1.1
|
||||
# Mitre: T1548.001,T1068
|
||||
# Functions Used: checkSnapConfineCVE20268933, echo_not_found, print_2title, print_info, print_3title
|
||||
# Global Variables: $capsB, $capsVB, $IAMROOT, $SEARCH_IN_FOLDER
|
||||
# Functions Used: checkSnapConfineCVE20263888, checkSnapConfineCVE20268933, echo_not_found, print_2title, print_info, print_3title
|
||||
# Global Variables: $capsB, $capsVB, $IAMROOT, $SEARCH_IN_FOLDER, $STRINGS
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $cap_name, $cap_value, $cap_line, $cap_status_file, $cap_default_sep, $cap_sep, $cap_color, $capVB, $capname, $capbins, $capsVB_vuln, $proc_status, $proc_pid, $proc_name, $proc_uid, $user_name, $proc_inh, $proc_prm, $proc_eff, $proc_bnd, $proc_amb, $proc_inh_dec, $proc_prm_dec, $proc_eff_dec, $proc_bnd_dec, $proc_amb_dec
|
||||
# Generated Global Variables: $cap_name, $cap_value, $cap_line, $cap_status_file, $cap_default_sep, $cap_sep, $cap_color, $capVB, $capname, $capbins, $capsVB_vuln, $proc_status, $proc_pid, $proc_name, $proc_uid, $user_name, $proc_inh, $proc_prm, $proc_eff, $proc_bnd, $proc_amb, $proc_inh_dec, $proc_prm_dec, $proc_eff_dec, $proc_bnd_dec, $proc_amb_dec, $binfmt_probe_count, $cap_inventory_count, $cap_path, $cap_rights, $cap_size
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -47,7 +47,7 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
# so we redirect stderr to prevent error propagation
|
||||
echo "$cap_name$cap_sep$(capsh --decode=0x"$cap_value" 2>/dev/null | sed -${E} "s,$capsB,${cap_color},")"
|
||||
else
|
||||
echo "$cap_name$cap_sep[Invalid capability format]"
|
||||
echo "$cap_name${cap_sep}[Invalid capability format]"
|
||||
fi
|
||||
done
|
||||
}
|
||||
@@ -104,8 +104,22 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
fi
|
||||
echo ""
|
||||
echo "Files with capabilities (limited to 50):"
|
||||
getcap -r / 2>/dev/null | head -n 50 | while read cb; do
|
||||
binfmt_probe_count=0
|
||||
cap_inventory_count=0
|
||||
getcap -r / 2>/dev/null | head -n 51 | while read cb; do
|
||||
cap_inventory_count=$((cap_inventory_count + 1))
|
||||
if [ "$cap_inventory_count" -gt 50 ]; then
|
||||
echo "Capability inventory is partial: more than 50 files have capabilities."
|
||||
break
|
||||
fi
|
||||
capsVB_vuln=""
|
||||
# Match the executable basename exactly; broad capsVB patterns also match wrappers.
|
||||
case "$cb" in
|
||||
*/gdb\ *cap_sys_ptrace*)
|
||||
echo "$cb" | sed -${E} "s,.*,${SED_RED_YELLOW},"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
for capVB in $capsVB; do
|
||||
capname="$(echo $capVB | cut -d ':' -f 1)"
|
||||
@@ -120,10 +134,33 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
if ! [ "$capsVB_vuln" ]; then
|
||||
echo "$cb" | sed -${E} "s,$capsB,${SED_RED},"
|
||||
fi
|
||||
# Reuse the capped getcap results. A helper that embeds the exact
|
||||
# registration path is only a static lead; never run it or read its input.
|
||||
cap_path=${cb%% *}
|
||||
cap_rights=${cb#* }
|
||||
cap_rights=${cap_rights#= }
|
||||
if [ "$binfmt_probe_count" -lt 8 ] && [ ! "$IAMROOT" ] && [ "$STRINGS" ] &&
|
||||
[ -f "$cap_path" ] && [ ! -L "$cap_path" ] &&
|
||||
[ -r "$cap_path" ] && [ -x "$cap_path" ] &&
|
||||
printf '%s\n' "$cap_rights" | grep -Eq '(^|[ ,])cap_dac_override(,cap_[a-z0-9_]+)*[+=][eip]*e[eip]*($|[[:space:]])'; then
|
||||
cap_size=$(stat -c %s "$cap_path" 2>/dev/null || stat -f %z "$cap_path" 2>/dev/null)
|
||||
case "$cap_size" in
|
||||
''|*[!0-9]*) ;;
|
||||
*)
|
||||
if [ "$cap_size" -le 1048576 ]; then
|
||||
binfmt_probe_count=$((binfmt_probe_count + 1))
|
||||
if LC_ALL=C "$STRINGS" -a "$cap_path" 2>/dev/null | grep -Fxq '/proc/sys/fs/binfmt_misc/register'; then
|
||||
printf ' binfmt_misc helper review candidate: %s (file cap_dac_override effective flag and embedded register path; effective helper privilege, caller control, writable proc mount, kernel policy, interpreter access, and SUID transition unverified).\n' "$cap_path"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if ! [ "$IAMROOT" ] && [ -w "$(echo $cb | cut -d" " -f1)" ]; then
|
||||
echo "$cb is writable" | sed -${E} "s,.*,${SED_RED},"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
checkSnapConfineCVE20268933
|
||||
checkSnapConfineCVE20263888
|
||||
fi
|
||||
@@ -1,15 +1,15 @@
|
||||
# Title: Interesting Permissions Files - Misconfigured ld.so
|
||||
# ID: IP_Misconfigured_ldso
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Checking misconfigurations of ld.so
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1574.006
|
||||
# Functions Used: print_2title, print_info
|
||||
# Global Variables: $IAMROOT, $ITALIC, $SEARCH_IN_FOLDER, $USER, $Wfolders, $ldsoconfdG, $wgroups
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $ini_path, $fpath
|
||||
# Generated Global Variables: $ini_path, $fpath, $ldso_writable_files
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -43,8 +43,9 @@ if ! [ "$SEARCH_IN_FOLDER" ] && ! [ "$IAMROOT" ]; then
|
||||
printf $GREEN$ITALIC"$fpath\n"$NC;
|
||||
fi
|
||||
|
||||
if [ "$(find "$fpath" -type f '(' '(' -user "$USER" ')' -or '(' -perm -o=w ')' -or '(' -perm -g=w -and '(' $wgroups ')' ')' ')' 2>/dev/null)" ]; then
|
||||
echo "You have write privileges over $(find "$fpath" -type f '(' '(' -user "$USER" ')' -or '(' -perm -o=w ')' -or '(' -perm -g=w -and '(' $wgroups ')' ')' ')' 2>/dev/null)" | sed -${E} "s,.*,${SED_RED_YELLOW},";
|
||||
ldso_writable_files=$(find "$fpath" -type f '(' '(' -user "$USER" ')' -or '(' -perm -o=w ')' -or '(' -perm -g=w -and '(' $wgroups ')' ')' ')' 2>/dev/null) || :
|
||||
if [ "$ldso_writable_files" ]; then
|
||||
echo "You have write privileges over $ldso_writable_files" | sed -${E} "s,.*,${SED_RED_YELLOW},";
|
||||
fi
|
||||
|
||||
for f in $ini_path; do
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
# Title: Interesting Permissions Files - /etc/profile.d/
|
||||
# ID: IP_Files_etc_profile_d
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Files (scripts) in /etc/profile.d/
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Global shell startup files and bounded virtual-environment source cues
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1546.004
|
||||
# Functions Used: check_critial_root_path, echo_not_found, print_2title, print_info
|
||||
# Global Variables: $IAMROOT, $MACPEAS, $profiledG, $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Generated Global Variables: $global_startup, $global_activation
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
check_global_venv_activation() {
|
||||
global_startup=$1
|
||||
[ -f "$global_startup" ] && [ -r "$global_startup" ] || return 0
|
||||
# Read only the first 64 KiB of each fixed startup file. Emit paths, never
|
||||
# source lines: a startup file may contain credentials alongside commands.
|
||||
dd if="$global_startup" bs=65536 count=1 2>/dev/null |
|
||||
awk '($1 == "source" || $1 == ".") && NF >= 2 {
|
||||
path = $2
|
||||
if (path ~ /^".*"$/ || path ~ /^\047.*\047$/)
|
||||
path = substr(path, 2, length(path) - 2)
|
||||
if (path ~ /^\/[[:alnum:]_.+\/-]+\/bin\/activate$/ && ++hits <= 8)
|
||||
print path
|
||||
}' |
|
||||
while IFS= read -r global_activation; do
|
||||
printf ' %s sources %s (review who runs this startup file and whether the target is writable)\n' "$global_startup" "$global_activation"
|
||||
ls -ld "$global_activation" 2>/dev/null || :
|
||||
done
|
||||
}
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
print_2title "Files (scripts) in /etc/profile.d/" "T1546.004"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/linux-basics/shell-startup-aliases-and-history.html#review-startup-and-history-files"
|
||||
@@ -21,6 +40,9 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
(ls -la /etc/profile.d/ 2>/dev/null | sed -${E} "s,$profiledG,${SED_GREEN},") || echo_not_found "/etc/profile.d/"
|
||||
check_critial_root_path "/etc/profile"
|
||||
check_critial_root_path "/etc/profile.d/"
|
||||
for global_startup in /etc/profile /etc/bash.bashrc /etc/bashrc; do
|
||||
check_global_venv_activation "$global_startup"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
@@ -1,15 +1,15 @@
|
||||
# Title: Interesting Files - Files inside /home
|
||||
# ID: IF_Others_homes
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Files inside /home
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Files inside home directories and HTTPie session paths
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1552.001
|
||||
# Functions Used: echo_not_found, print_2title
|
||||
# Global Variables: $HOMESEARCH, $SEARCH_IN_FOLDER, $USER
|
||||
# Global Variables: $HOME, $HOMESEARCH, $SEARCH_IN_FOLDER, $USER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Generated Global Variables: $httpie_home, $httpie_home_count, $httpie_config, $httpie_app, $httpie_sessions, $httpie_dir, $httpie_dir_count, $httpie_file, $httpie_file_count, $httpie_session_paths
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
@@ -18,4 +18,48 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
print_2title "Files inside others home (limit 20)" "T1552.001"
|
||||
(find $HOMESEARCH -type f 2>/dev/null | grep -v -i "/"$USER | head -n 20) || echo_not_found
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Fixed-depth globbing under at most 65 login homes; no JSON content reads.
|
||||
httpie_list_session_paths() {
|
||||
httpie_home_count=0
|
||||
httpie_dir_count=0
|
||||
httpie_file_count=0
|
||||
while IFS= read -r httpie_home; do
|
||||
[ "$httpie_home_count" -lt 65 ] || return
|
||||
httpie_home_count=$((httpie_home_count + 1))
|
||||
case "$httpie_home" in
|
||||
/*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
case "$httpie_home/" in
|
||||
*'/../'*|*'/./'*|*'//'*) continue ;;
|
||||
esac
|
||||
httpie_config="$httpie_home/.config"
|
||||
httpie_app="$httpie_config/httpie"
|
||||
httpie_sessions="$httpie_app/sessions"
|
||||
[ -d "$httpie_home" ] && [ -x "$httpie_home" ] && [ ! -L "$httpie_home" ] || continue
|
||||
[ -d "$httpie_config" ] && [ -x "$httpie_config" ] && [ ! -L "$httpie_config" ] || continue
|
||||
[ -d "$httpie_app" ] && [ -x "$httpie_app" ] && [ ! -L "$httpie_app" ] || continue
|
||||
[ -d "$httpie_sessions" ] && [ -x "$httpie_sessions" ] && [ ! -L "$httpie_sessions" ] || continue
|
||||
for httpie_dir in "$httpie_sessions"/*; do
|
||||
[ -d "$httpie_dir" ] && [ -x "$httpie_dir" ] && [ ! -L "$httpie_dir" ] || continue
|
||||
[ "$httpie_dir_count" -lt 32 ] || return
|
||||
httpie_dir_count=$((httpie_dir_count + 1))
|
||||
for httpie_file in "$httpie_dir"/*.json; do
|
||||
[ -f "$httpie_file" ] && [ -r "$httpie_file" ] && [ ! -L "$httpie_file" ] || continue
|
||||
[ "$httpie_file_count" -lt 24 ] || return
|
||||
printf '%s\n' "$httpie_file"
|
||||
httpie_file_count=$((httpie_file_count + 1))
|
||||
done
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
httpie_session_paths=$({ printf '%s\n' "$HOME"; awk -F: '$6 ~ /^\// && $7 !~ /(nologin|false)$/ && $6 != "/" && !seen[$6]++ { print $6; if (++n == 64) exit }' /etc/passwd 2>/dev/null; } | httpie_list_session_paths)
|
||||
if [ -n "$httpie_session_paths" ]; then
|
||||
print_2title "Readable HTTPie session files (paths only, max 24)" "T1552.001"
|
||||
printf '%s\n' "$httpie_session_paths"
|
||||
echo "Session files may contain auth or cookies; path discovery does not prove usable credentials."
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
@@ -1,26 +1,107 @@
|
||||
# Title: Interesting Files - Backup folders
|
||||
# ID: IF_Backup_folders
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Backup folders
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Backup folders, readable archives owned by other users, and redacted shell password assignments in immediate entries
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Mitre: T1552.001
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $DEBUG, $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Generated Global Variables: $entry, $backup_uid, $backup_groups, $archive_path, $archive_metadata, $archive_mode, $archive_owner, $archive_group, $archive_size, $archive_access
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
if [ "$PSTORAGE_BACKUPS" ] || [ "$DEBUG" ]; then
|
||||
backup_uid=$(id -u 2>/dev/null)
|
||||
backup_groups=$(id -G 2>/dev/null)
|
||||
report_readable_backup_archive() (
|
||||
archive_path=$1
|
||||
[ -n "$backup_uid" ] && [ "$backup_uid" != 0 ] || exit
|
||||
[ -f "$archive_path" ] && [ ! -L "$archive_path" ] && [ -r "$archive_path" ] || exit
|
||||
case "$archive_path" in
|
||||
*.tar|*.tar.gz|*.tgz|*.tar.bz2|*.tbz2|*.tar.xz|*.txz|*.tar.zst|*.zip|*.7z) ;;
|
||||
*) exit ;;
|
||||
esac
|
||||
archive_metadata=$(LC_ALL=C ls -ldn "$archive_path" 2>/dev/null | awk '{print $1, $3, $4, $5}')
|
||||
set -- $archive_metadata
|
||||
[ "$#" -eq 4 ] || exit
|
||||
archive_mode=$1 archive_owner=$2 archive_group=$3 archive_size=$4
|
||||
[ "$archive_owner" != "$backup_uid" ] || exit
|
||||
if [ "$archive_owner" != 0 ]; then
|
||||
# A small archive owned by another user can expose stale credentials.
|
||||
# Keep the existing root-owned reporting behavior, regardless of size.
|
||||
case "$archive_size" in ''|*[!0-9]*) exit ;; esac
|
||||
[ "${#archive_size}" -le 8 ] || exit
|
||||
[ "$archive_size" -le 10485760 ] || exit
|
||||
fi
|
||||
case "$archive_mode" in
|
||||
???????r*) archive_access=world-readable ;;
|
||||
????r*)
|
||||
case " $backup_groups " in
|
||||
*" $archive_group "*) archive_access=group-readable ;;
|
||||
*) exit ;;
|
||||
esac ;;
|
||||
*) exit ;;
|
||||
esac
|
||||
if [ "$archive_owner" = 0 ]; then
|
||||
printf ' Possible exposure: readable root-owned backup archive: %s (owner=%s group=%s mode=%s size=%s access=%s)\n' \
|
||||
"$archive_path" "$archive_owner" "$archive_group" "$archive_mode" "$archive_size" "$archive_access"
|
||||
else
|
||||
printf ' Possible exposure: readable other-user backup archive: %s (owner=%s group=%s mode=%s size=%s access=%s; contents unverified)\n' \
|
||||
"$archive_path" "$archive_owner" "$archive_group" "$archive_mode" "$archive_size" "$archive_access"
|
||||
fi
|
||||
)
|
||||
print_2title "Backup folders" "T1552.001"
|
||||
printf "%s\n" "$PSTORAGE_BACKUPS" | while read b ; do
|
||||
ls -ld "$b" 2> /dev/null | sed -${E} "s,backups|backup,${SED_RED},g";
|
||||
ls -l "$b" 2>/dev/null && echo ""
|
||||
printf "%s\n" "$PSTORAGE_BACKUPS" | while IFS= read -r b; do
|
||||
[ -d "$b" ] && [ -r "$b" ] && [ -x "$b" ] || continue
|
||||
ls -ld "$b" 2>/dev/null | sed -${E} "s,backups|backup,${SED_RED},g"
|
||||
# Prune each child: list immediate entries only and cap metadata calls.
|
||||
find "$b" ! -path "$b" -prune -print 2>/dev/null | head -n 30 |
|
||||
while IFS= read -r entry; do
|
||||
ls -ld "$entry" 2>/dev/null
|
||||
report_readable_backup_archive "$entry"
|
||||
done
|
||||
echo ""
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Only inspect small, readable regular files in the first few discovered
|
||||
# backup directories. Never print the assignment value.
|
||||
print_2title "Shell *_PASS assignments in backup folders (values redacted)" "T1552.001"
|
||||
printf "%s\n" "$PSTORAGE_BACKUPS" | head -n 12 |
|
||||
while IFS= read -r b; do
|
||||
[ -d "$b" ] && [ ! -L "$b" ] && [ -r "$b" ] && [ -x "$b" ] || continue
|
||||
find "$b" ! -path "$b" -prune -type f -size -65537c -print 2>/dev/null | head -n 30 |
|
||||
while IFS= read -r entry; do
|
||||
[ -f "$entry" ] && [ ! -L "$entry" ] && [ -r "$entry" ] || continue
|
||||
awk '
|
||||
NR > 200 { exit }
|
||||
{
|
||||
if (length($0) > 512) next
|
||||
line = $0
|
||||
sub(/^[[:space:]]*/, "", line)
|
||||
status = "active"
|
||||
if (line ~ /^#/) {
|
||||
status = "commented; stale/unknown"
|
||||
sub(/^#[[:space:]]*/, "", line)
|
||||
}
|
||||
sub(/^export[[:space:]]+/, "", line)
|
||||
if (line !~ /^[A-Za-z_][A-Za-z0-9_]*_PASS=/) next
|
||||
key = line
|
||||
sub(/=.*/, "", key)
|
||||
value = line
|
||||
sub(/^[^=]*=/, "", value)
|
||||
if (value !~ /[^[:space:]]/) next
|
||||
printf " %s:%d: %s=[REDACTED] (%s)\n", FILENAME, NR, key, status
|
||||
if (++found >= 5) exit
|
||||
}
|
||||
' "$entry" 2>/dev/null
|
||||
done
|
||||
done | head -n 50
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -9,7 +9,7 @@
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $DEBUG, $HOME, $MACPEAS
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $FILECMD, $SQLITEPYTHON, $tables, $columns, $INTCOLUMN
|
||||
# Generated Global Variables: $FILECMD, $SQLITEPYTHON, $tables, $columns, $INTCOLUMN, $escaped_t, $escaped_sql_t
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
@@ -25,45 +25,54 @@ fi
|
||||
|
||||
if [ "$PSTORAGE_DATABASE" ] || [ "$DEBUG" ]; then
|
||||
print_2title "Searching tables inside readable .db/.sql/.sqlite files (limit 100)" "T1005"
|
||||
FILECMD="$(command -v file 2>/dev/null || echo -n '')"
|
||||
printf "%s\n" "$PSTORAGE_DATABASE" | while read f; do
|
||||
FILECMD=$(command -v file 2>/dev/null)
|
||||
printf "%s\n" "$PSTORAGE_DATABASE" | while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
if [ "$FILECMD" ]; then
|
||||
echo "Found "$(file "$f") | sed -${E} "s,\.db|\.sql|\.sqlite|\.sqlite3,${SED_RED},g";
|
||||
printf 'Found %s\n' "$(file "$f")" | sed -${E} "s,\.db|\.sql|\.sqlite|\.sqlite3,${SED_RED},g";
|
||||
else
|
||||
echo "Found $f" | sed -${E} "s,\.db|\.sql|\.sqlite|\.sqlite3,${SED_RED},g";
|
||||
printf 'Found %s\n' "$f" | sed -${E} "s,\.db|\.sql|\.sqlite|\.sqlite3,${SED_RED},g";
|
||||
fi
|
||||
done
|
||||
SQLITEPYTHON=""
|
||||
echo ""
|
||||
printf "%s\n" "$PSTORAGE_DATABASE" | while read f; do
|
||||
printf "%s\n" "$PSTORAGE_DATABASE" | while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
SQLITEPYTHON=""
|
||||
if ([ -r "$f" ] && [ "$FILECMD" ] && file "$f" | grep -qi sqlite) || ([ -r "$f" ] && [ ! "$FILECMD" ]); then #If readable and filecmd and sqlite, or readable and not filecmd
|
||||
if [ "$(command -v sqlite3 2>/dev/null || echo -n '')" ]; then
|
||||
tables=$(sqlite3 $f ".tables" 2>/dev/null)
|
||||
if command -v sqlite3 >/dev/null 2>&1; then
|
||||
tables=$(sqlite3 "$f" "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%'" 2>/dev/null)
|
||||
#printf "$tables\n" | sed "s,user.*\|credential.*,${SED_RED},g"
|
||||
elif [ "$(command -v python 2>/dev/null || echo -n '')" ] || [ "$(command -v python3 2>/dev/null || echo -n '')" ]; then
|
||||
SQLITEPYTHON=$(command -v python 2>/dev/null || command -v python3 2>/dev/null || echo -n '')
|
||||
tables=$($SQLITEPYTHON -c "print('\n'.join([t[0] for t in __import__('sqlite3').connect('$f').cursor().execute('SELECT name FROM sqlite_master WHERE type=\'table\' and tbl_name NOT like \'sqlite_%\';').fetchall()]))" 2>/dev/null)
|
||||
elif command -v python >/dev/null 2>&1 || command -v python3 >/dev/null 2>&1; then
|
||||
SQLITEPYTHON=$(command -v python 2>/dev/null || command -v python3 2>/dev/null)
|
||||
tables=$("$SQLITEPYTHON" -c 'import sqlite3,sys; db=sqlite3.connect(sys.argv[1]); print("\n".join(row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type=? AND name NOT LIKE ?", ("table", "sqlite_%"))))' "$f" 2>/dev/null)
|
||||
#printf "$tables\n" | sed "s,user.*\|credential.*,${SED_RED},g"
|
||||
else
|
||||
tables=""
|
||||
fi
|
||||
if [ "$tables" ] || [ "$DEBUG" ]; then
|
||||
printf $GREEN" -> Extracting tables from$NC $f $DG(limit 20)\n"$NC
|
||||
printf "%s\n" "$tables" | while read t; do
|
||||
printf "%s\n" "$tables" | while IFS= read -r t; do
|
||||
columns=""
|
||||
# Search for credentials inside the table using sqlite3
|
||||
if [ -z "$SQLITEPYTHON" ]; then
|
||||
columns=$(sqlite3 $f ".schema $t" 2>/dev/null | grep "CREATE TABLE")
|
||||
escaped_sql_t=$(printf '%s' "$t" | sed "s/'/''/g")
|
||||
columns=$(sqlite3 "$f" "SELECT sql FROM sqlite_master WHERE name='$escaped_sql_t' AND sql IS NOT NULL" 2>/dev/null)
|
||||
# Search for credentials inside the table using python
|
||||
else
|
||||
columns=$($SQLITEPYTHON -c "print(__import__('sqlite3').connect('$f').cursor().execute('SELECT sql FROM sqlite_master WHERE type!=\'meta\' AND sql NOT NULL AND name =\'$t\';').fetchall()[0][0])" 2>/dev/null)
|
||||
columns=$("$SQLITEPYTHON" -c 'import sqlite3,sys; db=sqlite3.connect(sys.argv[1]); row=db.execute("SELECT sql FROM sqlite_master WHERE name=? AND sql IS NOT NULL", (sys.argv[2],)).fetchone(); print(row[0] if row else "")' "$f" "$t" 2>/dev/null)
|
||||
fi
|
||||
#Check found columns for interesting fields
|
||||
INTCOLUMN=$(echo "$columns" | grep -i "username\|passw\|credential\|email\|hash\|salt")
|
||||
if [ "$INTCOLUMN" ]; then
|
||||
printf ${BLUE}" --> Found interesting column names in$NC $t $DG(output limit 10)\n"$NC | sed -${E} "s,user.*|credential.*,${SED_RED},g"
|
||||
printf "$columns\n" | sed -${E} "s,username|passw|credential|email|hash|salt|$t,${SED_RED},g"
|
||||
(sqlite3 $f "select * from $t" || $SQLITEPYTHON -c "print(', '.join([str(x) for x in __import__('sqlite3').connect('$f').cursor().execute('SELECT * FROM \'$t\';').fetchall()[0]]))") 2>/dev/null | head
|
||||
printf "%s\n" "$columns" | sed -${E} "s,username|passw|credential|email|hash|salt,${SED_RED},g"
|
||||
if [ -z "$SQLITEPYTHON" ]; then
|
||||
escaped_t=$(printf '%s' "$t" | sed 's/"/""/g')
|
||||
sqlite3 "$f" "SELECT * FROM \"$escaped_t\" LIMIT 10" 2>/dev/null | head
|
||||
else
|
||||
"$SQLITEPYTHON" -c 'import sqlite3,sys; db=sqlite3.connect(sys.argv[1]); table=sys.argv[2].replace("\"", "\"\""); print("\n".join("|".join(str(v) if v is not None else "" for v in row) for row in db.execute("SELECT * FROM \""+table+"\" LIMIT 10")))' "$f" "$t" 2>/dev/null | head
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
@@ -76,4 +85,4 @@ echo ""
|
||||
if [ "$MACPEAS" ]; then
|
||||
print_2title "Downloaded Files" "T1005"
|
||||
sqlite3 ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2 'select LSQuarantineAgentName, LSQuarantineDataURLString, LSQuarantineOriginURLString, date(LSQuarantineTimeStamp + 978307200, "unixepoch") as downloadedDate from LSQuarantineEvent order by LSQuarantineTimeStamp' | sort | grep -Ev "\|\|\|"
|
||||
fi
|
||||
fi
|
||||
@@ -7,7 +7,7 @@
|
||||
# Version: 1.0
|
||||
# Mitre: T1005
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $SEARCH_IN_FOLDER
|
||||
# Global Variables: $SEARCH_IN_FOLDER, $FAST, $SUPERFAST, $TIMEOUT
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Fat linpeas: 0
|
||||
@@ -22,3 +22,254 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
ls -alhR /opt/lampp/htdocs/ 2>/dev/null | head
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if ! [ "$FAST" ] && ! [ "$SUPERFAST" ] && [ "$TIMEOUT" ] && command -v python3 >/dev/null 2>&1; then
|
||||
print_2title "Django settings and FileBasedCache permissions (bounded passive check)" "T1005"
|
||||
(
|
||||
if [ "$SEARCH_IN_FOLDER" ]; then
|
||||
set -- "$SEARCH_IN_FOLDER"
|
||||
else
|
||||
set -- /var/www /srv/www /opt /usr/src/app/app/settings.py
|
||||
fi
|
||||
"$TIMEOUT" 8 python3 -I -S - "$@" <<'DJANGO_FILE_CACHE_CHECK'
|
||||
import ast
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import time
|
||||
|
||||
if sys.version_info < (3, 6):
|
||||
raise SystemExit(0) # Scoped scandir context managers are unavailable.
|
||||
|
||||
BACKEND = "django.core.cache.backends.filebased.FileBasedCache"
|
||||
COOKIE_BACKEND = "django.contrib.sessions.backends.signed_cookies"
|
||||
PICKLE_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer"
|
||||
MAX_CANDIDATES = 30
|
||||
MAX_BYTES = 262144
|
||||
MAX_DIRS = 240
|
||||
MAX_ENTRIES = 5000
|
||||
MAX_OUTPUT = 10
|
||||
DEADLINE = time.monotonic() + 6
|
||||
|
||||
|
||||
def literal(node):
|
||||
if hasattr(ast, "Constant") and isinstance(node, ast.Constant):
|
||||
return node.value if isinstance(node.value, str) else None
|
||||
legacy_string = getattr(ast, "Str", ())
|
||||
return node.s if isinstance(node, legacy_string) else None
|
||||
|
||||
|
||||
def signed_pickle_session(source):
|
||||
if b"signed_cookies" not in source or b"PickleSerializer" not in source:
|
||||
return False
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except (SyntaxError, ValueError):
|
||||
return False
|
||||
names = ("SESSION_ENGINE", "SESSION_SERIALIZER")
|
||||
writes = {name: 0 for name in names}
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names):
|
||||
return False
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in ("exec", "eval", "globals", "locals"):
|
||||
return False
|
||||
if isinstance(node, ast.Name) and node.id in writes and isinstance(node.ctx, (ast.Store, ast.Del)):
|
||||
writes[node.id] += 1
|
||||
if any(writes[name] != 1 for name in names):
|
||||
return False
|
||||
settings = {}
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
||||
name = node.targets[0].id
|
||||
if name in writes:
|
||||
settings[name] = literal(node.value)
|
||||
return settings.get("SESSION_ENGINE") == COOKIE_BACKEND and settings.get("SESSION_SERIALIZER") == PICKLE_SERIALIZER
|
||||
|
||||
|
||||
def cache_entries(source):
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except (SyntaxError, ValueError):
|
||||
return [], "settings syntax could not be parsed"
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names):
|
||||
return [], "settings uses a wildcard import"
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in ("exec", "eval", "globals", "locals"):
|
||||
return [], "settings uses dynamic namespace operations"
|
||||
assignments = [node for node in tree.body if isinstance(node, ast.Assign)
|
||||
and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name)
|
||||
and node.targets[0].id == "CACHES"]
|
||||
references = [node for node in ast.walk(tree) if isinstance(node, ast.Name) and node.id == "CACHES"]
|
||||
if len(assignments) != 1 or len(references) != 1:
|
||||
return [], "CACHES is dynamic or assigned more than once"
|
||||
caches = assignments[0].value
|
||||
if not isinstance(caches, ast.Dict):
|
||||
return [], "CACHES is not a literal dictionary"
|
||||
aliases = []
|
||||
seen = set()
|
||||
for key, value in zip(caches.keys, caches.values):
|
||||
alias = literal(key)
|
||||
if alias is None or alias in seen or not isinstance(value, ast.Dict):
|
||||
return [], "CACHES has a dynamic or duplicate alias"
|
||||
seen.add(alias)
|
||||
fields = {}
|
||||
for field_key, field_value in zip(value.keys, value.values):
|
||||
field = literal(field_key)
|
||||
if field is None or field in fields:
|
||||
return [], "cache entry has a dynamic or duplicate field"
|
||||
fields[field] = field_value
|
||||
if literal(fields.get("BACKEND")) == BACKEND:
|
||||
location = literal(fields.get("LOCATION"))
|
||||
if location is None or not os.path.isabs(location) or os.path.normpath(location) != location:
|
||||
aliases.append((alias, None))
|
||||
else:
|
||||
aliases.append((alias, location))
|
||||
return aliases, None
|
||||
|
||||
|
||||
def candidate_files(roots):
|
||||
stack = [(root, 0) for root in reversed(roots[:3]) if os.path.isdir(root)]
|
||||
dirs = entries = candidates = 0
|
||||
while stack and dirs < MAX_DIRS and entries < MAX_ENTRIES and candidates < MAX_CANDIDATES and time.monotonic() < DEADLINE:
|
||||
directory, depth = stack.pop()
|
||||
dirs += 1
|
||||
try:
|
||||
with os.scandir(directory) as contents:
|
||||
for entry in contents:
|
||||
entries += 1
|
||||
if entries >= MAX_ENTRIES or time.monotonic() >= DEADLINE:
|
||||
break
|
||||
try:
|
||||
if entry.name == "settings.py" and entry.is_file(follow_symlinks=False):
|
||||
candidates += 1
|
||||
yield entry.path
|
||||
if candidates >= MAX_CANDIDATES:
|
||||
break
|
||||
elif depth < 5 and entry.is_dir(follow_symlinks=False):
|
||||
stack.append((entry.path, depth + 1))
|
||||
except OSError:
|
||||
continue
|
||||
except OSError:
|
||||
continue
|
||||
if candidates < MAX_CANDIDATES and time.monotonic() < DEADLINE:
|
||||
for path in roots[3:4]:
|
||||
if os.path.isfile(path) and not os.path.islink(path):
|
||||
candidates += 1
|
||||
yield path
|
||||
|
||||
|
||||
def process_consumer(settings):
|
||||
package = os.path.basename(os.path.dirname(settings))
|
||||
project = os.path.realpath(os.path.dirname(os.path.dirname(settings)))
|
||||
if not package or project == os.path.sep:
|
||||
return None
|
||||
try:
|
||||
with os.scandir("/proc") as processes:
|
||||
checked = 0
|
||||
for proc in processes:
|
||||
if checked >= 400 or time.monotonic() >= DEADLINE:
|
||||
break
|
||||
if not proc.name.isdigit():
|
||||
continue
|
||||
checked += 1
|
||||
try:
|
||||
if os.path.realpath(proc.path + "/cwd") != project:
|
||||
continue
|
||||
with open(proc.path + "/cmdline", "rb") as command_file:
|
||||
command = command_file.read(1024)
|
||||
if b"gunicorn" not in command.lower() or (package + ".wsgi").encode() not in command:
|
||||
continue
|
||||
with open(proc.path + "/status", encoding="ascii", errors="replace") as status_file:
|
||||
for line in status_file:
|
||||
if line.startswith("Uid:"):
|
||||
return int(line.split()[2]), proc.name
|
||||
except (OSError, ValueError, IndexError):
|
||||
continue
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def cache_metadata(location):
|
||||
samples = []
|
||||
try:
|
||||
with os.scandir(location) as contents:
|
||||
for index, entry in enumerate(contents):
|
||||
if index >= 100 or len(samples) >= 3:
|
||||
break
|
||||
if entry.name.endswith(".djcache"):
|
||||
try:
|
||||
info = entry.stat(follow_symlinks=False)
|
||||
if stat.S_ISREG(info.st_mode):
|
||||
samples.append("uid=%s mode=%04o" % (info.st_uid, stat.S_IMODE(info.st_mode)))
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
return ", ".join(samples) if samples else "none sampled"
|
||||
|
||||
|
||||
def inspect(settings):
|
||||
try:
|
||||
info = os.stat(settings, follow_symlinks=False)
|
||||
if not stat.S_ISREG(info.st_mode) or info.st_size > MAX_BYTES or info.st_size < 1:
|
||||
return []
|
||||
with open(settings, "rb") as settings_file:
|
||||
source = settings_file.read(MAX_BYTES + 1)
|
||||
if len(source) > MAX_BYTES:
|
||||
return []
|
||||
aliases, reason = cache_entries(source)
|
||||
session_candidate = signed_pickle_session(source)
|
||||
except (OSError, UnicodeError):
|
||||
return []
|
||||
if reason:
|
||||
findings = ["review Django cache settings %r: %s" % (settings, reason)] if b"FileBasedCache" in source else []
|
||||
if session_candidate:
|
||||
findings.append("review signed-cookie pickle session: settings=%r; SECRET_KEY access and active backend require confirmation" % settings)
|
||||
return findings
|
||||
findings = []
|
||||
for alias, location in aliases:
|
||||
if location is None:
|
||||
findings.append("review Django cache settings %r alias %r: LOCATION is dynamic or not a literal absolute path" % (settings, alias))
|
||||
continue
|
||||
try:
|
||||
directory = os.stat(location)
|
||||
if not stat.S_ISDIR(directory.st_mode):
|
||||
raise OSError("not a directory")
|
||||
except OSError:
|
||||
findings.append("review Django cache settings %r alias %r location %r: directory unavailable" % (settings, alias, location))
|
||||
continue
|
||||
try:
|
||||
writable = os.access(location, os.W_OK | os.X_OK, effective_ids=True)
|
||||
except (TypeError, NotImplementedError):
|
||||
writable = os.getuid() == os.geteuid() and os.access(location, os.W_OK | os.X_OK)
|
||||
sticky = bool(directory.st_mode & stat.S_ISVTX)
|
||||
symlinked = os.path.realpath(location) != os.path.abspath(location)
|
||||
consumer = process_consumer(settings) if writable else None
|
||||
consumer_text = "gunicorn pid=%s uid=%s" % (consumer[1], consumer[0]) if consumer else "consumer identity unverified"
|
||||
evidence = "settings=%r alias=%r location=%r dir_uid=%s mode=%04o access=%s sticky=%s symlink=%s %s cache_files=%s" % (
|
||||
settings, alias, location, directory.st_uid, stat.S_IMODE(directory.st_mode),
|
||||
"write+search" if writable else "no write+search", sticky, symlinked,
|
||||
consumer_text, cache_metadata(location))
|
||||
if writable and not sticky and not symlinked and consumer and consumer[0] != os.geteuid() and os.geteuid() != 0:
|
||||
findings.append("potential cross-user Django file cache replacement: " + evidence)
|
||||
else:
|
||||
findings.append("review Django file cache candidate: " + evidence)
|
||||
if session_candidate:
|
||||
findings.append("review signed-cookie pickle session: settings=%r; SECRET_KEY access and active backend require confirmation" % settings)
|
||||
return findings
|
||||
|
||||
|
||||
printed = 0
|
||||
for path in candidate_files(sys.argv[1:]):
|
||||
for finding in inspect(path):
|
||||
if printed >= MAX_OUTPUT or time.monotonic() >= DEADLINE:
|
||||
break
|
||||
print(finding, flush=True)
|
||||
printed += 1
|
||||
if printed >= MAX_OUTPUT:
|
||||
break
|
||||
DJANGO_FILE_CACHE_CHECK
|
||||
)
|
||||
echo ""
|
||||
fi
|
||||
@@ -1,21 +1,101 @@
|
||||
# Title: Interesting Files - passwords in config PHP files
|
||||
# Title: Interesting Files - passwords in PHP files
|
||||
# ID: IF_Passwords_php_files
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Searching passwords in config PHP files
|
||||
# Description: Searching passwords in PHP config and webroot files
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1552.001
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $DEBUG
|
||||
# Global Variables: $DEBUG, $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Generated Global Variables: $PHP_AUTH_FILES, $php_mysqli_timeout, $php_mysqli_files, $php_mysqli_started
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
if [ "$PSTORAGE_PHP_FILES" ] || [ "$DEBUG" ]; then
|
||||
print_2title "Searching passwords in config PHP files" "T1552.001"
|
||||
printf "%s\n" "$PSTORAGE_PHP_FILES" | while read c; do grep -EiIH "(pwd|passwd|password|PASSWD|PASSWORD|dbuser|dbpass).*[=:].+|define ?\('(\w*passw|\w*user|\w*datab)" "$c" 2>/dev/null | grep -Ev "function|password.*= ?\"\"|password.*= ?''" | sed '/^.\{150\}./d' | sort | uniq | sed -${E} "s,[pP][aA][sS][sS][wW]|[dD][bB]_[pP][aA][sS][sS],${SED_RED},g"; done
|
||||
PHP_AUTH_FILES=""
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
PHP_AUTH_FILES=$(find /var/www -maxdepth 5 -type f -size -1048576c \( -name login.php -o -name auth.php \) -print 2>/dev/null | head -n 40)
|
||||
fi
|
||||
|
||||
# A positional database password can appear in an ordinary PHP page without a
|
||||
# password-named key. Keep this pass separate so its output never includes source.
|
||||
if ! [ "$SEARCH_IN_FOLDER" ] && [ -d /var/www ]; then
|
||||
php_mysqli_timeout=$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null)
|
||||
if [ -z "$php_mysqli_timeout" ]; then
|
||||
echo "PHP mysqli positional credentials: unknown (timeout unavailable)."
|
||||
else
|
||||
php_mysqli_files=$("$php_mysqli_timeout" 5 find /var/www -maxdepth 5 -type f -name '*.php' -size -1048576c -print 2>/dev/null | head -n 40)
|
||||
if [ -n "$php_mysqli_files" ]; then
|
||||
print_2title "PHP mysqli positional credential candidates" "T1552.001"
|
||||
php_mysqli_started=$(date +%s)
|
||||
printf '%s\n' "$php_mysqli_files" | while IFS= read -r c; do
|
||||
if [ "$(($(date +%s) - php_mysqli_started))" -ge 5 ]; then
|
||||
echo "PHP mysqli positional credentials: partial (time limit reached)."
|
||||
break
|
||||
fi
|
||||
[ -f "$c" ] && [ -r "$c" ] || continue
|
||||
# shellcheck disable=SC2016 # The awk program intentionally reads PHP variables.
|
||||
"$php_mysqli_timeout" 1 awk '
|
||||
function trim(s) { sub(/^[[:space:]]+/, "", s); sub(/[[:space:]]+$/, "", s); return s }
|
||||
{
|
||||
if (length($0) > 4096) next
|
||||
line = $0
|
||||
low = tolower(line)
|
||||
if (low ~ /^[[:space:]]*(\/\/|#|\*)/) next
|
||||
if (!match(low, /(^|[^[:alnum:]_])new[[:space:]]+mysqli[[:space:]]*\(/)) next
|
||||
prefix = substr(line, 1, RSTART)
|
||||
prefix_quote = ""; prefix_escaped = 0; commented = 0
|
||||
for (j = 1; j <= length(prefix); j++) {
|
||||
ch = substr(prefix, j, 1)
|
||||
if (prefix_quote != "") {
|
||||
if (prefix_escaped) prefix_escaped = 0
|
||||
else if (ch == "\\") prefix_escaped = 1
|
||||
else if (ch == prefix_quote) prefix_quote = ""
|
||||
} else if (ch == "\"" || ch == sprintf("%c", 39)) prefix_quote = ch
|
||||
else if (ch == "#" || (ch == "/" && substr(prefix, j + 1, 1) == "/")) {
|
||||
commented = 1; break
|
||||
}
|
||||
}
|
||||
if (commented || prefix_quote != "") next
|
||||
start = RSTART + RLENGTH
|
||||
quote = ""; escaped = 0; depth = 0; argc = 1; arg = ""; third = ""; closed = 0
|
||||
for (i = start; i <= length(line); i++) {
|
||||
ch = substr(line, i, 1)
|
||||
if (quote != "") {
|
||||
arg = arg ch
|
||||
if (escaped) escaped = 0
|
||||
else if (ch == "\\") escaped = 1
|
||||
else if (ch == quote) quote = ""
|
||||
} else if (ch == "\"" || ch == sprintf("%c", 39)) {
|
||||
quote = ch; arg = arg ch
|
||||
} else if (ch == "(" || ch == "[" || ch == "{") {
|
||||
depth++; arg = arg ch
|
||||
} else if (ch == ")" && depth == 0) {
|
||||
if (argc == 3) third = trim(arg)
|
||||
closed = 1; break
|
||||
} else if (ch == ")" || ch == "]" || ch == "}") {
|
||||
depth--; arg = arg ch
|
||||
} else if (ch == "," && depth == 0) {
|
||||
if (argc == 3) third = trim(arg)
|
||||
argc++; arg = ""
|
||||
} else arg = arg ch
|
||||
}
|
||||
if (!closed || argc < 3 || third == "" || tolower(third) == "null" || third == "\"\"" || third == sprintf("%c%c", 39, 39)) next
|
||||
kind = "expression"
|
||||
if (third ~ /^\$[[:alpha:]_][[:alnum:]_]*$/) kind = "variable"
|
||||
else if (third ~ /^\047.*\047$/ || third ~ /^".*"$/) kind = "literal"
|
||||
printf "%s:%d: mysqli positional credentials candidate (third argument: %s)\n", FILENAME, FNR, kind
|
||||
}
|
||||
' "$c" 2>/dev/null
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "$PSTORAGE_PHP_FILES" ] || [ "$PHP_AUTH_FILES" ] || [ "$DEBUG" ]; then
|
||||
print_2title "Searching passwords in PHP files" "T1552.001"
|
||||
printf "%s\n%s\n" "$PSTORAGE_PHP_FILES" "$PHP_AUTH_FILES" | sed '/^$/d' | sort -u | while IFS= read -r c; do grep -EiIH "(pwd|passwd|password|PASSWD|PASSWORD|dbuser|dbpass).*[=:].+|define ?\('(\w*passw|\w*user|\w*datab)" "$c" 2>/dev/null | grep -Ev "function|password.*= ?\"\"|password.*= ?''" | sed '/^.\{150\}./d' | sort | uniq | sed -"${E}" "s,[pP][aA][sS][sS][wW]|[dD][bB]_[pP][aA][sS][sS],${SED_RED},g"; done
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
@@ -2,20 +2,81 @@
|
||||
# ID: IF_Passwords_in_logs
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Passwords inside logs
|
||||
# Description: Passwords inside logs, including long credential-bearing GET requests
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1552.001
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $SEARCH_IN_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Generated Global Variables: $log_find_exec_end, $lp_log_started, $lp_log_now, $lp_log_seen, $lp_log_file, $lp_log_hit, $lp_log_found, $lp_log_partial
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 0
|
||||
|
||||
|
||||
lp_http_log_credential_cues() (
|
||||
lp_log_started=$(date +%s 2>/dev/null || printf 0)
|
||||
lp_log_seen=0
|
||||
lp_log_found=0
|
||||
lp_log_partial=0
|
||||
for lp_log_file do
|
||||
[ "$lp_log_seen" -lt 8 ] || { lp_log_partial=1; break; }
|
||||
lp_log_now=$(date +%s 2>/dev/null || printf 0)
|
||||
if [ "$lp_log_started" -gt 0 ] && [ "$lp_log_now" -ge "$((lp_log_started + 5))" ]; then
|
||||
lp_log_partial=1
|
||||
break
|
||||
fi
|
||||
[ -f "$lp_log_file" ] && [ -r "$lp_log_file" ] && [ ! -L "$lp_log_file" ] || continue
|
||||
lp_log_seen=$((lp_log_seen + 1))
|
||||
# The final 64 KiB is enough to inspect recent requests without reading a
|
||||
# whole access log. Never pass raw request lines or parameter values onward.
|
||||
lp_log_hit=$(tail -c 65536 "$lp_log_file" 2>/dev/null | awk '
|
||||
length($0) > 4096 { next }
|
||||
{
|
||||
quote = index($0, "\"")
|
||||
if (!quote) next
|
||||
request = substr($0, quote + 1)
|
||||
if (request !~ /^GET[[:space:]]/) next
|
||||
split(request, fields, /[[:space:]]+/)
|
||||
uri = fields[2]
|
||||
mark = index(uri, "?")
|
||||
if (!mark) next
|
||||
query = substr(uri, mark + 1)
|
||||
sub(/#.*/, "", query)
|
||||
count = split(query, params, "&")
|
||||
for (i = 1; i <= count; i++) {
|
||||
equals = index(params[i], "=")
|
||||
if (equals < 2 || equals == length(params[i])) continue
|
||||
key = tolower(substr(params[i], 1, equals - 1))
|
||||
if (key == "pass" || key == "pwd" || key ~ /(^|[_-])pwd$/ ||
|
||||
key ~ /(password|passwd|passcode)$/) {
|
||||
print "found"
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
' 2>/dev/null)
|
||||
[ "$lp_log_hit" = found ] || continue
|
||||
if [ "$lp_log_found" -eq 0 ]; then
|
||||
printf '%s\n' 'HTTP access-log GET credential query parameters (values redacted):'
|
||||
lp_log_found=1
|
||||
fi
|
||||
printf ' %s: credential-bearing GET request present\n' "$lp_log_file"
|
||||
done
|
||||
[ "$lp_log_partial" -eq 0 ] || printf '%s\n' 'HTTP access-log credential inspection was partial (file or time cap reached).'
|
||||
)
|
||||
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
print_2title "Searching passwords inside logs (limit 70)" "T1552.001"
|
||||
(find /var/log/ /var/logs/ /private/var/log -type f -exec grep -R -H -i "pwd\|passw" "{}" \;) 2>/dev/null | sed '/^.\{150\}./d' | sort | uniq | grep -v "File does not exist:\|modules-config/config-set-passwords\|config-set-passwords already ran\|script not found or unable to stat:\|\"GET /.*\" 404" | head -n 70 | sed -${E} "s,pwd|passw,${SED_RED},"
|
||||
# Probe batching once; use the per-file terminator on older find implementations.
|
||||
log_find_exec_end='+'
|
||||
find /dev/null -exec true '{}' + >/dev/null 2>&1 || log_find_exec_end=';'
|
||||
# shellcheck disable=SC2067
|
||||
(find /var/log/ /var/logs/ /private/var/log -type f -exec grep -R -H -i "pwd\|passw" "{}" "$log_find_exec_end") 2>/dev/null | sed '/^.\{150\}./d' | sort | uniq | grep -v "File does not exist:\|modules-config/config-set-passwords\|config-set-passwords already ran\|script not found or unable to stat:\|\"GET /.*\" 404" | head -n 70 | sed -${E} "s,pwd|passw,${SED_RED},"
|
||||
lp_http_log_credential_cues \
|
||||
/var/log/apache2/access.log /var/log/apache2/access.log.1 \
|
||||
/var/log/httpd/access_log /var/log/httpd/access_log.1 \
|
||||
/var/log/nginx/access.log /var/log/nginx/access.log.1 \
|
||||
/var/www/logs/access_log /var/www/logs/access_log.1
|
||||
echo ""
|
||||
fi
|
||||
@@ -9,7 +9,7 @@
|
||||
# Functions Used: print_2title
|
||||
# Global Variables: $HOMESEARCH,$ITALIC, $pwd_in_variables1, $pwd_in_variables2, $pwd_in_variables3, $pwd_in_variables4, $pwd_in_variables5, $pwd_in_variables6, $pwd_in_variables7, $pwd_in_variables8, $pwd_in_variables9, $pwd_in_variables10, $pwd_in_variables11, $SEARCH_IN_FOLDER, $TIMEOUT, $backup_folders_row
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $ppicf
|
||||
# Generated Global Variables: $ppicf, $password_find_exec_end
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -18,11 +18,17 @@ if ! [ "$FAST" ] && ! [ "$SUPERFAST" ] && [ "$TIMEOUT" ]; then
|
||||
|
||||
##-- IF) Find possible files with passwords
|
||||
print_2title "Searching possible password variables inside key folders (limit 140)" "T1552.001"
|
||||
# Probe batching once; use the per-file terminator on older find implementations.
|
||||
password_find_exec_end='+'
|
||||
find /dev/null -exec true '{}' + >/dev/null 2>&1 || password_find_exec_end=';'
|
||||
if ! [ "$SEARCH_IN_FOLDER" ]; then
|
||||
timeout 150 find $HOMESEARCH -exec grep -HnRiIE "($pwd_in_variables1|$pwd_in_variables2|$pwd_in_variables3|$pwd_in_variables4|$pwd_in_variables5|$pwd_in_variables6|$pwd_in_variables7|$pwd_in_variables8|$pwd_in_variables9|$pwd_in_variables10|$pwd_in_variables11).*[=:].+" '{}' \; 2>/dev/null | sed '/^.\{150\}./d' | grep -Ev "^#" | grep -iv "linpeas" | sort | uniq | head -n 70 | sed -${E} "s,$pwd_in_variables1,${SED_RED},g" | sed -${E} "s,$pwd_in_variables2,${SED_RED},g" | sed -${E} "s,$pwd_in_variables3,${SED_RED},g" | sed -${E} "s,$pwd_in_variables4,${SED_RED},g" | sed -${E} "s,$pwd_in_variables5,${SED_RED},g" | sed -${E} "s,$pwd_in_variables6,${SED_RED},g" | sed -${E} "s,$pwd_in_variables7,${SED_RED},g" | sed -${E} "s,$pwd_in_variables8,${SED_RED},g" | sed -${E} "s,$pwd_in_variables9,${SED_RED},g" | sed -${E} "s,$pwd_in_variables10,${SED_RED},g" | sed -${E} "s,$pwd_in_variables11,${SED_RED},g" &
|
||||
timeout 150 find /var/www $backup_folders_row /tmp /etc /mnt /private -exec grep -HnRiIE "($pwd_in_variables1|$pwd_in_variables2|$pwd_in_variables3|$pwd_in_variables4|$pwd_in_variables5|$pwd_in_variables6|$pwd_in_variables7|$pwd_in_variables8|$pwd_in_variables9|$pwd_in_variables10|$pwd_in_variables11).*[=:].+" '{}' \; 2>/dev/null | sed '/^.\{150\}./d' | grep -Ev "^#" | grep -iv "linpeas" | sort | uniq | head -n 70 | sed -${E} "s,$pwd_in_variables1,${SED_RED},g" | sed -${E} "s,$pwd_in_variables2,${SED_RED},g" | sed -${E} "s,$pwd_in_variables3,${SED_RED},g" | sed -${E} "s,$pwd_in_variables4,${SED_RED},g" | sed -${E} "s,$pwd_in_variables5,${SED_RED},g" | sed -${E} "s,$pwd_in_variables6,${SED_RED},g" | sed -${E} "s,$pwd_in_variables7,${SED_RED},g" | sed -${E} "s,$pwd_in_variables8,${SED_RED},g" | sed -${E} "s,$pwd_in_variables9,${SED_RED},g" | sed -${E} "s,$pwd_in_variables10,${SED_RED},g" | sed -${E} "s,$pwd_in_variables11,${SED_RED},g" &
|
||||
# shellcheck disable=SC2067
|
||||
timeout 150 find $HOMESEARCH ! -type d -exec grep -HnRiIE "($pwd_in_variables1|$pwd_in_variables2|$pwd_in_variables3|$pwd_in_variables4|$pwd_in_variables5|$pwd_in_variables6|$pwd_in_variables7|$pwd_in_variables8|$pwd_in_variables9|$pwd_in_variables10|$pwd_in_variables11).*[=:].+" '{}' "$password_find_exec_end" 2>/dev/null | sed '/^.\{150\}./d' | grep -Ev "^#" | grep -iv "linpeas" | sort | uniq | head -n 70 | sed -${E} "s,$pwd_in_variables1,${SED_RED},g" | sed -${E} "s,$pwd_in_variables2,${SED_RED},g" | sed -${E} "s,$pwd_in_variables3,${SED_RED},g" | sed -${E} "s,$pwd_in_variables4,${SED_RED},g" | sed -${E} "s,$pwd_in_variables5,${SED_RED},g" | sed -${E} "s,$pwd_in_variables6,${SED_RED},g" | sed -${E} "s,$pwd_in_variables7,${SED_RED},g" | sed -${E} "s,$pwd_in_variables8,${SED_RED},g" | sed -${E} "s,$pwd_in_variables9,${SED_RED},g" | sed -${E} "s,$pwd_in_variables10,${SED_RED},g" | sed -${E} "s,$pwd_in_variables11,${SED_RED},g" &
|
||||
# shellcheck disable=SC2067
|
||||
timeout 150 find /var/www $backup_folders_row /tmp /etc /mnt /private ! -type d -exec grep -HnRiIE "($pwd_in_variables1|$pwd_in_variables2|$pwd_in_variables3|$pwd_in_variables4|$pwd_in_variables5|$pwd_in_variables6|$pwd_in_variables7|$pwd_in_variables8|$pwd_in_variables9|$pwd_in_variables10|$pwd_in_variables11).*[=:].+" '{}' "$password_find_exec_end" 2>/dev/null | sed '/^.\{150\}./d' | grep -Ev "^#" | grep -iv "linpeas" | sort | uniq | head -n 70 | sed -${E} "s,$pwd_in_variables1,${SED_RED},g" | sed -${E} "s,$pwd_in_variables2,${SED_RED},g" | sed -${E} "s,$pwd_in_variables3,${SED_RED},g" | sed -${E} "s,$pwd_in_variables4,${SED_RED},g" | sed -${E} "s,$pwd_in_variables5,${SED_RED},g" | sed -${E} "s,$pwd_in_variables6,${SED_RED},g" | sed -${E} "s,$pwd_in_variables7,${SED_RED},g" | sed -${E} "s,$pwd_in_variables8,${SED_RED},g" | sed -${E} "s,$pwd_in_variables9,${SED_RED},g" | sed -${E} "s,$pwd_in_variables10,${SED_RED},g" | sed -${E} "s,$pwd_in_variables11,${SED_RED},g" &
|
||||
else
|
||||
timeout 150 find $SEARCH_IN_FOLDER -exec grep -HnRiIE "($pwd_in_variables1|$pwd_in_variables2|$pwd_in_variables3|$pwd_in_variables4|$pwd_in_variables5|$pwd_in_variables6|$pwd_in_variables7|$pwd_in_variables8|$pwd_in_variables9|$pwd_in_variables10|$pwd_in_variables11).*[=:].+" '{}' \; 2>/dev/null | sed '/^.\{150\}./d' | grep -Ev "^#" | grep -iv "linpeas" | sort | uniq | head -n 70 | sed -${E} "s,$pwd_in_variables1,${SED_RED},g" | sed -${E} "s,$pwd_in_variables2,${SED_RED},g" | sed -${E} "s,$pwd_in_variables3,${SED_RED},g" | sed -${E} "s,$pwd_in_variables4,${SED_RED},g" | sed -${E} "s,$pwd_in_variables5,${SED_RED},g" | sed -${E} "s,$pwd_in_variables6,${SED_RED},g" | sed -${E} "s,$pwd_in_variables7,${SED_RED},g" | sed -${E} "s,$pwd_in_variables8,${SED_RED},g" | sed -${E} "s,$pwd_in_variables9,${SED_RED},g" | sed -${E} "s,$pwd_in_variables10,${SED_RED},g" | sed -${E} "s,$pwd_in_variables11,${SED_RED},g" &
|
||||
# shellcheck disable=SC2067
|
||||
timeout 150 find $SEARCH_IN_FOLDER ! -type d -exec grep -HnRiIE "($pwd_in_variables1|$pwd_in_variables2|$pwd_in_variables3|$pwd_in_variables4|$pwd_in_variables5|$pwd_in_variables6|$pwd_in_variables7|$pwd_in_variables8|$pwd_in_variables9|$pwd_in_variables10|$pwd_in_variables11).*[=:].+" '{}' "$password_find_exec_end" 2>/dev/null | sed '/^.\{150\}./d' | grep -Ev "^#" | grep -iv "linpeas" | sort | uniq | head -n 70 | sed -${E} "s,$pwd_in_variables1,${SED_RED},g" | sed -${E} "s,$pwd_in_variables2,${SED_RED},g" | sed -${E} "s,$pwd_in_variables3,${SED_RED},g" | sed -${E} "s,$pwd_in_variables4,${SED_RED},g" | sed -${E} "s,$pwd_in_variables5,${SED_RED},g" | sed -${E} "s,$pwd_in_variables6,${SED_RED},g" | sed -${E} "s,$pwd_in_variables7,${SED_RED},g" | sed -${E} "s,$pwd_in_variables8,${SED_RED},g" | sed -${E} "s,$pwd_in_variables9,${SED_RED},g" | sed -${E} "s,$pwd_in_variables10,${SED_RED},g" | sed -${E} "s,$pwd_in_variables11,${SED_RED},g" &
|
||||
fi
|
||||
wait
|
||||
echo ""
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Title: Container - checkDockerOverlayStorage
|
||||
# ID: checkDockerOverlayStorage
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 09-10-2026
|
||||
# Description: List a bounded set of already-mounted Docker overlay roots traversable by this unprivileged account.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Functions Used:
|
||||
# Global Variables:
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $cdos_mounts, $cdos_path, $cdos_root
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
checkDockerOverlayStorage() {
|
||||
# Optional paths allow a synthetic mount table and directory tree in tests.
|
||||
cdos_root="${1:-/var/lib/docker}"
|
||||
cdos_mounts="${2:-/proc/self/mountinfo}"
|
||||
[ "$(id -u 2>/dev/null)" = "0" ] && return
|
||||
[ -d "$cdos_root/overlay2" ] && [ -x "$cdos_root" ] &&
|
||||
[ -x "$cdos_root/overlay2" ] && [ -r "$cdos_mounts" ] || return 0
|
||||
|
||||
# The mount table gives exact active paths even when overlay2 is not listable.
|
||||
# Accept only overlay filesystem mounts at 64-hex-id merged roots.
|
||||
# Stop after twelve candidates.
|
||||
awk -v prefix="$cdos_root/overlay2/" '
|
||||
index($5, prefix) == 1 {
|
||||
for (i = 7; i <= NF; i++) if ($i == "-") break
|
||||
if (i >= NF || $(i + 1) != "overlay") next
|
||||
suffix = substr($5, length(prefix) + 1)
|
||||
if (length(suffix) != 71 || substr(suffix, 1, 64) !~ /^[0-9a-f]+$/ ||
|
||||
substr(suffix, 65) != "/merged") next
|
||||
if ($6 ~ /(^|,)nosuid(,|$)/ || $6 ~ /(^|,)noexec(,|$)/) next
|
||||
if (!seen[$5]++) {
|
||||
print $5
|
||||
if (++count >= 12) exit
|
||||
}
|
||||
}
|
||||
' "$cdos_mounts" 2>/dev/null | while IFS= read -r cdos_path; do
|
||||
[ -d "$cdos_path" ] && [ -x "$cdos_path" ] && printf '%s\n' "$cdos_path"
|
||||
done
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
# ID: checkDockerVersionExploits
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Description: Check if the container is vulnerable to CVE-2019-13139 and CVE-2019-5736
|
||||
# Description: Compare Docker version with the CVE-2019-13139, CVE-2019-5736, and CVE-2021-41091 fix thresholds.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Functions Used: echo_no, echo_not_found
|
||||
# Global Variables: $dockerVersion
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $VULN_CVE_2019_13139, $VULN_CVE_2019_5736, $VULN_CVE_2021_41091
|
||||
# Generated Global Variables: $docker_2019_status, $docker_41091_status, $VULN_CVE_2019_13139, $VULN_CVE_2019_5736, $VULN_CVE_2021_41091
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -21,18 +21,51 @@ checkDockerVersionExploits() {
|
||||
return
|
||||
fi
|
||||
|
||||
VULN_CVE_2019_13139="$(echo_no)"
|
||||
if [ "$(echo $dockerVersion | sed 's,\.,,g')" -lt "1895" ]; then
|
||||
VULN_CVE_2019_13139="Yes"
|
||||
fi
|
||||
# Docker fixed the build-ref issue in 18.09.4 and bundled a fixed runc in
|
||||
# 18.09.2. Compare components; concatenation misses zero-padded minor
|
||||
# releases such as 18.09.1. A separately packaged runc still needs review.
|
||||
docker_2019_status="$(printf '%s\n' "$dockerVersion" | awk '
|
||||
length($0) > 64 { exit }
|
||||
match($0, /^[0-9]+\.[0-9]+\.[0-9]+/) {
|
||||
suffix = substr($0, RLENGTH + 1)
|
||||
if (suffix ~ /^-(rc|alpha|beta|dev)/ ||
|
||||
(suffix != "" && suffix !~ /^[+-][A-Za-z0-9._-]+$/)) exit
|
||||
split(substr($0, 1, RLENGTH), version, ".")
|
||||
if (length(version[1]) > 4 || length(version[2]) > 4 ||
|
||||
length(version[3]) > 6) exit
|
||||
build = version[1] < 18 || (version[1] == 18 &&
|
||||
(version[2] < 9 || (version[2] == 9 && version[3] < 4)))
|
||||
runtime = version[1] < 18 || (version[1] == 18 &&
|
||||
(version[2] < 9 || (version[2] == 9 && version[3] < 2)))
|
||||
print (build ? "Yes" : "No") ":" (runtime ? "Yes" : "No")
|
||||
}
|
||||
')"
|
||||
case "$docker_2019_status" in
|
||||
Yes:Yes) VULN_CVE_2019_13139="Yes"; VULN_CVE_2019_5736="Yes" ;;
|
||||
Yes:No) VULN_CVE_2019_13139="Yes"; VULN_CVE_2019_5736="$(echo_no)" ;;
|
||||
No:No) VULN_CVE_2019_13139="$(echo_no)"; VULN_CVE_2019_5736="$(echo_no)" ;;
|
||||
*) VULN_CVE_2019_13139="$(echo_not_found)"; VULN_CVE_2019_5736="$(echo_not_found)" ;;
|
||||
esac
|
||||
|
||||
VULN_CVE_2019_5736="$(echo_no)"
|
||||
if [ "$(echo $dockerVersion | sed 's,\.,,g')" -lt "1893" ]; then
|
||||
VULN_CVE_2019_5736="Yes"
|
||||
fi
|
||||
|
||||
VULN_CVE_2021_41091="$(echo_no)"
|
||||
if [ "$(echo $dockerVersion | sed 's,\.,,g')" -lt "20109" ]; then
|
||||
VULN_CVE_2021_41091="Yes"
|
||||
fi
|
||||
# Numeric components avoid treating 24.0.9 as older than 20.10.9.
|
||||
# Package suffixes such as +dfsg1 do not change the upstream version.
|
||||
docker_41091_status="$(printf '%s\n' "$dockerVersion" | awk '
|
||||
length($0) <= 64 && match($0, /^[0-9]+\.[0-9]+\.[0-9]+/) {
|
||||
suffix = substr($0, RLENGTH + 1)
|
||||
if (suffix ~ /^-(rc|alpha|beta|dev)/ || (suffix != "" &&
|
||||
suffix !~ /^[+-][A-Za-z0-9._-]+$/)) exit
|
||||
split(substr($0, 1, RLENGTH), version, ".")
|
||||
if (length(version[1]) > 4 || length(version[2]) > 4 ||
|
||||
length(version[3]) > 6) exit
|
||||
if (version[1] < 20 ||
|
||||
(version[1] == 20 && (version[2] < 10 ||
|
||||
(version[2] == 10 && version[3] < 9)))) print "Yes"
|
||||
else print "No"
|
||||
}
|
||||
')"
|
||||
case "$docker_41091_status" in
|
||||
Yes) VULN_CVE_2021_41091="Yes" ;;
|
||||
No) VULN_CVE_2021_41091="$(echo_no)" ;;
|
||||
*) VULN_CVE_2021_41091="$(echo_not_found)" ;;
|
||||
esac
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Title: Functions - checkGlibcTunablesCVE20234911
|
||||
# ID: checkGlibcTunablesCVE20234911
|
||||
# Author: PEASS contributors
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Review installed GNU C Library package against vendor fixes for CVE-2023-4911 without executing a malformed tunables probe.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1068
|
||||
# Functions Used: print_3title, print_info
|
||||
# Global Variables: $ROOT_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $gt4911_root, $gt4911_release, $gt4911_id, $gt4911_codename, $gt4911_fixed, $gt4911_record, $gt4911_version
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
checkGlibcTunablesCVE20234911() {
|
||||
command -v dpkg-query >/dev/null 2>&1 || return 0
|
||||
command -v dpkg >/dev/null 2>&1 || return 0
|
||||
|
||||
gt4911_root="${ROOT_FOLDER:-/}"
|
||||
case "$gt4911_root" in */) ;; *) gt4911_root="$gt4911_root/" ;; esac
|
||||
gt4911_release="${gt4911_root}etc/os-release"
|
||||
[ -r "$gt4911_release" ] || return 0
|
||||
gt4911_id="$(sed -n 's/^ID=//p' "$gt4911_release" | head -n 1 | tr -d '"')"
|
||||
gt4911_codename="$(sed -n 's/^VERSION_CODENAME=//p' "$gt4911_release" | head -n 1 | tr -d '"')"
|
||||
|
||||
# Compare vendor package releases, not upstream glibc versions: Debian
|
||||
# bullseye backported the affected parser into its older 2.31 package.
|
||||
case "$gt4911_id:$gt4911_codename" in
|
||||
ubuntu:jammy) gt4911_fixed="2.35-0ubuntu3.4" ;;
|
||||
ubuntu:lunar) gt4911_fixed="2.37-0ubuntu2.1" ;;
|
||||
ubuntu:mantic|ubuntu:noble) gt4911_fixed="2.38-1ubuntu6" ;;
|
||||
debian:bullseye) gt4911_fixed="2.31-13+deb11u7" ;;
|
||||
debian:bookworm) gt4911_fixed="2.36-9+deb12u3" ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
|
||||
gt4911_record="$(dpkg-query --admindir="${gt4911_root}var/lib/dpkg" -W -f='$''{Status}|$''{Version}\n' libc6 2>/dev/null | head -n 1)"
|
||||
case "$gt4911_record" in
|
||||
'install ok installed|'*) gt4911_version="${gt4911_record#*|}" ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
[ -n "$gt4911_version" ] || return 0
|
||||
|
||||
print_3title "GNU C Library tunables package review (CVE-2023-4911)" "T1068"
|
||||
print_info "https://ubuntu.com/security/CVE-2023-4911"
|
||||
print_info "https://security-tracker.debian.org/tracker/CVE-2023-4911"
|
||||
echo "Installed libc6: $gt4911_version; vendor fixed threshold for $gt4911_id $gt4911_codename: $gt4911_fixed"
|
||||
if dpkg --compare-versions "$gt4911_version" lt "$gt4911_fixed"; then
|
||||
echo "Package below vendor fix: review candidate only. Confirm installed loader, package patch state, a reachable SUID target, and NoNewPrivs/nosuid or other mitigations; no exploit probe was run."
|
||||
else
|
||||
echo "Installed package meets this vendor's fixed threshold; confirm the running loader after package updates."
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# Title: Functions - checkLibblockdevCVE20256019
|
||||
# ID: checkLibblockdevCVE20256019
|
||||
# Author: PEASS-ng
|
||||
# Last Update: 08-10-2026
|
||||
# Description: Passive udisks/libblockdev and polkit evidence for CVE-2025-6019.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1068
|
||||
# Functions Used: lp_suse_numeric_version_lt, print_3title, print_info
|
||||
# Global Variables: $ROOT_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $bd6019_root, $bd6019_daemon, $bd6019_candidate, $bd6019_service, $bd6019_policy, $bd6019_package, $bd6019_os, $bd6019_id, $bd6019_version
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
checkLibblockdevCVE20256019() { (
|
||||
[ "$(uname -s 2>/dev/null)" = Linux ] || return 0
|
||||
bd6019_root="${ROOT_FOLDER:-/}"
|
||||
case "$bd6019_root" in */) ;; *) bd6019_root="$bd6019_root/" ;; esac
|
||||
bd6019_daemon=""
|
||||
for bd6019_candidate in usr/libexec/udisks2/udisksd usr/lib/udisks2/udisksd lib/udisks2/udisksd; do
|
||||
if [ -x "${bd6019_root}${bd6019_candidate}" ]; then
|
||||
bd6019_daemon="${bd6019_root}${bd6019_candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ -n "$bd6019_daemon" ] || return 0
|
||||
bd6019_service="${bd6019_root}usr/share/dbus-1/system-services/org.freedesktop.UDisks2.service"
|
||||
[ -r "$bd6019_service" ] || return 0
|
||||
sed -n '1,100p;101q' "$bd6019_service" | grep -Eq '^Name=org[.]freedesktop[.]UDisks2$' || return 0
|
||||
[ -x "${bd6019_root}usr/sbin/xfs_growfs" ] || [ -x "${bd6019_root}sbin/xfs_growfs" ] || return 0
|
||||
bd6019_policy="${bd6019_root}usr/share/polkit-1/actions/org.freedesktop.udisks2.policy"
|
||||
[ -r "$bd6019_policy" ] || return 0
|
||||
# The resize path is useful to an active user only when this action permits it.
|
||||
sed -n '1,1000p;1001q' "$bd6019_policy" | awk '
|
||||
/<action id="org[.]freedesktop[.]udisks2[.]modify-device">/ { in_action=1 }
|
||||
in_action && /<allow_active>[[:space:]]*yes[[:space:]]*<\/allow_active>/ { found=1 }
|
||||
in_action && /<\/action>/ { in_action=0 }
|
||||
END { exit !found }
|
||||
' || return 0
|
||||
bd6019_os="${bd6019_root}etc/os-release"
|
||||
[ -r "$bd6019_os" ] || return 0
|
||||
bd6019_id="$(sed -n '1,100{s/^ID=[" ]*\([^" ]*\).*/\1/p;};101q' "$bd6019_os" | head -n 1)"
|
||||
bd6019_version="$(sed -n '1,100{s/^VERSION_ID=[" ]*\([^" ]*\).*/\1/p;};101q' "$bd6019_os" | head -n 1)"
|
||||
case "$bd6019_id:$bd6019_version" in
|
||||
sles:15.6|sles:15.7|sled:15.6|sled:15.7|opensuse-leap:15.6|opensuse:15.6) ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
command -v timeout >/dev/null 2>&1 && command -v rpm >/dev/null 2>&1 || return 0
|
||||
bd6019_package="$(timeout 3 rpm --root "$bd6019_root" -q --qf '%{VERSION}-%{RELEASE}\n' libbd_fs2 2>/dev/null | awk 'NR == 1 && /^[0-9]/ { print }')"
|
||||
lp_suse_numeric_version_lt "$bd6019_package" "2.26-150400.3.5.1" || return 0
|
||||
print_3title "udisks/libblockdev XFS resize path (CVE-2025-6019)" "T1068"
|
||||
print_info "https://www.suse.com/security/cve/CVE-2025-6019.html"
|
||||
echo "udisks D-Bus activation, xfs_growfs, and allow_active=yes modify-device policy are present"
|
||||
printf 'libbd_fs2 package: %s (below SUSE fixed version 2.26-150400.3.5.1)\n' "$bd6019_package"
|
||||
echo "Effective patch status: unknown; verify vendor libblockdev advisory and local overrides"
|
||||
echo ""
|
||||
) ; }
|
||||
@@ -0,0 +1,79 @@
|
||||
# Title: Functions - checkPamAllowActiveCVE20256018
|
||||
# ID: checkPamAllowActiveCVE20256018
|
||||
# Author: PEASS-ng
|
||||
# Last Update: 08-10-2026
|
||||
# Description: Passive SUSE 15 PAM stack evidence for CVE-2025-6018.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1068
|
||||
# Functions Used: lp_suse_numeric_version_lt, print_3title, print_info
|
||||
# Global Variables: $ROOT_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $pa6018_root, $pa6018_os, $pa6018_id, $pa6018_version, $pa6018_auth, $pa6018_session, $pa6018_sshd, $pa6018_env_line, $pa6018_pam, $pa6018_pam_config, $pa6018_config, $pa6018_status
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
checkPamAllowActiveCVE20256018() { (
|
||||
[ "$(uname -s 2>/dev/null)" = Linux ] || return 0
|
||||
pa6018_root="${ROOT_FOLDER:-/}"
|
||||
case "$pa6018_root" in */) ;; *) pa6018_root="$pa6018_root/" ;; esac
|
||||
pa6018_os="${pa6018_root}etc/os-release"
|
||||
[ -r "$pa6018_os" ] || return 0
|
||||
pa6018_id="$(sed -n '1,100{s/^ID=[" ]*\([^" ]*\).*/\1/p;};101q' "$pa6018_os" | head -n 1)"
|
||||
pa6018_version="$(sed -n '1,100{s/^VERSION_ID=[" ]*\([^" ]*\).*/\1/p;};101q' "$pa6018_os" | head -n 1)"
|
||||
case "$pa6018_id:$pa6018_version" in
|
||||
sles:15*|sled:15*|opensuse-leap:15*|opensuse:15*) ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
|
||||
pa6018_auth="${pa6018_root}etc/pam.d/common-auth"
|
||||
pa6018_session="${pa6018_root}etc/pam.d/common-session"
|
||||
pa6018_sshd="${pa6018_root}etc/pam.d/sshd"
|
||||
[ -r "$pa6018_auth" ] && [ -r "$pa6018_session" ] && [ -r "$pa6018_sshd" ] || return 0
|
||||
# Inspect only the named SSH stack files. A user supplied PAM environment is
|
||||
# relevant only when pam_env precedes pam_systemd in this stack.
|
||||
sed -n '1,200p;201q' "$pa6018_sshd" | grep -Eq '^[[:space:]]*(auth[[:space:]]+(include|substack)[[:space:]]+common-auth|@include[[:space:]]+common-auth)' || return 0
|
||||
sed -n '1,200p;201q' "$pa6018_sshd" | grep -Eq '^[[:space:]]*(session[[:space:]]+(include|substack)[[:space:]]+common-session|@include[[:space:]]+common-session)' || return 0
|
||||
pa6018_env_line="$(sed -n '1,200p;201q' "$pa6018_auth" | grep -E '^[[:space:]]*auth[[:space:]].*pam_env[.]so([[:space:]]|$)' | head -n 1)"
|
||||
[ -n "$pa6018_env_line" ] || return 0
|
||||
sed -n '1,200p;201q' "$pa6018_session" | grep -Eq '^[[:space:]]*session[[:space:]].*pam_systemd[.]so([[:space:]]|$)' || return 0
|
||||
if printf '%s\n' "$pa6018_env_line" | grep -Eq '(^|[[:space:]])user_readenv=0($|[[:space:]])'; then
|
||||
return 0
|
||||
elif printf '%s\n' "$pa6018_env_line" | grep -Eq '(^|[[:space:]])user_readenv=1($|[[:space:]])'; then
|
||||
pa6018_config="explicit user_readenv=1"
|
||||
else
|
||||
pa6018_config="implicit user_readenv; effective default unknown"
|
||||
fi
|
||||
|
||||
pa6018_pam="unknown"
|
||||
pa6018_pam_config="unknown"
|
||||
if command -v timeout >/dev/null 2>&1 && command -v rpm >/dev/null 2>&1; then
|
||||
pa6018_pam="$(timeout 3 rpm --root "$pa6018_root" -q --qf '%{VERSION}-%{RELEASE}\n' pam 2>/dev/null | awk 'NR == 1 && /^[0-9]/ { print }')"
|
||||
[ -n "$pa6018_pam" ] || pa6018_pam="unknown"
|
||||
pa6018_pam_config="$(timeout 3 rpm --root "$pa6018_root" -q --qf '%{VERSION}-%{RELEASE}\n' pam-config 2>/dev/null | awk 'NR == 1 && /^[0-9]/ { print }')"
|
||||
[ -n "$pa6018_pam_config" ] || pa6018_pam_config="unknown"
|
||||
fi
|
||||
pa6018_status="unknown"
|
||||
case "$pa6018_id:$pa6018_version" in
|
||||
sles:15.6|sles:15.7|sled:15.6|sled:15.7|opensuse-leap:15.6|opensuse:15.6)
|
||||
if [ "$pa6018_config" = "explicit user_readenv=1" ]; then
|
||||
# The PAM update changes the default; an explicit setting overrides it.
|
||||
pa6018_status="explicit user_readenv=1 in the effective auth stack"
|
||||
elif lp_suse_numeric_version_lt "$pa6018_pam" "1.3.0-150000.6.83.1"; then
|
||||
# The package that controls pam_env's default is below SUSE's fix.
|
||||
# The actual stack still has pam_env in auth, regardless of whether
|
||||
# a newer pam-config package has been installed since it was written.
|
||||
pa6018_status="pam below SUSE fixed package version"
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
print_3title "SUSE PAM allow_active path (CVE-2025-6018)" "T1068"
|
||||
print_info "https://www.suse.com/security/cve/CVE-2025-6018.html"
|
||||
echo "SSH includes common-auth and common-session; common-auth loads pam_env before common-session loads pam_systemd"
|
||||
echo "pam_env: $pa6018_config; pam package: $pa6018_pam; pam-config package: $pa6018_pam_config"
|
||||
echo "Package evidence: $pa6018_status; effective patch status: unknown, verify vendor advisories and PAM configuration"
|
||||
echo ""
|
||||
) ; }
|
||||
@@ -0,0 +1,159 @@
|
||||
# Title: Function - checkRootWritableProcessPaths
|
||||
# ID: checkRootWritableProcessPaths
|
||||
# Author: PEASS-ng
|
||||
# Last Update: 2026-10-09
|
||||
# Description: Passively correlate observed UID 0 commands with modifiable executable, script, or literal PHP include paths.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1574
|
||||
# Functions Used: print_3title
|
||||
# Global Variables:
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $rwpp_proc_root, $rwpp_uid, $rwpp_seen, $rwpp_found, $rwpp_php_seen, $rwpp_entry, $rwpp_pid, $rwpp_effective_uid, $rwpp_kind, $rwpp_path, $rwpp_cmd, $rwpp_parent, $rwpp_reason, $rwpp_parent_uid, $rwpp_entry_uid, $rwpp_php_base, $rwpp_php_script, $rwpp_php_pid, $rwpp_php_lines, $rwpp_php_relative, $rwpp_php_target, $rwpp_php_walk, $rwpp_php_rest, $rwpp_php_safe, $rwpp_php_part, $rwpp_php_found
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
# Inspect literal includes in root PHP consumers. Do not follow dynamic
|
||||
# expressions, symlinks, parent traversal, or more than four consumers and
|
||||
# eight 8 KiB source prefixes per document root.
|
||||
rwpp_php_literal_includes() (
|
||||
rwpp_php_base=$1
|
||||
rwpp_php_script=$2
|
||||
rwpp_php_pid=$3
|
||||
case "$rwpp_php_base:$rwpp_php_script" in *[!A-Za-z0-9_./:-]*) exit 0 ;; esac
|
||||
case "$rwpp_php_base:$rwpp_php_script" in *'//'*|*'/./'*|*'/../'*|*'/.'|*'/..') exit 0 ;; esac
|
||||
[ -f "$rwpp_php_script" ] && [ -r "$rwpp_php_script" ] || exit 0
|
||||
[ -L "$rwpp_php_script" ] && exit 0
|
||||
rwpp_php_lines=$(LC_ALL=C dd if="$rwpp_php_script" bs=8192 count=1 2>/dev/null | sed -n '1,64p' |
|
||||
sed -nE "s/^[[:space:]]*(include|include_once|require|require_once)[[:space:]]*\\(?[[:space:]]*['\"]([A-Za-z0-9_./-]+\\.php)['\"][[:space:]]*\\)?[[:space:]]*;.*/\\2/p" |
|
||||
sed -n '1,4p')
|
||||
[ -n "$rwpp_php_lines" ] || exit 0
|
||||
rwpp_php_found=0
|
||||
for rwpp_php_relative in $rwpp_php_lines; do
|
||||
case "$rwpp_php_relative" in *'//'*|*'/./'*|*'/../'*|*'/.'|*'/..'|/*) continue ;; esac
|
||||
rwpp_php_target=${rwpp_php_script%/*}/$rwpp_php_relative
|
||||
[ -f "$rwpp_php_target" ] && [ -w "$rwpp_php_target" ] || continue
|
||||
rwpp_php_walk=''
|
||||
rwpp_php_rest=${rwpp_php_target#/}
|
||||
rwpp_php_safe=1
|
||||
while [ -n "$rwpp_php_rest" ]; do
|
||||
rwpp_php_part=${rwpp_php_rest%%/*}
|
||||
rwpp_php_walk=$rwpp_php_walk/$rwpp_php_part
|
||||
if [ -L "$rwpp_php_walk" ]; then rwpp_php_safe=0; break; fi
|
||||
case "$rwpp_php_rest" in */*) rwpp_php_rest=${rwpp_php_rest#*/} ;; *) rwpp_php_rest='' ;; esac
|
||||
done
|
||||
[ "$rwpp_php_safe" -eq 1 ] || continue
|
||||
printf 'PID %s: root PHP source %s includes writable PHP file %s (literal path; execution path and access policy require review)\n' \
|
||||
"$rwpp_php_pid" "$rwpp_php_script" "$rwpp_php_target"
|
||||
rwpp_php_found=$((rwpp_php_found + 1))
|
||||
[ "$rwpp_php_found" -lt 2 ] || break
|
||||
done
|
||||
)
|
||||
|
||||
checkRootWritableProcessPaths() {
|
||||
[ "$(uname -s 2>/dev/null)" = Linux ] || return 0
|
||||
rwpp_uid=$(id -u 2>/dev/null) || return 0
|
||||
[ "$rwpp_uid" != 0 ] || return 0
|
||||
rwpp_proc_root=${1:-/proc}
|
||||
[ -d "$rwpp_proc_root" ] || return 0
|
||||
rwpp_seen=0
|
||||
rwpp_found=0
|
||||
rwpp_php_seen=0
|
||||
|
||||
for rwpp_entry in "$rwpp_proc_root"/[0-9]*; do
|
||||
[ -d "$rwpp_entry" ] || continue
|
||||
rwpp_pid=${rwpp_entry##*/}
|
||||
case "$rwpp_pid" in *[!0-9]*|'') continue ;; esac
|
||||
rwpp_seen=$((rwpp_seen + 1))
|
||||
[ "$rwpp_seen" -le 200 ] || break
|
||||
[ -r "$rwpp_entry/status" ] && [ -r "$rwpp_entry/cmdline" ] || continue
|
||||
rwpp_effective_uid=$(awk '$1 == "Uid:" { print $3; exit }' "$rwpp_entry/status" 2>/dev/null)
|
||||
[ "$rwpp_effective_uid" = 0 ] || continue
|
||||
|
||||
# od preserves NUL argument boundaries. Reading 1025 bytes lets awk reject
|
||||
# a truncated command instead of interpreting an incomplete script operand.
|
||||
while IFS='|' read -r rwpp_kind rwpp_path rwpp_cmd; do
|
||||
case "$rwpp_kind" in
|
||||
phpdocroot)
|
||||
[ "$rwpp_php_seen" -lt 4 ] && [ -d "$rwpp_path" ] && [ ! -L "$rwpp_path" ] || continue
|
||||
rwpp_php_seen=$((rwpp_php_seen + 1))
|
||||
find "$rwpp_path" -maxdepth 1 -type f -name '*.php' -print 2>/dev/null | head -n 8 |
|
||||
while IFS= read -r rwpp_php_script; do
|
||||
rwpp_php_literal_includes "$rwpp_path" "$rwpp_php_script" "$rwpp_pid"
|
||||
done
|
||||
continue ;;
|
||||
phpscript)
|
||||
[ "$rwpp_php_seen" -lt 4 ] || continue
|
||||
rwpp_php_seen=$((rwpp_php_seen + 1))
|
||||
rwpp_php_literal_includes "${rwpp_path%/*}" "$rwpp_path" "$rwpp_pid"
|
||||
continue ;;
|
||||
esac
|
||||
[ -f "$rwpp_path" ] || continue
|
||||
rwpp_reason=''
|
||||
if [ -w "$rwpp_path" ]; then
|
||||
rwpp_reason='file writable'
|
||||
fi
|
||||
rwpp_parent=${rwpp_path%/*}
|
||||
[ -n "$rwpp_parent" ] || rwpp_parent=/
|
||||
if [ -d "$rwpp_parent" ] && [ -w "$rwpp_parent" ] && [ -x "$rwpp_parent" ]; then
|
||||
# For sticky directories either the directory or the entry must belong
|
||||
# to the caller. stat without -L gets the entry owner for symlinks.
|
||||
if [ -k "$rwpp_parent" ]; then
|
||||
rwpp_parent_uid=$(stat -Lc '%u' "$rwpp_parent" 2>/dev/null)
|
||||
rwpp_entry_uid=$(stat -c '%u' "$rwpp_path" 2>/dev/null)
|
||||
if [ "$rwpp_parent_uid" = "$rwpp_uid" ] || [ "$rwpp_entry_uid" = "$rwpp_uid" ]; then
|
||||
rwpp_reason="${rwpp_reason:+$rwpp_reason; }parent allows replacement"
|
||||
fi
|
||||
else
|
||||
rwpp_reason="${rwpp_reason:+$rwpp_reason; }parent allows replacement"
|
||||
fi
|
||||
fi
|
||||
[ -n "$rwpp_reason" ] || continue
|
||||
if [ "$rwpp_found" -eq 0 ]; then
|
||||
print_3title 'Observed UID 0 process paths modifiable by current user' 'T1574'
|
||||
fi
|
||||
printf 'PID %s: %s | %s: %s (%s)\n' "$rwpp_pid" "$rwpp_cmd" "$rwpp_kind" "$rwpp_path" "$rwpp_reason"
|
||||
rwpp_found=$((rwpp_found + 1))
|
||||
[ "$rwpp_found" -lt 12 ] || break
|
||||
done <<EOF
|
||||
$(dd if="$rwpp_entry/cmdline" bs=1025 count=1 2>/dev/null | od -An -v -tu1 | awk '
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
byte = $i + 0; count++
|
||||
if (count > 1024) invalid = 1
|
||||
if (byte == 0) {
|
||||
argc++; arg[argc] = current; current = ""; ended = 1
|
||||
} else {
|
||||
ended = 0
|
||||
if (byte < 32 || byte > 126) invalid = 1
|
||||
current = current sprintf("%c", byte)
|
||||
}
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (invalid || !ended || !argc) exit
|
||||
exe = arg[1]
|
||||
name = exe; sub(/^.*\//, "", name)
|
||||
command = exe
|
||||
for (i = 2; i <= argc && i <= 3; i++) command = command " " arg[i]
|
||||
gsub(/\|/, "?", command)
|
||||
if (exe ~ /^\// && exe !~ /\|/) print "executable|" exe "|" command
|
||||
if (name == "php" && exe ~ /^\//) {
|
||||
if (arg[2] == "-S" && arg[4] == "-t" && arg[5] ~ /^\/[A-Za-z0-9_.\/-]+$/)
|
||||
print "phpdocroot|" arg[5] "|" command
|
||||
else if (arg[2] == "-f" && arg[3] ~ /^\/[A-Za-z0-9_.\/-]+\.php$/)
|
||||
print "phpscript|" arg[3] "|" command
|
||||
else if (arg[2] ~ /^\/[A-Za-z0-9_.\/-]+\.php$/)
|
||||
print "phpscript|" arg[2] "|" command
|
||||
}
|
||||
if (name ~ /^(sh|bash|dash|ash|ksh|zsh|perl|ruby|php)$/ || name ~ /^python([23]([.][0-9]+)?)?$/) {
|
||||
i = 2
|
||||
if (arg[i] == "--") i++
|
||||
if (arg[i] ~ /^\// && arg[i] !~ /\|/) print "script|" arg[i] "|" command
|
||||
}
|
||||
}')
|
||||
EOF
|
||||
[ "$rwpp_found" -lt 12 ] || break
|
||||
done
|
||||
[ "$rwpp_found" -eq 0 ] || echo ''
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
# Title: Functions - checkSnapConfineCVE20263888
|
||||
# ID: checkSnapConfineCVE20263888
|
||||
# Author: Chack Agent
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Passive prerequisites for the snap-confine and tmpfiles race (CVE-2026-3888).
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1068
|
||||
# Functions Used: print_3title, print_info
|
||||
# Global Variables: $ROOT_FOLDER, $E, $SED_RED_YELLOW
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $sc3888_root, $sc3888_path, $sc3888_priv, $sc3888_dir, $sc3888_version, $sc3888_release, $sc3888_fixed, $sc3888_state, $sc3888_rule, $sc3888_age, $sc3888_timer
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
checkSnapConfineCVE20263888() {
|
||||
sc3888_root="${ROOT_FOLDER:-/}"
|
||||
case "$sc3888_root" in */) ;; *) sc3888_root="${sc3888_root}/" ;; esac
|
||||
sc3888_path="${sc3888_root}usr/lib/snapd/snap-confine"
|
||||
[ -f "$sc3888_path" ] || return 0
|
||||
|
||||
sc3888_priv=""
|
||||
if [ -u "$sc3888_path" ]; then
|
||||
sc3888_priv="setuid"
|
||||
elif command -v getcap >/dev/null 2>&1 &&
|
||||
getcap "$sc3888_path" 2>/dev/null | grep -q 'cap_sys_admin'; then
|
||||
sc3888_priv="file capabilities"
|
||||
fi
|
||||
[ -n "$sc3888_priv" ] || return 0
|
||||
|
||||
sc3888_release="$(sed -n 's/^VERSION_ID="\{0,1\}\([0-9][0-9.]*\)"\{0,1\}$/\1/p' "${sc3888_root}etc/os-release" 2>/dev/null | head -n 1)"
|
||||
sc3888_version=""
|
||||
if command -v dpkg-query >/dev/null 2>&1; then
|
||||
sc3888_version="$(dpkg-query --admindir="${sc3888_root}var/lib/dpkg" -W -f='$''{Status} $''{Version}\n' snapd 2>/dev/null | awk 'NR==1 && $1=="install" && $2=="ok" && $3=="installed" {print $4; exit}')"
|
||||
fi
|
||||
sc3888_fixed=""
|
||||
case "$sc3888_release" in
|
||||
16.04) sc3888_fixed="2.61.4ubuntu0.16.04.1+esm2" ;;
|
||||
18.04) sc3888_fixed="2.61.4ubuntu0.18.04.1+esm2" ;;
|
||||
20.04) sc3888_fixed="2.67.1+20.04ubuntu1~esm1" ;;
|
||||
22.04) sc3888_fixed="2.73+ubuntu22.04.1" ;;
|
||||
24.04) sc3888_fixed="2.73+ubuntu24.04.2" ;;
|
||||
25.10) sc3888_fixed="2.73+ubuntu25.10.1" ;;
|
||||
26.04) sc3888_fixed="2.74.1+ubuntu26.04.3" ;;
|
||||
esac
|
||||
sc3888_state="unknown"
|
||||
# Compare complete Debian versions only for Ubuntu packages on a known release.
|
||||
# A local rebuild, epoch, or unexpected suffix can contain a backported fix.
|
||||
if grep -q '^ID=\("\{0,1\}\)ubuntu\1$' "${sc3888_root}etc/os-release" 2>/dev/null &&
|
||||
[ -n "$sc3888_version" ] && [ -n "$sc3888_fixed" ] &&
|
||||
command -v dpkg >/dev/null 2>&1; then
|
||||
case "$sc3888_version" in
|
||||
*"$sc3888_release"*|*"ubuntu${sc3888_release}"*)
|
||||
if dpkg --compare-versions "$sc3888_version" ge "$sc3888_fixed"; then
|
||||
sc3888_state="at-or-above-fix"
|
||||
else
|
||||
sc3888_state="below-fix"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Inspect the usual effective tmp.conf by directory precedence. Other rule
|
||||
# names and runtime unit state need a full tmpfiles/systemd evaluation.
|
||||
sc3888_rule=""
|
||||
for sc3888_dir in etc run usr/local/lib usr/lib lib; do
|
||||
if [ -r "${sc3888_root}${sc3888_dir}/tmpfiles.d/tmp.conf" ]; then
|
||||
sc3888_rule="${sc3888_root}${sc3888_dir}/tmpfiles.d/tmp.conf"
|
||||
break
|
||||
fi
|
||||
done
|
||||
sc3888_age=""
|
||||
if [ -n "$sc3888_rule" ]; then
|
||||
sc3888_age="$(awk 'NR > 100 {exit} $1 ~ /^[dDqQ][!~+=-]*$/ && $2 == "/tmp" && $6 ~ /^[0-9]+[smhdwMy]*$/ {print $6; exit}' "$sc3888_rule" 2>/dev/null)"
|
||||
fi
|
||||
sc3888_timer="unit not found"
|
||||
if [ -e "${sc3888_root}usr/lib/systemd/system/systemd-tmpfiles-clean.timer" ] ||
|
||||
[ -e "${sc3888_root}lib/systemd/system/systemd-tmpfiles-clean.timer" ]; then
|
||||
sc3888_timer="unit present; runtime activation unverified"
|
||||
fi
|
||||
if [ -e "${sc3888_root}etc/systemd/system/timers.target.wants/systemd-tmpfiles-clean.timer" ]; then
|
||||
sc3888_timer="enable link present; runtime activation unverified"
|
||||
fi
|
||||
|
||||
print_3title "snap-confine/tmpfiles race prerequisites (CVE-2026-3888)" "T1068"
|
||||
print_info "https://ubuntu.com/security/CVE-2026-3888"
|
||||
echo "snap-confine: $sc3888_priv ($sc3888_path)"
|
||||
echo "snapd package: ${sc3888_version:-unknown}; Ubuntu release: ${sc3888_release:-unknown}"
|
||||
case "$sc3888_state" in
|
||||
at-or-above-fix) echo "Package version is at or above the Ubuntu advisory fix ($sc3888_fixed); verify local package provenance" ;;
|
||||
below-fix) echo "Package version is below the Ubuntu advisory fix ($sc3888_fixed); verify backports" ;;
|
||||
*) echo "Package fix status unclassified; verify distribution patches and package provenance" ;;
|
||||
esac
|
||||
if [ -n "$sc3888_age" ]; then
|
||||
echo "/tmp cleanup rule: age $sc3888_age ($sc3888_rule)"
|
||||
else
|
||||
echo "/tmp cleanup rule: not confirmed in standard tmp.conf locations"
|
||||
fi
|
||||
echo "systemd-tmpfiles-clean.timer: $sc3888_timer"
|
||||
if [ "$sc3888_state" = "below-fix" ] && [ -n "$sc3888_age" ] &&
|
||||
[ "$sc3888_timer" != "unit not found" ]; then
|
||||
echo "Potential CVE-2026-3888 prerequisites observed; timer activity, snap layout, and backports remain unverified" | sed -${E} "s,.*,${SED_RED_YELLOW},"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# Title: Function - checkSystemdWritableDropins
|
||||
# ID: checkSystemdWritableDropins
|
||||
# Author: PEASS-ng contributors
|
||||
# Last Update: 08-10-2026
|
||||
# Description: Passively identify writable systemd unit and service drop-in directories.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1543.002
|
||||
# Functions Used: print_3title, print_info
|
||||
# Global Variables: $E, $IAMROOT, $SED_RED_YELLOW
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $sdwdd_system_dir, $sdwdd_runtime_dir, $sdwdd_dir, $sdwdd_unit, $sdwdd_checked, $sdwdd_properties, $sdwdd_property, $sdwdd_active, $sdwdd_loaded, $sdwdd_user, $sdwdd_user_seen, $sdwdd_dynamic_user, $sdwdd_root_dir, $sdwdd_root_image, $sdwdd_private_users, $sdwdd_found
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
checkSystemdWritableDropins() {
|
||||
[ "$(uname -s 2>/dev/null)" = "Linux" ] || return 0
|
||||
[ -z "${IAMROOT:-}" ] || return 0
|
||||
[ "$(id -u 2>/dev/null)" != "0" ] || return 0
|
||||
command -v systemctl >/dev/null 2>&1 || return 0
|
||||
|
||||
# Optional directory arguments let tests use fixtures without touching
|
||||
# the host's systemd tree. Normal calls inspect fixed systemd paths only.
|
||||
sdwdd_system_dir=${1:-/etc/systemd/system}
|
||||
sdwdd_runtime_dir=${2:-/run/systemd/system}
|
||||
[ -d "$sdwdd_system_dir" ] || return 0
|
||||
|
||||
# A user may create a new unit even without directory read permission.
|
||||
# This is only a lead until a privileged manager loads and starts that unit.
|
||||
if [ -d "$sdwdd_runtime_dir" ] && [ ! -L "$sdwdd_system_dir" ] &&
|
||||
[ -w "$sdwdd_system_dir" ] && [ -x "$sdwdd_system_dir" ]; then
|
||||
print_3title "Writable systemd system unit directory" "T1543.002"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/interesting-files-permissions/write-to-root.html#service--socket-files"
|
||||
echo "$sdwdd_system_dir is writable and searchable; verify effective sudo start/restart policy, unit loading, and service identity before treating a new unit as executable" |
|
||||
sed -"${E}" "s,.*,${SED_RED_YELLOW},"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
command -v timeout >/dev/null 2>&1 || return 0
|
||||
sdwdd_checked=0
|
||||
sdwdd_found=""
|
||||
|
||||
for sdwdd_dir in "$sdwdd_system_dir"/*.service.d; do
|
||||
[ -d "$sdwdd_dir" ] && [ ! -L "$sdwdd_dir" ] || continue
|
||||
[ -w "$sdwdd_dir" ] && [ -x "$sdwdd_dir" ] || continue
|
||||
|
||||
# A writable directory can accept a new .conf even if it is empty and
|
||||
# every existing unit file is protected. Bound systemctl to 20 candidates.
|
||||
[ "$sdwdd_checked" -lt 20 ] || break
|
||||
sdwdd_checked=$((sdwdd_checked + 1))
|
||||
sdwdd_unit=${sdwdd_dir##*/}
|
||||
sdwdd_unit=${sdwdd_unit%.d}
|
||||
sdwdd_properties=$(timeout -k 1 2 systemctl show \
|
||||
-p LoadState -p ActiveState -p User -p DynamicUser \
|
||||
-p RootDirectory -p RootImage -p PrivateUsers \
|
||||
-- "$sdwdd_unit" 2>/dev/null) || continue
|
||||
|
||||
sdwdd_active=""
|
||||
sdwdd_loaded=""
|
||||
sdwdd_user=""
|
||||
sdwdd_user_seen=""
|
||||
sdwdd_dynamic_user=""
|
||||
sdwdd_root_dir=""
|
||||
sdwdd_root_image=""
|
||||
sdwdd_private_users=""
|
||||
while IFS= read -r sdwdd_property; do
|
||||
case "$sdwdd_property" in
|
||||
ActiveState=*) sdwdd_active=${sdwdd_property#ActiveState=} ;;
|
||||
LoadState=*) sdwdd_loaded=${sdwdd_property#LoadState=} ;;
|
||||
User=*) sdwdd_user=${sdwdd_property#User=}; sdwdd_user_seen=1 ;;
|
||||
DynamicUser=*) sdwdd_dynamic_user=${sdwdd_property#DynamicUser=} ;;
|
||||
RootDirectory=*) sdwdd_root_dir=${sdwdd_property#RootDirectory=} ;;
|
||||
RootImage=*) sdwdd_root_image=${sdwdd_property#RootImage=} ;;
|
||||
PrivateUsers=*) sdwdd_private_users=${sdwdd_property#PrivateUsers=} ;;
|
||||
esac
|
||||
done <<EOF
|
||||
$sdwdd_properties
|
||||
EOF
|
||||
|
||||
[ "$sdwdd_loaded" = "loaded" ] && [ "$sdwdd_active" = "active" ] || continue
|
||||
[ -n "$sdwdd_user_seen" ] || continue
|
||||
case "$sdwdd_user" in ""|root|0) ;; *) continue ;; esac
|
||||
[ "$sdwdd_dynamic_user" != "yes" ] || continue
|
||||
[ -z "$sdwdd_root_dir" ] && [ -z "$sdwdd_root_image" ] || continue
|
||||
[ "$sdwdd_private_users" != "yes" ] || continue
|
||||
|
||||
if [ -z "$sdwdd_found" ]; then
|
||||
print_3title "Writable systemd drop-in directories for active root services" "T1543.002"
|
||||
print_info "https://book.hacktricks.wiki/en/linux-hardening/linux-basics/linux-privilege-escalation/index.html#services"
|
||||
sdwdd_found=1
|
||||
fi
|
||||
echo "$sdwdd_unit: $sdwdd_dir (a new .conf could run on a later reload and restart)" |
|
||||
sed -"${E}" "s,.*,${SED_RED_YELLOW},"
|
||||
ls -ld "$sdwdd_dir" 2>/dev/null
|
||||
done
|
||||
|
||||
[ -z "$sdwdd_found" ] || echo ""
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
# Title: Interesting Perms Files - check_critial_root_path
|
||||
# ID: check_critial_root_path
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 22-08-2023
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Check if you have write privileges over critical root paths
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Functions Used:
|
||||
# Global Variables: $USER, $wgroups
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: folder_path
|
||||
# Generated Global Variables: folder_path, folder_writable_files, folder_nonroot_files
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
check_critial_root_path(){
|
||||
folder_path="$1"
|
||||
if [ -w "$folder_path" ]; then echo "You have write privileges over $folder_path" | sed -${E} "s,.*,${SED_RED_YELLOW},"; fi
|
||||
if [ "$(find $folder_path -type f '(' '(' -user $USER ')' -or '(' -perm -o=w ')' -or '(' -perm -g=w -and '(' $wgroups ')' ')' ')' 2>/dev/null)" ]; then echo "You have write privileges over $(find $folder_path -type f '(' '(' -user $USER ')' -or '(' -perm -o=w ')' -or '(' -perm -g=w -and '(' $wgroups ')' ')' ')')" | sed -${E} "s,.*,${SED_RED_YELLOW},"; fi
|
||||
if [ "$(find $folder_path -type f -not -user root 2>/dev/null)" ]; then echo "The following files aren't owned by root: $(find $folder_path -type f -not -user root 2>/dev/null)"; fi
|
||||
}
|
||||
folder_writable_files=$(find "$folder_path" -type f '(' '(' -user "$USER" ')' -or '(' -perm -o=w ')' -or '(' -perm -g=w -and '(' $wgroups ')' ')' ')' 2>/dev/null) || :
|
||||
if [ "$folder_writable_files" ]; then echo "You have write privileges over $folder_writable_files" | sed -${E} "s,.*,${SED_RED_YELLOW},"; fi
|
||||
folder_nonroot_files=$(find "$folder_path" -type f -not -user root 2>/dev/null) || :
|
||||
if [ "$folder_nonroot_files" ]; then echo "The following files aren't owned by root: $folder_nonroot_files"; fi
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
# Title: Function - check_pg_basebackup_boundary
|
||||
# ID: check_pg_basebackup_boundary
|
||||
# Author: PEASS-ng
|
||||
# Last Update: 2026-10-09
|
||||
# Description: Correlate bounded root cron backups with writable PostgreSQL data and executable output.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Functions Used: pgbb_mount_options
|
||||
# Global Variables: $PG_BASEBACKUP_DESTS
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $pgbb_command, $pgbb_count, $pgbb_cron, $pgbb_depth, $pgbb_dest, $pgbb_dest_meta, $pgbb_file, $pgbb_file_count, $pgbb_host, $pgbb_line, $pgbb_line_count, $pgbb_meta, $pgbb_opts, $pgbb_parent, $pgbb_path, $pgbb_size, $pgbb_source, $pgbb_source_meta, $pgbb_total, $pgbb_wrapper
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
pgbb_metadata() {
|
||||
stat -c '%u %a' "$1" 2>/dev/null || stat -f '%u %Lp' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
pgbb_literal_path() {
|
||||
[ "${#1}" -le 512 ] || return 1
|
||||
case "$1" in
|
||||
/*) case "$1" in *[!a-zA-Z0-9_./+-]*|*//*|*/../*|*/./*|*/..|*/.) return 1;; esac ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
pgbb_path=$1
|
||||
pgbb_depth=0
|
||||
while [ "$pgbb_path" != / ]; do
|
||||
pgbb_depth=$((pgbb_depth + 1))
|
||||
[ "$pgbb_depth" -le 32 ] || return 1
|
||||
pgbb_path=${pgbb_path%/*}
|
||||
[ -n "$pgbb_path" ] || pgbb_path=/
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
pgbb_no_symlinks() {
|
||||
pgbb_path=$1
|
||||
while [ "$pgbb_path" != / ] && [ "${pgbb_path%/}" != "$pgbb_path" ]; do pgbb_path=${pgbb_path%/}; done
|
||||
while [ "$pgbb_path" != / ]; do
|
||||
[ ! -L "$pgbb_path" ] || return 1
|
||||
pgbb_path=${pgbb_path%/*}
|
||||
[ -n "$pgbb_path" ] || pgbb_path=/
|
||||
done
|
||||
}
|
||||
|
||||
pgbb_parse_command() (
|
||||
case "$1" in
|
||||
*\$*|*\`*|*\;*|*\&*|*\|*|*\(*|*\)*|*\<*|*\>*|*\\*|*\"*|*\'*|*\**|*\?*|*\[*|*\]*) return 1 ;;
|
||||
esac
|
||||
set -f
|
||||
set -- $1
|
||||
[ "$1" = exec ] && shift
|
||||
case "$1" in /usr/bin/pg_basebackup|/usr/local/bin/pg_basebackup|pg_basebackup) shift ;; *) return 1 ;; esac
|
||||
pgbb_dest=
|
||||
pgbb_host=
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-D|--pgdata) shift; [ "$#" -gt 0 ] || return 1; [ -z "$pgbb_dest" ] || return 1; pgbb_dest=$1 ;;
|
||||
--pgdata=*) [ -z "$pgbb_dest" ] || return 1; pgbb_dest=${1#*=} ;;
|
||||
-h|--host) shift; [ "$#" -gt 0 ] || return 1; [ -z "$pgbb_host" ] || return 1; pgbb_host=$1 ;;
|
||||
--host=*) [ -z "$pgbb_host" ] || return 1; pgbb_host=${1#*=} ;;
|
||||
-d|--dbname|--dbname=*) return 1 ;;
|
||||
-F|--format) shift; [ "$1" = p ] || [ "$1" = plain ] || return 1 ;;
|
||||
--format=plain|--format=p) ;;
|
||||
-Fp) ;;
|
||||
-F*|--format=*) return 1 ;;
|
||||
-D*) return 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
[ -n "$pgbb_dest" ] || return 1
|
||||
pgbb_literal_path "$pgbb_dest" || return 1
|
||||
pgbb_literal_path "$pgbb_host" || return 1
|
||||
while [ "$pgbb_dest" != / ] && [ "${pgbb_dest%/}" != "$pgbb_dest" ]; do pgbb_dest=${pgbb_dest%/}; done
|
||||
[ "$pgbb_dest" != / ] || return 1
|
||||
printf '%s\n' "$pgbb_dest"
|
||||
)
|
||||
|
||||
pgbb_source_from_process() {
|
||||
ps -eo args 2>/dev/null | awk '
|
||||
NR > 256 { exit }
|
||||
{ for (i=1; i<NF-1; i++) if ($i ~ /(^|\/)postgres$/ && $(i+1)=="-D" && $(i+2) ~ /^\/[a-zA-Z0-9_.\/+\-]+$/) { found++; source=$(i+2) } }
|
||||
END { if (found == 1) print source }
|
||||
'
|
||||
}
|
||||
|
||||
pgbb_review_job() {
|
||||
pgbb_cron=$1
|
||||
pgbb_command=$2
|
||||
pgbb_source=$3
|
||||
pgbb_dest=$(pgbb_parse_command "$pgbb_command") || {
|
||||
printf 'PostgreSQL base-backup review lead: %s: literal plain destination could not be established\n' "$pgbb_cron"
|
||||
return 0
|
||||
}
|
||||
pgbb_literal_path "$pgbb_source" || return 0
|
||||
pgbb_no_symlinks "$pgbb_source" || return 0
|
||||
pgbb_no_symlinks "$pgbb_dest" || {
|
||||
printf 'PostgreSQL base-backup review lead: %s: destination contains a symlink\n' "$pgbb_cron"
|
||||
return 0
|
||||
}
|
||||
[ -d "$pgbb_source" ] && [ -w "$pgbb_source" ] && [ -x "$pgbb_source" ] || return 0
|
||||
pgbb_source_meta=$(pgbb_metadata "$pgbb_source") || return 0
|
||||
pgbb_parent=$pgbb_dest
|
||||
if [ -d "$pgbb_dest" ]; then
|
||||
[ -x "$pgbb_dest" ] && [ ! -w "$pgbb_dest" ] || return 0
|
||||
pgbb_dest_meta=$(pgbb_metadata "$pgbb_dest") || return 0
|
||||
elif [ ! -e "$pgbb_dest" ]; then
|
||||
pgbb_parent=${pgbb_dest%/*}
|
||||
[ -n "$pgbb_parent" ] || pgbb_parent=/
|
||||
[ -d "$pgbb_parent" ] && [ -x "$pgbb_parent" ] && [ ! -w "$pgbb_parent" ] || return 0
|
||||
pgbb_dest_meta=$(pgbb_metadata "$pgbb_parent") || return 0
|
||||
[ "${pgbb_dest_meta%% *}" = 0 ] || return 0
|
||||
printf 'PostgreSQL base-backup review lead: %s: output does not exist; resulting directory mode cannot be confirmed\n' "$pgbb_cron"
|
||||
return 0
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
[ "${pgbb_dest_meta%% *}" = 0 ] || return 0
|
||||
pgbb_path=$pgbb_parent
|
||||
while [ "$pgbb_path" != / ]; do
|
||||
[ -x "$pgbb_path" ] || return 0
|
||||
pgbb_path=${pgbb_path%/*}
|
||||
[ -n "$pgbb_path" ] || pgbb_path=/
|
||||
done
|
||||
pgbb_opts=$(pgbb_mount_options "$pgbb_dest")
|
||||
case ",$pgbb_opts," in
|
||||
,,) printf 'PostgreSQL base-backup review lead: %s: destination mount options unknown\n' "$pgbb_cron"; return 0 ;;
|
||||
*,nosuid,*|*,noexec,*|*,ro,*) return 0 ;;
|
||||
esac
|
||||
case ",$pgbb_opts," in *,rw,*) : ;; *) return 0 ;; esac
|
||||
printf 'PostgreSQL base-backup review candidate: %s\n' "$pgbb_cron"
|
||||
printf ' command: %s\n writable source: %s (uid mode %s)\n root output: %s (uid mode %s; mount %s)\n' "$pgbb_command" "$pgbb_source" "$pgbb_source_meta" "$pgbb_dest" "$pgbb_dest_meta" "$pgbb_opts"
|
||||
printf ' reason: root plain-format backup may copy lower-trust files and set-ID modes into an executable path; verify mode preservation\n'
|
||||
PG_BASEBACKUP_DESTS="${PG_BASEBACKUP_DESTS}${PG_BASEBACKUP_DESTS:+
|
||||
}$pgbb_dest"
|
||||
}
|
||||
|
||||
pgbb_scan_file() {
|
||||
pgbb_file=$1
|
||||
pgbb_source=$2
|
||||
[ -f "$pgbb_file" ] && [ -r "$pgbb_file" ] && [ ! -L "$pgbb_file" ] && [ ! -w "$pgbb_file" ] || return 0
|
||||
pgbb_meta=$(pgbb_metadata "$pgbb_file") || return 0
|
||||
[ "${pgbb_meta%% *}" = 0 ] || return 0
|
||||
pgbb_size=$(stat -c %s "$pgbb_file" 2>/dev/null || stat -f %z "$pgbb_file" 2>/dev/null) || return 0
|
||||
case "$pgbb_size" in ''|*[!0-9]*) return 0 ;; esac
|
||||
[ "$pgbb_size" -le 65536 ] || return 0
|
||||
pgbb_line_count=0
|
||||
while IFS= read -r pgbb_line; do
|
||||
pgbb_line_count=$((pgbb_line_count + 1))
|
||||
[ "$pgbb_line_count" -le 200 ] || break
|
||||
[ "${#pgbb_line}" -le 1024 ] || continue
|
||||
case "$pgbb_line" in ''|\#*|[[:space:]]\#*) continue ;; esac
|
||||
pgbb_total=$((pgbb_total + 1))
|
||||
[ "$pgbb_total" -le 400 ] || return 0
|
||||
set -f
|
||||
set -- $pgbb_line
|
||||
set +f
|
||||
case "$pgbb_file" in
|
||||
*/cron.d/*|/etc/crontab) [ "$#" -ge 7 ] && [ "$6" = root ] || continue; shift 6 ;;
|
||||
*/crontabs/root|*/cron/root) [ "$#" -ge 6 ] || continue; shift 5 ;;
|
||||
/etc/anacrontab) [ "$#" -ge 4 ] || continue; shift 3 ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
pgbb_command="$*"
|
||||
case "$pgbb_command" in *pg_basebackup*) : ;; *)
|
||||
set -- $pgbb_command
|
||||
case "$1" in /bin/sh|/bin/bash|/usr/bin/sh|/usr/bin/bash) shift ;; esac
|
||||
[ "$#" -eq 1 ] || continue
|
||||
pgbb_wrapper=$1
|
||||
pgbb_literal_path "$pgbb_wrapper" || continue
|
||||
pgbb_no_symlinks "$pgbb_wrapper" || continue
|
||||
[ -f "$pgbb_wrapper" ] && [ -r "$pgbb_wrapper" ] && [ ! -L "$pgbb_wrapper" ] && [ ! -w "$pgbb_wrapper" ] || continue
|
||||
pgbb_meta=$(pgbb_metadata "$pgbb_wrapper") || continue
|
||||
[ "${pgbb_meta%% *}" = 0 ] || continue
|
||||
pgbb_size=$(stat -c %s "$pgbb_wrapper" 2>/dev/null || stat -f %z "$pgbb_wrapper" 2>/dev/null) || continue
|
||||
case "$pgbb_size" in ''|*[!0-9]*) continue ;; esac
|
||||
[ "$pgbb_size" -le 8192 ] || continue
|
||||
pgbb_command=$(head -c 8192 "$pgbb_wrapper" 2>/dev/null | awk 'NR<=80 && /(^|[[:space:]\/])pg_basebackup([[:space:]]|$)/ { print; exit }')
|
||||
[ -n "$pgbb_command" ] || continue
|
||||
;;
|
||||
esac
|
||||
pgbb_count=$((pgbb_count + 1))
|
||||
[ "$pgbb_count" -le 20 ] || return 0
|
||||
pgbb_review_job "$pgbb_file:$pgbb_line_count" "$pgbb_command" "$pgbb_source"
|
||||
done < "$pgbb_file"
|
||||
}
|
||||
|
||||
pgbb_scan_files() {
|
||||
pgbb_source=$1
|
||||
shift
|
||||
pgbb_count=0
|
||||
pgbb_total=0
|
||||
pgbb_file_count=0
|
||||
for pgbb_file do
|
||||
pgbb_file_count=$((pgbb_file_count + 1))
|
||||
[ "$pgbb_file_count" -le 64 ] || break
|
||||
pgbb_scan_file "$pgbb_file" "$pgbb_source"
|
||||
[ "$pgbb_count" -lt 20 ] && [ "$pgbb_total" -lt 400 ] || break
|
||||
done
|
||||
}
|
||||
|
||||
check_pg_basebackup_boundary() {
|
||||
[ "$(id -u 2>/dev/null)" != 0 ] || return 0
|
||||
pgbb_source=$(pgbb_source_from_process)
|
||||
[ -n "$pgbb_source" ] || return 0
|
||||
pgbb_scan_files "$pgbb_source" /etc/crontab /var/spool/cron/crontabs/root /var/spool/cron/root /etc/anacrontab /etc/cron.d/*
|
||||
}
|
||||
@@ -5,10 +5,10 @@
|
||||
# Description: Report risky placement, ownership, and parent permissions for SUID and SGID files.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Functions Used:
|
||||
# Global Variables: $E, $IAMROOT, $ROOT_FOLDER, $SED_RED, $SED_RED_YELLOW
|
||||
# Functions Used: pgbb_mount_options
|
||||
# Global Variables: $E, $IAMROOT, $PG_BASEBACKUP_DESTS, $ROOT_FOLDER, $SED_RED, $SED_RED_YELLOW
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $priv_file_name, $priv_file_owner, $priv_file_parent, $priv_file_type, $priv_file_unusual
|
||||
# Generated Global Variables: $priv_file_name, $priv_file_owner, $priv_file_parent, $priv_file_type, $priv_file_unusual, $priv_backup_dest, $priv_backup_opts
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
@@ -26,6 +26,23 @@ check_privileged_file_location() {
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$priv_file_owner" = root ] && [ -f "$priv_file_name" ] && [ ! -L "$priv_file_name" ] && [ -x "$priv_file_name" ]; then
|
||||
while IFS= read -r priv_backup_dest; do
|
||||
[ -n "$priv_backup_dest" ] || continue
|
||||
case "$priv_file_name" in
|
||||
"$priv_backup_dest"/*)
|
||||
priv_backup_opts=$(pgbb_mount_options "$priv_file_name")
|
||||
case ",$priv_backup_opts," in ,,|*,nosuid,*|*,noexec,*) continue ;; esac
|
||||
echo "$priv_file_type root-owned file under a reviewed PostgreSQL backup destination: $priv_file_name" | sed -${E} "s,.*,${SED_RED_YELLOW},"
|
||||
priv_file_unusual=1
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done <<EOF_PRIV_BACKUP_DESTS
|
||||
$PG_BASEBACKUP_DESTS
|
||||
EOF_PRIV_BACKUP_DESTS
|
||||
fi
|
||||
|
||||
priv_file_parent="$(dirname "$priv_file_name")"
|
||||
if ! [ "$IAMROOT" ] && [ -d "$priv_file_parent" ] && [ -w "$priv_file_parent" ] && [ -x "$priv_file_parent" ] && ! [ -k "$priv_file_parent" ]; then
|
||||
echo "You can replace entries in the $priv_file_type file's parent directory: $priv_file_parent" | sed -${E} "s,.*,${SED_RED_YELLOW},"
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# Title: Function - check_sudo_terraform_override
|
||||
# ID: check_sudo_terraform_override
|
||||
# Author: PEASS-ng contributors
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Passively correlate a root Terraform sudo rule with a caller-writable provider override.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Functions Used:
|
||||
# Global Variables: $HOME, $ROOT_FOLDER
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $tf_policy, $tf_rules, $tf_config, $tf_config_size, $tf_rule_dir, $tf_source, $tf_override_dir, $tf_provider_name, $tf_binary, $tf_binary_access, $tf_project_file, $tf_project_size, $tf_project_count, $tf_source_seen, $tf_walk, $tf_rest, $tf_part
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
tf_plain_path() {
|
||||
case "$1" in /*) ;; *) return 1 ;; esac
|
||||
case "$1" in *'//'*|*'/./'*|*'/../'*|*'/.'|*'/..') return 1 ;; esac
|
||||
tf_walk=
|
||||
tf_rest=${1#/}
|
||||
while [ "$tf_rest" ]; do
|
||||
tf_part=${tf_rest%%/*}
|
||||
tf_walk="$tf_walk/$tf_part"
|
||||
[ ! -L "$tf_walk" ] || return 1
|
||||
case "$tf_rest" in
|
||||
*/*) tf_rest=${tf_rest#*/} ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
check_sudo_terraform_override() { (
|
||||
[ -z "$ROOT_FOLDER" ] || [ "$ROOT_FOLDER" = / ] || return 0
|
||||
[ -n "$HOME" ] && [ -d "$HOME" ] || return 0
|
||||
# A selected alternate config, or a home-changing Defaults entry, makes the
|
||||
# caller's HOME config an unreliable candidate for this narrow check.
|
||||
env | grep -q '^TF_CLI_CONFIG_FILE=.' && return 0
|
||||
tf_policy=$(printf '%s\n%s\n%s\n' "$1" "$2" "$3" | sed -n '1,400p;401q')
|
||||
printf '%s\n' "$tf_policy" | grep -Eq '(^|[[:space:],])!env_reset([[:space:],]|$)' || return 0
|
||||
printf '%s\n' "$tf_policy" | grep -Eq '(^|[[:space:],])(always_set_home|set_home)([[:space:],]|$)' && return 0
|
||||
|
||||
# Parse only an exact root-capable terraform -chdir=... apply command.
|
||||
# The output is a review clue; sudo's effective policy and HOME remain to be verified.
|
||||
tf_rules=$(printf '%s\n' "$tf_policy" | awk '
|
||||
/^[[:space:]]*\([^)]*\)[[:space:]]/ {
|
||||
line = $0
|
||||
runas = line
|
||||
sub(/^[^(]*\(/, "", runas); sub(/\).*/, "", runas)
|
||||
split(runas, parts, ":")
|
||||
users = parts[1]
|
||||
if (users ~ /!root/ || users !~ /(^|[[:space:],])(root|ALL)([[:space:],]|$)/) next
|
||||
sub(/^.*\)[[:space:]]*/, "", line)
|
||||
n = split(line, commands, /,[[:space:]]*/)
|
||||
for (i = 1; i <= n; i++) {
|
||||
command = commands[i]
|
||||
sub(/^[[:space:]]*/, "", command)
|
||||
while (command ~ /^(NOPASSWD|PASSWD|SETENV|NOSETENV):[[:space:]]*/)
|
||||
sub(/^[A-Z_]+:[[:space:]]*/, "", command)
|
||||
sub(/[[:space:]]*$/, "", command)
|
||||
if (command ~ /^\/[A-Za-z0-9_.\/-]*\/terraform -chdir=\/[A-Za-z0-9_.\/-]+ apply$/ &&
|
||||
command !~ /\/\.\.?\// && ++count <= 4) {
|
||||
sub(/^.* -chdir=/, "", command); sub(/ apply$/, "", command)
|
||||
if (!seen[command]++) print command
|
||||
}
|
||||
}
|
||||
}
|
||||
')
|
||||
[ -n "$tf_rules" ] || return 0
|
||||
|
||||
tf_config="$HOME/.terraformrc"
|
||||
tf_plain_path "$tf_config" || return 0
|
||||
[ -f "$tf_config" ] && [ -r "$tf_config" ] && [ -w "$tf_config" ] && [ ! -L "$tf_config" ] || return 0
|
||||
tf_config_size=$(stat -c %s "$tf_config" 2>/dev/null) ||
|
||||
tf_config_size=$(stat -f %z "$tf_config" 2>/dev/null) || return 0
|
||||
case "$tf_config_size" in ''|*[!0-9]*) return 0 ;; esac
|
||||
[ "$tf_config_size" -le 32768 ] || return 0
|
||||
|
||||
# Only report literal source-to-directory mappings inside dev_overrides.
|
||||
# Never print unrelated CLI configuration, which may contain credentials.
|
||||
sed -n '1,256p;257q' "$tf_config" 2>/dev/null | awk '
|
||||
/^[[:space:]]*provider_installation[[:space:]]*\{/ { installation = 1; next }
|
||||
installation && /^[[:space:]]*dev_overrides[[:space:]]*\{/ { overrides = 1; next }
|
||||
overrides && /^[[:space:]]*\}/ { overrides = 0; next }
|
||||
installation && /^[[:space:]]*\}/ { installation = 0; next }
|
||||
overrides && /^[[:space:]]*"[A-Za-z0-9._\/-]+"[[:space:]]*=[[:space:]]*"\/[A-Za-z0-9._\/-]+"[[:space:]]*(#.*)?$/ {
|
||||
split($0, fields, "\"")
|
||||
if (++count <= 8) print fields[2] "|" fields[4]
|
||||
}
|
||||
' | while IFS='|' read -r tf_source tf_override_dir; do
|
||||
tf_plain_path "$tf_override_dir" || continue
|
||||
[ -d "$tf_override_dir" ] && [ -w "$tf_override_dir" ] &&
|
||||
[ -x "$tf_override_dir" ] && [ ! -L "$tf_override_dir" ] || continue
|
||||
tf_provider_name=${tf_source##*/}
|
||||
tf_binary="$tf_override_dir/terraform-provider-$tf_provider_name"
|
||||
tf_plain_path "$tf_binary" || continue
|
||||
if [ -e "$tf_binary" ] || [ -L "$tf_binary" ]; then
|
||||
[ -f "$tf_binary" ] && [ -w "$tf_binary" ] &&
|
||||
[ -x "$tf_binary" ] && [ ! -L "$tf_binary" ] || continue
|
||||
tf_binary_access='caller-writable executable exists'
|
||||
else
|
||||
tf_binary_access='caller can create executable in override directory'
|
||||
fi
|
||||
printf '%s\n' "$tf_rules" | while IFS= read -r tf_rule_dir; do
|
||||
tf_plain_path "$tf_rule_dir" || continue
|
||||
[ -d "$tf_rule_dir" ] && [ ! -L "$tf_rule_dir" ] || continue
|
||||
tf_source_seen=
|
||||
tf_project_count=0
|
||||
for tf_project_file in "$tf_rule_dir"/*.tf; do
|
||||
tf_plain_path "$tf_project_file" || continue
|
||||
[ -f "$tf_project_file" ] && [ ! -L "$tf_project_file" ] || continue
|
||||
tf_project_count=$((tf_project_count + 1))
|
||||
[ "$tf_project_count" -le 8 ] || break
|
||||
tf_project_size=$(stat -c %s "$tf_project_file" 2>/dev/null) ||
|
||||
tf_project_size=$(stat -f %z "$tf_project_file" 2>/dev/null) || continue
|
||||
case "$tf_project_size" in ''|*[!0-9]*) continue ;; esac
|
||||
[ "$tf_project_size" -le 32768 ] || continue
|
||||
if sed -n '1,256p;257q' "$tf_project_file" 2>/dev/null | awk -v wanted="$tf_source" '
|
||||
/^[[:space:]]*source[[:space:]]*=/ {
|
||||
split($0, fields, "\"")
|
||||
if (fields[2] == wanted) found = 1
|
||||
}
|
||||
END { exit !found }
|
||||
'; then
|
||||
tf_source_seen=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ "$tf_source_seen" ] || continue
|
||||
printf 'Terraform sudo/provider override review candidate: root rule uses -chdir=%s apply; current HOME config %s is caller-writable\n' "$tf_rule_dir" "$tf_config"
|
||||
printf 'Provider source %s matches a bounded .tf file; override directory %s; %s: %s\n' "$tf_source" "$tf_override_dir" "$tf_binary_access" "$tf_binary"
|
||||
ls -ld "$tf_config" "$tf_override_dir" "$tf_binary" 2>/dev/null
|
||||
printf 'Verify effective sudo HOME, policy, provider loading, and execution context; this is candidate evidence, not proven root privilege.\n'
|
||||
done
|
||||
done
|
||||
); }
|
||||
@@ -0,0 +1,28 @@
|
||||
# Title: Functions - lp_suse_numeric_version_lt
|
||||
# ID: lp_suse_numeric_version_lt
|
||||
# Author: PEASS-ng
|
||||
# Last Update: 08-10-2026
|
||||
# Description: Compare numeric SUSE package version-release strings; return 2 when the format is unknown.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Mitre: T1068
|
||||
# Functions Used:
|
||||
# Global Variables:
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
lp_suse_numeric_version_lt() {
|
||||
case "$1" in *[!0-9.-]*|"") return 2 ;; esac
|
||||
case "$2" in *[!0-9.-]*|"") return 2 ;; esac
|
||||
awk -v installed="$1" -v fixed="$2" 'BEGIN {
|
||||
n = split(installed, a, /[.:-]/)
|
||||
m = split(fixed, b, /[.:-]/)
|
||||
for (i = 1; i <= n || i <= m; i++) {
|
||||
if (a[i] + 0 < b[i] + 0) exit 0
|
||||
if (a[i] + 0 > b[i] + 0) exit 1
|
||||
}
|
||||
exit 1
|
||||
}'
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Title: Function - lp_trusted_version_path
|
||||
# ID: lp_trusted_version_path
|
||||
# Author: PEASS-ng contributors
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Resolve configuration-supplied version probes to protected root-owned executables.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Functions Used:
|
||||
# Global Variables:
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $lp_version_path, $lp_version_uid, $lp_version_walk, $lp_version_count
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
lp_trusted_version_path() { (
|
||||
# Never execute a program solely because a readable configuration names it.
|
||||
# Execute the checked canonical path, not a replaceable symlink spelling.
|
||||
case "$1" in /*) ;; *) exit 1 ;; esac
|
||||
lp_version_path=$(readlink -f "$1" 2>/dev/null) ||
|
||||
lp_version_path=$(realpath "$1" 2>/dev/null) || exit 1
|
||||
case "$lp_version_path" in /*) ;; *) exit 1 ;; esac
|
||||
[ "${#lp_version_path}" -le 4096 ] &&
|
||||
[ -f "$lp_version_path" ] && [ -x "$lp_version_path" ] || exit 1
|
||||
lp_version_uid=$(id -u 2>/dev/null) || exit 1
|
||||
lp_version_walk=$lp_version_path
|
||||
lp_version_count=0
|
||||
while :; do
|
||||
lp_version_count=$((lp_version_count + 1))
|
||||
[ "$lp_version_count" -le 64 ] && [ ! -L "$lp_version_walk" ] || exit 1
|
||||
# Numeric ownership and symbolic mode work on GNU, BSD and BusyBox ls.
|
||||
LC_ALL=C ls -ldn "$lp_version_walk" 2>/dev/null | awk '
|
||||
NR == 1 && $3 == "0" && length($1) >= 10 &&
|
||||
substr($1, 6, 1) != "w" && substr($1, 9, 1) != "w" { safe = 1 }
|
||||
END { exit !safe }
|
||||
' || exit 1
|
||||
if [ "$lp_version_uid" != 0 ] && [ -w "$lp_version_walk" ]; then exit 1; fi
|
||||
[ "$lp_version_walk" != / ] || break
|
||||
lp_version_walk=${lp_version_walk%/*}
|
||||
[ -n "$lp_version_walk" ] || lp_version_walk=/
|
||||
done
|
||||
printf '%s\n' "$lp_version_path"
|
||||
) }
|
||||
@@ -0,0 +1,23 @@
|
||||
# Title: Function - pgbb_mount_options
|
||||
# ID: pgbb_mount_options
|
||||
# Author: PEASS-ng
|
||||
# Last Update: 2026-10-09
|
||||
# Description: Read the effective Linux mount options for a literal path.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Functions Used:
|
||||
# Global Variables:
|
||||
# Initial Functions:
|
||||
# Generated Global Variables:
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
pgbb_mount_options() {
|
||||
[ -r /proc/mounts ] || return 1
|
||||
awk -v path="$1" '
|
||||
$2 !~ /\\\\/ && (path == $2 || $2 == "/" || index(path, $2 "/") == 1) {
|
||||
if (length($2) > best) { best=length($2); opts=$4 }
|
||||
}
|
||||
END { if (best) print opts }
|
||||
' /proc/mounts
|
||||
}
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
|
||||
print_ps(){
|
||||
(ls -d /proc/*/ 2>/dev/null | while read f; do
|
||||
CMDLINE=$(cat $f/cmdline 2>/dev/null | grep -av "seds,"); #Delete my own sed processess
|
||||
(ls -d /proc/*/ 2>/dev/null | while read -r f; do
|
||||
CMDLINE=$(tr '\000' ' ' < "$f/cmdline" 2>/dev/null | grep -av "seds,"); #Delete my own sed processess
|
||||
if [ "$CMDLINE" ];
|
||||
then var USER2=ls -ld $f | awk '{print $3}'; PID=$(echo $f | cut -d "/" -f3);
|
||||
then USER2=$(ls -ld "$f" 2>/dev/null | awk '{print $3}'); PID=$(echo "$f" | cut -d "/" -f3);
|
||||
printf " %-13s %-8s %s\n" "$USER2" "$PID" "$CMDLINE";
|
||||
fi;
|
||||
done) 2>/dev/null | sort -r
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ ${NC}This tool enum and search possible misconfigurations$DG (known vulns, user,
|
||||
${YELLOW} -N${BLUE} Do not use colours
|
||||
${YELLOW} -z <N>${BLUE} Set number of threads for background checks (default: auto-detected CPU count, fallback: 2; must be >= 1)$NC"
|
||||
|
||||
while getopts ":h?asd:p:i:P:qo:T:LMwNDterVf:F:z:" opt; do
|
||||
while getopts ":h?asd:p:i:P:qno:T:LMwNDterVf:F:z:" opt; do
|
||||
case "$opt" in
|
||||
h|\?) printf "%s\n\n" "$HELP$NC"; exit 0;;
|
||||
a) FAST="";EXTRA_CHECKS="1";ONLINE_VULN_CHECKS="1";;
|
||||
|
||||
@@ -24,4 +24,4 @@ cap_setfcap:python|perl|ruby|php|node|lua|bash \
|
||||
cap_setpcap:python|perl|ruby|php|node|lua|bash \
|
||||
cap_setuid:peass{CAP_SETUID_HERE} \
|
||||
cap_setgid:peass{CAP_SETGID_HERE} \
|
||||
cap_net_raw:python|tcpdump|dumpcap|tcpflow"
|
||||
cap_net_raw:python|tcpdump|dumpcap|tcpflow|reaver"
|
||||
@@ -108,7 +108,6 @@ CVE-2021-27365 linux-iscsi pkg=linux-kernel,ver<=5.11.3,CONFIG_SLAB_FREELIST_HAR
|
||||
CVE-2021-3490 eBPF ALU32 bounds tracking for bitwise ops pkg=linux-kernel,ver>=5.7,ver<5.12,CONFIG_BPF_SYSCALL=y,sysctl:kernel.unprivileged_bpf_disabled!=1 ubuntu=20.04{kernel:5.8.0-(25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52)-*},ubuntu=21.04{kernel:5.11.0-16-*} 5 CONFIG_BPF_SYSCALL needs to be set && kernel.unprivileged_bpf_disabled != 1
|
||||
CVE-2021-3493 Ubuntu OverlayFS pkg=linux-kernel,ver>=3.13,ver<5.14,x86_64 ubuntu=(14.04|16.04|18.04|20.04|20.10) 1 Only Ubuntu is affected.
|
||||
CVE-2021-22555 Netfilter heap out-of-bounds write pkg=linux-kernel,ver>=2.6.19,ver<=5.12-rc6 ubuntu=20.04{kernel:5.8.0-*} 1 ip_tables kernel module must be loaded
|
||||
CVE-2022-0847 DirtyPipe pkg=linux-kernel,ver>=5.8,ver<=5.16.11 ubuntu=(20.04|21.04),debian=11 1
|
||||
CVE-2022-0995 watch_queue pkg=linux-kernel,ver>=5.8,ver<5.16.5,x86_64 ubuntu=21.10{kernel:5.13.0.37-generic} 1 Not 100% reliable, may need to be run a couple of times. It rare cases it may panic the kernel.
|
||||
CVE-2022-2586 nft_object UAF pkg=linux-kernel,ver>=5.12,ver<5.19,CONFIG_USER_NS=y,sysctl:kernel.unprivileged_userns_clone==1 ubuntu=(20.04){kernel:5.12.13} 1 kernel.unprivileged_userns_clone=1 required (to obtain CAP_NET_ADMIN)
|
||||
CVE-2022-32250 nft_object UAF (NFT_MSG_NEWSET) pkg=linux-kernel,ver<5.18.1,CONFIG_USER_NS=y,sysctl:kernel.unprivileged_userns_clone==1 ubuntu=(22.04){kernel:5.15.0-27-generic} 1 kernel.unprivileged_userns_clone=1 required (to obtain CAP_NET_ADMIN)
|
||||
@@ -462,10 +461,10 @@ CVE-2023-2593 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token
|
||||
CVE-2023-2598 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token from example repos; no matching rule defined in source suggesters
|
||||
CVE-2023-26083 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token from example repos; no matching rule defined in source suggesters
|
||||
CVE-2023-2612 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token from example repos; no matching rule defined in source suggesters
|
||||
CVE-2023-2640 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token from example repos; no matching rule defined in source suggesters
|
||||
CVE-2023-2640 catalog_reference_only 9999.9999.9999 0 Reference-only source token; narrow Ubuntu generic matcher is in KERNEL_CVE_DATA_26
|
||||
CVE-2023-31248 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token from example repos; no matching rule defined in source suggesters
|
||||
CVE-2023-32233 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token from example repos; no matching rule defined in source suggesters
|
||||
CVE-2023-32629 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token from example repos; no matching rule defined in source suggesters
|
||||
CVE-2023-32629 catalog_reference_only 9999.9999.9999 0 Reference-only source token; narrow Ubuntu generic matcher is in KERNEL_CVE_DATA_26
|
||||
CVE-2023-3269 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token from example repos; no matching rule defined in source suggesters
|
||||
CVE-2023-32832 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token from example repos; no matching rule defined in source suggesters
|
||||
CVE-2023-32837 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token from example repos; no matching rule defined in source suggesters
|
||||
@@ -653,6 +652,9 @@ EOF_DATA_22
|
||||
)"
|
||||
|
||||
KERNEL_CVE_DATA_23="$(cat <<'EOF_DATA_23'
|
||||
CVE-2022-0847 DirtyPipe pkg=linux-kernel,ver>=5.8,ver<5.10.102 upstream-branch-candidate 1 Upstream stable 5.10.102 fixes this branch; vendor backports and readable target-file prerequisites remain unverified
|
||||
CVE-2022-0847 DirtyPipe pkg=linux-kernel,ver>=5.11,ver<5.15.25 upstream-branch-candidate 1 Upstream stable 5.15.25 fixes this branch; vendor backports and readable target-file prerequisites remain unverified
|
||||
CVE-2022-0847 DirtyPipe pkg=linux-kernel,ver>=5.16,ver<5.16.11 upstream-branch-candidate 1 Upstream stable 5.16.11 fixes this branch; vendor backports and readable target-file prerequisites remain unverified
|
||||
CVE-2026-43499 GhostLock rtmutex UAF pkg=linux-kernel,ver>=2.6.39,ver<5.10.261,CONFIG_FUTEX_PI=y 1 Fixed in stable 5.10.261; priority-inheritance futexes must be enabled
|
||||
CVE-2026-43499 GhostLock rtmutex UAF pkg=linux-kernel,ver>=5.11,ver<5.15.212,CONFIG_FUTEX_PI=y 1 Fixed in stable 5.15.212; priority-inheritance futexes must be enabled
|
||||
CVE-2026-43499 GhostLock rtmutex UAF pkg=linux-kernel,ver>=5.16,ver<6.1.175,CONFIG_FUTEX_PI=y 1 Fixed in stable 6.1.175; priority-inheritance futexes must be enabled
|
||||
@@ -731,6 +733,7 @@ EOF_DATA_25
|
||||
)"
|
||||
|
||||
KERNEL_CVE_DATA_26="$(cat <<'EOF_DATA_26'
|
||||
CVE-2023-2640,CVE-2023-32629 Ubuntu OverlayFS userns copy-up pkg=linux-kernel,ver>=6.2.0-1,ver<6.2.0-26,CONFIG_USER_NS=y,cmd:grep -q '^ID=ubuntu$' /etc/os-release && grep -Eq '^VERSION_ID="?(22[.]04|23[.]04)"?$' /etc/os-release && uname -r | grep -Eq '^6[.]2[.]0-[0-9]+-generic$',cmd:test -r /proc/sys/kernel/unprivileged_userns_clone && test "$(cat /proc/sys/kernel/unprivileged_userns_clone)" = 1 1 Candidate only for Ubuntu 22.04 HWE 6.2 or 23.04 generic before 6.2.0-26; Canonical backports and Livepatch may alter effective status; verify running kernel package
|
||||
CVE-2026-53264 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token with a public net/sched privilege-escalation exploit; dedicated LinPEAS check handles prerequisites
|
||||
CVE-2026-68121 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token with a public PPPoE privilege-escalation exploit; no stable matcher added
|
||||
CVE-2026-74469 catalog_reference_only 9999.9999.9999 0 Reference-only CVE token with a public SCTP diagnostics privilege-escalation exploit; dedicated LinPEAS check handles prerequisites
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Title: Variables - PostgreSQL base-backup destinations
|
||||
# ID: pg_basebackup_dests
|
||||
# Author: PEASS-ng
|
||||
# Last Update: 2026-10-09
|
||||
# Description: Share confirmed backup destinations with the existing SUID and SGID checks.
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Functions Used:
|
||||
# Global Variables:
|
||||
# Initial Functions:
|
||||
# Generated Global Variables: $PG_BASEBACKUP_DESTS
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
PG_BASEBACKUP_DESTS=
|
||||
@@ -13,4 +13,4 @@
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
processesVB='jdwp|tmux |screen | inspect |--inspect=|--inspect |--inspect$|--inpect-brk|--remote-debugging-port'
|
||||
processesVB='jdwp|tmux |screen | inspect |(^|[[:space:]])--inspect(-brk)?([=[:space:]]|$)|--remote-debugging-port|(^|[[:space:]/])chromedriver([[:space:]]|$)'
|
||||
@@ -1,10 +1,10 @@
|
||||
# Title: Variables - sidB
|
||||
# ID: sidB
|
||||
# Author: Carlos Polop
|
||||
# Last Update: 05-09-2026
|
||||
# Last Update: 09-10-2026
|
||||
# Description: Dangerous sid binaries
|
||||
# License: GNU GPL
|
||||
# Version: 1.0
|
||||
# Version: 1.1
|
||||
# Functions Used:
|
||||
# Global Variables:
|
||||
# Initial Functions:
|
||||
@@ -26,8 +26,6 @@ sidB="/apache2$%Read_root_passwd__apache2_-f_/etc/shadow\(CVE-2019-0211\)\
|
||||
/dtappgather$%Solaris_7_<_11_\(SPARC/x86\)\(CVE-2017-3622\)\
|
||||
/dtprintinfo$%Solaris_10_\(x86\)_and_lower_versions_also_SunOS_5.7_to_5.10\
|
||||
/dtsession$%Oracle_Solaris_10_1/13_and_earlier\(CVE-2020-2696\)\
|
||||
/enlightenment_backlight$%Before_0.25.4_\(CVE-2022-37706\)\
|
||||
/enlightenment_ckpasswd$%Before_0.25.4_\(CVE-2022-37706\)\
|
||||
/enlightenment_sys$%Before_0.25.4_\(CVE-2022-37706\)\
|
||||
/eject$%FreeBSD_mcweject_0.9/SGI_IRIX_6.2\
|
||||
/ibstat$%IBM_AIX_Version_6.1/7.1\(09-2013\)\
|
||||
@@ -54,9 +52,10 @@ sidB="/apache2$%Read_root_passwd__apache2_-f_/etc/shadow\(CVE-2019-0211\)\
|
||||
/rdist$%Solaris_10/OpenSolaris\
|
||||
/rsh$%Apple_Mac_OSX_10.9.5/10.10.5\(09-2015\)\
|
||||
/screen$%GNU_Screen_4.5.0\
|
||||
/screen-4\.5\.0$%GNU_Screen_logfile_policy_candidate_verify_SUID_and_vendor_patch\
|
||||
/sdtcm_convert$%Sun_Solaris_7.0\
|
||||
/sendmail$%Sendmail_8.10.1/Sendmail_8.11.x/Linux_Kernel_2.2.x_2.4.0-test1_\(SGI_ProPack_1.2/1.3\)\
|
||||
/snap-confine$%Ubuntu_snapd<2.37_dirty_sock_Local_Privilege_Escalation\(CVE-2019-7304\)\
|
||||
/snap-confine$%snap-confine_SUID_helper_review_effective_identity_and_mount_policy\
|
||||
/sudo%check_if_the_sudo_version_is_vulnerable\
|
||||
/Serv-U%FTP_Server<15.1.7(CVE-2019-12181)\
|
||||
/sudoedit$%Sudo/SudoEdit_1.6.9p21/1.7.2p4/\(RHEL_5/6/7/Ubuntu\)/Sudo<=1.8.14\
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
sudoB="$(whoami)|ALL:ALL|ALL : ALL|ALL|env_keep|NOPASSWD|SETENV|/apache2|/cryptsetup|/mount|/restic|/usermod|/sbin/ldconfig|/usr/sbin/ldconfig|ldconfig -f|--password-command|--password-file|-o ProxyCommand|-o PreferredAuthentications"
|
||||
sudoB="$(whoami)|ALL:ALL|ALL : ALL|ALL|env_keep|NOPASSWD|SETENV|/apache2|/cryptsetup|/mount|/restic|/usermod|/sbin/ldconfig|/usr/sbin/ldconfig|ldconfig -f|--password-command|--password-file|-o ProxyCommand|-o PreferredAuthentications|(^|[[:space:],])/usr/bin/ssh([[:space:],]|\$)|(^|[[:space:],])/(usr/)?bin/systemctl[[:space:]]+status([[:space:],]|\$)|(^|[[:space:],])(/[[:alnum:]_.+-]+)*/nginx([[:space:],]|\$)|(^|[[:space:],])(/[[:alnum:]_.+-]+)*/ansible-playbook([[:space:]]*([,]|\$)|[[:space:]]+\*[[:space:]]*([,]|\$))|(^|[[:space:],])(/[[:alnum:]_.+-]+)*/dotnet([[:space:]]*([,]|\$))|(^|[[:space:],])(/[[:alnum:]_.+-]+)*/easy_install([[:space:]]*([,]|\$))|(^|[[:space:],])(/[[:alnum:]_.+-]+)*/tcpdump([[:space:]]*([,]|\$))|(^|[[:space:],])(/[[:alnum:]_.+-]+)*/git[[:space:]]+\^?apply([[:space:],]|\$)|(^|[[:space:],])(/[[:alnum:]_.+-]+)*/clamscan[[:space:]]+\^?--debug([[:space:],]|\$)|(^|[[:space:],])(/[[:alnum:]_.+-]+)*/pip[0-9.]*[[:space:]]+\^?download([[:space:],]|\$)|(^|[[:space:],])(/[[:alnum:]_.+-]+)*/luvit([[:space:]]*([,]|\$))|(^|[[:space:],])(/[[:alnum:]_.+-]+)*/chattr([[:space:],]|\$)"
|
||||
@@ -13,5 +13,5 @@
|
||||
# Small linpeas: 1
|
||||
|
||||
|
||||
sudoVB1=" \*|env_keep\W*\+=.*LD_PRELOAD|env_keep\W*\+=.*LD_LIBRARY_PATH|env_keep\W*\+=.*BASH_ENV|env_keep\W*\+=.* ENV|env_keep\W*\+=.*PATH|!env_reset|!requiretty|peass{SUDOVB1_HERE}"
|
||||
sudoVB1=" \*|env_keep\W*\+=.*LD_PRELOAD|env_keep\W*\+=.*LD_LIBRARY_PATH|env_keep\W*\+=.*BASH_ENV|env_keep\W*\+=.* ENV|env_keep\W*\+=.*PATH|!env_reset|!requiretty|/apport-cli([[:space:],]|$)|peass{SUDOVB1_HERE}"
|
||||
sudoVB2="peass{SUDOVB2_HERE}"
|
||||
@@ -36,9 +36,12 @@ class FileRecord:
|
||||
def __resolve_search_in(self, search_in):
|
||||
""" Resolve spacial values to the correct directories """
|
||||
|
||||
# The YAML catalog is cached globally; do not consume its selector list.
|
||||
search_in = list(search_in)
|
||||
|
||||
if "all" in search_in:
|
||||
search_in.remove("all")
|
||||
search_in = ROOT_FOLDER
|
||||
search_in = list(ROOT_FOLDER)
|
||||
|
||||
if "common" in search_in:
|
||||
search_in.remove("common")
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import gzip
|
||||
import io
|
||||
import re
|
||||
import requests
|
||||
import base64
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tarfile
|
||||
import zlib
|
||||
|
||||
from .peasLoaded import PEASLoaded
|
||||
from .peassRecord import PEASRecord
|
||||
@@ -175,28 +178,28 @@ class LinpeasBuilder:
|
||||
|
||||
if type == "d":
|
||||
find_line += "-type d "
|
||||
bash_find_var = f"FIND_DIR_{r[1:].replace('.','').replace('-','_').replace('{ROOT_FOLDER}','').upper()}"
|
||||
bash_find_var = f"FIND_DIR_{r[1:].replace('.','').replace('-','_').replace('/','_').replace('{ROOT_FOLDER}','').upper()}"
|
||||
self.bash_find_d_vars.add(bash_find_var)
|
||||
all_folder_regexes += regexes
|
||||
else:
|
||||
bash_find_var = f"FIND_{r[1:].replace('.','').replace('-','_').replace('{ROOT_FOLDER}','').upper()}"
|
||||
bash_find_var = f"FIND_{r[1:].replace('.','').replace('-','_').replace('/','_').replace('{ROOT_FOLDER}','').upper()}"
|
||||
self.bash_find_f_vars.add(bash_find_var)
|
||||
all_file_regexes += regexes
|
||||
|
||||
find_line += '-name \\"' + '\\" -o -name \\"'.join(regexes) + '\\"'
|
||||
find_line += '\\( -name \\"' + '\\" -o -name \\"'.join(regexes) + '\\" \\)'
|
||||
find_line = FIND_TEMPLATE.replace(FIND_LINE_MARKUP, find_line)
|
||||
find_line = f"{bash_find_var}={find_line}"
|
||||
finds.append(find_line)
|
||||
|
||||
# Buid folder and files finds when searching in a custom folder
|
||||
all_folder_regexes = list(set(all_folder_regexes))
|
||||
find_line = '$SEARCH_IN_FOLDER -type d -name \\"' + '\\" -o -name \\"'.join(all_folder_regexes) + '\\"'
|
||||
find_line = '$SEARCH_IN_FOLDER -type d \\( -name \\"' + '\\" -o -name \\"'.join(all_folder_regexes) + '\\" \\)'
|
||||
find_line = FIND_TEMPLATE.replace(FIND_LINE_MARKUP, find_line)
|
||||
find_line = f"FIND_DIR_CUSTOM={find_line}"
|
||||
finds_custom.append(find_line)
|
||||
|
||||
all_file_regexes = list(set(all_file_regexes))
|
||||
find_line = '$SEARCH_IN_FOLDER -name \\"' + '\\" -o -name \\"'.join(all_file_regexes) + '\\"'
|
||||
find_line = '$SEARCH_IN_FOLDER \\( -name \\"' + '\\" -o -name \\"'.join(all_file_regexes) + '\\" \\)'
|
||||
find_line = FIND_TEMPLATE.replace(FIND_LINE_MARKUP, find_line)
|
||||
find_line = f"FIND_CUSTOM={find_line}"
|
||||
finds_custom.append(find_line)
|
||||
@@ -348,52 +351,87 @@ class LinpeasBuilder:
|
||||
return bin_b64
|
||||
|
||||
def __get_gtfobins_lists(self) -> tuple:
|
||||
bins = []
|
||||
api_url = "https://api.github.com/repos/GTFOBins/GTFOBins.github.io/contents/_gtfobins?per_page=100"
|
||||
while api_url:
|
||||
r = requests.get(api_url, timeout=10)
|
||||
if not r.ok:
|
||||
break
|
||||
data = r.json()
|
||||
for entry in data:
|
||||
if entry.get("type") == "file" and entry.get("name"):
|
||||
bins.append(entry["name"])
|
||||
api_url = None
|
||||
link = r.headers.get("Link", "")
|
||||
for part in link.split(","):
|
||||
if 'rel="next"' in part:
|
||||
api_url = part.split(";")[0].strip().strip("<>")
|
||||
break
|
||||
if not bins:
|
||||
r = requests.get("https://github.com/GTFOBins/GTFOBins.github.io/tree/master/_gtfobins", timeout=10)
|
||||
bins = re.findall(r'_gtfobins/([a-zA-Z0-9_ \-]+)(?:\\.md)?', r.text)
|
||||
# One repository archive replaces a directory listing and hundreds of
|
||||
# sequential raw-file requests. Every build uses the current archive.
|
||||
archive_url = "https://codeload.github.com/GTFOBins/GTFOBins.github.io/tar.gz/refs/heads/master"
|
||||
try:
|
||||
response = requests.get(archive_url, timeout=(3, 8), stream=True)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
content = bytearray()
|
||||
for chunk in response.iter_content(chunk_size=64 * 1024):
|
||||
if len(content) + len(chunk) > 2 * 1024 * 1024:
|
||||
raise ValueError("GTFOBins archive exceeds size limit")
|
||||
content.extend(chunk)
|
||||
finally:
|
||||
response.close()
|
||||
categories = self.__gtfobins_archive_categories(content)
|
||||
self.__validate_gtfobins_categories(categories)
|
||||
except (requests.RequestException, tarfile.TarError, OSError, ValueError, EOFError, zlib.error) as exc:
|
||||
raise RuntimeError(
|
||||
"Cannot build GTFOBins lists: failed to download or validate the current archive"
|
||||
) from exc
|
||||
|
||||
sudoVB = []
|
||||
suidVB = []
|
||||
capsVB = []
|
||||
# sudoers aliases and sudo -l may list several commands on one line.
|
||||
sudo_command_boundary = "([[:space:]]*[,]|$)"
|
||||
|
||||
for b in bins:
|
||||
try:
|
||||
rb = requests.get(f"https://raw.githubusercontent.com/GTFOBins/GTFOBins.github.io/master/_gtfobins/{b}", timeout=5)
|
||||
except:
|
||||
try:
|
||||
rb = requests.get(f"https://raw.githubusercontent.com/GTFOBins/GTFOBins.github.io/master/_gtfobins/{b}", timeout=5)
|
||||
except:
|
||||
rb = requests.get(f"https://raw.githubusercontent.com/GTFOBins/GTFOBins.github.io/master/_gtfobins/{b}", timeout=5)
|
||||
if "sudo:" in rb.text:
|
||||
if len(b) <= 3:
|
||||
# Reduce false positives for short names.
|
||||
sudoVB.append("[^a-zA-Z0-9]" + b + sudo_command_boundary)
|
||||
else:
|
||||
sudoVB.append(b + sudo_command_boundary)
|
||||
if "suid:" in rb.text:
|
||||
suidVB.append("/"+b+"$")
|
||||
if "capabilities:" in rb.text:
|
||||
capsVB.append(b)
|
||||
|
||||
sudoVB = [
|
||||
("[^a-zA-Z0-9]" if len(name) <= 3 else "") + name + sudo_command_boundary
|
||||
for name in categories["sudo"]
|
||||
]
|
||||
suidVB = ["/" + name + "$" for name in categories["suid"]]
|
||||
capsVB = categories["capabilities"]
|
||||
return (suidVB, sudoVB, capsVB)
|
||||
|
||||
@staticmethod
|
||||
def __gtfobins_archive_categories(content: bytes) -> dict:
|
||||
categories = {"sudo": [], "suid": [], "capabilities": []}
|
||||
seen = set()
|
||||
members = 0
|
||||
total_bytes = 0
|
||||
# Bound the whole tar stream before parsing: extension headers and
|
||||
# padding are not included in the member sizes yielded by tarfile.
|
||||
with gzip.GzipFile(fileobj=io.BytesIO(content)) as compressed:
|
||||
expanded = compressed.read(16 * 1024 * 1024 + 1)
|
||||
if len(expanded) > 16 * 1024 * 1024:
|
||||
raise ValueError("GTFOBins archive exceeds expanded size limit")
|
||||
with tarfile.open(fileobj=io.BytesIO(expanded), mode="r:") as archive:
|
||||
for member in archive:
|
||||
members += 1
|
||||
if members > 2048:
|
||||
raise ValueError("GTFOBins archive has too many entries")
|
||||
# Skipped members must also fit the expanded-size budget:
|
||||
# tarfile traverses their payload to reach the next header.
|
||||
total_bytes += member.size
|
||||
if total_bytes > 16 * 1024 * 1024:
|
||||
raise ValueError("GTFOBins archive exceeds expanded size limit")
|
||||
marker = "/_gtfobins/"
|
||||
if not member.isfile() or marker not in member.name or member.size > 128 * 1024:
|
||||
continue
|
||||
name = member.name.split(marker, 1)[1]
|
||||
if not re.fullmatch(r"[A-Za-z0-9_ .+-]{1,80}", name) or name in seen:
|
||||
continue
|
||||
member_file = archive.extractfile(member)
|
||||
if member_file is None:
|
||||
continue
|
||||
seen.add(name)
|
||||
body = member_file.read().decode("utf-8", "replace")
|
||||
for category in categories:
|
||||
if category + ":" in body:
|
||||
categories[category].append(name)
|
||||
for category in categories:
|
||||
categories[category].sort()
|
||||
return categories
|
||||
|
||||
@staticmethod
|
||||
def __validate_gtfobins_categories(categories: dict) -> None:
|
||||
limits = {"suid": 185, "sudo": 250, "capabilities": 2}
|
||||
for category, minimum in limits.items():
|
||||
names = categories.get(category)
|
||||
if not isinstance(names, list) or len(names) <= minimum or names != sorted(set(names)):
|
||||
raise ValueError(f"Incomplete GTFOBins {category} list")
|
||||
if not all(isinstance(name, str) and re.fullmatch(r"[A-Za-z0-9_ .+-]{1,80}", name)
|
||||
for name in names):
|
||||
raise ValueError(f"Invalid GTFOBins {category} entry")
|
||||
|
||||
def __generate_regexes_search(self) -> str:
|
||||
regexes = REGEXES_LOADED["regular_expresions"]
|
||||
|
||||
@@ -8,7 +8,9 @@ class PEASLoaded:
|
||||
self.peasrecords = []
|
||||
for record in to_search:
|
||||
record_value = record["value"]
|
||||
if "linpeas" in str(record_value["config"].get("disable","")).lower():
|
||||
# Shared YAML uses value.disable; accept the older config.disable too.
|
||||
if any("linpeas" in str(disabled).lower() for disabled in (
|
||||
record_value.get("disable"), record_value["config"].get("disable"))):
|
||||
continue
|
||||
|
||||
filerecords = []
|
||||
@@ -28,4 +30,4 @@ class PEASLoaded:
|
||||
exec=record_value["config"].get("exec", DEFAULTS["exec"]),
|
||||
filerecords=filerecords
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
# A commented password must not be reported.
|
||||
# "commented" MSCHAPV2 "not-a-secret" [2]
|
||||
* PEAP,TTLS,TLS,FAST
|
||||
"certificate-user" TLS [2]
|
||||
"hashed-user" MSCHAPV2 hash:000102030405060708090a0b0c0d0e0f [2]
|
||||
"phase-one" MD5 "phase-one-secret"
|
||||
"wireless-admin" MSCHAPV2 "wireless-secret" [2]
|
||||
"tunnel-user" TTLS-PAP "tunnel-secret" [2]
|
||||
"malformed" MSCHAPV2 "trailing-secret" [2] unexpected
|
||||
@@ -0,0 +1,116 @@
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MODULE = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "builder/linpeas_parts/9_interesting_files/27_Passwords_in_logs.sh"
|
||||
)
|
||||
|
||||
|
||||
class AccessLogGetCredentialTests(unittest.TestCase):
|
||||
def run_probe(self, *paths):
|
||||
source = MODULE.read_text(encoding="utf-8").split(
|
||||
'\nif ! [ "$SEARCH_IN_FOLDER" ]; then', 1
|
||||
)[0]
|
||||
result = subprocess.run(
|
||||
["dash", "-c", source + '\nlp_http_log_credential_cues "$@"\n', "dash", *map(str, paths)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=8,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
return result.stdout
|
||||
|
||||
def test_long_get_request_reports_path_only(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
log = Path(tmp) / "access.log"
|
||||
secret = "SENSITIVE_ACCESS_LOG_PASSWORD"
|
||||
request = (
|
||||
'127.0.0.1 - - [04/Feb/2025:02:20:11 +0000] '
|
||||
f'"GET /join.php?loginUsername=user&loginPassword={secret}&loginForm=Login HTTP/1.1" '
|
||||
'302 329 "http://example.invalid/join.php" '
|
||||
'"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"\n'
|
||||
)
|
||||
self.assertGreater(len(request), 200)
|
||||
log.write_text(request, encoding="utf-8")
|
||||
output = self.run_probe(log)
|
||||
self.assertIn(f"{log}: credential-bearing GET request present", output)
|
||||
self.assertEqual(output.count("credential-bearing GET request present"), 1)
|
||||
self.assertNotIn(secret, output)
|
||||
self.assertNotIn("loginUsername", output)
|
||||
self.assertNotIn("/join.php", output)
|
||||
|
||||
def test_only_nonempty_query_values_in_get_request_count(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
log = Path(tmp) / "access.log"
|
||||
log.write_text(
|
||||
'127.0.0.1 "GET /login?password= HTTP/1.1" 200 1\n'
|
||||
'127.0.0.1 "POST /login?password=hidden HTTP/1.1" 200 1\n'
|
||||
'127.0.0.1 "GET /plain HTTP/1.1" 200 1 "https://site/?password=hidden" "ua"\n'
|
||||
'127.0.0.1 "GET /plain HTTP/1.1" 200 1 "ref" "password=hidden"\n'
|
||||
'127.0.0.1 "GET /login?password HTTP/1.1" 200 1\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.assertEqual(self.run_probe(log), "")
|
||||
log.write_text('127.0.0.1 "GET /login?loginPassword=present HTTP/1.1" 200 1\n')
|
||||
self.assertIn("credential-bearing GET request present", self.run_probe(log))
|
||||
|
||||
def test_tail_byte_limit_and_file_guard(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
log = root / "access.log"
|
||||
log.write_text(
|
||||
'127.0.0.1 "GET /login?password=too-old HTTP/1.1" 200 1\n'
|
||||
+ ("x" * 70000)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.assertEqual(self.run_probe(log), "")
|
||||
alias = root / "alias.log"
|
||||
alias.symlink_to(log)
|
||||
self.assertEqual(self.run_probe(alias), "")
|
||||
self.assertEqual(self.run_probe(root / "missing.log"), "")
|
||||
|
||||
def test_file_cap(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
paths = []
|
||||
for i in range(9):
|
||||
path = root / f"access-{i}.log"
|
||||
path.write_text(
|
||||
'127.0.0.1 "GET /login?password=TOP_SECRET_VALUE HTTP/1.1" 200 1\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
paths.append(path)
|
||||
output = self.run_probe(*paths)
|
||||
self.assertEqual(output.count("credential-bearing GET request present"), 8)
|
||||
self.assertIn("partial (file or time cap reached)", output)
|
||||
self.assertNotIn("TOP_SECRET_VALUE", output)
|
||||
|
||||
def test_short_legacy_log_entry_is_still_selected(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
log = root / "ordinary.log"
|
||||
log.write_text("short pwd=fixture-value\n", encoding="utf-8")
|
||||
module = MODULE.read_text(encoding="utf-8")
|
||||
legacy = module.split(' (find /var/log/', 1)[1].split(' lp_http_log_credential_cues', 1)[0]
|
||||
legacy = ' (find /var/log/' + legacy
|
||||
legacy = legacy.replace(
|
||||
"find /var/log/ /var/logs/ /private/var/log",
|
||||
f"find {root}",
|
||||
)
|
||||
result = subprocess.run(
|
||||
["dash", "-c", "E=E; SED_RED=''; log_find_exec_end='+';\n" + legacy],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=8,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("short =fixture-value", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,137 @@
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ActualGroupsTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.part = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "builder/linpeas_parts/6_users_information/19_Actual_groups.sh"
|
||||
)
|
||||
|
||||
def run_probe(self, groups, *, timeout_name="timeout", clock=None):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
fixture = root / "group"
|
||||
fixture.write_text(groups, encoding="utf-8")
|
||||
bindir = root / "bin"
|
||||
bindir.mkdir()
|
||||
for command in ("sed", "date"):
|
||||
(bindir / command).symlink_to(shutil.which(command))
|
||||
(bindir / "id").write_text(
|
||||
'#!/bin/sh\n'
|
||||
'case "$1" in\n'
|
||||
' -G) echo "100 200" ;;\n'
|
||||
' -g) echo "$FAKE_EFFECTIVE_GID" ;;\n'
|
||||
'esac\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(bindir / "newgrp").write_text(
|
||||
'#!/bin/sh\n'
|
||||
'printf "%s\\n" "$1" >> "$FAKE_PROBE_LOG"\n'
|
||||
'read -r command\n'
|
||||
'case "$1" in\n'
|
||||
' shadow_only) FAKE_EFFECTIVE_GID=300 ;;\n'
|
||||
' *) FAKE_EFFECTIVE_GID=100 ;;\n'
|
||||
'esac\n'
|
||||
'export FAKE_EFFECTIVE_GID\n'
|
||||
'exec /bin/sh -c "$command"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(bindir / "id").chmod(0o755)
|
||||
(bindir / "newgrp").chmod(0o755)
|
||||
if timeout_name:
|
||||
(bindir / timeout_name).write_text(
|
||||
'#!/bin/sh\n'
|
||||
'printf "%s\\n" "$1 $2 $3" >> "$FAKE_TIMEOUT_LOG"\n'
|
||||
'shift 3\n'
|
||||
'exec "$@"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(bindir / timeout_name).chmod(0o755)
|
||||
if clock is not None:
|
||||
(bindir / "date").unlink()
|
||||
(bindir / "date").write_text(
|
||||
'#!/bin/sh\n'
|
||||
'value=$(cat "$FAKE_CLOCK_FILE")\n'
|
||||
'printf "%s\\n" "$((value + 1))" > "$FAKE_CLOCK_FILE"\n'
|
||||
'echo "$value"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(bindir / "date").chmod(0o755)
|
||||
(root / "clock").write_text(str(clock), encoding="utf-8")
|
||||
(bindir / "cat").symlink_to("/bin/cat")
|
||||
|
||||
# Substitute only the group-file input; all commands remain fixtures.
|
||||
part = self.part.read_text(encoding="utf-8").replace(
|
||||
"done < /etc/group", f"done < {shlex.quote(str(fixture))}"
|
||||
)
|
||||
script = "\n".join(
|
||||
[
|
||||
'print_2title() { echo "TITLE: $1"; }',
|
||||
"IAMROOT=0 E=E groupsVB=NEVERMATCH groupsB=NEVERMATCH",
|
||||
"SED_RED_YELLOW='' SED_RED=''",
|
||||
part,
|
||||
'printf "ACTUAL=%s\\n" "$ActualGroup"',
|
||||
]
|
||||
)
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
PATH=str(bindir),
|
||||
FAKE_PROBE_LOG=str(root / "probes"),
|
||||
FAKE_TIMEOUT_LOG=str(root / "timeouts"),
|
||||
FAKE_CLOCK_FILE=str(root / "clock"),
|
||||
)
|
||||
result = subprocess.run(
|
||||
["/bin/sh", "-c", script],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
probes = (root / "probes").read_text().splitlines() if (root / "probes").exists() else []
|
||||
timeouts = (root / "timeouts").read_text().splitlines() if (root / "timeouts").exists() else []
|
||||
return result, probes, timeouts
|
||||
|
||||
def test_missing_token_group_is_detected_even_without_group_file_member(self):
|
||||
groups = "in_token:x:100:\nshadow_only:x:300:\ndenied:x:400:\n"
|
||||
result, probes, timeouts = self.run_probe(groups)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(probes, ["shadow_only", "denied"])
|
||||
self.assertEqual(timeouts, ["-k 1 1", "-k 1 1"])
|
||||
self.assertIn("Accessible group not shown in id: shadow_only (gid=300)", result.stdout)
|
||||
self.assertNotIn("Accessible group not shown in id: denied", result.stdout)
|
||||
self.assertIn("ACTUAL=|shadow_only|", result.stdout)
|
||||
|
||||
def test_no_timeout_skips_newgrp(self):
|
||||
result, probes, _ = self.run_probe("shadow_only:x:300:\n", timeout_name=None)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(probes, [])
|
||||
self.assertIn("ACTUAL=|", result.stdout)
|
||||
|
||||
def test_global_deadline_stops_before_remaining_rows(self):
|
||||
groups = "".join(f"candidate{i}:x:{300 + i}:\n" for i in range(30))
|
||||
result, probes, _ = self.run_probe(groups, clock=100)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(len(probes), 14)
|
||||
|
||||
def test_gtimeout_fallback(self):
|
||||
result, probes, timeouts = self.run_probe("shadow_only:x:300:\n", timeout_name="gtimeout")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(probes, ["shadow_only"])
|
||||
self.assertEqual(timeouts, ["-k 1 1"])
|
||||
|
||||
def test_group_name_is_passed_literally(self):
|
||||
result, probes, _ = self.run_probe('literal$(touch marker):x:300:\n')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(probes, ['literal$(touch marker)'])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,180 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MODULE = ROOT / "linPEAS/builder/linpeas_parts/7_software_information/Apache_nginx.sh"
|
||||
SIGNATURES = ROOT / "build_lists/sensitive_files.yaml"
|
||||
|
||||
|
||||
class ApachePipedRemcoTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.timeout = shutil.which("timeout")
|
||||
if cls.timeout is None:
|
||||
raise unittest.SkipTest("timeout is needed for bounded fixture runs")
|
||||
cls.module = MODULE.read_text(encoding="utf-8").replace("peass{Apache-Nginx}", "")
|
||||
|
||||
def run_check(self, function, *args):
|
||||
script = self.module + "\n" + f'TIMEOUT="{self.timeout}"\n{function} "$@"\n'
|
||||
result = subprocess.run(
|
||||
["sh", "-c", script, "fixture", *map(str, args)],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env={**os.environ, "SEARCH_IN_FOLDER": ""},
|
||||
timeout=8,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
return result.stdout
|
||||
|
||||
def test_piped_log_metadata_redacts_commands(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
site = Path(tmp) / "site.conf"
|
||||
site.write_text(
|
||||
'CustomLog "|$sh -c secret_token=hidden" common\n'
|
||||
'ErrorLog "|/usr/bin/rotatelogs /tmp/error 1M"\n'
|
||||
'TransferLog /var/log/apache/access.log\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
output = self.run_check("lp_apache_piped_log_check", tmp)
|
||||
self.assertIn("CustomLog: shell pipeline", output)
|
||||
self.assertIn("ErrorLog: direct pipeline", output)
|
||||
self.assertNotIn("secret_token", output)
|
||||
self.assertNotIn("/usr/bin/rotatelogs", output)
|
||||
self.assertNotIn("TransferLog:", output)
|
||||
self.assertNotIn("privilege escalation", output.lower())
|
||||
site.write_text("customlog '|$sh -c second_secret=hidden' common\n", encoding="utf-8")
|
||||
lowered = self.run_check("lp_apache_piped_log_check", tmp)
|
||||
self.assertIn("customlog: shell pipeline", lowered)
|
||||
self.assertNotIn("second_secret", lowered)
|
||||
|
||||
def test_ordinary_file_log_does_not_trigger(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
(Path(tmp) / "site.conf").write_text(
|
||||
"CustomLog /var/log/apache/access.log common\n", encoding="utf-8"
|
||||
)
|
||||
self.assertEqual(self.run_check("lp_apache_piped_log_check", tmp), "")
|
||||
|
||||
def test_remco_raw_value_correlates_without_backend_query(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
templates = root / "templates"
|
||||
templates.mkdir()
|
||||
template = templates / "site.conf.tmpl"
|
||||
template.write_text(
|
||||
'ServerName {{ getv(printf("/customers/%s/url", customer)) }}.example\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = root / "config"
|
||||
config.write_text(
|
||||
"[[resource]]\n"
|
||||
'name = "apache2"\n'
|
||||
"[[resource.template]]\n"
|
||||
f'src = "{template}"\n'
|
||||
'dst = "/etc/apache2/sites-enabled/site.conf"\n'
|
||||
'reload_cmd = "systemctl restart apache2.service"\n'
|
||||
"[resource.backend]\n"
|
||||
"[resource.backend.etcd]\n"
|
||||
"version = 3\n"
|
||||
'nodes = ["http://127.0.0.1:2379"]\n'
|
||||
'keys = ["/customers"]\n'
|
||||
"watch = true\n"
|
||||
"interval = 5\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
output = self.run_check("lp_remco_apache_template_check", config, templates, "root")
|
||||
self.assertIn("Candidate: root remco process", output)
|
||||
self.assertIn("backend write permission unknown", output)
|
||||
self.assertNotIn("/customers", output)
|
||||
self.assertNotIn(str(template), output)
|
||||
self.assertIn("process privilege", self.run_check(
|
||||
"lp_remco_apache_template_check", config, templates, "unknown"
|
||||
))
|
||||
|
||||
template.write_text(
|
||||
'ServerName {{ getv("/customers/x/url") | escape }}.example\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
filtered = self.run_check(
|
||||
"lp_remco_apache_template_check", config, templates, "root"
|
||||
)
|
||||
self.assertIn("template contents", filtered)
|
||||
self.assertNotIn("Candidate:", filtered)
|
||||
|
||||
def test_remco_oversize_config_is_unknown(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config = Path(tmp) / "config"
|
||||
config.write_text("a" * 65537, encoding="utf-8")
|
||||
output = self.run_check(
|
||||
"lp_remco_apache_template_check", config, Path(tmp) / "templates", "root"
|
||||
)
|
||||
self.assertIn("unknown", output)
|
||||
self.assertNotIn("Candidate", output)
|
||||
|
||||
def test_existing_apache_selector_is_preserved(self):
|
||||
section = next(
|
||||
entry for entry in yaml.safe_load(SIGNATURES.read_text(encoding="utf-8"))["search"]
|
||||
if entry["name"] == "Apache-Nginx"
|
||||
)["value"]
|
||||
commands = section["config"]["exec"]
|
||||
self.assertIn("lp_apache_piped_log_check", commands)
|
||||
self.assertIn("lp_remco_apache_template_check", commands)
|
||||
sites = next(item for item in section["files"] if item["name"] == "sites-enabled")
|
||||
pattern = sites["value"]["files"][0]["value"]["bad_regex"]
|
||||
for directive in ("ServerName", "DocumentRoot", "ProxyPass"):
|
||||
self.assertIn(directive, pattern)
|
||||
|
||||
def test_generated_file_inventory_withholds_pipe_command(self):
|
||||
section = next(
|
||||
entry for entry in yaml.safe_load(SIGNATURES.read_text(encoding="utf-8"))["search"]
|
||||
if entry["name"] == "Apache-Nginx"
|
||||
)["value"]
|
||||
default = next(item for item in section["files"] if item["name"] == "000-default.conf")
|
||||
sys.path.insert(0, str(ROOT / "linPEAS"))
|
||||
from builder.src.fileRecord import FileRecord
|
||||
from builder.src.linpeasBuilder import LinpeasBuilder
|
||||
from builder.src.peassRecord import PEASRecord
|
||||
|
||||
record = FileRecord(regex="000-default.conf", **default["value"])
|
||||
parent = PEASRecord("Apache-Nginx", True, [], [record])
|
||||
line = LinpeasBuilder.__new__(LinpeasBuilder)._LinpeasBuilder__construct_file_line(
|
||||
parent, record
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
file = Path(tmp) / "000-default.conf"
|
||||
file.write_text(
|
||||
"ServerName example.local\n"
|
||||
"DocumentRoot /var/www/html\n"
|
||||
"ProxyPass / http://localhost:1234/\n"
|
||||
'CustomLog "|$sh -c secret_token=hidden" common\n'
|
||||
"customlog '|$sh -c second_secret=hidden' common\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = subprocess.run(
|
||||
["sh", "-c", line],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env={
|
||||
**os.environ,
|
||||
"PSTORAGE_APACHE_NGINX": str(file),
|
||||
"E": "E",
|
||||
"SED_RED": "&",
|
||||
},
|
||||
timeout=4,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertNotIn("secret_token", result.stdout)
|
||||
self.assertNotIn("second_secret", result.stdout)
|
||||
for directive in ("ServerName", "DocumentRoot", "ProxyPass"):
|
||||
self.assertIn(directive, result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Apache's cached vhost display highlights explicit process identities."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
class ApacheRootVhostHighlightTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
catalog = yaml.safe_load((ROOT / "build_lists/sensitive_files.yaml").read_text())
|
||||
record, = [item for item in catalog["search"] if item["name"] == "Apache-Nginx"]
|
||||
files = record["value"]["files"]
|
||||
nested = files[0]["value"]["files"][0]["value"]
|
||||
direct = next(item["value"] for item in files
|
||||
if item["name"] == "000-default.conf")
|
||||
cls.records = ((nested["bad_regex"], nested["remove_regex"]),
|
||||
(direct["bad_regex"], direct["remove_regex"]))
|
||||
|
||||
def colorized(self, pattern, remove, line):
|
||||
# The generator places this YAML regex inside shell double quotes,
|
||||
# which reduces its doubled backslash before grep receives it.
|
||||
remove = remove.replace("\\\\", "\\")
|
||||
filtered = subprocess.run(
|
||||
["grep", "-Ev", remove], input=line + "\n", text=True,
|
||||
capture_output=True, timeout=2,
|
||||
)
|
||||
self.assertIn(filtered.returncode, (0, 1), filtered.stderr)
|
||||
result = subprocess.run(
|
||||
["sed", "-E", "s," + pattern + ",[IDENTITY-CUE],g"],
|
||||
input=filtered.stdout, text=True, capture_output=True, timeout=2,
|
||||
)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
return result.stdout.strip()
|
||||
|
||||
def test_active_identity_directives_and_existing_highlight(self):
|
||||
for pattern, remove in self.records:
|
||||
for line in ("AssignUserId root root", "AssignUserID 0 0",
|
||||
"assignuserid root 0", " ASSIGNUSERID 0 root ",
|
||||
"AssignUserId root staff", "AssignUserId app root",
|
||||
"AssignUserID app_user app-group"):
|
||||
with self.subTest(line=line):
|
||||
self.assertIn("[IDENTITY-CUE]", self.colorized(pattern, remove, line))
|
||||
for line in ("# AssignUserId app staff", " # AssignUserID 0 0",
|
||||
"NotAssignUserId root root", "AssignUserID app",
|
||||
"AssignUserID app/group staff"):
|
||||
with self.subTest(line=line):
|
||||
self.assertNotIn("[IDENTITY-CUE]", self.colorized(pattern, remove, line))
|
||||
self.assertEqual("", self.colorized(pattern, remove, "# AssignUserID app staff"))
|
||||
self.assertIn("[IDENTITY-CUE]", self.colorized(pattern, remove, "ServerName local.example"))
|
||||
|
||||
def test_generated_section_preserves_cached_vhost_inventory(self):
|
||||
sys.path.insert(0, str(ROOT / "linPEAS"))
|
||||
from builder.src.linpeasBuilder import LinpeasBuilder
|
||||
from builder.src.peasLoaded import PEASLoaded
|
||||
|
||||
builder = LinpeasBuilder.__new__(LinpeasBuilder)
|
||||
builder.ploaded = PEASLoaded()
|
||||
section = builder._LinpeasBuilder__generate_sections()["Apache-Nginx"]
|
||||
self.assertIn("PSTORAGE_APACHE_NGINX", section)
|
||||
self.assertIn('grep -E "sites-enabled$"', section)
|
||||
self.assertIn('grep -E "000-default\\.conf$"', section)
|
||||
for pattern, remove in self.records:
|
||||
self.assertIn(pattern, section)
|
||||
self.assertIn('grep -Ev "' + remove + '" | sed -${E} ', section)
|
||||
syntax = subprocess.run(["sh", "-n"], input=section, text=True,
|
||||
capture_output=True, timeout=2)
|
||||
self.assertEqual(0, syntax.returncode, syntax.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Authentication logs are cached by exact name and listed without contents."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TITLE = "Interesting logs"
|
||||
|
||||
|
||||
class AuthLogCatalogTests(unittest.TestCase):
|
||||
def test_exact_cached_path_only_selector(self):
|
||||
catalog = yaml.safe_load((ROOT / "build_lists/sensitive_files.yaml").read_text())
|
||||
record, = [item for item in catalog["search"] if item["name"] == TITLE]
|
||||
options = {item["name"]: item["value"] for item in record["value"]["files"]}
|
||||
self.assertEqual({"access.log", "error.log", "auth.log"}, options.keys())
|
||||
self.assertEqual(
|
||||
{"just_list_file": True, "type": "f", "search_in": ["common"]},
|
||||
options["auth.log"],
|
||||
)
|
||||
|
||||
sys.path.insert(0, str(ROOT / "linPEAS"))
|
||||
from builder.src.linpeasBuilder import LinpeasBuilder
|
||||
from builder.src.peasLoaded import PEASLoaded
|
||||
|
||||
builder = LinpeasBuilder.__new__(LinpeasBuilder)
|
||||
builder.ploaded = PEASLoaded()
|
||||
builder.hidden_files = set()
|
||||
builder.bash_find_f_vars = set()
|
||||
builder.bash_find_d_vars = set()
|
||||
builder.bash_storages = set()
|
||||
builder._LinpeasBuilder__get_files_to_search()
|
||||
finds, _ = builder._LinpeasBuilder__generate_finds()
|
||||
self.assertTrue(any('-name \\"auth.log\\"' in line for line in finds))
|
||||
builder._LinpeasBuilder__generate_storages()
|
||||
section = builder._LinpeasBuilder__generate_sections()[TITLE]
|
||||
self.assertNotIn('cat "$f"', section)
|
||||
self.assertNotIn("ls -lRA", section)
|
||||
self.assertEqual(0, subprocess.run(["sh", "-n"], input=section, text=True,
|
||||
capture_output=True, timeout=2).returncode)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
exact = root / "auth.log"
|
||||
exact.write_text("SECRET_DO_NOT_PRINT\n")
|
||||
rotated = root / "auth.log.1"
|
||||
rotated.write_text("OTHER_SECRET_DO_NOT_PRINT\n")
|
||||
unrelated = root / "authentication.log"
|
||||
unrelated.write_text("UNRELATED_SECRET_DO_NOT_PRINT\n")
|
||||
result = subprocess.run(
|
||||
["sh", "-c", "print_2title() { :; }; " + section],
|
||||
env={**os.environ,
|
||||
"PSTORAGE_INTERESTING_LOGS": "\n".join(map(str, (exact, rotated, unrelated))),
|
||||
"E": "E", "SED_RED": "&"},
|
||||
text=True, capture_output=True, timeout=2,
|
||||
)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn(str(exact), result.stdout)
|
||||
self.assertNotIn(str(rotated), result.stdout)
|
||||
self.assertNotIn(str(unrelated), result.stdout)
|
||||
self.assertNotIn("SECRET_DO_NOT_PRINT", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,96 @@
|
||||
"""The autologin password lead must remain exact-path and metadata-only."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TITLE = "Autologin"
|
||||
STORAGE = "PSTORAGE_AUTOLOGIN"
|
||||
|
||||
|
||||
class AutologinPasswordCatalogTests(unittest.TestCase):
|
||||
def test_exact_path_preserves_older_names_and_hides_password(self):
|
||||
catalog = yaml.safe_load((ROOT / "build_lists/sensitive_files.yaml").read_text())
|
||||
record, = [item for item in catalog["search"] if item["name"] == TITLE]
|
||||
self.assertEqual(record["value"]["disable"], ["winpeas"])
|
||||
files = {item["name"]: item["value"] for item in record["value"]["files"]}
|
||||
self.assertEqual(set(files), {"autologin", "autologin.conf", "passwd"})
|
||||
entry = files["passwd"]
|
||||
self.assertTrue(entry["just_list_file"])
|
||||
self.assertEqual(entry["type"], "f")
|
||||
self.assertEqual(entry["search_in"], ["${ROOT_FOLDER}etc", "${ROOT_FOLDER}mnt"])
|
||||
pattern = entry["check_extra_path"]
|
||||
for path in ("/etc/autologin/passwd",
|
||||
"/mnt/stateful_partition/etc/autologin/passwd",
|
||||
"/opt/autologin.conf", "/var/lib/app/autologin",
|
||||
"/opt/autologin"):
|
||||
self.assertIsNotNone(re.search(pattern, path), path)
|
||||
for path in ("/etc/passwd", "/tmp/etc/autologin/passwd",
|
||||
"/etc/autologin/passwd.bak", "/etc/other/passwd",
|
||||
"/mnt/other/etc/autologin/passwd"):
|
||||
self.assertIsNone(re.search(pattern, path), path)
|
||||
|
||||
sys.path.insert(0, str(ROOT / "linPEAS"))
|
||||
from builder.src.linpeasBuilder import LinpeasBuilder
|
||||
from builder.src.peasLoaded import PEASLoaded
|
||||
|
||||
builder = LinpeasBuilder.__new__(LinpeasBuilder)
|
||||
builder.ploaded = PEASLoaded()
|
||||
builder.hidden_files = set()
|
||||
builder.bash_find_f_vars = set()
|
||||
builder.bash_find_d_vars = set()
|
||||
builder.bash_storages = set()
|
||||
builder._LinpeasBuilder__get_files_to_search()
|
||||
builder._LinpeasBuilder__generate_finds()
|
||||
storage = next(line for line in builder._LinpeasBuilder__generate_storages()
|
||||
if line.startswith(STORAGE + "="))
|
||||
self.assertIn(pattern, storage)
|
||||
self.assertIn("$FIND_ETC", storage)
|
||||
self.assertIn("$FIND_MNT", storage)
|
||||
self.assertIn("$FIND_VAR", storage)
|
||||
selected = subprocess.run(
|
||||
["sh", "-c", storage + f'\nprintf "%s\n" "${STORAGE}"'],
|
||||
env={**os.environ, "ROOT_FOLDER": "/", "FIND_ETC": "\n".join((
|
||||
"/etc/autologin/passwd", "/etc/passwd", "/etc/autologin/passwd.bak",
|
||||
"/etc/autologin.conf", "/opt/autologin")),
|
||||
"FIND_MNT": "\n".join((
|
||||
"/mnt/stateful_partition/etc/autologin/passwd",
|
||||
"/mnt/other/etc/autologin/passwd")),
|
||||
"FIND_VAR": "/var/lib/app/autologin.conf"},
|
||||
text=True, capture_output=True, timeout=2,
|
||||
)
|
||||
self.assertEqual(selected.returncode, 0, selected.stderr)
|
||||
self.assertEqual(set(selected.stdout.splitlines()),
|
||||
{"/etc/autologin/passwd", "/mnt/stateful_partition/etc/autologin/passwd",
|
||||
"/etc/autologin.conf", "/opt/autologin",
|
||||
"/var/lib/app/autologin.conf"})
|
||||
|
||||
section = builder._LinpeasBuilder__generate_sections()[TITLE]
|
||||
passwd_line, = [line for line in section.splitlines()
|
||||
if 'echo_not_found "passwd"' in line]
|
||||
self.assertNotIn("cat ", passwd_line)
|
||||
self.assertEqual(subprocess.run(["sh", "-n"], input=section, text=True,
|
||||
capture_output=True, timeout=2).returncode, 0)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
password = Path(tmp) / "passwd"
|
||||
password.write_text("SECRET_MUST_STAY_HIDDEN")
|
||||
result = subprocess.run(
|
||||
["sh", "-c", "print_2title() { :; }; " + passwd_line],
|
||||
env={**os.environ, STORAGE: str(password), "E": "E", "SED_RED": "&"},
|
||||
text=True, capture_output=True, timeout=2,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn(str(password), result.stdout)
|
||||
self.assertNotIn("SECRET_MUST_STAY_HIDDEN", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Axel user configuration is a path-only lead in the existing file cache."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SECTION_NAME = "Axel download configuration candidates"
|
||||
|
||||
|
||||
class AxelUserConfigCatalogTests(unittest.TestCase):
|
||||
def test_exact_path_only_selector_and_generated_output(self):
|
||||
catalog = yaml.safe_load((ROOT / "build_lists/sensitive_files.yaml").read_text())
|
||||
records = [item for item in catalog["search"] if item["name"] == SECTION_NAME]
|
||||
self.assertEqual(1, len(records))
|
||||
options = records[0]["value"]
|
||||
self.assertEqual(["winpeas"], options["disable"])
|
||||
self.assertTrue(options["config"]["auto_check"])
|
||||
self.assertEqual(1, len(options["files"]))
|
||||
self.assertEqual(".axelrc", options["files"][0]["name"])
|
||||
selector = options["files"][0]["value"]
|
||||
self.assertEqual(["common"], selector["search_in"])
|
||||
self.assertEqual("f", selector["type"])
|
||||
self.assertTrue(selector["just_list_file"])
|
||||
for key in ("bad_regex", "good_regex", "line_grep", "only_bad_lines"):
|
||||
self.assertNotIn(key, selector)
|
||||
|
||||
sys.path.insert(0, str(ROOT / "linPEAS"))
|
||||
from builder.src.linpeasBuilder import LinpeasBuilder
|
||||
from builder.src.peasLoaded import PEASLoaded
|
||||
|
||||
builder = LinpeasBuilder.__new__(LinpeasBuilder)
|
||||
builder.ploaded = PEASLoaded()
|
||||
builder.hidden_files = set()
|
||||
builder.bash_find_f_vars = set()
|
||||
builder.bash_find_d_vars = set()
|
||||
builder.bash_storages = set()
|
||||
builder._LinpeasBuilder__get_files_to_search()
|
||||
finds, _ = builder._LinpeasBuilder__generate_finds()
|
||||
self.assertTrue(any('.axelrc' in line and ' -name ' in line for line in finds))
|
||||
section = builder._LinpeasBuilder__generate_sections()[SECTION_NAME]
|
||||
syntax = subprocess.run(["sh", "-n"], input=section, text=True,
|
||||
capture_output=True, timeout=2)
|
||||
self.assertEqual(0, syntax.returncode, syntax.stderr)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config = Path(tmp) / ".axelrc"
|
||||
backup = Path(tmp) / ".axelrc.bak"
|
||||
config.write_text("DO_NOT_PRINT_CONFIG_SECRET")
|
||||
backup.write_text("DO_NOT_PRINT_CONFIG_SECRET")
|
||||
result = subprocess.run(
|
||||
["sh", "-c", "print_2title() { :; }; " + section],
|
||||
env={**os.environ,
|
||||
"PSTORAGE_AXEL_DOWNLOAD_CONFIGURATION_CANDIDATES":
|
||||
"\n".join((str(config), str(backup))),
|
||||
"E": "E", "SED_RED": "&"},
|
||||
text=True, capture_output=True, timeout=2,
|
||||
)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn(str(config), result.stdout)
|
||||
self.assertNotIn(str(backup), result.stdout)
|
||||
self.assertNotIn("DO_NOT_PRINT_CONFIG_SECRET", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,266 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class BackupFoldersTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.repo_root = Path(__file__).resolve().parents[2]
|
||||
self.module = self.repo_root / "linPEAS/builder/linpeas_parts/9_interesting_files/13_Backup_folders.sh"
|
||||
|
||||
def test_backup_names_keep_file_and_directory_searches(self):
|
||||
config = yaml.safe_load((self.repo_root / "build_lists/sensitive_files.yaml").read_text())
|
||||
backup = next(record for record in config["search"] if record["name"] == "Backups")
|
||||
types = {}
|
||||
for record in backup["value"]["files"]:
|
||||
types.setdefault(record["name"], set()).add(record["value"]["type"])
|
||||
self.assertEqual(types["backup"], {"d", "f"})
|
||||
self.assertEqual(types["backups"], {"d", "f"})
|
||||
self.assertEqual(types["npbackup.conf"], {"f"})
|
||||
|
||||
def test_builder_emits_both_find_paths_for_backup_names(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
output_path = Path(temp) / "linpeas-backup.sh"
|
||||
subprocess.run(
|
||||
["python3", "-m", "builder.linpeas_builder", "--include",
|
||||
"IF_Backup_folders", "--output", str(output_path)],
|
||||
cwd=self.repo_root / "linPEAS", capture_output=True, text=True,
|
||||
check=True,
|
||||
)
|
||||
lines = output_path.read_text().splitlines()
|
||||
directory_find = next(line for line in lines if "FIND_DIR_VAR=" in line)
|
||||
file_find = next(line for line in lines if "FIND_VAR=" in line)
|
||||
storage = next(line for line in lines if "PSTORAGE_BACKUPS=" in line)
|
||||
for name in ("backup", "backups"):
|
||||
self.assertIn(f'-name \\"{name}\\"', directory_find)
|
||||
self.assertIn(f'-name \\"{name}\\"', file_find)
|
||||
self.assertIn("$FIND_DIR_VAR", storage)
|
||||
self.assertIn("$FIND_VAR", storage)
|
||||
|
||||
def run_module(self, paths):
|
||||
script = 'print_2title() { :; }; . "$BACKUP_MODULE"'
|
||||
env = dict(os.environ, BACKUP_MODULE=str(self.module),
|
||||
PSTORAGE_BACKUPS="\n".join(map(str, paths)),
|
||||
SEARCH_IN_FOLDER="", DEBUG="", E="E", SED_RED="")
|
||||
result = subprocess.run(["sh", "-c", script], env=env,
|
||||
capture_output=True, text=True, check=True)
|
||||
return result.stdout
|
||||
|
||||
def test_lists_immediate_metadata_and_skips_exact_name_file(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
backup_dir = root / "backup"
|
||||
backup_dir.mkdir()
|
||||
archive = backup_dir / "opaque.zip.aes"
|
||||
archive.write_text("private fixture contents")
|
||||
nested = backup_dir / "nested"
|
||||
nested.mkdir()
|
||||
(nested / "deep.txt").write_text("nested fixture contents")
|
||||
exact_file = root / "backups"
|
||||
exact_file.write_text("exact-name file contents")
|
||||
|
||||
output = self.run_module([backup_dir, exact_file])
|
||||
self.assertEqual(output.count(str(archive)), 1)
|
||||
self.assertIn(str(nested), output)
|
||||
self.assertNotIn("deep.txt", output)
|
||||
self.assertNotIn(str(exact_file), output)
|
||||
self.assertNotIn("fixture contents", output)
|
||||
|
||||
def test_caps_each_directory_at_thirty_entries(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
backups = Path(temp) / "backups"
|
||||
backups.mkdir()
|
||||
for index in range(40):
|
||||
(backups / f"item{index:02d}").touch()
|
||||
|
||||
output = self.run_module([backups])
|
||||
entries = [line for line in output.splitlines()
|
||||
if f"{backups}/item" in line]
|
||||
self.assertEqual(len(entries), 30)
|
||||
|
||||
def test_redacted_backup_shell_pass_assignments_are_bounded(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
backups = root / "backup"
|
||||
backups.mkdir()
|
||||
script = backups / "offsite-backup.sh"
|
||||
script.write_text(
|
||||
"OFFSITE_PASS=active-secret\n"
|
||||
"# export DB_PASS='old-secret'\n"
|
||||
"BYPASS=wrong-secret\n"
|
||||
"OFFSITE_PASSWORD=wrong-secret\n"
|
||||
"echo OFFSITE_PASS=wrong-secret\n"
|
||||
"OFFSITE_PASS =wrong-secret\n"
|
||||
"# note OTHER_PASS=wrong-secret\n"
|
||||
"EMPTY_PASS=\n"
|
||||
"LONG_PASS=" + "x" * 513 + "\n"
|
||||
)
|
||||
(backups / "late.sh").write_text("# filler\n" * 200 + "LATE_PASS=late-secret\n")
|
||||
(backups / "oversized.sh").write_text("BIG_PASS=big-secret\n" + "x" * 65536)
|
||||
outside = root / "outside.sh"
|
||||
outside.write_text("LINK_PASS=link-secret\n")
|
||||
(backups / "linked.sh").symlink_to(outside)
|
||||
os.mkfifo(backups / "pipe.sh")
|
||||
locked = backups / "locked.sh"
|
||||
locked.write_text("LOCKED_PASS=locked-secret\n")
|
||||
locked.chmod(0)
|
||||
if os.access(locked, os.R_OK):
|
||||
# Privileged test runners can still read mode-000 files.
|
||||
locked.unlink()
|
||||
|
||||
output = self.run_module([backups])
|
||||
findings = [line for line in output.splitlines() if "[REDACTED]" in line]
|
||||
self.assertEqual(findings, [
|
||||
f" {script}:1: OFFSITE_PASS=[REDACTED] (active)",
|
||||
f" {script}:2: DB_PASS=[REDACTED] (commented; stale/unknown)",
|
||||
])
|
||||
for value in ("active-secret", "old-secret", "wrong-secret", "late-secret",
|
||||
"big-secret", "link-secret", "locked-secret"):
|
||||
self.assertNotIn(value, output)
|
||||
self.assertLessEqual(len(findings), 50)
|
||||
|
||||
def test_backup_pass_scan_caps_directories_and_findings(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
paths = []
|
||||
for index in range(13):
|
||||
backups = Path(temp) / f"backup{index:02d}"
|
||||
backups.mkdir()
|
||||
(backups / "vars.sh").write_text(
|
||||
"".join(f"KEY{key}_PASS=secret{key}\n" for key in range(6))
|
||||
)
|
||||
paths.append(backups)
|
||||
|
||||
output = self.run_module(paths)
|
||||
findings = [line for line in output.splitlines() if "[REDACTED]" in line]
|
||||
self.assertEqual(len(findings), 50)
|
||||
self.assertFalse(any("KEY5_PASS" in line for line in findings))
|
||||
self.assertFalse(any(str(paths[-1] / "vars.sh") in line for line in findings))
|
||||
self.assertNotIn("secret0", output)
|
||||
|
||||
def test_readable_root_archives_report_only_actual_group_or_world_access(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
backups = root / "backups"
|
||||
backups.mkdir()
|
||||
for name in ("group copy.tar.gz", "locked.tar.gz", "other-group.zip",
|
||||
"world-readable.txz", "ordinary.txt", "nested.tar"):
|
||||
(backups / name).write_text("fixture contents")
|
||||
(backups / "group copy.tar.gz").chmod(0o640)
|
||||
(backups / "locked.tar.gz").chmod(0o600)
|
||||
(backups / "other-group.zip").chmod(0o640)
|
||||
nested = backups / "subdir"
|
||||
nested.mkdir()
|
||||
(nested / "deep.tar.gz").write_text("fixture contents")
|
||||
(backups / "link.tar.gz").symlink_to(backups / "group copy.tar.gz")
|
||||
|
||||
bin_dir = root / "bin"
|
||||
bin_dir.mkdir()
|
||||
for name in ("ls", "find", "head", "awk", "cut", "sed"):
|
||||
(bin_dir / name).symlink_to(shutil.which(name))
|
||||
self.assertFalse((bin_dir / "stat").exists())
|
||||
|
||||
script = r'''
|
||||
print_2title() { :; }
|
||||
id() {
|
||||
case "$1" in
|
||||
-u) printf '%s\n' 1000 ;;
|
||||
-G) printf '%s\n' '1000 4242' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
ls() {
|
||||
if [ "$1" = -ldn ]; then
|
||||
case "$2" in
|
||||
*'group copy.tar.gz') printf '%s\n' '-rw-r----- 1 0 4242 123 fixture' ;;
|
||||
*'locked.tar.gz') printf '%s\n' '-rw------- 1 0 4242 123 fixture' ;;
|
||||
*'other-group.zip') printf '%s\n' '-rw-r----- 1 0 8888 123 fixture' ;;
|
||||
*'world-readable.txz') printf '%s\n' '-rw-r--r-- 1 0 8888 123 fixture' ;;
|
||||
*'ordinary.txt') printf '%s\n' '-rw-r--r-- 1 0 8888 123 fixture' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
else
|
||||
command ls "$@"
|
||||
fi
|
||||
}
|
||||
stat() { return 127; }
|
||||
. "$BACKUP_MODULE"
|
||||
'''
|
||||
env = dict(os.environ, BACKUP_MODULE=str(self.module),
|
||||
PSTORAGE_BACKUPS=str(backups), SEARCH_IN_FOLDER="", DEBUG="",
|
||||
E="E", SED_RED="", PATH=str(bin_dir))
|
||||
result = subprocess.run(["/bin/sh", "-c", script], env=env,
|
||||
capture_output=True, text=True, check=True)
|
||||
findings = [line for line in result.stdout.splitlines()
|
||||
if "Possible exposure:" in line]
|
||||
self.assertEqual(len(findings), 2, result.stdout)
|
||||
self.assertTrue(any("group copy.tar.gz" in line and
|
||||
"access=group-readable" in line for line in findings))
|
||||
self.assertTrue(any("world-readable.txz" in line and
|
||||
"access=world-readable" in line for line in findings))
|
||||
for name in ("locked.tar.gz", "other-group.zip", "ordinary.txt",
|
||||
"nested.tar", "deep.tar.gz", "link.tar.gz"):
|
||||
self.assertFalse(any(name in line for line in findings), name)
|
||||
self.assertNotIn("fixture contents", result.stdout)
|
||||
|
||||
def test_small_other_user_archives_are_candidates_without_reading_contents(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
backups = Path(temp) / "backups"
|
||||
backups.mkdir()
|
||||
names = ("other-user.tar.gz", "shared.zip", "too-large.tar.gz",
|
||||
"current-user.tar.gz", "locked.tar.gz", "ordinary.txt")
|
||||
for name in names:
|
||||
(backups / name).write_text("private fixture contents")
|
||||
(backups / "link.tar.gz").symlink_to(backups / "other-user.tar.gz")
|
||||
|
||||
script = r'''
|
||||
print_2title() { :; }
|
||||
id() {
|
||||
case "$1" in
|
||||
-u) printf '%s\n' 1000 ;;
|
||||
-G) printf '%s\n' '1000 4242' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
ls() {
|
||||
if [ "$1" = -ldn ]; then
|
||||
case "$2" in
|
||||
*'other-user.tar.gz') printf '%s\n' '-rw-r--r-- 1 2000 2000 123 fixture' ;;
|
||||
*'shared.zip') printf '%s\n' '-rw-r----- 1 2000 4242 10485760 fixture' ;;
|
||||
*'too-large.tar.gz') printf '%s\n' '-rw-r--r-- 1 2000 2000 10485761 fixture' ;;
|
||||
*'current-user.tar.gz') printf '%s\n' '-rw-r--r-- 1 1000 1000 123 fixture' ;;
|
||||
*'locked.tar.gz') printf '%s\n' '-rw------- 1 2000 2000 123 fixture' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
else
|
||||
command ls "$@"
|
||||
fi
|
||||
}
|
||||
. "$BACKUP_MODULE"
|
||||
'''
|
||||
env = dict(os.environ, BACKUP_MODULE=str(self.module),
|
||||
PSTORAGE_BACKUPS=str(backups), SEARCH_IN_FOLDER="", DEBUG="",
|
||||
E="E", SED_RED="")
|
||||
result = subprocess.run(["/bin/sh", "-c", script], env=env,
|
||||
capture_output=True, text=True, check=True)
|
||||
findings = [line for line in result.stdout.splitlines()
|
||||
if "Possible exposure:" in line]
|
||||
self.assertEqual(len(findings), 2, result.stdout)
|
||||
self.assertTrue(any("other-user.tar.gz" in line and
|
||||
"access=world-readable" in line and
|
||||
"contents unverified" in line for line in findings))
|
||||
self.assertTrue(any("shared.zip" in line and
|
||||
"access=group-readable" in line and
|
||||
"size=10485760" in line for line in findings))
|
||||
for name in ("too-large.tar.gz", "current-user.tar.gz", "locked.tar.gz",
|
||||
"ordinary.txt", "link.tar.gz"):
|
||||
self.assertFalse(any(name in line for line in findings), name)
|
||||
self.assertNotIn("private fixture contents", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,115 @@
|
||||
import os
|
||||
import shlex
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class BelowLogDirectoryTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.module = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "builder/linpeas_parts/7_software_information/Below_Log_Directory.sh"
|
||||
)
|
||||
|
||||
def _fixture(self, base, directory_mode=0o777, installed=True):
|
||||
old_umask = os.umask(0)
|
||||
try:
|
||||
directory = base / "below-logs"
|
||||
directory.mkdir(mode=directory_mode)
|
||||
binary = base / "below"
|
||||
if installed:
|
||||
fd = os.open(binary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o755)
|
||||
os.close(fd)
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
self.assertEqual(stat.S_IMODE(directory.stat().st_mode), directory_mode)
|
||||
return binary, directory, directory / "error_root.log"
|
||||
|
||||
def _run(self, binary, directory, log, sudo_rule=True, deny_access=False, symlink_log=False, ls_mode=None):
|
||||
sudo_text = f" (root) NOPASSWD: {binary} *" if sudo_rule is True else sudo_rule or ""
|
||||
script = "\n".join(
|
||||
[
|
||||
"SEARCH_IN_FOLDER=1",
|
||||
"MACPEAS=",
|
||||
f"sudo_l_output={shlex.quote(sudo_text)}",
|
||||
"sudo_l_cached_output=",
|
||||
"sudo_l_password_output=",
|
||||
"ps() { return 1; }",
|
||||
'print_2title() { printf "TITLE: %s\\n" "$1"; }',
|
||||
f". {shlex.quote(str(self.module))}",
|
||||
(
|
||||
f"ls() {{ printf '%s\\n' {shlex.quote(ls_mode)}; }}"
|
||||
if ls_mode
|
||||
else ":"
|
||||
),
|
||||
"lp_below_effective_access() { return 1; }" if deny_access else ":",
|
||||
(
|
||||
f"lp_below_is_symlink() {{ [ \"$1\" = {shlex.quote(str(log))} ]; }}"
|
||||
if symlink_log
|
||||
else ":"
|
||||
),
|
||||
f"lp_below_check_log_directory {shlex.quote(str(binary))} "
|
||||
f"{shlex.quote(str(directory))} {shlex.quote(str(log))}",
|
||||
]
|
||||
)
|
||||
return subprocess.run(["sh", "-c", script], capture_output=True, text=True, check=False)
|
||||
|
||||
def test_writable_nonsticky_directory_and_root_sudo_rule(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
binary, directory, log = self._fixture(Path(temp))
|
||||
result = self._run(binary, directory, log)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("potential privileged log-file exposure", result.stdout)
|
||||
self.assertIn("effective write/search access; nonsticky", result.stdout)
|
||||
self.assertIn("Patch status: unknown", result.stdout)
|
||||
self.assertNotIn("Vulnerable to CVE", result.stdout)
|
||||
|
||||
def test_sticky_directory_is_excluded(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
binary, directory, log = self._fixture(Path(temp))
|
||||
result = self._run(binary, directory, log, ls_mode="drwxrwxrwt")
|
||||
self.assertEqual(result.stdout, "")
|
||||
|
||||
def test_no_effective_access_is_excluded_even_if_mode_is_writable(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
binary, directory, log = self._fixture(Path(temp))
|
||||
result = self._run(binary, directory, log, deny_access=True, ls_mode="drwxrwxrwx+")
|
||||
self.assertEqual(result.stdout, "")
|
||||
|
||||
def test_owner_writable_0755_uses_effective_access_not_world_bits(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
binary, directory, log = self._fixture(Path(temp), 0o755)
|
||||
result = self._run(binary, directory, log)
|
||||
self.assertIn("effective write/search access", result.stdout)
|
||||
|
||||
def test_installed_binary_and_privileged_path_are_both_required(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
binary, directory, log = self._fixture(Path(temp), installed=False)
|
||||
self.assertEqual(self._run(binary, directory, log).stdout, "")
|
||||
old_umask = os.umask(0)
|
||||
try:
|
||||
fd = os.open(binary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o755)
|
||||
os.close(fd)
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
self.assertEqual(self._run(binary, directory, log, sudo_rule=False).stdout, "")
|
||||
|
||||
def test_negated_sudo_rule_is_not_privileged_evidence(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
binary, directory, log = self._fixture(Path(temp))
|
||||
result = self._run(binary, directory, log, sudo_rule=f" (root) NOPASSWD: !{binary}")
|
||||
self.assertEqual(result.stdout, "")
|
||||
|
||||
def test_existing_symlink_is_labeled_without_reading_target(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
binary, directory, log = self._fixture(Path(temp))
|
||||
result = self._run(binary, directory, log, symlink_log=True)
|
||||
self.assertIn("is a symlink (target not read)", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Exercise the bounded, passive binfmt capability-helper clue."""
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MODULE = Path(__file__).resolve().parents[1] / "builder/linpeas_parts/8_interesting_perms_files/4_Capabilities.sh"
|
||||
REGISTER = b"/proc/sys/fs/binfmt_misc/register"
|
||||
|
||||
|
||||
class BinfmtCapabilityHelperTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
|
||||
def binary(self, name, payload=REGISTER, executable=True):
|
||||
path = self.root / name
|
||||
path.write_bytes(payload)
|
||||
path.chmod(0o755 if executable else 0o644)
|
||||
return path
|
||||
|
||||
def scan(self, records):
|
||||
source = MODULE.read_text()
|
||||
start = source.index(" binfmt_probe_count=0\n cap_inventory_count=0\n getcap -r / 2>/dev/null | head -n 51 | while read cb; do")
|
||||
end = source.index("\n checkSnapConfineCVE20268933", start)
|
||||
listing = source[start:end]
|
||||
record_text = "\n".join(records)
|
||||
script = (
|
||||
"E=E; SED_RED='<R>'; SED_RED_YELLOW='<H>'; capsB='cap_dac_override'; "
|
||||
"capsVB=''; IAMROOT=''; STRINGS=\"$(command -v strings)\";\n"
|
||||
f"getcap() {{ printf '%s\\n' {shlex.quote(record_text)}; }}\n"
|
||||
f"{listing}\n"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["sh", "-c", script],
|
||||
env=os.environ,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
return result.stdout
|
||||
|
||||
def test_effective_capability_and_exact_register_literal(self):
|
||||
path = self.binary("helper", b"prefix\0" + REGISTER + b"\0suffix")
|
||||
for rights in ("cap_dac_override=ep", "cap_dac_override+eip", "cap_dac_override,cap_net_raw=ep"):
|
||||
with self.subTest(rights=rights):
|
||||
output = self.scan([f"{path} {rights}"])
|
||||
self.assertIn(f"binfmt_misc helper review candidate: {path}", output)
|
||||
self.assertNotIn("prefix", output)
|
||||
|
||||
def test_missing_effective_capability_and_unrelated_literal(self):
|
||||
path = self.binary("helper")
|
||||
for rights in ("cap_dac_override=p", "cap_dac_override=ip", "cap_net_raw=ep"):
|
||||
with self.subTest(rights=rights):
|
||||
self.assertNotIn("binfmt_misc helper review candidate", self.scan([f"{path} {rights}"]))
|
||||
unrelated = self.binary("unrelated", b"/proc/sys/fs/binfmt_misc/register-extra")
|
||||
self.assertNotIn("binfmt_misc helper review candidate", self.scan([f"{unrelated} cap_dac_override=ep"]))
|
||||
|
||||
def test_oversized_symlink_and_non_executable_are_skipped(self):
|
||||
oversized = self.binary("oversized", REGISTER + b"x" * 1048576)
|
||||
target = self.binary("target")
|
||||
link = self.root / "link"
|
||||
link.symlink_to(target)
|
||||
non_exec = self.binary("nonexec", executable=False)
|
||||
for path in (oversized, link, non_exec):
|
||||
with self.subTest(path=path):
|
||||
self.assertNotIn("binfmt_misc helper review candidate", self.scan([f"{path} cap_dac_override=ep"]))
|
||||
|
||||
def test_at_most_eight_qualifying_binaries_are_inspected(self):
|
||||
records = []
|
||||
for i in range(9):
|
||||
path = self.binary(f"helper{i}")
|
||||
records.append(f"{path} cap_dac_override=ep")
|
||||
output = self.scan(records)
|
||||
self.assertEqual(output.count("binfmt_misc helper review candidate:"), 8)
|
||||
self.assertNotIn("binfmt_misc helper review candidate: " + str(self.root / "helper8"), output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loaded 100 of 269 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user