#!/usr/bin/env bash # ============================================================================= # Stantinel PHP2Shell-zelftest, versie 1.0 (21-10-2026) # https://stantinel.nl/php2shell # # WAT DIT SCRIPT DOET # Het controleert op deze server of de geinstalleerde PHP-versies de reparatie # hebben voor PHP2Shell (CVE-2026-109618 en CVE-2026-109619), en of de # tijdelijke mitigatie aan staat. Per PHP-versie zie je: # GEREPAREERD de reparatie zit erin (changelog of versienummer) # NIET GEREPAREERD de reparatie zit er aantoonbaar niet in # ONBEKEND niet vast te stellen; vraag het je leverancier # # HOE # 1. Pakketten: dpkg (Debian, Ubuntu, Sury) of rpm (RHEL, Alma, Rocky, Remi). # 2. Op Debian/Ubuntu: zoekt CVE-2026-109618 in # /usr/share/doc/phpX.Y-*/changelog.Debian.gz. Dat is de betrouwbaarste # toets, want Ubuntu repareerde zonder het versienummer te verhogen. # Op rpm-systemen: rpm -q --changelog. # 3. Anders: vergelijkt de upstreamversie met de gerepareerde versies van # php.net: 8.5.12, 8.4.27, 8.3.36, 8.2.35. Voor 8.1 en ouder heeft php.net # geen reparatie; alleen een distributie of Sury kan die leveren. # 4. Mitigatie: vraagt met php -r de waarden van # session.upload_progress.enabled en session.use_strict_mode op, per SAPI # (cli, fpm, apache2) met de ini-bestanden van die SAPI, en kijkt in de # FPM-pools of daar iets anders wordt gezet. # # WAT HET NIET DOET # - Het verandert niets: alleen lezen. Het installeert niets, herstart niets. # - Het verstuurt niets. Er gaat geen enkel gegeven van deze server naar buiten. # - Root is niet nodig. Als root zie je soms meer FPM-poolbestanden. # - PHP in Docker-containers, en instellingen per site (.user.ini, .htaccess, # php_admin_value in een Apache-vhost) worden niet getoetst. # # GEBRUIK # curl -fsSL https://stantinel.nl/php2shell/check.sh | bash # of eerst bekijken: # curl -fsSL -o php2shell-check.sh https://stantinel.nl/php2shell/check.sh # less php2shell-check.sh # bash php2shell-check.sh # # Geen garantie: dit is een hulpmiddel, geen audit. Bronnen: # https://sansec.io/research/php2shell # https://cert.europa.eu/publications/security-advisories/2026-018/pdf # ============================================================================= set -u export LC_ALL=C CVE1="CVE-2026-109618" CVE2="CVE-2026-109619" if [ -t 1 ]; then ROOD=$'\e[31;1m'; GROEN=$'\e[32;1m'; GEEL=$'\e[33;1m'; VET=$'\e[1m'; UIT=$'\e[0m' else ROOD=""; GROEN=""; GEEL=""; VET=""; UIT="" fi # Gerepareerde upstreamversies volgens php.net (21-10-2026). vast_upstream() { case "$1" in 8.5) echo "8.5.12" ;; 8.4) echo "8.4.27" ;; 8.3) echo "8.3.36" ;; 8.2) echo "8.2.35" ;; *) echo "" ;; esac } # Sury-builds met de reparatie (21-10-2026, 16:45): upstream plus Debian-revisie. vast_sury() { case "$1" in 5.6) echo "5.6.40-104" ;; 7.0) echo "7.0.33-92" ;; 7.2) echo "7.2.34-66" ;; 7.3) echo "7.3.33-35" ;; 7.4) echo "7.4.33-31" ;; 8.0) echo "8.0.30-25" ;; *) echo "" ;; esac } # versie_ge A B: waar als A >= B (upstreamnummers, sort -V) versie_ge() { [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" = "$2" ] } echo "${VET}Stantinel PHP2Shell-zelftest 1.0${UIT} (alleen lezen, verstuurt niets)" echo "Server: $(hostname 2>/dev/null) Tijd: $(date '+%Y-%m-%d %H:%M %Z')" if [ -r /etc/os-release ]; then . /etc/os-release 2>/dev/null echo "Systeem: ${PRETTY_NAME:-onbekend}" fi echo AANTAL_NIET=0; AANTAL_ONB=0; AANTAL_OK=0; GEZIEN="" regel() { # status versie toelichting local s="$1" v="$2" t="$3" kleur case "$s" in "GEREPAREERD") kleur="$GROEN"; AANTAL_OK=$((AANTAL_OK+1)) ;; "NIET GEREPAREERD") kleur="$ROOD"; AANTAL_NIET=$((AANTAL_NIET+1)) ;; *) kleur="$GEEL"; AANTAL_ONB=$((AANTAL_ONB+1)) ;; esac printf ' %-12s %s%-17s%s %s\n' "$v" "$kleur" "$s" "$UIT" "$t" } echo "${VET}1. Geinstalleerde PHP-versies${UIT}" # ---- Debian, Ubuntu, Sury --------------------------------------------------- if command -v dpkg-query >/dev/null 2>&1; then while read -r pkg ver; do [ -n "$pkg" ] || continue tak=$(echo "$pkg" | sed -n 's/^php\([0-9]\+\.[0-9]\+\)-common$/\1/p') [ -n "$tak" ] || { [ "$pkg" = "php5-common" ] && tak="5.x"; } [ -n "$tak" ] || continue GEZIEN="$GEZIEN $tak" upstream=$(echo "$ver" | sed 's/^[0-9]*://; s/-.*$//; s/+.*$//') bron="distributie" echo "$ver" | grep -q 'sury' && bron="Sury" # Changelog: phpX.Y-common, en anders een ander phpX.Y-pakket. cl=""; cl_gelezen=0 for f in /usr/share/doc/php${tak}-common/changelog.Debian.gz /usr/share/doc/php${tak}-*/changelog.Debian.gz /usr/share/doc/php5-common/changelog.Debian.gz; do [ -r "$f" ] || continue cl_gelezen=1 if zgrep -q -e "$CVE1" -e "$CVE2" "$f" 2>/dev/null; then cl="$f"; break; fi done if [ -n "$cl" ]; then regel "GEREPAREERD" "PHP $tak" "pakket $ver ($bron), $CVE1 staat in de changelog" continue fi vu=$(vast_upstream "$tak") if [ -n "$vu" ] && versie_ge "$upstream" "$vu"; then regel "GEREPAREERD" "PHP $tak" "pakket $ver, upstream $upstream is $vu of nieuwer" continue fi # Bekende gerepareerde builds, voor als de documentatie niet is meegeinstalleerd. vs=$(vast_sury "$tak") deb_kern=$(echo "$ver" | sed 's/^[0-9]*://; s/+.*$//') if [ "$bron" = "Sury" ] && [ -n "$vs" ] && dpkg --compare-versions "$deb_kern" ge "$vs" 2>/dev/null; then regel "GEREPAREERD" "PHP $tak" "pakket $ver, Sury-build $vs of nieuwer" continue fi if [ "$tak" = "8.3" ] && echo "$ver" | grep -q 'ubuntu0\.24\.04' && dpkg --compare-versions "$ver" ge "8.3.6-0ubuntu0.24.04.13" 2>/dev/null; then regel "GEREPAREERD" "PHP $tak" "pakket $ver, Ubuntu 24.04-build 8.3.6-0ubuntu0.24.04.13 of nieuwer" continue fi if [ "$cl_gelezen" = 1 ]; then if [ "$bron" = "Sury" ] && [ -n "$vs" ]; then regel "NIET GEREPAREERD" "PHP $tak" "pakket $ver (Sury): CVE niet in de changelog; Sury-build $vs heeft de reparatie" elif [ -z "$vu" ]; then regel "NIET GEREPAREERD" "PHP $tak" "pakket $ver ($bron): CVE niet in de changelog; php.net repareert $tak niet" else regel "NIET GEREPAREERD" "PHP $tak" "pakket $ver ($bron): CVE niet in de changelog, upstream lager dan $vu" fi else regel "ONBEKEND" "PHP $tak" "pakket $ver: geen changelog leesbaar (documentatie niet geinstalleerd?)" fi done < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\n' 'php*-common' 2>/dev/null | awk '$1 ~ /^.i/ {print $2, $3}') fi # ---- RHEL, Alma, Rocky, Fedora, Remi ---------------------------------------- if command -v rpm >/dev/null 2>&1 && ! command -v dpkg-query >/dev/null 2>&1; then while read -r pkg; do [ -n "$pkg" ] || continue ver=$(rpm -q --qf '%{VERSION}' "$pkg" 2>/dev/null) tak=$(echo "$ver" | cut -d. -f1,2) GEZIEN="$GEZIEN $tak" if rpm -q --changelog "$pkg" 2>/dev/null | grep -q -e "$CVE1" -e "$CVE2"; then regel "GEREPAREERD" "PHP $tak" "pakket $pkg, $CVE1 staat in de changelog"; continue fi vu=$(vast_upstream "$tak") if [ -n "$vu" ] && versie_ge "$ver" "$vu"; then regel "GEREPAREERD" "PHP $tak" "pakket $pkg, upstream $ver is $vu of nieuwer"; continue fi regel "NIET GEREPAREERD" "PHP $tak" "pakket $pkg: CVE niet in de changelog en versie lager dan ${vu:-een gerepareerde versie}" done < <(rpm -qa 2>/dev/null | grep -E '^(php|php[0-9]+-php)-common-[0-9]' | sort -u) fi # ---- Losse binaries (eigen build, Plesk, cPanel, CloudLinux, DirectAdmin) ---- for b in /usr/local/bin/php /opt/plesk/php/*/bin/php /opt/cpanel/ea-php*/root/usr/bin/php \ /opt/alt/php*/usr/bin/php /usr/local/php*/bin/php /opt/remi/php*/root/usr/bin/php \ /usr/local/lsws/lsphp*/bin/php; do [ -x "$b" ] || continue v=$("$b" -n -r 'echo PHP_VERSION;' 2>/dev/null) || continue tak=$(echo "$v" | cut -d. -f1,2) vu=$(vast_upstream "$tak") if [ -n "$vu" ] && versie_ge "${v%%-*}" "$vu"; then regel "GEREPAREERD" "PHP $tak" "$b ($v)" else regel "ONBEKEND" "PHP $tak" "$b ($v): geen pakketgegevens; vraag je leverancier of de reparatie erin zit" fi done if [ -z "$GEZIEN" ] && [ $((AANTAL_OK+AANTAL_NIET+AANTAL_ONB)) -eq 0 ]; then if command -v php >/dev/null 2>&1; then v=$(php -n -r 'echo PHP_VERSION;' 2>/dev/null) tak=$(echo "$v" | cut -d. -f1,2); vu=$(vast_upstream "$tak") if [ -n "$vu" ] && versie_ge "${v%%-*}" "$vu"; then regel "GEREPAREERD" "PHP $tak" "$(command -v php) ($v)" else regel "ONBEKEND" "PHP $tak" "$(command -v php) ($v): geen pakketgegevens gevonden"; fi else echo " Geen PHP gevonden op deze server (buiten eventuele containers)." fi fi if command -v docker >/dev/null 2>&1 || command -v podman >/dev/null 2>&1; then echo " Let op: PHP in containers wordt niet getoetst. Draai dit script ook in elke container met PHP." fi echo # ---- Mitigatie --------------------------------------------------------------- echo "${VET}2. Tijdelijke mitigatie (Sansec)${UIT}" echo " Gewenst: session.upload_progress.enabled = 0 en session.use_strict_mode = 1" PHPCODE='if(!extension_loaded("session")){echo "geen-sessie";exit;} echo var_export((bool)ini_get("session.upload_progress.enabled"),true),"|",var_export((bool)ini_get("session.use_strict_mode"),true);' toon_mitigatie() { # label uitvoer local l="$1" u="$2" up sm if [ "$u" = "geen-sessie" ]; then printf ' %-26s sessie-extensie niet geladen\n' "$l"; return; fi up=${u%%|*}; sm=${u##*|} if [ "$up" = "false" ] && [ "$sm" = "true" ]; then printf ' %-26s %sACTIEF%s (upload_progress uit, strict_mode aan)\n' "$l" "$GROEN" "$UIT" else printf ' %-26s %sNIET ACTIEF%s (upload_progress.enabled=%s, use_strict_mode=%s)\n' "$l" "$ROOD" "$UIT" \ "$([ "$up" = "true" ] && echo 1 || echo 0)" "$([ "$sm" = "true" ] && echo 1 || echo 0)" fi } gedaan=0 for d in /etc/php/*/; do [ -d "$d" ] || continue tak=$(basename "$d") bin="" for kandidaat in "/usr/bin/php$tak" "/usr/bin/php"; do if [ -x "$kandidaat" ] && [ "$("$kandidaat" -n -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null)" = "$tak" ]; then bin="$kandidaat"; break; fi done if [ -z "$bin" ]; then for sapi in fpm apache2 cli; do [ -f "$d$sapi/php.ini" ] && printf ' %-26s geen php-cli voor %s; lees %s zelf na\n' "PHP $tak ($sapi)" "$tak" "$d$sapi/php.ini" done continue fi for sapi in cli fpm apache2 cgi; do [ -f "$d$sapi/php.ini" ] || continue u=$(PHP_INI_SCAN_DIR="$d$sapi/conf.d" "$bin" -c "$d$sapi/php.ini" -r "$PHPCODE" 2>/dev/null) [ -n "$u" ] || u="?|?" toon_mitigatie "PHP $tak ($sapi)" "$u"; gedaan=1 done if [ -d "$d/fpm/pool.d" ]; then over=$(grep -sHnE '^[[:space:]]*php_(admin_)?(value|flag)\[session\.(upload_progress\.enabled|use_strict_mode)\]' "$d"fpm/pool.d/*.conf 2>/dev/null) if [ -n "$over" ]; then echo " FPM-pools van $tak zetten zelf een waarde (die gaat voor php.ini):" echo "$over" | sed 's/^/ /' fi [ -r "$d/fpm/pool.d" ] || echo " (FPM-pools van $tak niet leesbaar zonder root)" fi done if [ "$gedaan" = 0 ] && command -v php >/dev/null 2>&1; then toon_mitigatie "php (cli)" "$(php -r "$PHPCODE" 2>/dev/null || echo '?|?')" for p in /etc/php-fpm.d /etc/opt/remi/*/php-fpm.d; do [ -d "$p" ] || continue over=$(grep -sHnE '^[[:space:]]*php_(admin_)?(value|flag)\[session\.(upload_progress\.enabled|use_strict_mode)\]' "$p"/*.conf 2>/dev/null) [ -n "$over" ] && { echo " FPM-pools in $p zetten zelf een waarde:"; echo "$over" | sed 's/^/ /'; } done echo " Let op: op dit systeem gebruiken CLI en FPM vaak hetzelfde php.ini; controleer ook /etc/php-fpm.d." fi echo " Het gaat om fpm, apache2 en cgi: die bedienen websites. De cli-regel is ter informatie." echo " Niet getoetst: .user.ini, .htaccess en php_admin_value in vhosts of panelen (Plesk, cPanel, DirectAdmin)." echo # ---- Samenvatting ------------------------------------------------------------ echo "${VET}3. Samenvatting${UIT}" echo " Gerepareerd: $AANTAL_OK Niet gerepareerd: $AANTAL_NIET Onbekend: $AANTAL_ONB" if [ "$AANTAL_NIET" -gt 0 ]; then echo " ${ROOD}Actie nodig:${UIT} installeer de updates (apt/dnf), herstart PHP-FPM en Apache, en zet tot dan de mitigatie aan." echo " Voor PHP 8.1 en ouder: alleen Ubuntu/Debian-beveiligingsupdates of deb.sury.org leveren nog een reparatie." elif [ "$AANTAL_ONB" -gt 0 ]; then echo " Niet alles was vast te stellen. Vraag je leverancier of hoster om bevestiging." fi echo " Na een update moet PHP-FPM opnieuw starten, anders draait de oude code nog in het geheugen." echo " Hulp nodig? Stannet kijkt het voor je na: team@stannet.nl"