Am 21. Oktober 2026 hat das PHP-Projekt Notfall-Updates für zwei kritische Lücken veröffentlicht, zusammen bekannt als PHP2Shell: CVE-2026-109618 und CVE-2026-109619, beide CVSS 9,2. Angreifer brauchen weder Login noch eine Schwachstelle in Ihrer Anwendung – die Lücke steckt in PHP selbst. In unserem ersten Beitrag ging es um sichere Versionen und Sofortmaßnahmen; hier geht es um die tatsächliche Ausnutzung und ein Skript, das alle PHP-Versionen eines Servers automatisch prüft und patcht.
Die zwei Lücken
| CVE | Typ | Kurz |
|---|---|---|
| CVE-2026-109618 | RCE via Out-of-Memory-Corruption | Geht PHP mitten in der Anfrage der Speicher aus, bleiben interne Daten halb initialisiert und werden beim Aufräumen als gültig behandelt. |
| CVE-2026-109619 | RCE via Stale Global State | Zustand eines abgestürzten Requests bleibt im Worker; ein späterer Request liest ihn erneut. Betroffener Pfad: Session Upload Progress (PHP_SESSION_UPLOAD_PROGRESS). |
Beide werden erreicht, während PHP die Anfrage einliest – ein einziger POST genügt. Das Abschalten von session.upload_progress betrifft nur CVE-2026-109619, nicht CVE-2026-109618.
Wie ausgenutzt wurde
- Angriffe liefen schon vor dem Patch – erste Treffer rund fünfeinhalb Stunden vor dem Release, erste Versuche laut Amazon sogar Tage vorher.
- Das BSI warnte mit Stufe „3 / Orange“; Behörden und Kommunen nahmen Webauftritte vom Netz.
- Innerhalb von Stunden folgten automatisierte Massen-Scans, Overload-Requests gegen den Multipart-Parser (Bodies bis 37 MB) und das Nachladen echter Malware (u. a. Mirai-Botnetz, Go-RAT, Webshells). Ein Ablauf führte in ~81 Minuten vom Scan zur abgelegten Webshell.
Indikatoren: POSTs mit PHP_SESSION_UPLOAD_PROGRESS (Signal, kein Beweis), hunderte leere Datei-Felder, User-Agents wie php2shell-scan/109618, PHP-Worker, die in Serie mit Speicherfehlern abstürzen oder eine Shell/curl/wget starten. Standen Sie länger verwundbar im Netz: auf Kompromittierung prüfen und Zugangsdaten/API-Schlüssel rotieren.
Sichere Versionen
PHP 8.2.35 · 8.3.36 · 8.4.27 · 8.5.12. Für 8.1 und älter gibt es keinen Fix – jetzt umsteigen.
- Ubuntu/Debian: Version bleibt oft gleich (
php -vmeldet weiter8.3.6). Entscheidend ist die Paketversion:dpkg -l 'php*-common'(abgesichert z. B. ab8.3.6-0ubuntu0.24.04.13). - Docker: Version im Container prüfen (
docker exec <container> php -v);docker pullallein reicht nicht.
Einziger vollständiger Schutz ist das Update. Notfall-Settings (bis zum Patch, Worker neu starten): gegen 109619 session.upload_progress.enabled=0 (auf 8.5 zusätzlich fatal_error_backtraces=0), gegen 109618 memory_limit=-1.
Automatisiert patchen
Es ist wichtig zu beachten, dass vor dem Patchen UNBEDINGT eine Sicherung erstellt werden muss!
Unser Bash-Skript erkennt alle installierten PHP-Versionen, prüft den Schutzstatus, installiert ausschließlich PHP-Updates und gibt am Ende eine klare Zusammenfassung aus. Es legt seinen Ablageordner /etc/scripts selbst an. Sicherheitsnetze: nur PHP wird angefasst (sonst Überspringen), eigene php.ini/FPM-Pools bleiben erhalten, altes ppa:ondrej/php wird reversibel auf packages.sury.org umgestellt, gesperrte Pakete (apt-mark hold) bleiben unberührt. Vorher Snapshot anlegen.
Es gibt genau zwei Aufrufe – mit oder ohne --check:
1. Nur prüfen – zeigt den Status an, verändert nichts:
./php-patch.sh --check

2. Prüfen + patchen – wenn der Prüflauf passt, die Updates einspielen:
./php-patch.sh

Das vollständige Skript:
php-patch.sh (klicken zum Auf-/Zuklappen)
#!/usr/bin/env bash
# =============================================================================
# php-cve-update.sh – PHP-Sicherheitsupdate (CVE-2026-109618 / CVE-2026-109619)
# =============================================================================
#
# WAS MACHT DAS SKRIPT?
# 1. Schaut nach, welche PHP-Versionen auf dem Server installiert sind.
# 2. Prüft für jede Version, ob sie gegen die Lücke geschützt ist.
# 3. Wenn ein Update mit dem Fix da ist, wird es installiert
# (NUR PHP-Pakete, sonst nichts).
# 4. Zeigt am Ende eine Zusammenfassung: geschützt / nicht geschützt /
# was du noch tun musst.
#
# WOHER KOMMT PHP? (das Skript erkennt das selbst)
# - Sury (ppa:ondrej/php oder packages.sury.org):
# Das alte Launchpad-PPA wird automatisch auf packages.sury.org
# umgestellt, weil der Fix nur dort zuverlässig ankommt.
# - Ubuntu / Debian (Pakete der Distribution):
# Es wird geprüft, ob das Ubuntu-/Debian-Security-Update den Fix
# enthält, und es wird ggf. installiert. Am Repo wird nichts geändert.
#
# AUFRUF (als root, im Skript-Verzeichnis):
# ./php-patch.sh --check # NUR prüfen (verändert nichts)
# ./php-patch.sh # prüfen + patchen
#
# SICHERHEITSNETZE:
# - Vorher Snapshot machen!
# - Es wird nur PHP aktualisiert. Würde apt irgendetwas anderes ändern
# oder entfernen, wird diese PHP-Version übersprungen.
# - Eigene php.ini / FPM-Pools bleiben erhalten (--force-confold).
# - Das PPA wird nur umbenannt (*.disabled), nicht gelöscht.
# - Klappt "apt-get update" nach der Umstellung nicht, wird alles
# zurückgedreht und NICHTS installiert.
# - Abgeschaltete PHP-FPM-Dienste werden NICHT wieder gestartet.
#
# EXIT-CODE: 0 = alles geschützt 1 = Handlungsbedarf / Fehler
# =============================================================================
# ---------------------------------------------------------------------------
# EINSTELLUNGEN – bei der nächsten Lücke nur diesen Block anpassen
# ---------------------------------------------------------------------------
# Erste Sury-/Upstream-Version mit Fix pro PHP-Zweig:
declare -A MIN_VERSION=(
[8.2]="8.2.35"
[8.3]="8.3.36"
[8.4]="8.4.27"
[8.5]="8.5.12"
)
# CVE-Nummern – danach wird im Changelog gesucht (Ubuntu/Debian-PHP und
# Sury-Backports für alte Versionen wie 8.1):
CVE_IDS="CVE-2026-109618 CVE-2026-109619"
KEYRING="/usr/share/keyrings/debsuryorg-archive-keyring.gpg"
KEYRING_URL="https://packages.sury.org/debsuryorg-archive-keyring.deb"
SURY_LIST="/etc/apt/sources.list.d/php.list"
LOGFILE="/var/log/php-cve-update.log"
HELD_FILE="/root/php-held-packages.txt" # Liste gesperrter PHP-Pakete (zum Wieder-Sperren)
APT_LOCKS="/var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock"
WEB_CONF_DIRS="/etc/apache2/sites-enabled /etc/apache2/conf-enabled /etc/nginx/sites-enabled /etc/nginx/conf.d /etc/caddy"
SCRIPT_DIR="/etc/scripts" # Ablageordner; wird vom Skript automatisch angelegt
# ===========================================================================
# AUSGABE-HELFER (Farben nur im Terminal, im Log ohne Farben)
# ===========================================================================
C_OK="" C_WARN="" C_ERR="" C_INFO="" C_BOLD="" C_OFF=""
setup_colors() {
if [ -t 1 ]; then
C_OK=$'\e[32m'; C_WARN=$'\e[33m'; C_ERR=$'\e[31m'; C_INFO=$'\e[36m'
C_BOLD=$'\e[1m'; C_OFF=$'\e[0m'
fi
}
step() { echo; echo "${C_BOLD}------ Schritt $1: $2 ------${C_OFF}"; }
ok() { echo " ${C_OK}[OK]${C_OFF} $*"; }
info() { echo " ${C_INFO}[INFO]${C_OFF} $*"; }
warn() { echo " ${C_WARN}[ACHTUNG]${C_OFF} $*"; }
err() { echo " ${C_ERR}[FEHLER]${C_OFF} $*"; }
sub() { echo " $*"; }
# ===========================================================================
# APT-HELFER
# ===========================================================================
# PIDs, die gerade einen apt/dpkg-Lock halten (leer = apt ist frei).
apt_lock_holders() {
if command -v fuser >/dev/null; then
# shellcheck disable=SC2086
fuser $APT_LOCKS 2>/dev/null | tr -s ' ' '\n' | grep -E '^[0-9]+$' | sort -u
else
pgrep -x 'apt-get|apt|dpkg|aptitude' 2>/dev/null
fi
}
# Wartet bis zu 10 Minuten, bis apt frei ist.
wait_for_apt() {
local waited=0 max=600 pids
while :; do
pids=$(apt_lock_holders)
[ -z "$pids" ] && return 0
if [ "$waited" -eq 0 ]; then
info "apt ist gerade von einem anderen Programm belegt – ich warte (max. 10 min)..."
ps -o pid,etime,cmd -p "$(echo $pids | tr ' ' ',')" 2>/dev/null | sed 's/^/ /'
fi
if [ "$waited" -ge "$max" ]; then
err "apt ist nach 10 Minuten immer noch belegt."
return 1
fi
sleep 5; waited=$((waited + 5))
done
}
# apt-get update – versucht es erneut, falls z. B. Checkmk (mk_apt) kurz den Lock hat.
# LC_ALL=C: apt-Meldungen immer auf Englisch, damit die Erkennung unten
# auch auf deutschsprachigen Servern funktioniert.
# Label-Änderung: Das alte ondrej-PPA hat sein "Label" geändert (Hinweis auf
# packages.sury.org). Das ist harmlos und wird automatisch akzeptiert.
apt_update_retry() {
local i out rc label_ok="" HASH_TRIES=0
for i in $(seq 1 60); do
# shellcheck disable=SC2086
out=$(LC_ALL=C apt-get -o DPkg::Lock::Timeout=300 $label_ok update -qq </dev/null 2>&1); rc=$?
if [ "$rc" -eq 0 ]; then
[ -n "$out" ] && echo "$out" | sed 's/^/ /'
return 0
fi
if [ -z "$label_ok" ] && echo "$out" | grep -q "changed its 'Label' value"; then
info "Das alte PPA hat seinen Namen geändert (Hinweis auf packages.sury.org) – wird akzeptiert."
label_ok="-o Acquire::AllowReleaseInfoChange::Label=true"
continue
fi
# Hash-Fehler = Ubuntu/Debian-Spiegelserver ist gerade mitten im Abgleich.
# 3x mit Pause neu versuchen, danach ohne Übersetzungsdateien (i18n).
if echo "$out" | grep -q 'Hash Sum mismatch'; then
HASH_TRIES=$(( ${HASH_TRIES:-0} + 1 ))
if [ "$HASH_TRIES" -le 3 ]; then
info "Spiegelserver liefert gerade fehlerhafte Dateien (Abgleich läuft) – neuer Versuch in 30 s ($HASH_TRIES/3)..."
rm -rf /var/lib/apt/lists/partial/* 2>/dev/null
sleep 30; continue
elif [[ "$label_ok" != *Languages* ]]; then
info "Versuche es ohne Übersetzungsdateien..."
label_ok="$label_ok -o Acquire::Languages=none"
continue
fi
fi
if echo "$out" | grep -q 'Could not get lock'; then
[ "$i" -eq 1 ] && info "Paketlisten kurz gesperrt (z. B. Monitoring) – versuche erneut..."
sleep 5; continue
fi
echo "$out" | sed 's/^/ /'
return 1
done
echo "$out" | sed 's/^/ /'
return 1
}
# Bietet packages.sury.org Pakete für diesen Codename (z. B. focal = nein)?
# Ohne curl oder bei Netzproblemen: "ja" annehmen (dann greift notfalls der Rollback).
sury_has_release() {
command -v curl >/dev/null || return 0
local code
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "https://packages.sury.org/php/dists/$1/InRelease")
[ "$code" = "404" ] && return 1
return 0
}
# Ist eine neuere Version im Repo als installiert?
newer_available() { # $1=installiert $2=candidate
[ -n "$2" ] && [ "$2" != "(none)" ] && dpkg --compare-versions "$2" gt "$1"
}
# ===========================================================================
# PHP-HELFER
# ===========================================================================
# Kommt dieses PHP von Sury? (sonst: Ubuntu/Debian)
is_sury_version() {
[[ "$1" == *deb.sury.org* || "$1" =~ \+0~20[0-9]{6} ]]
}
# Zählt, wie viele der CVE_IDS im Text auf stdin vorkommen.
count_cves() {
local text id n=0
text=$(cat)
for id in $CVE_IDS; do
grep -q -- "$id" <<<"$text" && n=$((n + 1))
done
echo "$n"
}
# Wie viele CVEs stehen im Changelog der INSTALLIERTEN Version?
cves_in_installed_changelog() {
local V="$1" p cl
for p in common cli fpm; do
cl="/usr/share/doc/php$V-$p/changelog.Debian.gz"
if [ -f "$cl" ]; then zcat "$cl" 2>/dev/null | count_cves; return; fi
done
echo 0
}
# Wie viele CVEs stehen im Changelog der VERFÜGBAREN Version? (-1 = nicht abrufbar)
cves_in_candidate_changelog() {
local out
out=$(timeout 60 apt-get changelog "php$1-common" 2>/dev/null </dev/null) || { echo -1; return; }
[ -z "$out" ] && { echo -1; return; }
count_cves <<<"$out"
}
# Ist PHP-Version $1 geschützt? Ausgabe: yes / partial / no
protection_status() {
local V="$1" inst="$2" min="${MIN_VERSION[$1]:-}" n total
total=$(echo $CVE_IDS | wc -w)
if is_sury_version "$inst" && [ -n "$min" ]; then
dpkg --compare-versions "$inst" ge "$min" && { echo yes; return; }
echo no; return
fi
# Ubuntu/Debian-PHP oder alte Sury-Version (8.1 & älter): Changelog entscheidet
n=$(cves_in_installed_changelog "$V")
if [ "$n" -ge "$total" ]; then echo yes
elif [ "$n" -gt 0 ]; then echo partial
else echo no
fi
}
# Wird PHP-FPM / mod_php $1 von einer Website genutzt?
used_by_web() {
local V="$1" s socks dirs=""
for s in $WEB_CONF_DIRS; do [ -d "$s" ] && dirs="$dirs $s"; done
[ -e "/etc/apache2/mods-enabled/php$V.load" ] && return 0 # mod_php aktiv
[ -z "$dirs" ] && return 1
# shellcheck disable=SC2086
grep -RqsF "php$V-fpm" $dirs && return 0 # Standard-Socket
socks=$(grep -hE '^[[:space:]]*listen[[:space:]]*=' /etc/php/"$V"/fpm/pool.d/*.conf 2>/dev/null \
| sed 's/^[^=]*=[[:space:]]*//; s/[[:space:]]*$//')
for s in $socks; do
# shellcheck disable=SC2086
grep -RqsF -- "$s" $dirs && return 0 # eigene Pool-Sockets
done
return 1
}
# Aktualisiert NUR die installierten PHP-Pakete einer Version.
# Bricht ab, wenn apt dabei Nicht-PHP-Pakete ändern oder etwas entfernen würde.
upgrade_php() {
local V="$1" PKGS SIM FOREIGN
PKGS=$(dpkg -l "php$V-*" "libapache2-mod-php$V" 2>/dev/null | awk '/^[hi]i/{print $2}')
# shellcheck disable=SC2086
SIM=$(LC_ALL=C apt-get -s install --only-upgrade $PKGS </dev/null 2>/dev/null | awk '/^(Inst|Remv) /{print $1" "$2}')
FOREIGN=$(echo "$SIM" | awk 'NF && ($1=="Remv" || $2 !~ /^(php|libapache2-mod-php)/)')
if [ -n "$FOREIGN" ]; then
warn "apt würde dabei auch andere Pakete ändern/entfernen – PHP $V wird NICHT angefasst:"
echo "$FOREIGN" | sed 's/^/ /'
sub "Bitte manuell ansehen: apt-get -s install --only-upgrade 'php$V-*'"
return 1
fi
if [ -z "$SIM" ]; then
info "Nichts zu aktualisieren."
return 0
fi
info "Installiere Update für $(echo "$SIM" | grep -c '^Inst') PHP-$V-Pakete ..."
# shellcheck disable=SC2086
if ! DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=300 install -y -qq --only-upgrade \
-o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef \
$PKGS </dev/null >/tmp/php-cve-update.apt.log 2>&1; then
err "Installation fehlgeschlagen. Details: /tmp/php-cve-update.apt.log"
return 1
fi
ok "Update installiert."
# Nur neu starten, was vorher lief – abgeschaltete Dienste bleiben aus.
if systemctl is-active --quiet "php$V-fpm" 2>/dev/null; then
systemctl restart "php$V-fpm" && ok "php$V-fpm neu gestartet."
fi
[ -e "/etc/apache2/mods-enabled/php$V.load" ] && RESTART_APACHE=1
return 0
}
# ===========================================================================
# HAUPTPROGRAMM
# (alles in main(), damit das Skript komplett geladen ist, bevor es startet)
# ===========================================================================
main() {
local CHECK_ONLY="" CODENAME DISABLED="" CREATED="" RESTART_APACHE="" USES_PPA="" NO_SURY=""
local V VERS INST CAND SRC STATUS N f TOTAL_CVES HELD
declare -A R_STATUS R_VERSION R_SRC R_FPM R_USED R_UPD R_WEBSAPI R_HELD
local TODO=()
case "${1:-}" in
--check) CHECK_ONLY=1 ;;
"") ;;
-h|--help) sed -n '2,40p' "$0" 2>/dev/null || echo "Optionen: --check"; return 0 ;;
*) echo "Unbekannte Option: $1 (erlaubt: --check)"; return 1 ;;
esac
[ "$(id -u)" -eq 0 ] || { echo "Bitte als root starten."; return 1; }
command -v apt-get >/dev/null || { echo "Kein Debian/Ubuntu-System (apt fehlt)."; return 1; }
# Ablageordner automatisch anlegen; bei Datei-Ausfuehrung eine Kopie dorthin legen
mkdir -p "$SCRIPT_DIR" 2>/dev/null
if [ -f "$0" ] && [ "$(cd "$(dirname "$0")" 2>/dev/null && pwd)" != "$SCRIPT_DIR" ]; then
cp -f "$0" "$SCRIPT_DIR/php-cve-update.sh" 2>/dev/null && chmod +x "$SCRIPT_DIR/php-cve-update.sh" 2>/dev/null
fi
setup_colors
# Ausgabe auf Bildschirm UND ins Log (im Log ohne Farbcodes)
exec > >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >>"$LOGFILE")) 2>&1
# shellcheck disable=SC1091
. /etc/os-release
CODENAME="${VERSION_CODENAME:-}"
TOTAL_CVES=$(echo $CVE_IDS | wc -w)
echo "${C_BOLD}=================================================================${C_OFF}"
echo "${C_BOLD} PHP-Sicherheitsupdate $(date '+%d.%m.%Y %H:%M') Server: $(hostname)${C_OFF}"
echo " System: ${PRETTY_NAME:-unbekannt}"
echo " Lücken: $CVE_IDS"
[ -n "$CHECK_ONLY" ] && echo " ${C_WARN}Modus: NUR PRÜFEN – es wird nichts verändert${C_OFF}"
echo "${C_BOLD}=================================================================${C_OFF}"
VERS=$(dpkg -l 'php*-common' 2>/dev/null | awk '/^[hi]i/{print $2}' | sed -n 's/^php\([0-9]\.[0-9]\)-common$/\1/p')
if [ -z "$VERS" ]; then
echo; ok "Auf diesem Server ist kein PHP installiert – nichts zu tun."
return 0
fi
# ---------------------------------------------------------------- Schritt 1
step 1 "Warten, bis apt frei ist"
wait_for_apt || { err "Abbruch – es wurde nichts verändert. Später nochmal starten."; return 1; }
ok "apt ist frei."
# ---------------------------------------------------------------- Schritt 2
step 2 "Paketquellen prüfen"
grep -rqsE 'ppa\.launchpad(content)?\.net/ondrej/php' /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources /etc/apt/sources.list && USES_PPA=1
if ! dpkg-query -W -f='${Version}\n' 'php*-common' 2>/dev/null | grep -qE 'deb\.sury\.org|\+0~20[0-9]{6}'; then
ok "PHP kommt von ${NAME:-der Distribution} (offizielle Pakete) – Paketquellen bleiben unverändert."
elif [ -z "$USES_PPA" ]; then
ok "PHP kommt von Sury (packages.sury.org) – passt."
elif ! sury_has_release "$CODENAME"; then
NO_SURY=1
USES_PPA=""
warn "Server nutzt noch das alte Launchpad-PPA, aber packages.sury.org bietet"
sub "für ${PRETTY_NAME:-dieses System} ($CODENAME) KEINE Pakete an."
sub "-> Für dieses Betriebssystem kommen von Sury keine PHP-Updates mehr."
sub " Paketquellen bleiben unverändert."
elif [ -n "$CHECK_ONLY" ]; then
warn "Server nutzt noch das alte Launchpad-PPA (ondrej/php)."
sub "Dort kommt der Fix später oder gar nicht an. Ohne --check stellt das"
sub "Skript automatisch auf packages.sury.org um."
else
info "Server nutzt noch das alte Launchpad-PPA -> stelle auf packages.sury.org um."
if [ ! -f "$KEYRING" ]; then
dpkg -s ca-certificates >/dev/null 2>&1 || apt-get -o DPkg::Lock::Timeout=300 -y -qq install ca-certificates </dev/null >/dev/null
command -v curl >/dev/null || apt-get -o DPkg::Lock::Timeout=300 -y -qq install curl </dev/null >/dev/null
curl -fsSLo /tmp/debsuryorg-archive-keyring.deb "$KEYRING_URL" \
&& dpkg -i /tmp/debsuryorg-archive-keyring.deb </dev/null >/dev/null
fi
if [ ! -f "$KEYRING" ]; then
err "Sury-Schlüssel konnte nicht installiert werden – Paketquellen bleiben unverändert."
else
for f in $(grep -rlE 'ppa\.launchpad(content)?\.net/ondrej/php' /etc/apt/sources.list.d/ 2>/dev/null | grep -v '\.disabled$'); do
mv -n "$f" "$f.disabled" && DISABLED="$DISABLED $f" && info "PPA deaktiviert: $(basename "$f")"
done
if ! grep -rqs 'packages.sury.org/php' /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; then
echo "deb [signed-by=$KEYRING] https://packages.sury.org/php/ $CODENAME main" > "$SURY_LIST"
CREATED=1
info "packages.sury.org eingetragen ($CODENAME)."
fi
fi
fi
# ---------------------------------------------------------------- Schritt 3
step 3 "Paketlisten aktualisieren (apt-get update)"
info "Läuft ... das kann 1–3 Minuten dauern, bitte NICHT abbrechen."
# Falls doch jemand Strg+C drückt: Umstellung sauber zurückdrehen.
if [ -n "$DISABLED$CREATED" ]; then
trap 'echo; warn "Abgebrochen – mache die Umstellung rückgängig ..."
for f in $DISABLED; do mv -n "$f.disabled" "$f"; done
[ -n "$CREATED" ] && mv -f "$SURY_LIST" "$SURY_LIST.failed"
info "Paketquellen sind wieder wie vorher. Es wurde nichts installiert."
exit 130' INT TERM
fi
if apt_update_retry; then
trap - INT TERM
ok "Paketlisten aktuell."
[ -n "$DISABLED$CREATED" ] && ok "Umstellung auf packages.sury.org erfolgreich."
rm -f "$SURY_LIST.failed" # Rest eines früheren Fehlversuchs (eigene Datei)
else
trap - INT TERM
err "apt-get update ist fehlgeschlagen."
if [ -n "$DISABLED$CREATED" ]; then
info "Mache die Umstellung rückgängig – es wird NICHTS installiert."
for f in $DISABLED; do mv -n "$f.disabled" "$f"; done
[ -n "$CREATED" ] && mv -f "$SURY_LIST" "$SURY_LIST.failed"
apt_update_retry >/dev/null
fi
return 1
fi
# ---------------------------------------------------------------- Schritt 4
step 4 "PHP-Versionen prüfen und patchen"
for V in $VERS; do
INST=$(dpkg-query -W -f='${Version}' "php$V-common")
CAND=$(LC_ALL=C apt-cache policy "php$V-common" | awk '/Candidate:/{print $2}')
if is_sury_version "$INST"; then SRC="Sury"; else SRC="${NAME:-Distribution}"; fi
echo
echo " ${C_BOLD}PHP $V${C_OFF} (Quelle: $SRC)"
sub "installiert: $INST"
[ "$CAND" != "$INST" ] && sub "verfügbar: $CAND"
STATUS=$(protection_status "$V" "$INST")
# Gesperrte Pakete (apt-mark hold) werden bewusst NICHT angefasst.
HELD=$(dpkg -l "php$V-*" "libapache2-mod-php$V" 2>/dev/null | awk '/^hi/{print $2}')
if [ -n "$HELD" ]; then
R_HELD[$V]=1
echo "$HELD" >> "$HELD_FILE"
sort -u -o "$HELD_FILE" "$HELD_FILE"
warn "$(echo "$HELD" | wc -l) PHP-$V-Pakete sind gegen Updates gesperrt (apt-mark hold)."
[ "$STATUS" != "yes" ] && sub "Werden NICHT automatisch aktualisiert – siehe Zusammenfassung."
fi
if [ "$STATUS" != "yes" ] && [ -z "$HELD" ] && newer_available "$INST" "$CAND"; then
if [ -n "$CHECK_ONLY" ]; then
R_UPD[$V]=1
if is_sury_version "$CAND" && [ -n "${MIN_VERSION[$V]:-}" ]; then
if dpkg --compare-versions "$CAND" ge "${MIN_VERSION[$V]}"; then
info "Update mit Fix ist verfügbar – wird im Prüf-Modus NICHT installiert."
else
info "Update verfügbar, enthält den Fix aber noch nicht (braucht ${MIN_VERSION[$V]})."
fi
else
N=$(cves_in_candidate_changelog "$V")
if [ "$N" -ge "$TOTAL_CVES" ]; then info "Update mit Fix ist verfügbar – wird im Prüf-Modus NICHT installiert."
elif [ "$N" -gt 0 ]; then info "Update verfügbar, enthält aber nur einen Teil der Fixes."
elif [ "$N" -eq 0 ]; then info "Update verfügbar, enthält den Fix laut Changelog aber noch nicht."
else info "Update verfügbar – ob der Fix drin ist, lässt sich vorab nicht prüfen."
fi
fi
else
upgrade_php "$V"
INST=$(dpkg-query -W -f='${Version}' "php$V-common")
STATUS=$(protection_status "$V" "$INST")
fi
fi
case "$STATUS" in
yes) ok "GESCHÜTZT" ;;
partial) warn "NUR TEILWEISE GESCHÜTZT (nicht alle CVEs im Changelog)" ;;
*) warn "NICHT GESCHÜTZT" ;;
esac
if [ "$SRC" = "Sury" ] && [ -z "${MIN_VERSION[$V]:-}" ]; then
warn "PHP $V bekommt offiziell keine Updates mehr – Umstieg auf PHP 8.3+ planen."
fi
# Für die Zusammenfassung merken
R_STATUS[$V]=$STATUS
R_VERSION[$V]=$INST
R_SRC[$V]=$SRC
if systemctl list-unit-files "php$V-fpm.service" 2>/dev/null | grep -q "php$V-fpm"; then
R_FPM[$V]=$(systemctl is-active "php$V-fpm" 2>/dev/null)
else
R_FPM[$V]="-"
fi
if used_by_web "$V"; then R_USED[$V]="ja"; else R_USED[$V]="nein"; fi
# Gibt es überhaupt eine Web-Schnittstelle (FPM, mod_php, CGI)? Reines CLI ist nicht betroffen.
if [ "${R_FPM[$V]}" != "-" ] || dpkg -l "libapache2-mod-php$V" "php$V-cgi" 2>/dev/null | grep -q '^ii'; then
R_WEBSAPI[$V]=1
fi
done
if [ -n "$RESTART_APACHE" ] && systemctl is-active --quiet apache2; then
systemctl restart apache2 && ok "apache2 neu gestartet (mod_php)."
fi
# ---------------------------------------------------------------- Zusammenfassung
echo
echo "${C_BOLD}=================================================================${C_OFF}"
echo "${C_BOLD} ZUSAMMENFASSUNG – $(hostname)${C_OFF}"
echo "${C_BOLD}=================================================================${C_OFF}"
printf " %-8s %-20s %-10s %-10s %s\n" "PHP" "Status" "FPM" "Website?" "Version"
printf " %-8s %-20s %-10s %-10s %s\n" "---" "------" "---" "--------" "-------"
for V in $VERS; do
case "${R_STATUS[$V]}" in
yes) S="${C_OK}GESCHÜTZT${C_OFF} " ;;
partial) S="${C_WARN}TEILWEISE${C_OFF} " ;;
*) S="${C_ERR}NICHT GESCHÜTZT${C_OFF} " ;;
esac
printf " %-8s %s %-10s %-10s %s\n" "$V" "$S" "${R_FPM[$V]}" "${R_USED[$V]}" "${R_VERSION[$V]}"
# Handlungsempfehlungen
if [ "${R_STATUS[$V]}" != "yes" ] && [ -n "${R_HELD[$V]:-}" ]; then
TODO+=("PHP $V ist ungeschützt, weil die Pakete gesperrt sind (apt-mark hold). Erst klären, warum gesperrt
(z. B. Nextcloud-Setup), dann: freigeben, Skript erneut starten, wieder sperren:
apt-mark unhold \$(cat $HELD_FILE)
<Skript erneut starten>
apt-mark hold \$(cat $HELD_FILE)")
elif [ "${R_STATUS[$V]}" != "yes" ] && [ -n "${R_UPD[$V]:-}" ]; then
TODO+=("Für PHP $V ist ein Update verfügbar -> Skript OHNE --check starten.")
elif [ "${R_STATUS[$V]}" != "yes" ] && [ -z "${R_WEBSAPI[$V]:-}" ]; then
TODO+=("PHP $V ist nur als Kommandozeile (CLI) installiert – die Lücke betrifft nur Web-PHP (FPM/mod_php/CGI).
Kein akutes Risiko. Update einspielen, sobald verfügbar.")
elif [ "${R_STATUS[$V]}" != "yes" ]; then
if [ "${R_FPM[$V]}" = "active" ] && [ "${R_USED[$V]}" = "nein" ]; then
TODO+=("PHP $V ist ungeschützt, läuft aber, obwohl es anscheinend keine Website nutzt.
Kurz selbst prüfen, dann abschalten: systemctl disable --now php$V-fpm")
elif [ "${R_FPM[$V]}" = "active" ] || [ "${R_USED[$V]}" = "ja" ]; then
local MIT=""
if [ "${R_FPM[$V]}" = "active" ]; then
MIT="echo 'session.upload_progress.enabled = 0' > /etc/php/$V/fpm/conf.d/99-cve-2026-109619.ini && systemctl restart php$V-fpm"
fi
if [ -e "/etc/apache2/mods-enabled/php$V.load" ]; then
[ -n "$MIT" ] && MIT="$MIT
"
MIT="${MIT}echo 'session.upload_progress.enabled = 0' > /etc/php/$V/apache2/conf.d/99-cve-2026-109619.ini && systemctl restart apache2"
fi
[ -z "$MIT" ] && MIT="echo 'session.upload_progress.enabled = 0' > /etc/php/$V/fpm/conf.d/99-cve-2026-109619.ini && systemctl restart php$V-fpm"
TODO+=("PHP $V ist ungeschützt und in Benutzung. Notlösung bis zum Update (schützt nur gegen CVE-2026-109619):
$MIT
Skript später erneut starten, sobald ${R_SRC[$V]} das Update veröffentlicht hat.")
else
TODO+=("PHP $V ist ungeschützt, läuft aber nicht – kein akutes Risiko. Bei Gelegenheit deinstallieren.")
fi
elif [ "${R_USED[$V]}" = "nein" ] && [ "${R_FPM[$V]}" = "active" ]; then
TODO+=("PHP $V ist gepatcht, wird aber anscheinend nicht genutzt. Kurz prüfen, dann abschalten:
systemctl disable --now php$V-fpm")
fi
done
[ -n "$USES_PPA" ] && [ -n "$CHECK_ONLY" ] && \
TODO+=("Skript OHNE --check starten, damit das alte PPA auf packages.sury.org umgestellt wird.")
[ -n "$NO_SURY" ] && \
TODO+=("WICHTIG: ${PRETTY_NAME:-Dieses System} wird von Sury nicht mehr unterstützt – PHP bekommt hier
keine Updates mehr. Lösung: Server auf ein aktuelles Ubuntu/Debian upgraden oder Websites
auf einen neuen Server umziehen. Bis dahin Notlösung setzen und ungenutzte PHP-Versionen abschalten.")
echo
if [ "${#TODO[@]}" -eq 0 ]; then
echo " ${C_OK}${C_BOLD}ALLES ERLEDIGT – kein Handlungsbedarf.${C_OFF}"
else
echo " ${C_WARN}${C_BOLD}WAS DU JETZT NOCH TUN SOLLTEST:${C_OFF}"
N=1
for f in "${TODO[@]}"; do
echo " $N. $f"
N=$((N + 1))
done
fi
echo
echo " \"Website?\" = in Apache/Nginx/Caddy-Config gefunden (bitte im Zweifel selbst prüfen)"
echo " Log: $LOGFILE"
echo "${C_BOLD}=================================================================${C_OFF}"
for V in $VERS; do [ "${R_STATUS[$V]}" = "yes" ] || return 1; done
return 0
}
main "$@"
Die nächste Lücke kommt bestimmt
PHP2Shell hat gezeigt, wie schnell aus einem Leak eine weltweite Angriffswelle wird. Entscheidend ist, wie schnell die nächste Lücke auf Ihren Systemen geschlossen wird. Mit einem Wartungsvertrag übernehmen wir das für Sie – Managed Webhosting, WordPress Wartung & Support, Hetzner Systemadministration, Checkmk Monitoring und OpenVAS Schwachstellenscan.
Sie sind unsicher, ob Ihr Server abgesichert ist? Schreiben Sie uns über das Kontaktformular oder rufen Sie an: +49 731 388 62810 (Mo–Fr, 08:30–17:30 Uhr). Wir prüfen Ihre Systeme und bringen sie auf den aktuellen Stand.

Angehender Fachinformatiker – Systemintegration und Hobby-Entwickler.
Wordpress, Crowdsec, SEO und teilweise Entwicklung ist mein Themengebiet.