testing php2shell detection

PHP2Shell Threat Hunting: Suricata-Signaturen, Falco-Runtime-Regeln und Auditd

php2shell.run Research Lab •

PHP2Shell Threat Hunting: Suricata-Signaturen, Falco-Runtime-Regeln und Auditd

<time datetime="2026-10-23">Geprüft am 23. Oktober 2026</time>

PHP2Shell (CVE-2026-109618 und CVE-2026-109619) hinterlässt in Anwendungslogs kaum Spuren. Die Requests sehen wie gewöhnliche multipart/form-data-Uploads aus, und die Kompromittierung erfolgt, bevor Benutzercode ausgeführt wird. Wer nur auf Anwendungsfehler oder WAF-Alarme schaut, wird einen Angriff verpassen. Belastbare Erkennung entsteht durch drei unabhängige Ebenen: Netzwerk, Laufzeit und Kernel-Audit. Dieses Dokument liefert konkrete, produktionsnahe Regeln und beschreibt, wo ihre Grenzen liegen.


1. Architektur der Erkennung

 ┌─────────────────────────────────────────────────────────────┐
 │ Ebene 1: Netzwerk (Suricata / Zeek / Corelight)             │
 │  - multipart-Body mit PHP_SESSION_UPLOAD_PROGRESS           │
 │  - kleine, wiederholte POSTs auf Nicht-Upload-Pfade         │
 └───────────────────────────┬─────────────────────────────────┘
                             ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ Ebene 2: Host-Runtime (Falco / Tetragon / eBPF)             │
 │  - php-fpm-Kindprozess startet sh, curl, wget, nc           │
 │  - unerwartete ausgehende Verbindungen                      │
 └───────────────────────────┬─────────────────────────────────┘
                             ▼
 ┌─────────────────────────────────────────────────────────────┐
 │ Ebene 3: Kernel-Audit (auditd / eBPF)                       │
 │  - execve, connect, openat unter www-data                   │
 │  - forensische Rekonstruktion                               │
 └─────────────────────────────────────────────────────────────┘

Die Ebenen ersetzen einander nicht. Netzwerkregeln lassen sich durch Kodierung umgehen, Runtime-Regeln durch Kernel-Rootkits unterlaufen, und Auditd verhindert nichts – es dokumentiert. Erst die Korrelation erzeugt ein belastbares Bild.


2. Netzwerkebene: Suricata

2.1 Produktionsregel

# /etc/suricata/rules/php2shell.rules
alert http any any -> $HOME_NET any (
  msg:"PHP2Shell CVE-2026-109618/109619 multipart upload progress probe";
  flow:established,to_server;
  http.method; content:"POST";
  http.content_type; content:"multipart/form-data"; nocase;
  file.data; content:"PHP_SESSION_UPLOAD_PROGRESS"; nocase; distance:0;
  threshold: type limit, track by_src, count 5, seconds 60;
  classtype:attempted-admin;
  reference:cve,2026-109618;
  reference:cve,2026-109619;
  sid:10261092; rev:1;
)
# multipart-POST auf Pfade, die keine Uploads annehmen sollten
alert http any any -> $HOME_NET any (
  msg:"PHP2Shell multipart POST to non-upload path";
  flow:established,to_server;
  http.method; content:"POST";
  http.content_type; content:"multipart/form-data"; nocase;
  http.uri; content:"/"; pcre:"/^\/(api|static|assets|health)/i";
  threshold: type limit, track by_src, count 10, seconds 60;
  sid:10261093; rev:1;
)

2.2 Deployment und Prüfung

sudo cp php2shell.rules /etc/suricata/rules/
sudo sed -i 's/^#\s*-\s*php2shell.rules/- php2shell.rules/' \
  /etc/suricata/suricata.yaml

sudo suricata -T -c /etc/suricata/suricata.yaml -v
sudo systemctl reload suricata
sudo tail -f /var/log/suricata/fast.log | grep -E '1026109[23]'

Die Regel stützt sich auf file.data, also auf den reassemblierten Dateistrom. Prüfen Sie stream.reassembly.depth; ist der Wert zu klein, werden kurze Bodies zwar meist erkannt, aber aufgeteilte Feldnamen können durchrutschen. Ein zweiter Blick gilt http.content_type: Einige Proxys normalisieren den Header, andere nicht. Testen Sie die Regel daher mit echten Requests aus Ihrer Infrastruktur.

2.3 Zeek-Korrelation

event http_entity_data(c: connection, is_orig: bool, length: count, data: string) {
    if (is_orig && /PHP_SESSION_UPLOAD_PROGRESS/ in data) {
        NOTICE([$note=Notice::ACTION_LOG,
                $msg="PHP2Shell multipart progress field observed",
                $conn=c]);
    }
}

Zeek eignet sich besonders für die Langzeitkorrelation: Ein einzelner Treffer ist schwach, aber wiederholte Treffer von derselben Quelle innerhalb weniger Minuten sind ein starkes Signal.


3. Host-Runtime: Falco

PHP-FPM-Worker starten normalerweise keine Shell. Wenn doch, ist das ein kritischer Hinweis – unabhängig davon, ob PHP2Shell die Ursache ist.

# /etc/falco/falco_rules.local.yaml
- rule: PHP-FPM Child Spawns Shell or Downloader
  desc: php-fpm-Worker startet Shell oder Downloader (PHP2Shell-RCE-Verdacht)
  condition: >
    spawned_process and
    proc.pname in (php-fpm, php-fpm8.1, php-fpm8.2, php-fpm8.3, php-fpm8.4, php-fpm8.5) and
    proc.name in (sh, bash, dash, zsh, curl, wget, nc, ncat, socat)
  output: >
    "PHP-FPM spawned suspicious process (user=%user.name cmd=%proc.cmdline
     parent=%proc.pname pid=%proc.pid)"
  priority: CRITICAL
  tags: [process, mitre_execution, php2shell]

- rule: PHP-FPM Outbound Connection to Unusual Port
  desc: ungewöhnliche ausgehende Verbindung eines php-fpm-Workers
  condition: >
    outbound and
    proc.pname in (php-fpm, php-fpm8.3, php-fpm8.4, php-fpm8.5) and
    not fd.sport in (80, 443) and
    not fd.rport in (80, 443, 3306, 6379, 5432)
  output: >
    "PHP-FPM outbound connection (cmd=%proc.cmdline dport=%fd.rport)"
  priority: WARNING
  tags: [network, mitre_exfiltration]
sudo falcoctl artifact install falco-rules 2>/dev/null || true
sudo systemctl restart falco
sudo journalctl -u falco -f | grep -i php2shell

In Kubernetes sollten container.name und k8s.ns.name in die Bedingung aufgenommen werden. Andernfalls vermischen sich Alarme aus verschiedenen Workloads. Für Batch-Jobs, die legitim curl verwenden, sind eng gefasste Ausnahmen über proc.cmdline nötig. Dokumentieren Sie jede Ausnahme mit Begründung und Ablaufdatum; sonst entsteht schleichend eine Lücke.


4. Kernel-Ebene: Auditd

# Prozesse unter www-data/nginx verfolgen
sudo auditctl -a always,exit -F arch=b64 -S execve \
  -F auid=-1 -F euid=33 -k php2shell_exec

# Ausgehende Verbindungen
sudo auditctl -a always,exit -F arch=b64 -S connect -F euid=33 -k php2shell_net

# Ausführbare Dateien in /tmp
sudo auditctl -w /tmp -p x -k php2shell_tmp_exec
# Persistente Regeln
sudo tee /etc/audit/rules.d/php2shell.rules >/dev/null <<'EOF'
-a always,exit -F arch=b64 -S execve -F euid=33 -k php2shell_exec
-a always,exit -F arch=b64 -S connect -F euid=33 -k php2shell_net
-w /tmp -p x -k php2shell_tmp_exec
EOF

sudo augenrules --load
sudo auditctl -l | grep php2shell
# Forensische Auswertung
sudo ausearch -k php2shell_exec -ts recent --format text | head -50
sudo aureport -k -i --summary

Auditd ist mächtig, aber teuer. Begrenzen Sie die Regeln auf die relevanten UIDs und Syscalls, und leiten Sie die Logs an ein zentrales SIEM weiter. Lokale Logs sind bei einem erfolgreichen Angriff oft die ersten, die verschwinden.


5. Korrelation und Incident-Ablauf

Ein einzelnes Signal reicht nicht. Suchen Sie nach der Kombination aus:

  1. mindestens fünf PHP_SESSION_UPLOAD_PROGRESS-Multipart-POSTs innerhalb von 60 Sekunden,
  2. unmittelbar folgenden SIGSEGV/SIGABRT-Worker-Beendigungen,
  3. Prozessstarts von sh, curl, wget unter www-data,
  4. ausgehenden Verbindungen auf ungewöhnliche Ports.
sudo ausearch -k php2shell_exec -ts recent | aureport -f -i | head -20
journalctl -u php8.3-fpm --since "10 min ago" | grep -E 'segfault|SIGSEGV'

Reaktionsreihenfolge

  1. Quell-IP am Perimeter sperren und betroffene Session-IDs invalidieren.
  2. Worker neu starten, Core-Dump und Speicherabbild sichern.
  3. session.upload_progress.enabled = Off setzen, bis der Patch eingespielt ist.
  4. Egress blockieren und Anmeldedaten rotieren.
  5. Forensik dokumentieren und Erkennungsregeln nachschärfen.

6. Schwellwerte, Fehlalarme und Log-Pipeline

Die größte praktische Hürde ist nicht das Schreiben der Regel, sondern das Kalibrieren. Ein Schwellwert von fünf Treffern pro Minute ist für einen kleinen Blog zu empfindlich und für ein stark frequentiertes Portal zu träge. Messen Sie zunächst zwei Wochen lang im reinen Beobachtungsmodus und leiten Sie den Schwellwert aus dem 99,9-Perzentil des Normalverkehrs ab. Wichtig ist, die Verteilung pro Zielpfad zu betrachten: Ein Upload-Endpunkt erzeugt naturgemäß mehr Multipart-Requests als eine API.

Für die Log-Pipeline gilt: Alle drei Ebenen sollten in dasselbe Zeitnormal und idealerweise in dasselbe SIEM fließen. Suricata liefert flow_id, Falco liefert Prozess- und Container-Kontexte, Auditd liefert Syscall-Details. Eine gemeinsame Korrelation über Quell-IP, Zeitfenster und Container-ID ist deutlich aussagekräftiger als drei getrennte Dashboards. Achten Sie darauf, dass die Uhren synchronisiert sind (chrony/NTP), sonst verschieben sich Ereignisse und die Korrelation scheitert.

Schließlich sollten Sie die Erkennung regelmäßig mit harmlosen Testrequests überprüfen. Eine Regel, die nie ausgelöst hat, ist möglicherweise falsch konfiguriert – nicht unbedingt ein Zeichen dafür, dass keine Angriffe stattfinden. Ein monatlicher Smoke-Test schützt vor stiller Degradation, insbesondere nach Regel-Updates oder Änderungen an der Stream-Reassembly-Konfiguration.


7. FAQ

F: Reicht Suricata allein? Nein. Kodierte Feldnamen und aufgeteilte Boundaries umgehen Signaturprüfungen. Falco und Auditd sowie der Patch sind zwingend erforderlich.

F: Falco läuft im Container nicht. Prüfen Sie, ob der eBPF- oder Kernelmodul-Treiber auf dem Host geladen ist und die nötigen Capabilities vorhanden sind. In managed Kubernetes ist der Runtime-Security-Dienst des Providers oft die praktikablere Alternative.

F: Auditd erzeugt zu viele Logs. Beschränken Sie auf euid=33 und execve, ergänzen Sie -F success=1. Lokale Aufbewahrung von 7 bis 14 Tagen ist üblich; die Langzeitanalyse gehört ins SIEM.

F: Wie teste ich die Regeln ohne Produktionsrisiko? Erzeugen Sie in einer Staging-Umgebung harmlose Multipart-Requests mit dem Feldnamen und prüfen Sie, ob die erwarteten Alarme entstehen. Anschließend die Schwellenwerte an das normale Traffic-Aufkommen anpassen.