#!/usr/bin/env bash # ============================================================================= # php2shell.sh – Prüfung und Übergangsmaßnahmen für „PHP2Shell“ # CVE-2026-109618 · CVE-2026-109619 (CVSS v4.0: 9.2) # # Version 1.0.0 · Datenstand 2026-10-21 # Lizenz MIT (siehe Dateiende) # Autor hostspezial GmbH # Beitrag https://www.hostspezial.de/aktuelles/php2shell-cve-2026-109618-patchen.html # ----------------------------------------------------------------------------- # WICHTIG # · Das Skript schließt die Lücke NICHT. Vollständig schützt nur ein PHP-Update # (8.2.35 · 8.3.36 · 8.4.27 · 8.5.12) oder ein Backport Ihres Herstellers. # · "check" ist rein lesend. "mitigate" und "rollback" ändern Konfiguration und # starten Dienste neu – nur nach Rückfrage (oder mit --yes), mit Backup. # · Nutzung auf eigene Verantwortung, ohne Gewähr. # ----------------------------------------------------------------------------- # Erkennt u. a.: Debian/Ubuntu (inkl. Backports), RHEL/Alma/Rocky/Fedora, # SUSE, Alpine, Arch · Apache (mod_php, FastCGI), nginx, LiteSpeed · PHP-FPM, # CGI, CLI · Plesk (inkl. Panel + Domains), cPanel EA-PHP, CloudLinux alt-php, # DirectAdmin, ISPConfig, CloudPanel, HestiaCP, Imunify360, TuxCare, Remi, sury # · Docker-/Podman-Container (Erkennung und Versionsprüfung) # ============================================================================= if [ -z "${BASH_VERSINFO:-}" ] || [ "${BASH_VERSINFO[0]}" -lt 4 ]; then echo "php2shell.sh benötigt bash >= 4 (Alpine: apk add bash)." >&2; exit 3 fi readonly VERSION="1.0.0" readonly DATA_DATE="2026-10-21" readonly SELF="${0##*/}" if [[ "$0" == */* ]]; then RUN="$0"; else RUN="bash $0"; fi # so, wie es aufgerufen wurde export PATH="$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin" # ----------------------------------------------------------------------------- # Datenbasis – bei neuen Erkenntnissen hier aktualisieren # ----------------------------------------------------------------------------- declare -A FIX=( [8.2]=8.2.35 [8.3]=8.3.36 [8.4]=8.4.27 [8.5]=8.5.12 ) # Upstream-Fixversionen declare -A UBU=( [8.1.2]=8.1.2-1ubuntu2.28 [8.3.6]=8.3.6-0ubuntu0.24.04.13 \ [8.5.4]=8.5.4-0ubuntu1.5 [8.5.9]=8.5.9-0ubuntu2.1 ) # Ubuntu USN-8948-1 readonly BLOG_URL="https://www.hostspezial.de/aktuelles/php2shell-cve-2026-109618-patchen.html" readonly CVE_RE='CVE-2026-10961[89]' readonly SOURCES=( "PHP-Advisory CVE-2026-109618: https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" "PHP-Advisory CVE-2026-109619: https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" "Ubuntu USN-8948-1: https://ubuntu.com/security/notices/USN-8948-1" "Plesk-Advisory: https://support.plesk.com/hc/en-us/articles/44397729587351" "BSI-Warnung: https://www.bsi.bund.de/SharedDocs/Cybersicherheitswarnungen/DE/2026/2026-297981-1032.html" ) readonly MARK="php2shell-mitigation" readonly DROPIN="zz-php2shell-mitigation.ini" readonly APACHE_SNIPPET="zz-php2shell-mitigation" readonly STATE_DIR="/var/lib/php2shell" readonly MANIFEST="$STATE_DIR/manifest" readonly LOG_FILE="/var/log/php2shell.log" readonly PANEL_PORTS="8443 8880 2083 2087 2096 2222 8083 8090 7080 2030 2031" # ----------------------------------------------------------------------------- # Optionen # ----------------------------------------------------------------------------- usage() { cat <&2; exit 3; } for a in "$@"; do case "$a" in check|mitigate|rollback) MODE="$a" ;; --memlimit) MEMLIMIT=1 ;; --fix-pools) FIX_POOLS=1 ;; --all) ALL=1 ;; --web) WEB=1 ;; --no-scan) SCAN=0 ;; --since=*) SINCE="${a#*=}" ;; --scan-path=*) EXTRA_SCAN+=("${a#*=}") ;; --scan-timeout=*) SCAN_TIMEOUT="${a#*=}" ;; --json) JSON=1 ;; -y|--yes) YES=1 ;; -n|--dry-run) DRY=1 ;; --no-restart) RESTART=0 ;; --no-color) NOCOLOR=1 ;; -V|--version) echo "$SELF $VERSION (Datenstand $DATA_DATE)"; exit 0 ;; -h|--help) usage; exit 0 ;; *) die "Unbekannte Option '$a' – siehe $SELF --help" ;; esac done if [ "$MODE" != mitigate ]; then [ $MEMLIMIT -eq 1 ] && die "--memlimit wirkt nur mit 'mitigate'. Gemeint ist vermutlich: $RUN mitigate --memlimit" [ $FIX_POOLS -eq 1 ] && die "--fix-pools wirkt nur mit 'mitigate'." [ $ALL -eq 1 ] && die "--all wirkt nur mit 'mitigate'." fi if [ "$MODE" = check ]; then [ $DRY -eq 1 ] && die "--dry-run wirkt nur mit 'mitigate' oder 'rollback'." [ $YES -eq 1 ] && die "--yes wirkt nur mit 'mitigate' oder 'rollback'." else [ $WEB -eq 1 ] && die "--web wirkt nur mit 'check'." [ $JSON -eq 1 ] && die "--json wirkt nur mit 'check'." fi [[ "$SCAN_TIMEOUT" =~ ^[0-9]+$ ]] || die "--scan-timeout erwartet Sekunden." SINCE_EPOCH=$(date -d "$SINCE" +%s 2>/dev/null) || die "--since: Datum nicht lesbar ('$SINCE'), Format JJJJ-MM-TT." IS_ROOT=0; [ "$(id -u)" -eq 0 ] && IS_ROOT=1 if [ "$MODE" != check ] && [ $IS_ROOT -eq 0 ]; then die "'$MODE' muss als root ausgeführt werden."; fi # ----------------------------------------------------------------------------- # Ausgabe und Befundspeicher # ----------------------------------------------------------------------------- if [ $NOCOLOR -eq 1 ] || [ $JSON -eq 1 ] || [ -n "${NO_COLOR:-}" ] || [ ! -t 1 ]; then R=; G=; Y=; B=; D=; N= else R=$'\e[31m'; G=$'\e[32m'; Y=$'\e[33m'; B=$'\e[1m'; D=$'\e[2m'; N=$'\e[0m' fi F_LVL=(); F_SEC=(); F_TXT=(); F_DET=(); NCRIT=0; NWARN=0; SEC_N=0; SEC_NAME="" p() { [ $JSON -eq 1 ] || [ $QUIET -eq 1 ] || printf '%s\n' "$*"; } rec() { F_LVL+=("$1"); F_SEC+=("$SEC_NAME"); F_TXT+=("$2"); F_DET+=(""); } ok() { rec ok "$*"; p " ${G}[OK]${N} $*"; } info() { rec info "$*"; p " [INFO] $*"; } warn() { rec warn "$*"; NWARN=$((NWARN+1)); p " ${Y}[WARN]${N} $*"; } crit() { rec crit "$*"; NCRIT=$((NCRIT+1)); p " ${R}[KRIT]${N} $*"; } sub() { local i=$(( ${#F_DET[@]} - 1 )); [ $i -ge 0 ] && F_DET[i]+="$*"$'\n'; p " $*"; } hdr() { SEC_N=$((SEC_N+1)); SEC_NAME="$*"; p ""; p "${B}== $SEC_N. $* ==${N}"; } log() { [ $DRY -eq 1 ] && return; mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null; printf '%s [%s] %s\n' "$(date '+%F %T')" "$MODE" "$*" >>"$LOG_FILE" 2>/dev/null; } json_esc() { local s=$1; s=${s//\\/\\\\}; s=${s//\"/\\\"}; s=${s//$'\n'/\\n}; s=${s//$'\t'/\\t}; s=${s//$'\r'/}; s=${s//$'\e'/}; printf '"%s"' "$s"; } # Bilanz der Übergangsmaßnahme (ungepatchte, web-relevante Laufzeiten) UNF_N=0; UNF_UP=0; UNF_MEM=0; MEM_GAPS=""; UP_GAPS="" tally() { # Name upload_progress memory_limit [Grund] UNF_N=$((UNF_N+1)) if [ "$2" = aus ]; then UNF_UP=$((UNF_UP+1)); else UP_GAPS+="$1"$'\n'; fi if [ "$3" = "-1" ]; then UNF_MEM=$((UNF_MEM+1)); else MEM_GAPS+="$1 ($3${4:+, $4})"$'\n'; fi } RECS=(); STALE_N=0; UNUSED_N=0; SUSPECT=0; VERDICT="" recommend() { local r; for r in "${RECS[@]}"; do [ "$r" = "$*" ] && return; done; RECS+=("$*"); } # ----------------------------------------------------------------------------- # Temporäre Dateien, Aufräumen, Sperre # ----------------------------------------------------------------------------- TMPD=$(mktemp -d "${TMPDIR:-/tmp}/php2shell.XXXXXX") || die "mktemp fehlgeschlagen" PROBE="$TMPD/probe.php" cat >"$PROBE" <<'PHP' /dev/null || return 0 { exec 9>"/run/php2shell.lock"; } 2>/dev/null || exec 9>"/tmp/php2shell.lock" flock -n 9 || die "Eine andere Instanz von $SELF läuft bereits." } # ----------------------------------------------------------------------------- # Hilfsfunktionen # ----------------------------------------------------------------------------- ver_ge() { # true, wenn $2 >= $1 (numerische Teile x.y.z) local IFS=. a b i x y read -ra a <<<"$1"; read -ra b <<<"$2" for i in 0 1 2; do x=${a[i]:-0}; y=${b[i]:-0}; x=${x//[!0-9]/}; y=${y//[!0-9]/}; x=${x:-0}; y=${y:-0} (( 10#$y > 10#$x )) && return 0 (( 10#$y < 10#$x )) && return 1 done return 0 } base_ver() { grep -oE '^[0-9]+\.[0-9]+\.[0-9]+' <<<"$1"; } mm_ver() { grep -oE '^[0-9]+\.[0-9]+' <<<"$1"; } same_root() { local r; r=$(readlink "/proc/$1/root" 2>/dev/null) || return 0; [ "$r" = "/" ]; } # unlesbar (kein root) = Host proc_start() { local st bt hz st=$(sed -E 's/^.*\) //' "/proc/$1/stat" 2>/dev/null | awk '{print $20}') bt=$(awk '/^btime/{print $2}' /proc/stat 2>/dev/null); hz=$(getconf CLK_TCK 2>/dev/null || echo 100) [ -n "$st" ] && [ -n "$bt" ] && echo $(( bt + st / hz )) } # Wurde die Datei nach dem Prozessstart ersetzt? (ctime > Startzeit) replaced_after_start() { local s c; s=$(proc_start "$1"); c=$(stat -c %Z "$2" 2>/dev/null) [ -n "$s" ] && [ -n "$c" ] && [ "$c" -gt $(( s + 5 )) ] } has_systemd() { [ -d /run/systemd/system ] && command -v systemctl >/dev/null; } unit_of_pid() { sed -nE 's#.*/([^/]+\.service).*#\1#p' "/proc/$1/cgroup" 2>/dev/null | head -n1; } apache_ctl() { command -v apache2ctl || command -v apachectl || command -v httpd; } apache_pid() { pgrep -o -x apache2 2>/dev/null || pgrep -o -x httpd 2>/dev/null; } run_t() { if command -v timeout >/dev/null; then timeout "$@"; else shift; "$@"; fi; } # Paketzuordnung PKG=none command -v dpkg-query >/dev/null && PKG=dpkg [ $PKG = none ] && command -v rpm >/dev/null && rpm -qf /bin/sh >/dev/null 2>&1 && PKG=rpm [ $PKG = none ] && command -v apk >/dev/null && PKG=apk [ $PKG = none ] && command -v pacman >/dev/null && PKG=pacman pkg_of() { local f; f=$(readlink -f "$1" 2>/dev/null) || return case $PKG in dpkg) dpkg -S "$f" 2>/dev/null | grep -v '^diversion' | head -n1 | cut -d: -f1 ;; rpm) rpm -qf --qf '%{NAME}\n' "$f" 2>/dev/null | grep -v 'not owned' | head -n1 ;; apk) apk info --who-owns "$f" 2>/dev/null | sed -n 's/.* is owned by \(.*\)/\1/p' | head -n1 ;; pacman) pacman -Qqo "$f" 2>/dev/null | head -n1 ;; esac } pkg_ver() { [ -n "$1" ] || return case $PKG in dpkg) dpkg-query -W -f='${Version}' "$1" 2>/dev/null ;; rpm) rpm -q --qf '%{VERSION}-%{RELEASE}' "$1" 2>/dev/null ;; apk) sed -nE "s/^${1}-([0-9].*)$/\1/p" <<<"$1" ;; pacman) pacman -Q "$1" 2>/dev/null | awk '{print $2}' ;; esac } # 0 = CVE im Changelog · 1 = Changelog ohne CVE · 2 = kein Changelog verfügbar changelog_state() { local p="$1" f found=2 cl [ -n "$p" ] || return 2 case $PKG in dpkg) for f in /usr/share/doc/"$p"/changelog.Debian.gz /usr/share/doc/"$p"/changelog.gz; do [ -e "$f" ] || continue; found=1 zgrep -qsE "$CVE_RE" "$f" && return 0 done return $found ;; rpm) cl=$(rpm -q --changelog "$p" 2>/dev/null); [ -z "$cl" ] && return 2 grep -qE "$CVE_RE" <<<"$cl" && return 0; return 1 ;; esac return 2 } # ----------------------------------------------------------------------------- # Umgebung # ----------------------------------------------------------------------------- OS_PRETTY=""; PANELS=(); WEBSERVERS=(); IN_CONTAINER=0; IS_PLESK=0; MANAGED=0 detect_env() { [ -r /etc/os-release ] && OS_PRETTY=$(sed -nE 's/^PRETTY_NAME="?([^"]*)"?$/\1/p' /etc/os-release | head -n1) [ -z "$OS_PRETTY" ] && OS_PRETTY=$(uname -sr) { [ -f /.dockerenv ] || [ -f /run/.containerenv ] || grep -qa 'container=' /proc/1/environ 2>/dev/null; } && IN_CONTAINER=1 { command -v plesk >/dev/null && [ -d /opt/psa ] || [ -d /usr/local/psa ]; } && { PANELS+=("Plesk"); IS_PLESK=1; } [ -d /usr/local/cpanel ] && PANELS+=("cPanel") [ -d /usr/local/directadmin ] && PANELS+=("DirectAdmin") [ -d /usr/local/ispconfig ] && PANELS+=("ISPConfig") { command -v clpctl >/dev/null || [ -d /home/clp ]; } && PANELS+=("CloudPanel") [ -d /usr/local/hestia ] && PANELS+=("HestiaCP") [ -d /usr/local/vesta ] && PANELS+=("VestaCP") [ -d /usr/local/CyberCP ] && PANELS+=("CyberPanel") { [ -d /var/www/froxlor ] || [ -d /var/www/html/froxlor ]; } && PANELS+=("Froxlor") [ -d /etc/webmin/virtual-server ] && PANELS+=("Virtualmin") [ ${#PANELS[@]} -gt 0 ] && MANAGED=1 { [ -f /etc/cloudlinux-release ] || uname -r | grep -q lve; } && PANELS+=("CloudLinux") { [ -d /opt/alt/php-internal ] || command -v imunify360-agent >/dev/null; } && PANELS+=("Imunify360") apache_pid >/dev/null && WEBSERVERS+=("Apache") pgrep -x nginx >/dev/null 2>&1 && WEBSERVERS+=("nginx") { pgrep -f 'litespeed|lshttpd|openlitespeed' >/dev/null 2>&1; } && WEBSERVERS+=("LiteSpeed") pgrep -x caddy >/dev/null 2>&1 && WEBSERVERS+=("Caddy") pgrep -x frankenphp >/dev/null 2>&1 && WEBSERVERS+=("FrankenPHP") return 0 } # ----------------------------------------------------------------------------- # Inventar: PHP-Installationen und ihre SAPIs # Schlüssel einer Installation: @, z. B. /usr@8.1, /opt/plesk/php/8.3@ # ----------------------------------------------------------------------------- INST_KEYS=() declare -A ART=() APID=() ACONF=() ASTALE=() SAPI_ORDER="fpm modphp lsphp cgi panel cli" WEB_SAPIS=" fpm modphp lsphp cgi panel " inst_key_for() { local p=$1 n ver pre n=${p##*/} [ "$p" = /usr/bin/sw-engine ] && { echo "sw-engine@"; return; } ver=$(sed -nE 's/^(php|php-fpm|php-cgi|libphp|mod_php|lsphp)([0-9]+(\.[0-9]+)?)(\.so)?$/\2/p' <<<"$n") [[ "$ver" =~ ^([0-9])([0-9])$ ]] && ver="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" pre=$(sed -E 's#/(s?bin|lib(64)?/(apache2|httpd)(/modules)?|libexec/apache2)/[^/]+$##' <<<"$p") [ -z "$pre" ] && pre="/" echo "$pre@$ver" } add_art() { # sapi pfad [pid] [conf] [stale] local sapi=$1 path=$2 pid=${3:-} conf=${4:-} st=${5:-} key k found=0 [ -n "$path" ] || return key=$(inst_key_for "$path") for k in "${INST_KEYS[@]}"; do [ "$k" = "$key" ] && found=1 && break; done [ $found -eq 0 ] && INST_KEYS+=("$key") if [ -z "${ART[$key|$sapi]:-}" ] || [ -n "$pid" ]; then ART[$key|$sapi]=$path; fi [ -n "$pid" ] && APID[$key|$sapi]=$pid [ -n "$conf" ] && ACONF[$key|$sapi]=$conf [ -n "$st" ] && ASTALE[$key|$sapi]=$st return 0 } # Schlüssel ohne Zweig mit genau einem gleichnamigen Zweig-Schlüssel zusammenführen merge_keys() { local k o pre cands s new=() for k in "${INST_KEYS[@]}"; do [[ "$k" == *@ ]] || continue pre=${k%@}; cands=() for o in "${INST_KEYS[@]}"; do [[ "$o" == "$pre@"?* ]] && cands+=("$o"); done [ ${#cands[@]} -eq 1 ] || continue for s in $SAPI_ORDER; do if [ -n "${ART[$k|$s]:-}" ] && [ -z "${ART[${cands[0]}|$s]:-}" ]; then ART[${cands[0]}|$s]=${ART[$k|$s]}; APID[${cands[0]}|$s]=${APID[$k|$s]:-} ACONF[${cands[0]}|$s]=${ACONF[$k|$s]:-}; ASTALE[${cands[0]}|$s]=${ASTALE[$k|$s]:-} fi unset "ART[$k|$s]" done done for k in "${INST_KEYS[@]}"; do for s in $SAPI_ORDER; do [ -n "${ART[$k|$s]:-}" ] && { new+=("$k"); break; }; done done INST_KEYS=("${new[@]}") } apache_conf_files() { local c; c=$(apache_ctl) || return { "$c" -t -D DUMP_INCLUDES 2>/dev/null | grep -oE '/[^[:space:]]+' ; } | sort -u | while read -r f; do [ -f "$f" ] && echo "$f"; done } CONTAINER_FPM=0 discover() { local p exe conf b m st ap sr # 1) laufende PHP-FPM-Master (Wahrheit: was wirklich läuft) for p in $(pgrep -f 'php-fpm[0-9.]*: master process' 2>/dev/null | sort -u); do same_root "$p" || { CONTAINER_FPM=$((CONTAINER_FPM+1)); continue; } exe=$(readlink "/proc/$p/exe" 2>/dev/null) || continue st=""; [[ "$exe" == *" (deleted)" ]] && { st=deleted; exe=${exe% (deleted)}; } [[ "${exe##*/}" == php-fpm* || "${exe##*/}" == php*-fpm* ]] || continue conf=$(tr '\0' ' ' <"/proc/$p/cmdline" 2>/dev/null | sed -n 's/.*master process (\([^)]*\)).*/\1/p') add_art fpm "$exe" "$p" "$conf" "$st" done # 2) installierte, ggf. nicht laufende FPM-Binaries for b in /usr/sbin/php-fpm* /usr/sbin/php*-fpm /usr/local/sbin/php-fpm /opt/plesk/php/*/sbin/php-fpm \ /opt/cpanel/ea-php*/root/usr/sbin/php-fpm /opt/alt/php*/usr/sbin/php-fpm \ /opt/remi/php*/root/usr/sbin/php-fpm /usr/local/php*/sbin/php-fpm*; do [ -f "$b" ] && [ -x "$b" ] && add_art fpm "$(readlink -f "$b")" done # 3) mod_php: im laufenden Apache geladene Module, sonst aus der Konfiguration ap=$(apache_pid) if [ -n "$ap" ] && same_root "$ap"; then while read -r m; do [ -z "$m" ] && continue st=""; [[ "$m" == *" (deleted)" ]] && { st=deleted; m=${m% (deleted)}; } add_art modphp "$m" "$ap" "" "$st" done < <(grep -oE '/[^ ]*(libphp|mod_php)[^ /]*\.so( \(deleted\))?' "/proc/$ap/maps" 2>/dev/null | sort -u) fi if ! printf '%s\n' "${!ART[@]}" | grep -q '|modphp$'; then sr=$("$(apache_ctl 2>/dev/null)" -S 2>/dev/null | sed -n 's/^ServerRoot: "\(.*\)"/\1/p') while read -r m; do [ -z "$m" ] && continue; [[ "$m" = /* ]] || m="${sr:-/etc/httpd}/$m" [ -f "$m" ] && add_art modphp "$(readlink -f "$m")" done < <(apache_conf_files | xargs -r grep -hiE '^[[:space:]]*LoadModule[[:space:]]+php[0-9]*_module' 2>/dev/null | awk '{print $3}' | sort -u) fi # 4) LiteSpeed lsphp for b in /usr/local/lsws/lsphp*/bin/lsphp /opt/cpanel/ea-php*/root/usr/bin/lsphp /opt/alt/php*/usr/bin/lsphp; do [ -f "$b" ] && [ -x "$b" ] && add_art lsphp "$(readlink -f "$b")" done # 5) CGI/FastCGI for b in /usr/bin/php-cgi* /opt/plesk/php/*/bin/php-cgi /opt/cpanel/ea-php*/root/usr/bin/php-cgi \ /opt/alt/php*/usr/bin/php-cgi /usr/local/php*/bin/php-cgi /usr/local/bin/php-cgi; do [ -f "$b" ] && [ -x "$b" ] && add_art cgi "$(readlink -f "$b")" done # 6) CLI for b in "$(command -v php 2>/dev/null)" /usr/bin/php[0-9]* /usr/local/bin/php /opt/plesk/php/*/bin/php \ /opt/cpanel/ea-php*/root/usr/bin/php /opt/alt/php*/usr/bin/php /opt/remi/php*/root/usr/bin/php \ /usr/local/php*/bin/php /usr/local/lsws/lsphp*/bin/php; do [ -n "$b" ] && [ -f "$b" ] && [ -x "$b" ] && add_art cli "$(readlink -f "$b")" done # 7) PHP interner Verwaltungsoberflächen [ -x /usr/bin/sw-engine ] && add_art panel /usr/bin/sw-engine "$(pgrep -o -f 'sw-engine-fpm: master' 2>/dev/null)" [ -x /usr/local/cpanel/3rdparty/bin/php ] && add_art panel "$(readlink -f /usr/local/cpanel/3rdparty/bin/php)" [ -x /usr/local/lsws/admin/fcgi-bin/admin_php ] && add_art panel /usr/local/lsws/admin/fcgi-bin/admin_php merge_keys } inst_label() { local k=$1 pre=${1%@*} ver=${1##*@} case "$pre" in sw-engine) echo "Plesk-Panel (sw-engine)" ;; /opt/plesk/php/*) echo "Plesk PHP ${pre##*/}" ;; /opt/cpanel/ea-php*) echo "cPanel EA-PHP $(sed -E 's#.*/ea-php([0-9])([0-9]+).*#\1.\2#' <<<"$pre")" ;; /opt/alt/php-internal*) echo "Sicherheitssoftware (internes PHP)" ;; /opt/alt/php*) echo "CloudLinux alt-php $(sed -E 's#.*/php([0-9])([0-9]+).*#\1.\2#' <<<"$pre")" ;; /opt/remi/php*) echo "Remi PHP ${pre##*/php}" ;; /usr/local/lsws/admin*) echo "LiteSpeed WebAdmin" ;; /usr/local/lsws/*) echo "LiteSpeed ${pre##*/}" ;; /usr/local/cpanel/*) echo "cPanel intern" ;; /usr/local/php*) echo "DirectAdmin/Custom ${pre##*/}" ;; /usr/local) echo "PHP aus Quellcode/Docker-Image (/usr/local)" ;; /usr) echo "Distribution${ver:+ PHP $ver}" ;; *) echo "$pre${ver:+ ($ver)}" ;; esac } update_hint() { local k=$1 pre=${1%@*} pv=${2:-} case "$pre" in sw-engine|/opt/plesk/php/*) echo "plesk installer update" ;; /opt/cpanel/ea-php*) echo "dnf/yum update 'ea-php*'" ;; /opt/alt/php-internal*) echo "Hersteller-Update der Sicherheitssoftware einspielen (Paket alt-php-internal)" ;; /opt/alt/php*) echo "dnf/yum update 'alt-php*'" ;; /opt/remi/php*) echo "dnf update 'php*' (Remi)" ;; /usr/local/lsws/*) echo "lsphp-Pakete des LiteSpeed-Repositorys aktualisieren" ;; /usr/local/php*) echo "DirectAdmin: cd /usr/local/directadmin/custombuild && ./build update && ./build php" ;; /usr/local) echo "PHP neu bauen bzw. Container-Image aktualisieren und neu ausrollen" ;; *) case $PKG in dpkg) echo "apt update && apt install --only-upgrade '?name(^php)' '?name(^libapache2-mod-php)'" ;; rpm) echo "dnf upgrade 'php*' (bzw. yum update 'php*')" ;; apk) echo "apk update && apk upgrade" ;; pacman) echo "pacman -Syu" ;; *) echo "PHP über den Hersteller/Paketmanager aktualisieren" ;; esac ;; esac } # Fix-Status: setzt FS (FIXED|UNFIXED|EOL|UNKNOWN) und FR (Begründung) fix_status() { local key=$1 art=$2 ver=$3 base mm pkg="" pv="" cs bd FS=UNKNOWN; FR="" base=$(base_ver "$ver"); mm=$(mm_ver "$ver") [ -z "$base" ] && { FR="Version nicht ermittelbar"; return; } if [ "$art" = /usr/bin/sw-engine ]; then if [ -n "${FIX[$mm]:-}" ] && ver_ge "${FIX[$mm]}" "$base"; then FS=FIXED; FR="Version ≥ ${FIX[$mm]}" else FS=UNFIXED; FR="Panel-PHP – Plesk-Update abwarten: plesk installer update"; fi return fi if [ -n "$art" ] && [ -e "$art" ]; then pkg=$(pkg_of "$art"); pv=$(pkg_ver "$pkg"); fi changelog_state "$pkg"; cs=$? if [ $cs -eq 0 ]; then FS=FIXED; FR="Fix im Paket-Changelog ($pkg $pv)"; return; fi if [ -n "${FIX[$mm]:-}" ] && ver_ge "${FIX[$mm]}" "$base"; then FS=FIXED; FR="Version ≥ ${FIX[$mm]}"; return; fi if [[ "$pv" == *tuxcare* ]]; then FS=UNKNOWN; FR="TuxCare-ELS-Paket ($pkg $pv) ohne Fix im Changelog – TuxCare-Advisory prüfen"; return; fi local uv=$pv; [ -z "$uv" ] && [[ "$ver" =~ [0-9]ubuntu[0-9] ]] && uv=$ver if [[ "$uv" =~ [0-9]ubuntu[0-9] ]]; then if [ -n "${UBU[$base]:-}" ]; then if { command -v dpkg >/dev/null && dpkg --compare-versions "$uv" ge "${UBU[$base]}"; }; then FS=FIXED; FR="Ubuntu-Backport ≥ ${UBU[$base]}" else FS=UNFIXED; FR="Ubuntu-Paket $uv < ${UBU[$base]}"; fi else FS=UNFIXED; FR="Ubuntu-Paket ${pkg:+$pkg }$uv ohne Fix im Changelog (Ubuntu 20.04 und älter: Fix nur mit Ubuntu Pro/ESM)" fi return fi if [[ "$pkg" == alt-php-internal* ]]; then FS=UNFIXED; FR="internes PHP einer Sicherheitssoftware ($pkg $pv) – Fix kommt über deren Update"; return; fi if [[ "$pkg" == plesk-php* ]] && [ -z "${FIX[$mm]:-}" ]; then bd=$(grep -oE '\.[0-9]{6}\.' <<<"$pv" | tr -d . | head -n1) if [ -n "$bd" ] && [ "$bd" -ge 261021 ] 2>/dev/null; then FS=UNKNOWN; FR="Plesk-Build $bd nach Fix-Release – Plesk-Advisory prüfen"; return; fi FS=EOL; FR="Zweig $mm ist EOL; Plesk-Fix laut Advisory in Arbeit ($pkg $pv) – bis dahin Domains auf ≥ 8.2 umstellen"; return fi if [ -n "${FIX[$mm]:-}" ]; then FS=UNFIXED; FR="Ziel ${FIX[$mm]} oder Hersteller-Backport${pkg:+ (Paket $pkg $pv)}" else FS=EOL; FR="Zweig $mm ist EOL – kein Upstream-Fix${pkg:+ ($pkg $pv)}; Hersteller-Backport prüfen oder auf ≥ 8.2 wechseln"; fi } # Fix-Status nur anhand eines Versionsstrings (Webtest, Container) declare -A VER_FS=() fix_status_ver() { local ver=$1 base mm; base=$(base_ver "$ver"); mm=$(mm_ver "$ver") FS=UNKNOWN; FR="" [ -n "${VER_FS[$ver]:-}" ] && { FS=${VER_FS[$ver]}; FR="wie lokale Installation $ver"; return; } [ -z "$base" ] && { FR="Version nicht ermittelbar"; return; } if [ -n "${FIX[$mm]:-}" ] && ver_ge "${FIX[$mm]}" "$base"; then FS=FIXED; FR="Version ≥ ${FIX[$mm]}"; return; fi if [[ "$ver" =~ [0-9]ubuntu[0-9] ]] && [ -n "${UBU[$base]:-}" ] && command -v dpkg >/dev/null; then dpkg --compare-versions "$ver" ge "${UBU[$base]}" && { FS=FIXED; FR="Ubuntu-Backport"; return; } FS=UNFIXED; FR="Ubuntu-Paket < ${UBU[$base]}"; return fi if [[ "$ver" =~ [-+~] ]]; then FS=UNKNOWN; FR="Distributionsversion $ver – Backport nicht prüfbar"; return; fi if [ -n "${FIX[$mm]:-}" ]; then FS=UNFIXED; FR="Ziel ${FIX[$mm]}"; else FS=EOL; FR="Zweig $mm ist EOL"; fi } # ----------------------------------------------------------------------------- # Werte je SAPI ermitteln # ----------------------------------------------------------------------------- declare -A S_VER=() S_UP=() S_MEM=() S_POST=() S_SCAN=() S_INI=() S_FS=() S_FR=() S_NOTE=() S_USE=() I_FS=() I_FR=() I_VER=() I_USED=() PL_USE=() PL_DOMS=() probe_bin() { "$@" -q "$PROBE" 2>/dev/null | tail -n1; } set_vals() { # key sapi "ver|up|mem|ini|scan|post" local IFS='|' v u m i s pm read -r v u m i s pm <<<"$3" S_VER[$1|$2]=$v; S_UP[$1|$2]=$u; S_MEM[$1|$2]=$m; S_INI[$1|$2]=$i; S_SCAN[$1|$2]=$s; S_POST[$1|$2]=$pm } analyze_sapi() { local k=$1 s=$2 path=${ART[$1|$2]} pre=${1%@*} br=${1##*@} out ver cli case $s in fpm) out=$("$path" -i 2>/dev/null) ver=$(sed -n 's/^PHP Version => //p' <<<"$out" | head -n1) [ -z "$ver" ] && ver=$("$path" -v 2>/dev/null | awk 'NR==1{print $2}') set_vals "$k" fpm "$ver|$(sed -n 's/^session.upload_progress.enabled => \([^ ]*\) .*/\1/p' <<<"$out" | sed 's/On/AN/;s/Off/aus/')|$(sed -n 's/^memory_limit => \([^ ]*\) .*/\1/p' <<<"$out")|$(sed -n 's/^Loaded Configuration File => //p' <<<"$out" | sed 's/(none)//')|$(sed -n 's/^Scan this dir for additional .ini files => //p' <<<"$out" | sed 's/(none)//')|$(sed -n 's/^post_max_size => \([^ ]*\) .*/\1/p' <<<"$out")" ;; modphp) cli=${ART[$k|cli]:-} if [ -n "$br" ] && [ -f "/etc/php/$br/apache2/php.ini" ] && [ -x "/usr/bin/php$br" ]; then out=$(PHP_INI_SCAN_DIR="/etc/php/$br/apache2/conf.d" probe_bin "/usr/bin/php$br" -c "/etc/php/$br/apache2/php.ini") out=$(awk -F'|' -v OFS='|' -v I="/etc/php/$br/apache2/php.ini" -v D="/etc/php/$br/apache2/conf.d" '{ $4=I; $5=D; print }' <<<"$out") elif [ -n "$cli" ]; then out=$(probe_bin "$cli"); S_NOTE[$k|modphp]="Werte wie CLI ermittelt" else out=""; fi set_vals "$k" modphp "$out" ver=$(pkg_ver "$(pkg_of "$path")") [ -n "$ver" ] && S_VER[$k|modphp]=$ver # mod_php-Erzwingung durch dieses Skript if apache_snippet_active; then S_UP[$k|modphp]=aus grep -qsE 'php_admin_value[[:space:]]+memory_limit[[:space:]]+-1' "$(apache_snippet_path)" && S_MEM[$k|modphp]=-1 fi ;; lsphp) cli="$(dirname "$path")/php" if [ -x "$cli" ]; then set_vals "$k" lsphp "$(probe_bin "$cli")"; S_NOTE[$k|lsphp]="Werte über $cli ermittelt" else S_VER[$k|lsphp]=$("$path" -v 2>/dev/null | awk 'NR==1{print $2}'); fi ;; panel) set_vals "$k" panel "$(probe_bin "$path")" if [ "$path" = /usr/bin/sw-engine ] && [ -f /opt/psa/admin/conf/php.ini ]; then out=$(probe_bin "$path" -c /opt/psa/admin/conf/php.ini) [ "$(cut -d'|' -f2 <<<"$out")" = AN ] && S_UP[$k|panel]=AN [ "$(cut -d'|' -f3 <<<"$out")" != "-1" ] && [ "${S_MEM[$k|panel]}" = "-1" ] && S_MEM[$k|panel]=$(cut -d'|' -f3 <<<"$out") fi ;; cgi|cli) set_vals "$k" "$s" "$(probe_bin "$path")" ;; esac fix_status "$k" "$path" "${S_VER[$k|$s]:-}" S_FS[$k|$s]=$FS; S_FR[$k|$s]=$FR [ -n "${S_VER[$k|$s]:-}" ] && [ "$FS" != UNKNOWN ] && VER_FS[${S_VER[$k|$s]}]=$FS } # Wird diese SAPI tatsächlich für Webanfragen genutzt? Setzt S_USE (Beschreibung) – leer = nicht aktiv sapi_usage() { local k=$1 s=$2 path=${ART[$1|$2]} pid=${APID[$1|$2]:-} br="" n [[ "$k" == /opt/plesk/php/* ]] && br=${k%@*} && br=${br##*/} S_USE[$k|$s]="" case $s in fpm) n=${PL_USE[$br|fpm]:-0} if [ -n "$br" ] && [ "$n" -gt 0 ]; then S_USE[$k|$s]="genutzt von $n Domain(s)${pid:+, PID $pid}" elif [ -n "$pid" ]; then S_USE[$k|$s]="aktiv (PID $pid)"; fi ;; modphp) [ -n "$pid" ] && S_USE[$k|$s]="im laufenden Apache geladen (PID $pid)" ;; cgi) n=${PL_USE[$br|cgi]:-0} if [ -n "$br" ] && [ "$n" -gt 0 ]; then S_USE[$k|$s]="FastCGI für $n Domain(s)" elif pgrep -f "^$path" >/dev/null 2>&1; then S_USE[$k|$s]="aktiv"; fi ;; lsphp) pgrep -f "$path" >/dev/null 2>&1 && S_USE[$k|$s]="aktiv" ;; panel) { [ -n "$pid" ] || pgrep -f "$path" >/dev/null 2>&1; } && S_USE[$k|$s]="aktiv${pid:+ (PID $pid)}" ;; esac return 0 } plesk_usage() { local name php handler v t while IFS=$'\t' read -r name php handler; do [ "$php" = true ] || continue [[ "$handler" =~ plesk-php([0-9])([0-9]+)-(fpm|fastcgi|cgi) ]] || continue v="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"; t=${BASH_REMATCH[3]}; [ "$t" = fpm ] || t=cgi PL_USE[$v|$t]=$(( ${PL_USE[$v|$t]:-0} + 1 )); PL_DOMS[$v]+="$name " done < <(plesk db -Ne "SELECT d.name, IFNULL(h.php,'-'), IFNULL(h.php_handler_id,'-') FROM domains d JOIN hosting h ON h.dom_id=d.id" 2>/dev/null) } rank() { case $1 in UNFIXED|EOL) echo 3;; UNKNOWN) echo 2;; FIXED) echo 1;; *) echo 0;; esac; } sapi_name() { case $1 in fpm) echo "PHP-FPM";; modphp) echo "mod_php";; lsphp) echo "lsphp";; cgi) echo "CGI/FastCGI";; panel) echo "Panel";; cli) echo "CLI";; esac; } # Apache-Snippet dieses Skripts apache_snippet_path() { if [ -d /etc/apache2/conf-available ]; then echo "/etc/apache2/conf-available/$APACHE_SNIPPET.conf" elif [ -d /etc/httpd/conf.d ]; then echo "/etc/httpd/conf.d/$APACHE_SNIPPET.conf" elif [ -d /etc/apache2/conf.d ]; then echo "/etc/apache2/conf.d/$APACHE_SNIPPET.conf" fi } apache_snippet_active() { local p; p=$(apache_snippet_path); [ -n "$p" ] && [ -f "$p" ] || return 1 [ -d /etc/apache2/conf-enabled ] && [[ "$p" == /etc/apache2/conf-available/* ]] && { [ -e "/etc/apache2/conf-enabled/$APACHE_SNIPPET.conf" ] || return 1; } return 0 } # ----------------------------------------------------------------------------- # FPM-Pools # ----------------------------------------------------------------------------- fpm_main_conf() { # key local k=$1 c=${ACONF[$1|fpm]:-} b=${ART[$1|fpm]} [ -z "$c" ] && c=$("$b" -t 2>&1 | sed -n 's/.*configuration file \(.*\) test is successful.*/\1/p' | head -n1) [ -f "$c" ] && echo "$c" } fpm_conf_files() { # main.conf local conf=$1 dir pat f echo "$conf"; dir=$(dirname "$conf") grep -hE '^[[:space:]]*include[[:space:]]*=' "$conf" 2>/dev/null | sed -E 's/^[[:space:]]*include[[:space:]]*=[[:space:]]*//; s/[[:space:]]*;.*$//; s/^"//; s/"$//' | while read -r pat; do [[ "$pat" = /* ]] || pat="$dir/$pat" for f in $pat; do [ -f "$f" ] && echo "$f"; done done } # Ausgabe: Datei|Pool|Direktive|Wert pool_overrides() { local f for f in "$@"; do awk -v F="$f" -v M="$MARK" ' /^[ \t]*\[/ { sec=$0; gsub(/[][ \t]/, "", sec) } index($0, ">>> " M " >>>") { inM=1 } index($0, "<<< " M " <<<") { inM=0 } /^[ \t]*php(_admin)?_(value|flag)\[(memory_limit|session\.upload_progress\.enabled)\]/ { l=$0; sub(/^[ \t]+/, "", l); split(l, kv, "="); k=kv[1]; v=kv[2] gsub(/[ \t]/, "", k); gsub(/^[ \t]+|[ \t]+$/, "", v); sub(/[ \t]*;.*$/, "", v) print F "|" sec "|" k "|" v "|" (inM ? 1 : 0) }' "$f" 2>/dev/null done } is_on() { [[ "${1,,}" =~ ^(1|on|true|yes)$ ]]; } # ----------------------------------------------------------------------------- # Webseiten (Name|Docroot) für --web und die Indizien-Suche # ----------------------------------------------------------------------------- VHOSTS=() discover_vhosts() { local line name root file ln declare -A seen=() if [ $IS_PLESK -eq 1 ]; then while IFS=$'\t' read -r name root php; do [ -n "$name" ] && [ "$root" != NULL ] && [ -z "${seen[$name]:-}" ] && { VHOSTS+=("$name|$root|$php"); seen[$name]=1; } done < <(plesk db -Ne "SELECT d.name, IFNULL(h.www_root,'NULL'), IFNULL(h.php,'false') FROM domains d JOIN hosting h ON h.dom_id=d.id ORDER BY d.name" 2>/dev/null) return fi if command -v nginx >/dev/null && pgrep -x nginx >/dev/null 2>&1; then while IFS='|' read -r name root; do [ -n "$name" ] && [ -d "$root" ] && [ -z "${seen[$name]:-}" ] && { VHOSTS+=("$name|$root|?"); seen[$name]=1; } done < <(nginx -T 2>/dev/null | awk ' /^[ \t]*server[ \t]*\{/ && !inS { inS=1; depth=0; name=""; root="" } inS { if (depth==1 && $1=="server_name" && name=="") { for (i=2;i<=NF;i++){ g=$i; gsub(/;/,"",g); if (g!="_" && g!="" && g !~ /[*~$]/) { name=g; break } } } if (depth==1 && $1=="root" && root=="") { r=$2; gsub(/[;"]/,"",r); if (r !~ /\$/) root=r } o=gsub(/\{/,"{"); c=gsub(/\}/,"}"); depth+=o-c if (depth<=0) { if (name!="" && root!="") print name "|" root; inS=0 } }') fi local ctl; ctl=$(apache_ctl) if [ -n "$ctl" ] && apache_pid >/dev/null; then while read -r name file ln; do [ -z "${seen[$name]:-}" ] || continue root=$(awk -v L="$ln" 'NR>=L { if (tolower($1)=="documentroot") { r=$2; gsub(/"/,"",r); print r; exit } if (NR>L && tolower($0) ~ /<\/virtualhost>/) exit }' "$file" 2>/dev/null) [ -n "$root" ] && [ -d "$root" ] && { VHOSTS+=("$name|$root|?"); seen[$name]=1; } done < <("$ctl" -S 2>/dev/null | sed -nE 's/.*(namevhost|[0-9*]+:[0-9]+)[[:space:]]+([^[:space:]]+)[[:space:]]+\((\/[^:]+):([0-9]+)\).*/\2 \3 \4/p') fi } # ============================================================================= # ANALYSE # ============================================================================= analyze() { detect_env discover local k s worst wr r sev line stale pid any_web unit why # ---------------------------------------------------------------- Umgebung hdr "Umgebung" info "System: $OS_PRETTY$( [ $IN_CONTAINER -eq 1 ] && echo ' (Container)')" info "Webserver: ${WEBSERVERS[*]:-keiner laufend erkannt} · Panels/Plattform: ${PANELS[*]:-keine}" [ $IS_ROOT -eq 0 ] && warn "Nicht als root gestartet – Ergebnisse unvollständig (Pakete, Pools, Prozesse)." [ $CONTAINER_FPM -gt 0 ] && info "$CONTAINER_FPM PHP-FPM-Master laufen in Containern – siehe Abschnitt Container." # --------------------------------------------------------- Installationen hdr "PHP-Installationen" [ $IS_PLESK -eq 1 ] && plesk_usage if [ ${#INST_KEYS[@]} -eq 0 ]; then info "Keine PHP-Installation auf dem Host gefunden."; fi local used use_txt unused_list="" for k in "${INST_KEYS[@]}"; do worst=""; wr=0; any_web=0; stale=""; used=0 for s in $SAPI_ORDER; do [ -n "${ART[$k|$s]:-}" ] || continue analyze_sapi "$k" "$s"; sapi_usage "$k" "$s" [[ "$WEB_SAPIS" == *" $s "* ]] && any_web=1 [ -n "${S_USE[$k|$s]}" ] && used=1 done for s in $SAPI_ORDER; do [ -n "${ART[$k|$s]:-}" ] || continue [ $any_web -eq 1 ] && [ "$s" = cli ] && continue r=$(rank "${S_FS[$k|$s]}"); [ "$r" -gt "$wr" ] && { wr=$r; worst=$s; } [ -n "${ASTALE[$k|$s]:-}" ] && stale+="$s " done [ -z "$worst" ] && worst=cli I_FS[$k]=${S_FS[$k|$worst]}; I_FR[$k]=${S_FR[$k|$worst]}; I_VER[$k]=${S_VER[$k|$worst]}; I_USED[$k]=$used local sapis=""; for s in $SAPI_ORDER; do [ -n "${ART[$k|$s]:-}" ] && sapis+="$(sapi_name $s), "; done use_txt="nicht aktiv genutzt"; [ $used -eq 1 ] && use_txt="aktiv genutzt" [ $any_web -eq 0 ] && use_txt="nur Kommandozeile" line="$(inst_label "$k") · PHP ${I_VER[$k]:-?} · $use_txt" case "${I_FS[$k]}" in FIXED) if [ -n "$stale" ]; then crit "$line → GEPATCHT, ABER NEUSTART FEHLT" sub "Das Update ist installiert, der laufende Dienst nutzt aber noch die alte, verwundbare Version." recommend "Dienste neu starten, damit der installierte Fix wirkt: $RUN mitigate (startet betroffene Dienste neu)" STALE_N=$((STALE_N+1)) else ok "$line → gepatcht"; sub "Nachweis: ${I_FR[$k]}"; fi ;; UNKNOWN) warn "$line → Status unklar"; sub "${I_FR[$k]}" ;; *) if [ $used -eq 1 ]; then crit "$line → NICHT GEPATCHT" sub "${I_FR[$k]}" sub "Update: $(update_hint "$k")" recommend "PHP aktualisieren – $(inst_label "$k"): $(update_hint "$k")" [ "${I_FS[$k]}" = EOL ] && recommend "EOL-Versionen (< 8.2) ablösen: Websites auf 8.3/8.4 umstellen" elif [ $any_web -eq 1 ]; then warn "$line → nicht gepatcht" sub "${I_FR[$k]}" sub "Empfehlung: entfernen oder aktualisieren – sobald eine Website diese Version nutzt, ist sie angreifbar." unused_list+="$(inst_label "$k"), "; UNUSED_N=$((UNUSED_N+1)) else warn "$line → nicht gepatcht (nur CLI, nicht über das Web erreichbar)"; sub "${I_FR[$k]}" fi ;; esac for s in $SAPI_ORDER; do [ -n "${ART[$k|$s]:-}" ] || continue [ $any_web -eq 1 ] && [ "$s" = cli ] && continue sub "$(printf '%-12s %-24s upload_progress=%-3s memory_limit=%-5s post_max_size=%-5s %s' "$(sapi_name $s)" "${S_VER[$k|$s]:-?}" "${S_UP[$k|$s]:-?}" "${S_MEM[$k|$s]:-?}" "${S_POST[$k|$s]:-?}" "${S_USE[$k|$s]:-–}${S_NOTE[$k|$s]:+ · ${S_NOTE[$k|$s]}}")" case "${ASTALE[$k|$s]:-}" in deleted) sub " ↳ laufender Prozess nutzt eine bereits gelöschte (alte) Datei – Neustart nötig" ;; esac if [ "${S_FS[$k|$s]}" != FIXED ] && [[ "$WEB_SAPIS" == *" $s "* ]] && [ -n "${S_USE[$k|$s]}" ]; then [ $IS_PLESK -eq 1 ] && [[ "$s" == fpm || "$s" == cgi ]] && [[ "$k" == /opt/plesk/* ]] && continue # Plesk: Bilanz je Domain tally "$(inst_label "$k") $(sapi_name $s)" "${S_UP[$k|$s]:-?}" "${S_MEM[$k|$s]:-?}" "" fi done if [[ "$k" == /opt/plesk/php/* ]]; then local bv=${k%@*}; bv=${bv##*/}; [ -n "${PL_DOMS[$bv]:-}" ] && sub "Domains: $(tr ' ' '\n' <<<"${PL_DOMS[$bv]}" | grep . | head -n 8 | tr '\n' ' ')$( [ "$(wc -w <<<"${PL_DOMS[$bv]}")" -gt 8 ] && echo '…')"; fi done [ -n "$unused_list" ] && recommend "Ungenutzte, ungepatchte PHP-Versionen entfernen: ${unused_list%, }" # --------------------------------------------------------------- Overrides hdr "Abweichende Einstellungen (Overrides)" local found=0 conf files f sec dir val for k in "${INST_KEYS[@]}"; do [ -n "${ART[$k|fpm]:-}" ] || continue conf=$(fpm_main_conf "$k") || continue; [ -n "$conf" ] || continue mapfile -t files < <(fpm_conf_files "$conf") while IFS='|' read -r f sec dir val mk; do [ -z "$f" ] && continue; found=1 if [ "$mk" = 1 ]; then [ -z "${POOL_MARKED[$f|$sec]:-}" ] && info "FPM-Pool $sec ($f): Übergangsmaßnahme dieses Skripts aktiv" POOL_MARKED[$f|$sec]=1 elif [[ "$dir" == *upload_progress* ]] && is_on "$val"; then if [ "${I_FS[$k]}" = FIXED ]; then warn "FPM-Pool $sec ($f): $dir = $val" else crit "FPM-Pool $sec ($f): $dir = $val – schaltet upload_progress wieder ein"; fi else info "FPM-Pool $sec ($f): $dir = $val" fi if [ "${I_FS[$k]}" != FIXED ] && ! { [ $IS_PLESK -eq 1 ] && [[ "$f" == /opt/plesk/* ]]; }; then POOL_ISSUES+=("$k|$f|$sec|$dir|$val|$mk") fi done < <(pool_overrides "${files[@]}") done # Pool-Einträge zur Bilanz (pro Pool zusammengefasst) declare -A pmem=() pup=() local pi pk pf ps pd pv for pi in "${POOL_ISSUES[@]}"; do IFS='|' read -r pk pf ps pd pv _pm <<<"$pi" if [[ "$pd" == *memory_limit* ]]; then pmem[$pk|$pf|$ps]=$pv; else pup[$pk|$pf|$ps]=$pv; fi done for pi in $(printf '%s\n' "${!pmem[@]}" "${!pup[@]}" | sort -u); do IFS='|' read -r pk pf ps <<<"$pi" local u=${S_UP[$pk|fpm]:-?} m=${S_MEM[$pk|fpm]:-?} why="" [ -n "${pup[$pi]:-}" ] && { is_on "${pup[$pi]}" && u=AN || u=aus; } [ -n "${pmem[$pi]:-}" ] && { m=${pmem[$pi]}; why="Pool-Override in $pf"; } tally "FPM-Pool $ps ($(inst_label "$pk"))" "$u" "$m" "$why" done # Apache-Konfiguration local ap_over ap_over=$(apache_conf_files | grep -v "$APACHE_SNIPPET" | xargs -r grep -HniE '^[[:space:]]*php_(admin_)?(value|flag)[[:space:]]+(memory_limit|session\.upload_progress\.enabled)' 2>/dev/null) if [ -n "$ap_over" ]; then found=1 while read -r line; do if grep -qiE 'upload_progress\.enabled[[:space:]]+(on|1)' <<<"$line"; then crit "Apache: $line"; else info "Apache: $line"; fi done <<<"$ap_over" fi apache_snippet_active && info "Apache: mod_php-Erzwingung dieses Skripts aktiv ($(apache_snippet_path))" [ $found -eq 0 ] && ok "Keine Pool- oder Apache-Overrides für upload_progress/memory_limit gefunden" # ------------------------------------------------------------- Plesk-Domains [ $IS_PLESK -eq 1 ] && plesk_section # ----------------------------------------------------------------- Container container_section # ------------------------------------------------------------------ Webtest if [ $WEB -eq 1 ]; then web_section; fi # ------------------------------------------------------------ Erreichbarkeit hdr "Erreichbarkeit von Verwaltungsoberflächen" local lp=() addr port while read -r addr; do port=${addr##*:} [[ " $PANEL_PORTS " == *" $port "* ]] || continue [[ "$addr" =~ ^(0\.0\.0\.0|\*|\[::\]|::): ]] && lp+=("$port") done < <(ss -ltnH 2>/dev/null | awk '{print $4}' | sort -u) if [ ${#lp[@]} -gt 0 ]; then warn "Verwaltungs-Ports auf allen Schnittstellen offen: $(printf '%s\n' "${lp[@]}" | sort -un | tr '\n' ' ')" sub "Solange Panel-PHP ungepatcht ist, per Firewall auf eigene IP-Adressen beschränken." else ok "Keine Verwaltungs-Ports auf allen Schnittstellen gefunden"; fi } # ----------------------------------------------------------------------------- plesk_section() { hdr "Plesk: Domains" local name php handler root ver bin v dini up mem pool pup pmem msg why declare -A used=() while IFS=$'\t' read -r name php handler root; do [ -z "$name" ] && continue ver=""; [[ "$handler" =~ plesk-php([0-9])([0-9]+)- ]] && ver="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" [ -n "$ver" ] && used[$ver]=1 bin="/opt/plesk/php/$ver/bin/php" if [ -z "$ver" ] || [ ! -x "$bin" ]; then [ "$php" = true ] && info "$name: Handler $handler (kein Plesk-PHP)"; continue; fi v=$("$bin" -r 'echo PHP_VERSION;' 2>/dev/null) fix_status "/opt/plesk/php/$ver@" "$bin" "$v" if [ "$php" != true ]; then [ "$FS" = FIXED ] && ok "$name: PHP deaktiviert (Handler $handler)" || info "$name: PHP deaktiviert, Handler $handler → auf gepatchte Version umstellen, damit $ver entfernt werden kann" continue fi dini="/var/www/vhosts/system/$name/etc/php.ini" if [ -f "$dini" ]; then IFS='|' read -r v up mem _ _ <<<"$(probe_bin "$bin" -c "$dini")"; else IFS='|' read -r v up mem _ _ <<<"$(probe_bin "$bin")"; fi pool=""; pup=""; pmem="" [[ "$handler" == *-fpm ]] && pool="/opt/plesk/php/$ver/etc/php-fpm.d/$name.conf" if [ -f "$pool" ]; then pup=$(grep -shE '^[[:space:]]*php(_admin)?_(flag|value)\[session\.upload_progress\.enabled\]' "$pool" | tail -n1 | sed -E 's/.*=[[:space:]]*//; s/[[:space:]]+$//') pmem=$(grep -shE '^[[:space:]]*php(_admin)?_value\[memory_limit\]' "$pool" | tail -n1 | sed -E 's/.*=[[:space:]]*//; s/[[:space:]]+$//') [ -n "$pup" ] && { is_on "$pup" && up=AN || up=aus; } [ -n "$pmem" ] && mem=$pmem fi msg="$name: $handler (PHP $v) upload_progress=$up memory_limit=$mem" if [ "$FS" = FIXED ]; then ok "$msg"; continue; fi crit "$msg" if [ "$FS" = EOL ]; then sub "EOL – Domain auf 8.3/8.4 umstellen: plesk bin domain -u $name -php_handler_id plesk-php83-fpm" else sub "nicht gepatcht: $FR"; fi why="" if [ "$mem" != "-1" ]; then if [ -n "$pmem" ]; then why="Domain-Einstellung"; sub "memory_limit=$mem kommt aus den Domain-Einstellungen → Plesk: Domain › PHP-Einstellungen › memory_limit = -1"; fi fi tally "Plesk-Domain $name" "$up" "$mem" "$why" done < <(plesk db -Ne "SELECT d.name, IFNULL(h.php,'-'), IFNULL(h.php_handler_id,'-'), IFNULL(h.www_root,'-') FROM domains d JOIN hosting h ON h.dom_id=d.id ORDER BY d.name" 2>/dev/null) hdr "Plesk: ungenutzte PHP-Versionen" local d b any=0 for d in /opt/plesk/php/*/; do b=$(basename "$d"); [ -x "$d/bin/php" ] || continue [ -n "${used[$b]:-}" ] && continue; any=1 fix_status "/opt/plesk/php/$b@" "$d/bin/php" "$("$d/bin/php" -r 'echo PHP_VERSION;' 2>/dev/null)" if [ "$FS" = FIXED ]; then info "PHP $b wird von keiner Domain genutzt" else info "PHP $b wird von keiner Domain genutzt und ist nicht gepatcht → entfernen (Tools & Einstellungen › Updates › Komponenten)"; fi done [ $any -eq 0 ] && ok "Alle installierten Plesk-PHP-Versionen werden genutzt" } # ----------------------------------------------------------------------------- container_section() { local rt id name img out v up mem _a _b shown=0 for rt in docker podman; do command -v "$rt" >/dev/null || continue while IFS='|' read -r id name img; do [ -z "$id" ] && continue out=$(run_t 15 "$rt" exec -i "$id" php -q <"$PROBE" 2>/dev/null | tail -n1) [[ "$out" =~ ^[0-9]+\.[0-9]+ ]] || continue [ $shown -eq 0 ] && hdr "Container"; shown=1 IFS='|' read -r v up mem _a _b <<<"$out" fix_status_ver "$v" case $FS in FIXED) ok "$rt $name ($img): PHP $v" ;; UNKNOWN) warn "$rt $name ($img): PHP $v – $FR" ;; *) crit "$rt $name ($img): PHP $v upload_progress=$up memory_limit=$mem – nicht gepatcht ($FR)" sub "Image aktualisieren und Container neu erstellen" recommend "Container-Images mit PHP aktualisieren und neu ausrollen" tally "Container $name" "$up" "$mem" "Container" ;; esac done < <(run_t 15 "$rt" ps --format '{{.ID}}|{{.Names}}|{{.Image}}' 2>/dev/null) done if [ $shown -eq 0 ] && [ $CONTAINER_FPM -gt 0 ]; then hdr "Container"; warn "$CONTAINER_FPM PHP-FPM in Containern erkannt, aber nicht prüfbar (kein docker/podman-Zugriff)" fi } # ----------------------------------------------------------------------------- web_section() { hdr "Live-Test über den Webserver" [ $IS_ROOT -eq 1 ] || { warn "Live-Test erfordert root."; return; } command -v curl >/dev/null || { warn "curl fehlt – Live-Test übersprungen."; return; } discover_vhosts [ ${#VHOSTS[@]} -eq 0 ] && { info "Keine Websites aus der Webserver-Konfiguration ermittelbar."; return; } local ips=(127.0.0.1) pub e name root php f res code body wv wup wmem n=0 pub=$(hostname -I 2>/dev/null | awk '{print $1}'); [ -n "$pub" ] && ips+=("$pub") info "Teste ${#VHOSTS[@]} Website(s); Testdateien werden sofort wieder gelöscht." for e in "${VHOSTS[@]}"; do IFS='|' read -r name root php <<<"$e" [ "$php" = false ] && continue [ -d "$root" ] && [ -w "$root" ] || { info "$name: Docroot nicht beschreibbar – übersprungen"; continue; } n=$((n+1)); [ $n -gt 300 ] && { info "Mehr als 300 Websites – Rest übersprungen."; break; } f="$root/php2shell-probe-$RANDOM$RANDOM.php"; WEBPROBES+=("$f") cp "$PROBE" "$f" && chown "$(stat -c '%u:%g' "$root")" "$f" 2>/dev/null; chmod 0644 "$f" code=""; body="" local sch done_=0 for ip in "${ips[@]}"; do for sch in https http; do res=$(curl -skL --max-time 10 -w $'\n%{http_code}' --resolve "$name:443:$ip" --resolve "www.$name:443:$ip" \ --resolve "$name:80:$ip" --resolve "www.$name:80:$ip" "$sch://$name/${f##*/}" 2>/dev/null) code=${res##*$'\n'}; body=$(printf '%s\n' "${res%$'\n'*}" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+' | tail -n1) [ "$code" = 200 ] && [[ "$body" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]] && { done_=1; break; } done [ $done_ -eq 1 ] && break done rm -f -- "$f" if [ "$code" = 200 ] && [[ "$body" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then IFS='|' read -r wv wup wmem _ _ <<<"$body"; fix_status_ver "$wv" if [ "$FS" = FIXED ]; then ok "$name: PHP $wv (gepatcht) upload_progress=$wup memory_limit=$wmem" elif [ "$wup" = AN ]; then crit "$name: PHP $wv nicht gepatcht, upload_progress=AN" else warn "$name: PHP $wv nicht gepatcht, Übergangsmaßnahme aktiv (memory_limit=$wmem)"; fi else info "$name: kein auswertbares Ergebnis (HTTP ${code:-–}; Weiterleitung, Schutz oder Front-Controller)" fi done } # ----------------------------------------------------------------------------- # Indizien für eine Kompromittierung # ----------------------------------------------------------------------------- STRICT_RE='(eval|assert)[[:space:]]*\([[:space:]]*(stripslashes[[:space:]]*\([[:space:]]*)?\$_(POST|GET|REQUEST|COOKIE|SERVER)|eval[[:space:]]*\([[:space:]]*(base64_decode|gzinflate|gzuncompress|str_rot13|rawurldecode)|(system|shell_exec|passthru|exec|popen|proc_open)[[:space:]]*\([[:space:]]*\$_(POST|GET|REQUEST|COOKIE|SERVER)|\$_(POST|GET|REQUEST|COOKIE)[[:space:]]*\[[^]]+\][[:space:]]*\([[:space:]]*\$_' BROAD_RE='eval[[:space:]]*\(|assert[[:space:]]*\([[:space:]]*\$|base64_decode[[:space:]]*\(|gzinflate[[:space:]]*\(|gzuncompress[[:space:]]*\(|str_rot13[[:space:]]*\(|(system|shell_exec|passthru|popen|proc_open|pcntl_exec)[[:space:]]*\(|\$_(POST|GET|REQUEST|COOKIE|FILES)|move_uploaded_file|create_function' CACHE_RE='/(tmp/cache|tmp/templates_c|tmp/assets|wp-content/cache|storage/framework/views|var/cache|bootstrap/cache)/' UPLOAD_RE='/(wp-content/uploads|uploads|upload|tmp)/' NOT_UPLOAD_RE='/(plugins|vendor|third-party|3rdparty|node_modules)/' NORMAL_RE='/wp-content/(plugins|themes|mu-plugins|languages|upgrade)/|/wp-(includes|admin)/|/vendor/|/(custom_)?apps/|/3rdparty/|/lib/private/|/core/|/config/config\.php$|/wp-config\.php$' scan_section() { hdr "Indizien für eine Kompromittierung (seit $SINCE)" local roots=() r e cand x prune=() cfg dd for r in /var/www /srv/www /srv/http /usr/share/nginx /home/*/public_html /home/*/www /home/*/htdocs \ /home/*/domains/*/public_html /var/www/clients "${EXTRA_SCAN[@]}"; do [ -d "$r" ] && roots+=("$(readlink -f "$r")") done [ ${#VHOSTS[@]} -eq 0 ] && discover_vhosts for e in "${VHOSTS[@]}"; do r=${e#*|}; r=${r%%|*}; [ -d "$r" ] && roots+=("$(readlink -f "$r")"); done # verschachtelte Pfade entfernen mapfile -t roots < <(printf '%s\n' "${roots[@]}" | sort -u | awk 'BEGIN{p=""} { if (p!="" && index($0, p"/")==1) next; print; p=$0 }') [ ${#roots[@]} -eq 0 ] && { info "Keine Webverzeichnisse gefunden (eigene Pfade mit --scan-path=…)."; return; } # Ausschlüsse: Panel-Systemordner, Datenverzeichnisse mit Nutzerdateien, VCS, Abhängigkeiten prune=(-path /var/www/vhosts/system -o -name .git -o -name node_modules -o -name .snapshot) while read -r x; do [ -n "$x" ] && prune+=(-o -path "$(dirname "$x")"); done < <(run_t 60 find "${roots[@]}" -maxdepth 5 -name .ocdata 2>/dev/null) while read -r cfg; do dd=$(grep -oE "'datadirectory'[[:space:]]*=>[[:space:]]*'[^']+'" "$cfg" 2>/dev/null | sed -E "s/.*=>[[:space:]]*'([^']+)'/\1/") [ -n "$dd" ] && [ -d "$dd" ] && prune+=(-o -path "$dd" -o -path "$(readlink -f "$dd")") done < <(run_t 60 find "${roots[@]}" -maxdepth 5 -path '*/config/config.php' 2>/dev/null) info "Durchsuche: ${roots[*]}" sub "ausgenommen: Datenverzeichnisse mit Nutzerdateien, Versionsverwaltung, Abhängigkeiten, Panel-Systemordner; generierte Cache-Dateien nur mit strenger Prüfung" local list rc list=$(run_t "$SCAN_TIMEOUT" find "${roots[@]}" -xdev \( "${prune[@]}" \) -prune -o -type f \ \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' -o -name '*.php[0-9]' -o -name '.user.ini' \) -print 2>/dev/null); rc=$? [ $rc -eq 124 ] && warn "Suche nach $SCAN_TIMEOUT s abgebrochen – Ergebnis unvollständig (--scan-timeout=… erhöhen)." [ -z "$list" ] && { ok "Keine PHP-Dateien in den Webverzeichnissen gefunden"; scan_runtime_indicators; return; } local stats newf stats=$(tr '\n' '\0' <<<"$list" | xargs -0 -r stat -c '%Y %n' 2>/dev/null) newf=$(awk -v t="$SINCE_EPOCH" '$1>=t { sub(/^[0-9]+ /,""); print }' <<<"$stats") # 1) neue/geänderte Dateien (ohne Caches) local newcode newcache n odd sus newcode=$(grep -vE "$CACHE_RE" <<<"$newf" | grep -v '\.user\.ini$' | grep .) newcache=$(grep -E "$CACHE_RE" <<<"$newf" | grep .) n=$(grep -c . <<<"$newcode") if [ "$n" -gt 0 ]; then info "$n neue/geänderte PHP-Dateien, gruppiert (Updates von Plugins/Apps sind normal):" sed -E 's#(/wp-content/(plugins|themes|mu-plugins)/[^/]+)/.*#\1#; t; s#(/(custom_)?apps/[^/]+)/.*#\1#; t; s#(/vendor/[^/]+)/.*#\1#; t; s#^((/[^/]+){6}).*#\1#' <<<"$newcode" \ | sort | uniq -c | sort -rn | head -n 15 | while read -r line; do sub "$line"; done odd=$(grep -vE "$NORMAL_RE" <<<"$newcode" | head -n 25) if [ -n "$odd" ]; then warn "Neue/geänderte PHP-Dateien außerhalb üblicher Plugin-/App-/Core-Ordner – bitte prüfen:" while read -r x; do sub "$(stat -c '%y' "$x" 2>/dev/null | cut -c1-16) $x"; done <<<"$odd"; fi else ok "Keine neuen oder geänderten PHP-Dateien (außer Caches)"; fi sus=$(printf '%s\n%s\n' "$newcode" "$newcache" | grep . | tr '\n' '\0' | xargs -0 -r grep -lsE "$STRICT_RE" 2>/dev/null | head -n 25) if [ -n "$sus" ]; then SUSPECT=1; crit "Webshell-typische Muster in neuen/geänderten Dateien:" while read -r x; do sub "$x"; sub " ↳ $(grep -osE ".{0,40}($STRICT_RE).{0,40}" "$x" | head -n1 | tr -d '\r' | cut -c1-120)"; done <<<"$sus" recommend "Webshell-Verdacht prüfen: Dateien sichern, analysieren, Zugangsdaten rotieren, Server ggf. neu aufsetzen" else ok "Keine Webshell-typischen Muster in neuen/geänderten Dateien"; fi # 2) PHP in Upload-/Tmp-Ordnern (unabhängig vom Datum) local upl bad="" old="" plain=0 f snip isnew upl=$(grep -E "$UPLOAD_RE" <<<"$list" | grep -vE "$NOT_UPLOAD_RE|$CACHE_RE" | grep .) if [ -n "$upl" ]; then while read -r f; do isnew=0; awk -v t="$SINCE_EPOCH" -v f="$f" '$1>=t { sub(/^[0-9]+ /,""); if ($0==f) { found=1; exit } } END{exit !found}' <<<"$stats" && isnew=1 if grep -qsiE "$BROAD_RE" "$f"; then snip=$(grep -osiE ".{0,30}($BROAD_RE).{0,30}" "$f" | head -n1 | tr -d '\r' | cut -c1-110) if grep -qsE "$STRICT_RE" "$f" || [ $isnew -eq 1 ]; then bad+="$(stat -c '%y' "$f" | cut -c1-16) $f"$'\n'" ↳ $snip"$'\n' else old+="$(stat -c '%y' "$f" | cut -c1-16) $f"$'\n'" ↳ $snip"$'\n'; fi else plain=$((plain+1)); fi done <<<"$upl" if [ -n "$bad" ]; then SUSPECT=1; crit "PHP-Dateien in Upload-Ordnern mit Webshell-Mustern oder neu seit $SINCE:" while read -r x; do [ -n "$x" ] && sub "$x"; done <<<"$(head -n 40 <<<"$bad")"; fi if [ -n "$old" ]; then warn "Ältere PHP-Dateien in Upload-Ordnern mit Code-Mustern – häufig von Erweiterungen erzeugt, Fundstelle prüfen:" while read -r x; do [ -n "$x" ] && sub "$x"; done <<<"$(head -n 40 <<<"$old")"; fi [ $plain -gt 0 ] && info "$plain unauffällige PHP-Dateien in Upload-Ordnern" recommend "PHP-Ausführung in Upload-Ordnern per Webserver- oder Panel-Regel unterbinden" else ok "Keine PHP-Dateien in Upload-Ordnern"; fi # 3) Caches nur mit strenger Prüfung local cs; cs=$(grep -E "$CACHE_RE" <<<"$list" | grep . | tr '\n' '\0' | xargs -0 -r grep -lsE "$STRICT_RE" 2>/dev/null | head -n 20) [ -n "$cs" ] && { SUSPECT=1; crit "Webshell-typische Muster in App-Cache-Dateien:"; while read -r x; do sub "$x"; done <<<"$cs"; } # 4) .user.ini, die upload_progress einschalten local ui; ui=$(grep '\.user\.ini$' <<<"$list" | grep . | tr '\n' '\0' | xargs -0 -r grep -lsiE 'session\.upload_progress\.enabled[[:space:]]*=[[:space:]]*(1|on|true)' 2>/dev/null) [ -n "$ui" ] && { warn ".user.ini schaltet upload_progress ein:"; while read -r x; do sub "$x"; done <<<"$ui"; } scan_runtime_indicators } scan_runtime_indicators() { local kids seg kids=$(ps -eo pid=,ppid=,user=,comm=,args= 2>/dev/null | awk ' { pid[$1]=$4; usr[$1]=$3; line[$1]=$0; par[$1]=$2 } END { for (p in pid) { pp=par[p] if (pid[pp] ~ /^(php-fpm|php-cgi|php|lsphp|apache2|httpd|sw-engine)/ && usr[p]!="root" && pid[p] ~ /^(sh|bash|dash|zsh|ash|curl|wget|nc|ncat|netcat|socat|perl|python[0-9.]*|ruby)$/) print line[p] } }') if [ -n "$kids" ]; then SUSPECT=1; crit "Shells/Netzwerk-Tools als Kindprozess von PHP oder Webserver (Momentaufnahme):"; while read -r x; do sub "$x"; done <<<"$kids" sub "Hinweis: Cronjobs, Medienverarbeitung oder Backup-Erweiterungen können legitime Ursachen sein." else ok "Keine verdächtigen Kindprozesse von PHP/Webserver (Momentaufnahme)"; fi seg=$( { journalctl -k --since "$SINCE" 2>/dev/null; dmesg 2>/dev/null; } | grep -iE '(php|apache2|httpd|lsphp).*segfault' | sort -u | tail -n 5 grep -hsE 'SIGSEGV|exited on signal 11|Segmentation fault|core dumped' /var/log/php*-fpm*.log /var/log/php*-fpm/*.log \ /var/log/plesk-php*-fpm/error.log /var/log/apache2/error.log /var/log/httpd/error_log /opt/cpanel/ea-php*/root/usr/var/log/php-fpm/error.log 2>/dev/null | tail -n 5 ) if [ -n "$seg" ]; then warn "Abstürze von PHP/Webserver (Segfaults) – möglicher Hinweis auf Exploit-Versuche:"; while read -r x; do [ -n "$x" ] && sub "$x"; done <<<"$seg" else ok "Keine PHP-/Apache-Segfaults in Kernel- und Dienst-Logs"; fi # Crontabs, die seit dem Stichtag geändert wurden (nicht root) local cr; cr=$(find /var/spool/cron/crontabs /var/spool/cron -maxdepth 1 -type f ! -name root -newermt "$SINCE" 2>/dev/null | sort -u) if [ -n "$cr" ]; then warn "Seit $SINCE geänderte Crontabs von Benutzern – auf unbekannte Einträge prüfen:" while read -r x; do sub "$x ($(stat -c '%y' "$x" | cut -c1-16))"; done <<<"$cr" else ok "Keine seit $SINCE geänderten Benutzer-Crontabs"; fi # Speicherfehler (CVE-2026-109618 nutzt Speichermangel) local oom; oom=$(find /var/log /var/www/vhosts/system /opt/cpanel -maxdepth 4 -type f \( -name '*.log' -o -name 'error_log' \) -newermt "$SINCE" 2>/dev/null \ | head -n 2000 | xargs -r grep -hsc 'Allowed memory size of' 2>/dev/null | awk '{s+=$1} END{print s+0}') if [ "${oom:-0}" -gt 0 ]; then info "$oom Meldungen 'Allowed memory size' in Logs seit $SINCE – auf ungepatchten Systemen Hinweis auf Angriffsversuche, sonst meist Anwendungsfehler" else ok "Keine Speicherlimit-Fehler in Logs seit $SINCE"; fi } # ----------------------------------------------------------------------------- # Ergebnis # ----------------------------------------------------------------------------- summary() { hdr "Ergebnis" local vc vt if [ $UNF_N -gt 0 ] && [ $UNF_UP -lt $UNF_N ]; then VERDICT="UNGESCHÜTZT"; vc=$R; vt="Aktiv genutzte PHP-Versionen sind ungepatcht und ohne Übergangsmaßnahme angreifbar." elif [ $UNF_N -gt 0 ]; then VERDICT="ÜBERGANGSWEISE ABGESICHERT"; vc=$Y; vt="Ungepatchte PHP-Versionen sind im Einsatz; die Übergangsmaßnahme ist aktiv, ersetzt aber nicht das Update." elif [ $STALE_N -gt 0 ]; then VERDICT="NEUSTART ERFORDERLICH"; vc=$R; vt="Updates sind installiert, laufende Dienste nutzen aber noch die alte, verwundbare Version." elif [ $UNUSED_N -gt 0 ]; then VERDICT="AUFRÄUMEN EMPFOHLEN"; vc=$Y; vt="Keine aktiv genutzte PHP-Version ist verwundbar. Installierte, ungenutzte und ungepatchte Versionen sollten entfernt werden." else VERDICT="GEPATCHT"; vc=$G; vt="Alle aktiv genutzten PHP-Versionen sind gepatcht." fi p " ${vc}${B}Gesamturteil: $VERDICT${N}" p " $vt" [ $SUSPECT -eq 1 ] && p " ${R}${B}Achtung: Hinweise auf eine mögliche Kompromittierung – Abschnitt Indizien prüfen.${N}" p " ${D}($NCRIT kritische Befunde, $NWARN Warnungen)${N}" if [ $UNF_N -gt 0 ]; then p "" p " ${B}Übergangsmaßnahme bei $UNF_N ungepatchten, web-relevanten Laufzeiten:${N}" if [ $UNF_UP -eq $UNF_N ]; then p " ${G}✔${N} session.upload_progress aus $UNF_UP/$UNF_N (CVE-2026-109619, Session-Pfad)" else p " ${R}✘${N} session.upload_progress aus $UNF_UP/$UNF_N (CVE-2026-109619, Session-Pfad) – fehlt bei:" while read -r x; do [ -n "$x" ] && p " - $x"; done <<<"$UP_GAPS" recommend "Übergangsmaßnahme setzen: $RUN mitigate"; fi if [ $UNF_MEM -eq $UNF_N ]; then p " ${G}✔${N} memory_limit = -1 $UNF_MEM/$UNF_N (CVE-2026-109618, OOM-Pfad)" else p " ${Y}✘${N} memory_limit = -1 $UNF_MEM/$UNF_N (CVE-2026-109618, OOM-Pfad) – fehlt bei:" while read -r x; do [ -n "$x" ] && p " - $x"; done <<<"$MEM_GAPS" p " ${D}optional; Zielkonflikt: ohne Limit kann ein Skript den gesamten RAM belegen${N}" if grep -v -e 'Pool-Override' -e 'Domain-Einstellung' -e 'Container' <<<"$MEM_GAPS" | grep -q .; then recommend "Optional zusätzlich: $RUN mitigate --memlimit"; fi grep -q 'Pool-Override' <<<"$MEM_GAPS" && recommend "FPM-Pools mit eigenem memory_limit: $RUN mitigate --memlimit --fix-pools (bei Panels in den Domain-Einstellungen)" grep -q 'Domain-Einstellung' <<<"$MEM_GAPS" && recommend "Plesk: memory_limit der betroffenen Domains auf -1 setzen oder Domains auf gepatchte PHP-Version umstellen" fi fi if [ ${#RECS[@]} -gt 0 ]; then p ""; p " ${B}Empfohlene Schritte:${N}" local i=1 r; for r in "${RECS[@]}"; do p " $i. $r"; i=$((i+1)); done fi p "" p " ${D}Übergangsmaßnahmen senken das Risiko, schließen die Lücke aber nicht – vollständig hilft nur das Update.${N}" p " ${D}$SELF $VERSION · Datenstand $DATA_DATE · Anleitung: $BLOG_URL${N}" } print_json() { local i k s first=1 printf '{\n "tool": "php2shell.sh", "version": %s, "data_date": %s,\n' "$(json_esc "$VERSION")" "$(json_esc "$DATA_DATE")" printf ' "host": %s, "timestamp": %s, "os": %s,\n' "$(json_esc "$(hostname -f 2>/dev/null || hostname)")" "$(json_esc "$(date -Iseconds)")" "$(json_esc "$OS_PRETTY")" printf ' "verdict": %s, "suspected_compromise": %s,\n' "$(json_esc "$VERDICT")" "$([ $SUSPECT -eq 1 ] && echo true || echo false)" printf ' "summary": {"critical": %d, "warnings": %d, "unpatched_runtimes": %d, "upload_progress_off": %d, "memory_limit_unlimited": %d},\n' \ "$NCRIT" "$NWARN" "$UNF_N" "$UNF_UP" "$UNF_MEM" printf ' "installations": [' for k in "${INST_KEYS[@]}"; do [ $first -eq 1 ] && first=0 || printf ',' printf '\n {"label": %s, "key": %s, "status": %s, "reason": %s, "version": %s, "sapis": [' \ "$(json_esc "$(inst_label "$k")")" "$(json_esc "$k")" "$(json_esc "${I_FS[$k]:-}")" "$(json_esc "${I_FR[$k]:-}")" "$(json_esc "${I_VER[$k]:-}")" local f2=1 for s in $SAPI_ORDER; do [ -n "${ART[$k|$s]:-}" ] || continue [ $f2 -eq 1 ] && f2=0 || printf ', ' printf '{"sapi": %s, "path": %s, "version": %s, "status": %s, "upload_progress": %s, "memory_limit": %s, "pid": %s, "in_use": %s, "restart_needed": %s}' \ "$(json_esc "$s")" "$(json_esc "${ART[$k|$s]}")" "$(json_esc "${S_VER[$k|$s]:-}")" "$(json_esc "${S_FS[$k|$s]:-}")" \ "$(json_esc "${S_UP[$k|$s]:-}")" "$(json_esc "${S_MEM[$k|$s]:-}")" "$(json_esc "${APID[$k|$s]:-}")" "$(json_esc "${S_USE[$k|$s]:-}")" \ "$([ -n "${ASTALE[$k|$s]:-}" ] && echo true || echo false)" done printf ']}' done printf '\n ],\n "findings": [' for i in "${!F_LVL[@]}"; do [ "$i" -gt 0 ] && printf ',' printf '\n {"level": %s, "section": %s, "text": %s, "details": %s}' \ "$(json_esc "${F_LVL[$i]}")" "$(json_esc "${F_SEC[$i]}")" "$(json_esc "${F_TXT[$i]}")" "$(json_esc "${F_DET[$i]%$'\n'}")" done printf '\n ],\n "recommendations": [' for i in "${!RECS[@]}"; do [ "$i" -gt 0 ] && printf ','; printf '\n %s' "$(json_esc "${RECS[$i]}")"; done printf '\n ]\n}\n' } # ============================================================================= # ÜBERGANGSMASSNAHME # ============================================================================= BACKUP_DIR="" backup_file() { local f=$1; [ -e "$f" ] || return 0 [ -z "$BACKUP_DIR" ] && BACKUP_DIR="$STATE_DIR/backup/$(date +%Y%m%d-%H%M%S)" mkdir -p "$BACKUP_DIR$(dirname "$f")" && cp -a "$f" "$BACKUP_DIR$f" } manifest_add() { mkdir -p "$STATE_DIR"; grep -qxF "$1" "$MANIFEST" 2>/dev/null || echo "$1" >>"$MANIFEST"; } confirm() { [ $YES -eq 1 ] && return 0 [ -t 0 ] || die "Keine interaktive Eingabe möglich – mit --yes bestätigen oder --dry-run verwenden." local a; read -r -p " Fortfahren? [j/N] " a; [[ "$a" =~ ^[jJyY]$ ]] } mitigation_block() { # mem(0/1) fe85(0/1) echo "; $MARK – Übergangsmaßnahme gegen CVE-2026-109618/-109619" echo "; erstellt $(date '+%F %T') von $SELF $VERSION · entfernen mit: $SELF rollback" echo "session.upload_progress.enabled = 0" [ "$1" = 1 ] && echo "memory_limit = -1" [ "$2" = 1 ] && echo "fatal_error_backtraces = 0" return 0 } do_mitigate() { acquire_lock QUIET=1; analyze; QUIET=0; SEC_N=0 p "${B}$SELF $VERSION – Übergangsmaßnahme$( [ $DRY -eq 1 ] && echo ' (Probelauf)')${N}" p "System: $OS_PRETTY · Webserver: ${WEBSERVERS[*]:-–} · Panels: ${PANELS[*]:-–}" declare -A T_DIR=() T_MEM=() T_FE=() T_INI=() local k s dir ini unf mem fe any_mod=0 mem_mod=0 manual=() for k in "${INST_KEYS[@]}"; do unf=0; [ "${I_FS[$k]}" != FIXED ] && unf=1 [ $unf -eq 0 ] && [ $ALL -eq 0 ] && continue for s in $SAPI_ORDER; do [ -n "${ART[$k|$s]:-}" ] || continue if [ "$s" = modphp ]; then any_mod=1; [ $unf -eq 1 ] && [ $MEMLIMIT -eq 1 ] && mem_mod=1; fi dir=${S_SCAN[$k|$s]:-}; dir=${dir%%:*} if [ "$s" = lsphp ]; then for d in "${k%@*}"/etc/php/*/litespeed/conf.d "${k%@*}"/etc/php.d; do [ -d "$d" ] && T_DIR[$d]=$k; done; fi mem=0; [ $unf -eq 1 ] && [ $MEMLIMIT -eq 1 ] && mem=1 fe=0; [ $unf -eq 1 ] && [ "$(mm_ver "${I_VER[$k]}")" = 8.5 ] && fe=1 if [ -n "$dir" ] && [ -d "$dir" ]; then T_DIR[$dir]=$k; [ $mem -eq 1 ] && T_MEM[$dir]=1; [ $fe -eq 1 ] && T_FE[$dir]=1 grep -qsE '^[[:space:]]*memory_limit[[:space:]]*=[[:space:]]*-1' "$dir/$DROPIN" && [ $unf -eq 1 ] && T_MEM[$dir]=1 # bleibt erhalten else ini=${S_INI[$k|$s]:-} if [ -n "$ini" ] && [ -f "$ini" ]; then T_INI[$ini]=$k; [ $mem -eq 1 ] && T_MEM[$ini]=1; [ $fe -eq 1 ] && T_FE[$ini]=1 else manual+=("$(inst_label "$k") $(sapi_name "$s"): keine php.ini/kein Scan-Verzeichnis – session.upload_progress.enabled = 0 manuell setzen"); fi fi done done apache_snippet_active && grep -qsE 'memory_limit[[:space:]]+-1' "$(apache_snippet_path)" && [ $any_mod -eq 1 ] && mem_mod=1 # Pools local pi pk pf ps pd pv pm pools_fix=() pools_manual=() declare -A pseen=() for pi in "${POOL_ISSUES[@]}"; do IFS='|' read -r pk pf ps pd pv pm <<<"$pi" [ "$pm" = 1 ] && continue [ -n "${POOL_MARKED[$pf|$ps]:-}" ] && [ $MEMLIMIT -eq 0 ] && continue if [[ "$pd" == *upload_progress* ]] && ! is_on "$pv"; then continue; fi if [[ "$pd" == *memory_limit* ]] && { [ $MEMLIMIT -eq 0 ] || [ "$pv" = "-1" ]; }; then continue; fi [ -n "${pseen[$pf|$ps]:-}" ] && continue; pseen[$pf|$ps]=1 if [ $FIX_POOLS -eq 1 ] && [ $MANAGED -eq 0 ]; then pools_fix+=("$pk|$pf|$ps") else pools_manual+=("Pool [$ps] in $pf ($pd = $pv)"); fi done # Plan hdr "Geplante Änderungen" local n=0 d for d in $(printf '%s\n' "${!T_DIR[@]}" | sort); do n=$((n+1)); p " + $d/$DROPIN$( [ -n "${T_MEM[$d]:-}" ] && echo ' (+ memory_limit = -1)')$( [ -n "${T_FE[$d]:-}" ] && echo ' (+ fatal_error_backtraces = 0)')" done for d in $(printf '%s\n' "${!T_INI[@]}" | sort); do n=$((n+1)); p " + Block in $d (kein Scan-Verzeichnis vorhanden)"; done local snip=""; [ $any_mod -eq 1 ] && snip=$(apache_snippet_path) if [ $any_mod -eq 1 ]; then if [ -n "$snip" ]; then n=$((n+1)); p " + $snip (mod_php: php_admin_flag$( [ $mem_mod -eq 1 ] && echo ' + memory_limit'))" else manual+=("mod_php erkannt, aber kein Apache-Konfigurationsordner – php_admin_flag session.upload_progress.enabled off manuell setzen"); fi fi for pi in "${pools_fix[@]}"; do IFS='|' read -r pk pf ps <<<"$pi"; n=$((n+1)); p " + Pool [$ps] in $pf: php_admin-Werte ergänzen"; done [ $n -eq 0 ] && p " (keine – alle Installationen gepatcht; mit --all auch gepatchte absichern)" for d in "${manual[@]}"; do p " ! manuell: $d"; done for d in "${pools_manual[@]}"; do if [ $MANAGED -eq 1 ]; then p " ! manuell (Panel-verwaltet): $d → im Panel je Website anpassen" else p " ! nicht angepasst: $d → mit --fix-pools übernehmen"; fi done # Dienste local -a units=() pids=(); declare -A useen=() add_unit() { local u=$1 pd=$2; [ -n "$u" ] && [ -n "${useen[$u]:-}" ] && return; [ -n "$u" ] && useen[$u]=1; units+=("${u:--}"); pids+=("$pd"); } for k in "${INST_KEYS[@]}"; do [ "${I_FS[$k]}" != FIXED ] || [ $ALL -eq 1 ] || [ -n "$(for s in $SAPI_ORDER; do echo "${ASTALE[$k|$s]:-}"; done | tr -d '\n')" ] || continue for s in fpm panel; do [ -n "${APID[$k|$s]:-}" ] && add_unit "$(unit_of_pid "${APID[$k|$s]}")" "${APID[$k|$s]}" done if [ -n "${ART[$k|modphp]:-}" ] || [ -n "${ART[$k|cgi]:-}" ]; then local ap; ap=$(apache_pid); [ -n "$ap" ] && add_unit "$(unit_of_pid "$ap")" "$ap"; fi [ -n "${ART[$k|lsphp]:-}" ] && [ -x /usr/local/lsws/bin/lswsctrl ] && add_unit "lsws.service" "" done if [ $RESTART -eq 1 ] && [ ${#units[@]} -gt 0 ]; then p " ↻ Neustart: $(for i in "${!units[@]}"; do [ "${units[$i]}" != - ] && printf '%s ' "${units[$i]}" || printf 'PID %s ' "${pids[$i]}"; done)" fi [ $RESTART -eq 0 ] && p " ↻ Neustart: übersprungen (--no-restart) – Änderungen wirken erst nach Neustart der PHP-Dienste" if [ $DRY -eq 1 ]; then p ""; p "Probelauf – es wurde nichts geändert."; exit 0; fi [ $n -eq 0 ] && [ ${#units[@]} -eq 0 ] && { p ""; p "Nichts zu tun."; exit 0; } p ""; confirm || { p "Abgebrochen – nichts geändert."; exit 3; } # Ausführen [ $n -gt 0 ] && hdr "Änderungen" log "mitigate gestartet (memlimit=$MEMLIMIT fix-pools=$FIX_POOLS all=$ALL)" for d in $(printf '%s\n' "${!T_DIR[@]}" | sort); do backup_file "$d/$DROPIN" mitigation_block "${T_MEM[$d]:-0}" "${T_FE[$d]:-0}" >"$d/$DROPIN" && chmod 0644 "$d/$DROPIN" \ && { p " ✔ $d/$DROPIN"; manifest_add "dropin|$d/$DROPIN"; log "geschrieben: $d/$DROPIN"; } || p " ✘ $d/$DROPIN konnte nicht geschrieben werden" done for d in $(printf '%s\n' "${!T_INI[@]}" | sort); do backup_file "$d" sed -i "/>>> $MARK >>>/,/<<< $MARK <<>> $MARK >>>"; echo "[PHP]"; mitigation_block "${T_MEM[$d]:-0}" "${T_FE[$d]:-0}"; echo "; <<< $MARK <<<"; } >>"$d" \ && { p " ✔ Block in $d"; manifest_add "iniblock|$d"; log "Block ergänzt: $d"; } done if [ -n "$snip" ]; then backup_file "$snip" { echo "# $MARK – Übergangsmaßnahme gegen CVE-2026-109618/-109619 ($SELF $VERSION)" echo "# entfernen mit: $SELF rollback" for m in php_module php7_module php5_module; do echo ""; echo " php_admin_flag session.upload_progress.enabled off" [ $mem_mod -eq 1 ] && echo " php_admin_value memory_limit -1" echo "" done; } >"$snip" [[ "$snip" == /etc/apache2/conf-available/* ]] && command -v a2enconf >/dev/null && a2enconf -q "$APACHE_SNIPPET" >/dev/null 2>&1 p " ✔ $snip"; manifest_add "apache|$snip"; log "Apache-Snippet: $snip" fi for pi in "${pools_fix[@]}"; do IFS='|' read -r pk pf ps <<<"$pi" backup_file "$pf" pool_insert "$pf" "$ps" && { p " ✔ Pool [$ps] in $pf"; manifest_add "pool|$pf"; log "Pool ergänzt: $pf [$ps]"; } done # Konfigurationstests hdr "Konfigurationstests" local bad=0 c conf for k in "${INST_KEYS[@]}"; do [ -n "${ART[$k|fpm]:-}" ] || continue conf=$(fpm_main_conf "$k"); [ -n "$conf" ] || continue if "${ART[$k|fpm]}" -t -y "$conf" >/dev/null 2>&1; then p " ✔ $(inst_label "$k") PHP-FPM" else p " ✘ $(inst_label "$k") PHP-FPM: Test fehlgeschlagen – Pool-Änderungen werden zurückgenommen"; bad=1 for pi in "${pools_fix[@]}"; do IFS='|' read -r pk pf ps <<<"$pi"; [ "$pk" = "$k" ] && [ -f "$BACKUP_DIR$pf" ] && cp -a "$BACKUP_DIR$pf" "$pf"; done fi done c=$(apache_ctl) if [ -n "$c" ] && apache_pid >/dev/null; then if "$c" -t >/dev/null 2>&1; then p " ✔ Apache" else p " ✘ Apache: Konfigurationstest fehlgeschlagen"; bad=1 if [ -n "$snip" ]; then [[ "$snip" == /etc/apache2/* ]] && command -v a2disconf >/dev/null && a2disconf -q "$APACHE_SNIPPET" >/dev/null 2>&1 rm -f "$snip"; p " Apache-Snippet wieder entfernt" "$c" -t >/dev/null 2>&1 && p " ✔ Apache (ohne Snippet)" || p " ✘ Apache-Konfiguration war bereits vorher fehlerhaft – kein Neustart" fi fi fi # Neustart if [ $RESTART -eq 1 ] && [ ${#units[@]} -gt 0 ]; then hdr "Dienste neu starten" restart_services units pids fi [ -n "$BACKUP_DIR" ] && p "" && p " Sicherung der vorherigen Dateien: $BACKUP_DIR" p " Protokoll: $LOG_FILE" log "mitigate beendet" # Kontrolle p ""; p "${B}Kontrolle:${N}" reset_state; SCAN=0; WEB=0; analyze; summary } restart_services() { # Namen der Arrays: units pids local -n _u=$1 _p=$2; local i u pd c for i in "${!_u[@]}"; do u=${_u[$i]}; pd=${_p[$i]} if [ "$u" != - ] && has_systemd; then if systemctl restart "$u" 2>/dev/null 9>&- && systemctl is-active -q "$u"; then p " ✔ $u"; log "neu gestartet: $u" else p " ✘ $u konnte nicht neu gestartet werden – bitte prüfen: systemctl status $u"; log "FEHLER Neustart: $u"; fi continue fi # ohne systemd c=$(tr '\0' ' ' <"/proc/$pd/cmdline" 2>/dev/null) if [[ "$c" == *"php-fpm"*master* ]]; then kill -USR2 "$pd" 9>&- && p " ✔ PHP-FPM (PID $pd) neu geladen" || p " ✘ PHP-FPM (PID $pd)" elif [[ "$c" == *apache2* || "$c" == *httpd* ]]; then # vollständiger Stopp/Start: ein "graceful"/HUP lädt mod_php nicht neu local ac w; ac=$(apache_ctl); "$ac" -k stop >/dev/null 2>&1 9>&- for w in $(seq 1 20); do kill -0 "$pd" 2>/dev/null || break; sleep 0.5; done if "$ac" -k start >/dev/null 2>&1 9>&- && sleep 1 && apache_pid >/dev/null; then p " ✔ Apache neu gestartet"; log "Apache neu gestartet" else p " ✘ Apache konnte nicht gestartet werden – bitte sofort prüfen: $ac -t; $ac -k start"; log "FEHLER Apache-Start"; fi elif [ "$u" = lsws.service ] && [ -x /usr/local/lsws/bin/lswsctrl ]; then /usr/local/lsws/bin/lswsctrl restart >/dev/null 9>&- && p " ✔ LiteSpeed" else p " ! Dienst (PID ${pd:-?}) bitte manuell neu starten"; fi done sleep 2 } # Maßnahme am Ende eines Pool-Abschnitts einfügen pool_insert() { # datei pool local f=$1 pool=$2 tmp tmp=$(mktemp "$TMPD/pool.XXXX") awk -v P="[$pool]" -v M="$MARK" -v MEM="$MEMLIMIT" ' function block() { print "; >>> " M " >>>"; print "php_admin_flag[session.upload_progress.enabled] = off"; if (MEM==1) print "php_admin_value[memory_limit] = -1"; print "; <<< " M " <<<" } /^[ \t]*;[ \t]*>>> / && index($0,M) { skip=1 } skip { if (index($0, "<<< " M " <<<")) skip=0; next } /^[ \t]*\[/ { h=$0; gsub(/[ \t]/,"",h); if (inP) { block(); inP=0 } if (h==P) inP=1 } { print } END { if (inP) block() }' "$f" >"$tmp" && cat "$tmp" >"$f" } reset_state() { INST_KEYS=(); ART=(); APID=(); ACONF=(); ASTALE=(); S_VER=(); S_UP=(); S_MEM=(); S_SCAN=(); S_INI=(); S_FS=(); S_FR=(); S_NOTE=() I_FS=(); I_FR=(); I_VER=(); VER_FS=(); POOL_ISSUES=(); POOL_MARKED=(); PANELS=(); WEBSERVERS=(); VHOSTS=() F_LVL=(); F_SEC=(); F_TXT=(); F_DET=(); NCRIT=0; NWARN=0; SEC_N=0 UNF_N=0; UNF_UP=0; UNF_MEM=0; MEM_GAPS=""; UP_GAPS=""; RECS=(); STALE_N=0; UNUSED_N=0; SUSPECT=0; S_POST=(); S_USE=(); I_USED=(); PL_USE=(); PL_DOMS=(); CONTAINER_FPM=0; IS_PLESK=0; MANAGED=0 } do_rollback() { acquire_lock QUIET=1; detect_env; discover; QUIET=0; SEC_N=0 p "${B}$SELF $VERSION – Übergangsmaßnahme entfernen$( [ $DRY -eq 1 ] && echo ' (Probelauf)')${N}" local files=() f t d k s declare -A seen=() addf() { [ -n "${seen[$1]:-}" ] && return; seen[$1]=1; files+=("$1"); } # aus Manifest while IFS='|' read -r t f; do [ -n "$f" ] && [ -e "$f" ] && addf "$t|$f"; done <"$MANIFEST" 2>/dev/null # zusätzlich suchen (auch Dateien früherer Versionen dieses Skripts) for k in "${INST_KEYS[@]}"; do for s in $SAPI_ORDER; do [ -n "${ART[$k|$s]:-}" ] || continue case $s in fpm) d=$("${ART[$k|$s]}" -i 2>/dev/null | sed -n 's/^Scan this dir for additional .ini files => //p') ;; modphp) d="/etc/php/${k##*@}/apache2/conf.d" ;; *) d=$(probe_bin "${ART[$k|$s]}" | cut -d'|' -f5) ;; esac for f in "$d"/zz-php2shell*.ini; do [ -f "$f" ] && grep -qsiE "$MARK|PHP2Shell interim mitigation" "$f" && addf "dropin|$f"; done done; done for f in /etc/apache2/conf-available/$APACHE_SNIPPET.conf /etc/apache2/conf-available/zz-php2shell.conf /etc/httpd/conf.d/$APACHE_SNIPPET.conf \ /etc/httpd/conf.d/zz-php2shell.conf /etc/apache2/conf.d/$APACHE_SNIPPET.conf; do [ -f "$f" ] && grep -qsiE "$MARK|PHP2Shell interim mitigation" "$f" && addf "apache|$f" done hdr "Geplante Änderungen" [ ${#files[@]} -eq 0 ] && { p " Keine Übergangsmaßnahme dieses Skripts gefunden."; return 0; } for f in "${files[@]}"; do case ${f%%|*} in dropin|apache) p " - ${f#*|}" ;; iniblock|pool) p " - Block in ${f#*|}" ;; esac done p " ${D}Manuell angelegte Dateien ohne Markierung bleiben unverändert.${N}" [ $DRY -eq 1 ] && { p ""; p "Probelauf – es wurde nichts geändert."; return 0; } p ""; confirm || { p "Abgebrochen – nichts geändert."; exit 3; } hdr "Änderungen" log "rollback gestartet" for f in "${files[@]}"; do t=${f%%|*}; f=${f#*|}; backup_file "$f" case $t in dropin) rm -f "$f" && p " ✔ entfernt: $f" ;; apache) [[ "$f" == /etc/apache2/conf-available/* ]] && command -v a2disconf >/dev/null && a2disconf -q "$(basename "$f" .conf)" >/dev/null 2>&1 rm -f "$f" && p " ✔ entfernt: $f" ;; iniblock) sed -i "/; >>> $MARK >>>/,/; <<< $MARK <<>> $MARK >>>/,/; <<< $MARK <</dev/null) $(pgrep -o -f 'sw-engine-fpm: master' 2>/dev/null); do same_root "$pd" || continue; u=$(unit_of_pid "$pd"); [ -n "$u" ] && [ -n "${useen[$u]:-}" ] && continue; [ -n "$u" ] && useen[$u]=1 units+=("${u:--}"); pids+=("$pd") done ap=$(apache_pid); [ -n "$ap" ] && { u=$(unit_of_pid "$ap"); units+=("${u:--}"); pids+=("$ap"); } c=$(apache_ctl); if [ -n "$c" ] && [ -n "$ap" ] && ! "$c" -t >/dev/null 2>&1; then p " ✘ Apache-Konfigurationstest fehlgeschlagen – Apache wird nicht neu gestartet"; fi restart_services units pids fi [ -n "$BACKUP_DIR" ] && p " Sicherung: $BACKUP_DIR" log "rollback beendet" } # ============================================================================= # Start # ============================================================================= POOL_ISSUES=(); declare -A POOL_MARKED=() case "$MODE" in check) [ $JSON -eq 1 ] || p "${B}$SELF $VERSION – PHP2Shell-Prüfung · $(hostname -f 2>/dev/null || hostname) · $(date '+%F %T')${N}" analyze [ $SCAN -eq 1 ] && scan_section summary [ $JSON -eq 1 ] && print_json ;; mitigate) do_mitigate ;; rollback) do_rollback; exit 0 ;; esac if [ $NCRIT -gt 0 ]; then exit 2; elif [ $NWARN -gt 0 ]; then exit 1; else exit 0; fi # ============================================================================= # MIT License # # Copyright (c) 2026 hostspezial GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # =============================================================================