Realtime-Attack-Map
SURICATA / LIVE FEED

PHP2Shell mit Wazuh erkennen und begrenzen

Zwei kritische PHP-Lücken ohne Login-Pflicht, ein öffentlicher Exploit und wenig Zeit zum Handeln.

PHP2Shell mit Wazuh erkennen und begrenzen

Natürlich habe ich meine Systeme längst gepatcht, dennoch möchte ich kurz beschreiben, wie ich vielleicht erfolgreiche Angriffe auf meine Infrastruktur erkennen kann. Man weiß schileßlich nie, ob man etwas übersehen hat. Welche Überlegungen ich dazu hatte und welche Indikatoren dazu erforderlich sind, zeige ich im Folgenden auf.

Inhaltsverzeichnis

    Warum PHP2Shell so ernst zu nehmen ist

    Hinter PHP2Shell stecken zwei Schwachstellen im PHP-Kern, CVE-2026-109618 und CVE-2026-109619. Beide werden mit CVSS v4.0 9.2 bewertet, also als kritisch. Betroffen ist jede PHP-Installation, die Webanfragen beantwortet, egal ob über PHP-FPM, Apache mit mod_php, php-cgi oder eine vergleichbare Laufzeit. Für einen Angriff ist kein Login nötig, der Server muss lediglich über das Netzwerk erreichbar sein.

    Hinzu kommt, dass ein Proof-of-Concept per anonymem Beitrag auf Hacker News auftauchte, noch bevor der koordinierte Patch-Prozess abgeschlossen war. Berichte über erste Angriffswellen sprechen von Webshells und Cryptominern, und Ransomware-Gruppen sollen die Lücke ebenfalls für den Einstieg nutzen. Ein ungepatchter Webserver ist damit ein realistisches Ziel.

    Wie groß ist der Aufwand, um Zugriff auf fremde Server zu erhalten?

    Bisher war der Weg zur Shell aufwendig. Die Lücke existierte seit Jahrzehnten, doch die Ausnutzung verlangte eine verschachtelte Kette von Schritten, die kaum jemand ohne tiefes Spezialwissen nachbauen konnte. Genau hier sehen die Forscher von Wiz das Problem: Frontier-KI-Modelle haben diese Hürde deutlich gesenkt, und aus einer lange übersehenen Schwachstelle wurde in kurzer Zeit ein funktionierender Exploit.

    Für die Verteidigung heißt das, dass sich die Zeit zwischen Veröffentlichung und Massenausnutzung verkürzt.

    Wer auf den nächsten Wartungszyklus wartet, sollte mit einem Angriff rechnen, bevor das Update eingespielt ist.

    Auch mir ist ein explosionsartiger Anstieg PHP-bezogener Zugriffsversuche nicht entgangen.

    Erkennung über Prozessüberwachung am Beispiel von Nextcloud

    Ein Exploit, der Befehle ausführt, startet häufig einen Kindprozess des PHP-Workers. Typisch sind Shells oder Download-Werkzeuge wie curl und wget, die unter dem Account des Webservers laufen. Ein Skript prüft periodisch die Prozesstabelle und meldet Shells und Netzwerkprogramme, deren Elternprozess php-fpm ist.

    #!/bin/bash
    # M. Meister
    # Meldet Shells und Downloader, die direkt von PHP-FPM-Workern gestartet wurden
    ps -eo pid=,ppid=,comm=,args= | awk '
    { pid=$1; comm[pid]=$3; line[pid]=$0 }
    END {
      for (p in line) {
        split(line[p], f, " ")
        parent = comm[f[2]]
        child  = f[3]
        if (parent ~ /^php-fpm/ &&
            child  ~ /^(sh|bash|dash|zsh|nc|ncat|netcat|curl|wget|python3?|perl)$/)
          print "PHP2SHELL_CHILD host=" ENVIRON["HOSTNAME"] " parent=" parent " " line[p]
      }
    }'
    

    Eingebunden wird das Skript über das Command-Modul des Wazuh-Agenten:

    <wodle name="command">
      <disabled>no</disabled>
      <tag>php2shell_children</tag>
      <command>/var/ossec/etc/php2shell_children.sh</command>
      <interval>1m</interval>
      <ignore_output>no</ignore_output>
      <run_on_start>yes</run_on_start>
      <timeout>30</timeout>
    </wodle>
    

    Die Methode hat eine klare Schwäche. Ein Prozess, der nach wenigen Sekunden endet, fällt zwischen zwei Läufen durch. Genau diese Lücke schließt auditd.

    Erkennung über auditd

    auditd protokolliert jeden execve-Aufruf direkt im Kernel. Ein Shell-Start unter dem Webserver-Account landet damit im Audit-Log, egal wie kurz der Prozess lebt. Zusätzlich stehen Prozess-ID, Elternprozess, Argumente und UID im Ereignis. Für die Auswertung ist das deutlich aussagekräftiger als eine Momentaufnahme der Prozesstabelle.

    Der Webserver-Account ist bei Debian und Ubuntu meist www-data mit der UID 33. Die UID prüft man zuerst:

    id -u www-data
    

    Die Regeldatei lautet /etc/audit/rules.d/php2shell.rules. Falls die UID abweicht, ersetzt man die 33 entsprechend. Entstehende Logs werden mit dem Key php2shell_web_exec markiert:

    -a always,exit -F arch=b64 -S execve -F euid=33 -k php2shell_web_exec
    -a always,exit -F arch=b32 -S execve -F euid=33 -k php2shell_web_exec
    

    Die zweite Zeile betrifft 32-Bit-Prozesse und entfällt auf Systemen ohne diese Unterstützung. Danach werden die Regeln geladen und kontrolliert:

    sudo augenrules --load
    sudo auditctl -l
    

    Die Audit-Log-Datei wird über einen localfile-Eintrag in die ossec.conf des Agenten eingebunden:

    <localfile>
      <log_format>audit</log_format>
      <location>/var/log/audit/audit.log</location>
    </localfile>
    

    Auf dem Manager erkennt eine eigene Regel verdächtige Programme, die mit dem Key php2shell_web_exec protokolliert wurden:

    <group name="local,auditd,php2shell,">
      <rule id="100610" level="12">
        <if_sid>80700</if_sid>
        <field name="audit.key">php2shell_web_exec</field>
        <field name="audit.exe" type="pcre2">(?i)/(sh|bash|dash|zsh|python[0-9.]*|perl|curl|wget|nc|ncat|socat|whoami|uname|mkfifo)$</field>
        <description>PHP2Shell-Verdacht: Webserver-Account hat ein verdächtiges Programm gestartet: $(audit.exe)</description>
        <mitre>
          <id>T1059.004</id>
          <id>T1505.003</id>
        </mitre>
      </rule>
    </group>
    

    Die Parent-Regel 80700 und die Feldnamen müssen in der eingesetzten Wazuh-Version geprüft werden, etwa mit wazuh-logtest. Zur Kontrolle lassen sich die Audit-Ereignisse direkt auf dem Host abfragen:

    sudo ausearch -k php2shell_web_exec -i
    

    Die Grenze von auditd ist die Nextcloud selbst. Nextcloud startet als www-data legitime Helfer wie soffice, ffmpeg oder pdftoppm für die Vorschaugenerierung. Diese Ereignisse erscheinen genauso wie ein Angriff und müssen über eine Ausnahmeliste in der Reaktion herausgefiltert werden. Aber das sieht man über die Zeit und kann es entsprechend finetunen.

    Erkennung über Dateiüberwachung

    Persistenz braucht fast immer eine Dateiänderung. Wer eine Webshell ablegt oder eine bestehende PHP-Datei manipuliert, hinterlässt Spuren im Dateisystem, die sich zuverlässig beobachten lassen. Bei Nextcloud bietet sich die Überwachung von Core, Libraries, Apps und Konfiguration an. Das Datenverzeichnis schließt man aus, weil dort ständig Uploads entstehen und die Alarmflut die Erkennung unbrauchbar machen würde.

    <syscheck>
      <directories realtime="yes" check_all="yes" report_changes="yes">/var/www/nextcloud/lib</directories>
      <directories realtime="yes" check_all="yes" report_changes="yes">/var/www/nextcloud/core</directories>
      <directories realtime="yes" check_all="yes">/var/www/nextcloud/apps</directories>
      <directories check_all="yes">/var/www/nextcloud/config</directories>
      <directories realtime="yes" check_all="yes">/tmp</directories>
      <ignore>/var/www/nextcloud/data</ignore>
    </syscheck>
    

    Darauf aufbauend erkennen eigene Regeln neue PHP-Dateien und verdächtige Funktionsaufrufe in geänderten Dateien. Funktionen wie eval oder base64_decode sind allerdings kein Beweis für Schadcode, denn legitime Anwendungen verwenden sie ebenfalls. Der Treffer ist deshalb ein Hinweis zur Prüfung und kein Urteil. Aber diese Alarmbewertung unterstützt ja seit langem meine integrierte lokale KI.

    Wazuh on Steroids: AI-Powered Incident Analysis On-Demand
    Upgrade Wazuh with an AI-powered active response for on-demand, deep-dive incident analysis.

    Logs und Korrelation

    Webserver- und Anwendungslogs liefern den Kontext zu einem Vorfall. Standard-Access-Logs enthalten keine Request-Bodies, weshalb der eigentliche Exploit dort selten sichtbar ist. Aussagekräftiger sind Laufzeitfehler wie erschöpfter Speicher oder abstürzende Worker, die sich bei Angriffsversuchen häufen.

    <group name="local,php2shell,">
      <rule id="100620" level="8">
        <match type="pcre2">(?i)(Allowed memory size.*exhausted|Out of memory|PHP Fatal error|child .*exited on signal|segmentation fault)</match>
        <description>PHP2Shell-Verdacht: PHP-Laufzeitfehler oder Worker-Absturz</description>
      </rule>
    
      <rule id="100630" level="14" frequency="2" timeframe="300">
        <if_matched_sid>100620</if_matched_sid>
        <same_source_ip />
        <description>PHP2Shell-Verdacht: Laufzeitfehler gefolgt von weiteren Auffälligkeiten</description>
      </rule>
    </group>
    

    Ein einzelner Laufzeitfehler sagt wenig. Eindeutig wird es, wenn ein Absturz, ein auditd-Eintrag für eine Shell und eine neue PHP-Datei kurz hintereinander auftreten. Wazuh bildet solche Abfolgen über frequency und timeframe ab, und genau diese Kombination liefert die verlässlichsten Hinweise.

    Reaktion mit Active Response und Firewall-Drops

    Sobald die Erkennung anschlägt, kann Active Response eingreifen. Ein Skript beendet den verdächtigen Prozess anhand der Prozess-ID aus dem auditd-Ereignis, und firewall-drop sperrt die Quell-IP für einen definierten Zeitraum. Das Skript prüft vorher eine Ausnahmeliste und protokolliert jeden Aufruf, damit sich der Testlauf auswerten lässt:

    #!/bin/bash
    # M. Meister
    # Beendet den im Alarm genannten Prozess, sofern er nicht auf der Ausnahmeliste steht
    # Benötigt jq; DRY_RUN=1 protokolliert nur, ohne einzugreifen
    LOG=/var/ossec/logs/active-responses.log
    ALLOW="soffice|soffice.bin|ffmpeg|pdftoppm|convert|gs"
    DRY_RUN=1
    
    INPUT=$(cat)
    CMD=$(echo "$INPUT" | jq -r '.command')
    PID=$(echo "$INPUT" | jq -r '.parameters.alert.data.audit.pid // empty')
    NAME=$(echo "$INPUT" | jq -r '.parameters.alert.data.audit.exe // empty' | xargs -r basename)
    
    echo "$(date -Is) cmd=$CMD pid=$PID exe=$NAME dry_run=$DRY_RUN" >> "$LOG"
    
    if [ "$CMD" = "add" ] && [ -n "$PID" ] && [ "$DRY_RUN" = "0" ] \
       && ! echo "$NAME" | grep -Eq "^($ALLOW)$"; then
      kill -9 "$PID" 2>/dev/null
    fi
    

    Erstmal mit einem DRY_RUN testen ;-)

    Die Konfiguration auf dem Manager verbindet Skript und Regel:

    <command>
      <name>kill-php-child</name>
      <executable>kill-php-child.sh</executable>
      <timeout_allowed>no</timeout_allowed>
    </command>
    
    <active-response>
      <disabled>no</disabled>
      <command>kill-php-child</command>
      <location>local</location>
      <rules_id>100610</rules_id>
    </active-response>
    
    <active-response>
      <disabled>no</disabled>
      <command>firewall-drop</command>
      <location>local</location>
      <rules_id>100630</rules_id>
      <timeout>600</timeout>
    </active-response>
    

    Das Beenden des Prozesses verhindert den Shell-Start im Moment des Alarms, die Sperre der Quell-IP stoppt weitere Anfragen aus derselben Richtung. Beides ist nützlich, aber vor dem Scharfschalten braucht es den Testlauf im Logmodus. Die Feldnamen im JSON der Active-Response-Parameter sollten mit einem echten Alarm abgeglichen werden.

    Eine KI-gestützte Auswertung kann die Alarme bündeln und Handlungsempfehlungen ableiten, etwa welche Dateien zu prüfen sind oder welche Konten gesperrt werden sollten. Die Entscheidung über einen Eingriff bleibt jedoch bei einem Menschen, denn auch eine generierte Empfehlung kann falsch liegen. Einen für mich neuen Ansatz habe ich in München kennengelernt, als wir die KI per MCP an wazuh angebunden haben.

    SANS SEC555: Eindrücke aus München
    Wie man Angriffe erkennt, wenn sich Dateien ständig ändern, und warum lokale KI im Betrieb sinnvoll ist.

    Wo die Erkennung zu spät kommt

    Alle beschriebenen Maßnahmen reagieren erst, wenn ein Angriff läuft. Ein Exploit, der innerhalb des PHP-Workers weiterarbeitet, ohne eine Shell zu starten, erzeugt keinen execve-Eintrag. Eine Webshell, die bereits abgelegt wurde, überlebt jedes Update, wenn niemand sie entfernt. Und Active Response kann Daten, die schon abgeflossen sind, nicht zurückholen.

    Die Erkennung ist damit ein Sicherheitsnetz für den Fall, dass ein Patch fehlt oder verspätet kommt. Sie ersetzt ihn nicht.

    Proaktiv gibt es nur den Patch

    Die Lücke lässt sich nur durch ein Update schließen. Die Fixes stecken in PHP 8.5.12, 8.4.27, 8.3.36 und 8.2.35. Distributionen liefern teilweise zurückportierte Fixes unter alter Versionsnummer, deshalb reicht ein Blick auf php -v nicht immer aus. Die Herstellerinformationen der eigenen Distribution sind der verlässlichere Maßstab. PHP 8.1 und älter erhält keine regulären Updates mehr, dort hilft nur eine Migration.

    Nach dem Update müssen die PHP-Prozesse neu gestartet werden, sonst laufen die alten Worker mit der verwundbaren Version weiter. Danach lohnt sich die Frage, ob der Server schon vorher kompromittiert war. Dateiänderungen, auditd-Einträge und verdächtige Prozesse geben darauf Hinweise. Ein Update entfernt keine Hintertür, die bereits eingerichtet wurde.

    Fazit

    PHP2Shell ist eine kritische Schwachstelle, die ohne Login ausnutzbar ist und durch KI-Werkzeuge leichter zugänglich wird. Die Kombination aus auditd, Prozess- und Dateiüberwachung sowie Active Response verkürzt die Reaktionszeit deutlich, verhindert den Angriff aber nicht. Das wirksame Gegenmittel bleibt das Update.