defense php2shell seccomp ebpf defense

Zero-Trust PHP-FPM: Syscall-Härtung mit Linux Seccomp-BPF und eBPF

php2shell.run Research Lab •

Zero-Trust PHP-FPM: Syscall-Härtung mit Linux Seccomp-BPF und eBPF

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

Der letzte Schritt jeder PHP2Shell-Kette (CVE-2026-109618, CVE-2026-109619) ist die Ausführung eines Prozesses. Der Patch ist die beste Lösung; wer ihn nicht sofort ausrollen kann, kann die Kette jedoch im Kernel unterbrechen. Seccomp-BPF filtert Systemaufrufe unumkehrbar, eBPF liefert die nötige Sichtbarkeit. Zusammen entsteht eine Konfiguration, in der eine erfolgreiche Speicherkorruption nicht automatisch zu einer Shell führt. Dieses Dokument beschreibt die praktische Umsetzung und die Grenzen des Ansatzes.


1. Architektur der Syscall-Unterbrechung

 PHP-FPM-Worker
    │
    │  system("curl ... | sh")
    ▼
 ┌─────────────────────────────────────────────┐
 │ glibc: fork() / clone() / execve()          │
 └──────────────────┬──────────────────────────┘
                    ▼
 ┌─────────────────────────────────────────────┐
 │ Seccomp-BPF-Filter (im Kernel)              │
 │  - execve   → SCMP_ACT_ERRNO(EPERM)         │
 │  - execveat → SCMP_ACT_ERRNO(EPERM)         │
 │  - clone/fork → je nach Workload            │
 └──────────────────┬──────────────────────────┘
                    ▼
        [ EPERM ] Shell-Start scheitert, Kette endet

Seccomp ist kein Sandbox-Ersatz, sondern eine letzte Barriere. Es verhindert nicht die Speicherkorruption, sondern nur die darauffolgende Prozessausführung. Genau das ist im PHP2Shell-Szenario der entscheidende Unterschied zwischen „Datenleck“ und „vollständiger Übernahme“.


2. Seccomp-Profil für PHP-FPM

Ein produktions-taugliches Profil sollte mit einer Allowlist arbeiten und execve explizit verbieten. Das folgende Beispiel ist bewusst kompakt; für reale Deployments muss die Liste um die von Ihren Erweiterungen benötigten Aufrufe ergänzt werden.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86"],
  "syscalls": [
    {
      "names": [
        "read", "write", "close", "fstat", "lstat", "poll", "lseek",
        "mmap", "mprotect", "munmap", "brk", "rt_sigaction",
        "rt_sigprocmask", "rt_sigreturn", "ioctl", "pread64", "pwrite64",
        "readv", "writev", "access", "pipe", "select", "sched_yield",
        "mremap", "msync", "mincore", "madvise", "dup", "dup2",
        "nanosleep", "getpid", "sendfile", "socket", "connect", "accept",
        "sendto", "recvfrom", "sendmsg", "recvmsg", "shutdown", "bind",
        "listen", "getsockname", "getpeername", "setsockopt", "getsockopt",
        "clone", "exit", "wait4", "kill", "uname", "fcntl", "flock",
        "fsync", "fdatasync", "truncate", "ftruncate", "getdents",
        "getcwd", "chdir", "rename", "mkdir", "rmdir", "unlink",
        "readlink", "chmod", "fchmod", "chown", "fchown", "umask",
        "gettimeofday", "getrlimit", "getrusage", "getuid", "geteuid",
        "getgid", "getegid", "setuid", "setgid", "getppid", "setsid",
        "getrandom", "futex", "epoll_create", "epoll_ctl", "epoll_wait",
        "openat", "newfstatat", "unlinkat", "renameat", "readlinkat",
        "fchmodat", "faccessat", "epoll_create1", "epoll_pwait", "dup3",
        "pipe2", "prlimit64", "statx", "rseq", "exit_group"
      ],
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "names": ["execve", "execveat"],
      "action": "SCMP_ACT_ERRNO",
      "errnoRet": 1
    }
  ]
}

Zwei Details sind wichtig. Erstens muss clone erlaubt bleiben, wenn Erweiterungen Threads starten; andernfalls brechen Bildverarbeitung oder Datenbanktreiber. Zweitens sollte fork nur dann erlaubt werden, wenn ein Worker wirklich Kindprozesse benötigt. Für reine Web-Workloads ist die Blockade von execve der größte Gewinn bei minimalem Risiko.

2.1 Test mit dem Log-Modus

Bevor Sie blockieren, sammeln Sie. SCMP_ACT_LOG protokolliert unerlaubte Aufrufe, ohne sie zu verhindern.

{
  "defaultAction": "SCMP_ACT_LOG",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": []
}
sudo dmesg | grep -i seccomp
sudo journalctl -k --since "1 hour ago" | grep -i seccomp

Nach ein bis zwei Tagen Normalbetrieb kennen Sie die tatsächlich benötigten Aufrufe und können die Allowlist präzise fassen. Das ist deutlich sicherer als eine vorgefertigte Liste zu übernehmen.


3. Deployment in Docker und Kubernetes

3.1 Docker

sudo mkdir -p /etc/docker/seccomp
sudo cp php-fpm-seccomp.json /etc/docker/seccomp/php-fpm.json

docker run -d --name php-app \
  --security-opt seccomp=/etc/docker/seccomp/php-fpm.json \
  --security-opt no-new-privileges:true \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  -v /var/www/html:/var/www/html:ro \
  -v /var/lib/php/sessions:/var/lib/php/sessions:rw \
  php:8.3-fpm

noexec auf /tmp ist ein wichtiger Zusatz: Selbst wenn ein Angreifer eine Binärdatei ablegt, kann er sie nicht direkt ausführen. Kombiniert mit execve-Blockade sinkt die Erfolgswahrscheinlichkeit erheblich.

3.2 Kubernetes

apiVersion: v1
kind: Pod
metadata:
  name: php-fpm
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/php-fpm-seccomp.json
  containers:
    - name: php
      image: php:8.3-fpm
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: sessions
          mountPath: /var/lib/php/sessions
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: sessions
      emptyDir: {}
    - name: tmp
      emptyDir: { medium: Memory }

Der localhostProfile-Pfad bezieht sich auf /var/lib/kubelet/seccomp/ auf dem Node. Stellen Sie sicher, dass das Profil auf allen Nodes identisch ausgerollt wird; sonst schlägt das Scheduling fehl oder Pods verhalten sich unterschiedlich.


4. eBPF: Sichtbarkeit statt Blockade

Seccomp verhindert, eBPF erklärt. Mit bpftrace oder Tetragon lassen sich Ausführungsversuche beobachten, auch wenn sie bereits blockiert wurden.

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_execve
/ comm == "php-fpm" || comm == "php-fpm8.3" /
{
  printf("%s pid=%d comm=%s file=%s\n",
         strftime("%H:%M:%S", nsecs), pid, comm, str(args->filename));
}'
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: php-fpm-exec
spec:
  kprobes:
    - call: "sys_execve"
      syscall: true
      args:
        - index: 0
          type: "string"
      selectors:
        - matchPIDs:
            - operator: In
              values: [33]
          matchActions:
            - action: Post

Die Kombination ist entscheidend: Seccomp verhindert die Ausführung, eBPF dokumentiert den Versuch. So entsteht ein belastbarer Audit-Trail, ohne dass der Angreifer erfolgreich sein muss.


5. Validierung und Rollback

# 1) Profil-Syntax und Startfähigkeit
sudo docker run --rm \
  --security-opt seccomp=/etc/docker/seccomp/php-fpm.json \
  php:8.3-fpm php -v

# 2) Blockade prüfen
sudo docker exec php-app sh -c 'echo test' || echo "execve blockiert (erwartet)"

# 3) Regressionstest der Anwendung
curl -sS https://example.com/health -o /dev/null -w '%{http_code}\n'
curl -sS -F "file=@/etc/hostname" https://example.com/upload/ -o /dev/null -w '%{http_code}\n'

# 4) Kernel-Logs
sudo journalctl -k --since "10 min ago" | grep -i seccomp
# Rollback: Container ohne Profil starten
docker rm -f php-app
docker run -d --name php-app php:8.3-fpm

Ein Rollback sollte Teil des Change-Plans sein. Gerade in der Nacht nach einem Notfallpatch ist die Versuchung groß, ein strenges Profil ohne Test zu aktivieren; ein fehlgeschlagener Rollback kann dann den Ausfall verlängern.


6. Integration in Audit- und Alarmierungsketten

Seccomp-Ereignisse landen im Kernel-Log und sind damit nur lokal sichtbar. Für einen belastbaren Betrieb müssen sie in dieselbe Pipeline wie Falco, Auditd und Suricata fließen. Ein execve-Blockadeereignis allein ist ein technischer Hinweis; kombiniert mit einem unmittelbar vorausgehenden Multipart-Burst und einem Worker-Neustart wird daraus ein hoher Confidence-Score. Definieren Sie diese Korrelation explizit und testen Sie sie mit harmlosen Requests, bevor Sie sie zur Alarmierung nutzen.

Achten Sie darauf, dass die Seccomp-Logs nicht überlaufen. Bei falsch konfigurierten Profilen kann ein einzelner Request hunderte blockierte Aufrufe erzeugen. Rate-Limits und Deduplizierung im Log-Shipper verhindern, dass die eigentlichen Signale untergehen. Für Kubernetes empfiehlt sich ein DaemonSet, das Profile und Logs zentral verwaltet, statt sie pro Namespace manuell zu pflegen.


7. Grenzen und Risiken

Seccomp schützt nicht vor Speicherkorruption, Informationslecks oder Denial-of-Service innerhalb des erlaubten Syscall-Sets. Ein Angreifer kann weiterhin Daten lesen, Sessions manipulieren oder den Worker zum Absturz bringen. Zudem erzeugt eine zu enge Allowlist Ausfälle, die im Incident-Stress leicht fehlinterpretiert werden. Beginnen Sie mit execve-Blockade, messen Sie, und erweitern Sie schrittweise.

Ein weiterer Punkt ist die Wartbarkeit. Seccomp-Profile müssen bei PHP-Upgrades, neuen Erweiterungen und Basisimage-Wechseln überprüft werden. Automatisieren Sie den Smoke-Test in der CI und versionieren Sie das Profil gemeinsam mit dem Image. Ein veraltetes Profil ist ein latentes Ausfallrisiko.


8. FAQ

F1: Bricht die Blockade Bildverarbeitung oder Mailversand? Ja, wenn diese Funktionen externe Prozesse starten. Lagern Sie solche Aufgaben in separate Worker oder Sidecars aus und halten Sie den Haupt-PHP-FPM-Pool frei von execve.

F2: Wie teste ich sicher in Produktion? Mit SCMP_ACT_LOG beginnen, Aufrufe sammeln, Allowlist ableiten, dann auf SCMP_ACT_ERRNO wechseln. Canary-Pods zuerst, dann schrittweise ausrollen.

F3: Ersetzt Seccomp den Patch? Nein. Es verhindert die Ausführung, nicht die Korruption. Patchen Sie auf 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 und deaktivieren Sie session.upload_progress, solange das nicht möglich ist.

F4: Wie prüfe ich no_new_privs?

grep NoNewPrivs /proc/$(pgrep -f 'php-fpm: pool' | head -1)/status

Der Wert 1 bedeutet aktiv.


9. Fazit

Zero-Trust-PHP-FPM bedeutet, die Annahme „der Worker darf alles“ durch eine explizite Allowlist zu ersetzen. Seccomp-BPF, no_new_privs, read-only Root-Dateisystem und eBPF-Beobachtung bilden zusammen eine Barriere, die auch nach einer erfolgreichen Speicherkorruption greift. Sie ersetzt keinen Patch, macht aber den Unterschied zwischen einem kontrollierten Vorfall und einer vollständigen Übernahme.