File-Upload-Schwachstellen ausloten, ohne Gesetze zu brechen
Wie man Upload-Validierungen im eigenen Lab umgeht, Bugklassen kartiert und Webserver gegen RCE per Datei haertet.

Ein fehlkonfigurierter Upload-Endpunkt ist noch immer einer der billigsten Wege, ein bescheidenes Formular in Remote Code Execution zu verwandeln. In einem Lab aus DVWA, OWASP Juice Shop und einem Nginx-1.25-Reverse-Proxy kamen wir vom 'Avatar-Upload' in unter 40 Minuten zu einer www-data-Shell. Vor jedem Payload zaehlt der Vertrag: das Ziel ist unsere eigene VM, isoliert in einem 10.10.0.0/24-Netz ohne Internet-Route, und das Ziel ist, gehaertete Regeln zu schreiben, nicht Skalps zu sammeln. Dieser Durchgang deckt die gesamte Angriffsflaeche von Datei-Uploads ab, jede Bug-Klasse mit Vektor, Orakel und dem Fix, der sie dauerhaft schliesst.
Der Vertrag: nur autorisiertes Lab
Jede Technik hier wird gegen Infrastruktur demonstriert, die uns gehoert und die wir ausdruecklich brechen duerfen. Sie gegen ein Produktiv-SaaS 'nur zum Pruefen' zu wiederholen ist kein Bug-Bounty, sondern in den meisten Computerstrafgesetzen eine Straftat, und der Unterschied zwischen Forscher und Angeklagtem ist meist ein schriftlicher Scope und ein VM-Snapshot. Baue das Lab so, wie es Web-Pentest von Null beschreibt: Docker auf einer isolierten Bridge, keine Route zum Host, Burp Suite als abfangender Proxy. Snapshotte vor jedem Lauf, damit du genau belegen kannst, was du beruehrt hast, und sauber zuruecksetzen kannst.
Client-seitige und Blacklist-Umgehung
Die erste Bug-Klasse ist client-seitige Endungsvalidierung. Legacy-PHP liefert oft eine Blacklist (.php, .phtml, .php5) ohne echte Content-Type-Pruefung. shell.php in shell.pHp.jpg umbenennen, mit Burp abfangen und den Content-Type auf image/jpeg zwingen besiegt rund 80 Prozent der Amateurfilter in Firmen-CTFs. Gross-Klein-Mischung schlaegt naive Kleinbuchstaben-Blacklists; nachgestellte Punkte und Leerzeichen (shell.php.) schlagen Windows-orientierte; und alternative Handler wie .phtml, .phar oder .pht laufen oft, wo .php blockiert ist. Das Orakel ist simpel: fordere den hochgeladenen Pfad an und sieh, ob der Server deine Marker-Zeichenkette oder den Rohquelltext liefert. Laeuft er, war die Blacklist die einzige Kontrolle und sie versagte.
Content-Type- und MIME-Verwirrung
Die zweite Schicht ist, dem client-deklarierten MIME-Typ zu vertrauen. Ein Backend, das nur den Multipart-Content-Type-Header prueft, ist trivial umgehbar, denn dieser Header ist angreifer-kontrolliert; setze ihn auf image/png und ein PHP-Payload rutscht durch. Etwas bessere Backends rufen getimagesize() oder schnueffeln die ersten Bytes, was die Latte hebt, aber nicht reisst. Das richtige Orakel im Test ist, deklarierten Typ von echtem Typ zu trennen: sende eine echte Bildendung mit ausfuehrbarem Inhalt, dann einen falschen Bild-Header vor Code, und beobachte, welche Kombination der Server speichert und ausfuehrbar ausliefert. Jeder Pfad, in dem allein der deklarierte Typ die Annahme entscheidet, ist ein Fund.
Pfad-Parsing: doppelte Endungen und Null-Bytes
Eine klassische Flaeche ist das Pfad-Parsing des Servers. Altes Nginx-Doppel-Parsing (CVE-2013-4547 und Nachfahren) liess eine Datei namens shell.jpg gefolgt von einem Null-Byte und .php Stacks austricksen, die Nginx und PHP-FPM mit schludrigem pathinfo kombinieren. Im Lab rekonstruierte ich das mit nginx:1.14-alpine und php:7.2-fpm rein zur historischen Referenz; mit Nginx 1.25 und cgi.fix_pathinfo=0 stirbt derselbe Payload. Die Lektion: Upload-Sicherheit entscheidet nicht die Anwendung allein; Webserver, FastCGI-Handler und die fix_pathinfo-Einstellung entscheiden gemeinsam, ob ein Bildverzeichnis je Code ausfuehren kann. Diese Uebung passt zur Input-Parsing-Denkweise aus SQL-Injection in der Praxis.
Magic Bytes, Polyglots und die Bild-Pipeline
Validiert das Backend Magic Bytes, aendert sich das Spiel. GIF/PHP-Polyglots schlagen getimagesize(), scheitern aber an finfo plus Imagick mit Reprocessing, also greift man die Transformations-Pipeline an statt des Validators. ImageMagick mit lockeren Policies parst noch MVG und SVG, und die ImageTragick-Familie taucht in LMS- und CMS-Forks immer wieder auf. Bei einem Fintech-Kunden fand ich einen Beleg-Upload, der convert ohne Memory- oder Delegate-Limits aufrief, was ein SVG in eine SSRF und dann in einen Datei-Read verwandelte. Holt die Pipeline entfernte Ressourcen, wird der Upload zum Request-Forgery-Primitive, weshalb diese Klasse direkt an SSRF und Cloud-Metadata anschliesst. Reprozessiere jedes Bild, deaktiviere gefaehrliche Coder und lass convert nie einer URL folgen.
Wo die Datei landet: Speicher, Traversal, Ausliefern
Eine unterschaetzte Flaeche ist, wo die Datei landet. Speicher unter /var/www/uploads, direkt von Apache ausgeliefert, ist der Klassiker, doch dasselbe Problem versteckt sich in S3-Buckets mit falscher Content-Disposition und in CDNs, die HTML reprozessieren. Path Traversal im Dateinamen (Punkt-Punkt-Slash-Sequenzen auf /tmp/cron.d/ oder einen Webroot) wirkt noch gegen Node-Middleware, die Multers originalname vertraut. Das Orakel: lade einen harmlosen Marker mit Traversal-Namen hoch und pruefe, wo er auf der Platte materialisiert; entkommt er dem vorgesehenen Verzeichnis, waehlt ein Angreifer den Schreibort. Fuer einen formaleren Test-Plan pro Endpunkt, besonders bei APIs, folge der REST- und GraphQL-Pentest-Checkliste.
Den Endpunkt methodisch fuzzen
Manuelle Bypaesse finden den ersten Bug; methodisches Fuzzing findet den Rest. Richte Burp Intruder oder ein kleines Skript auf das Upload-Feld und iteriere drei Dimensionen zugleich: die Endung (eine Wortliste aus .php, .phtml, .phar, .pht, .php7, .inc, Gross-Klein-Mischung, nachgestellter Punkt und Leerzeichen), den deklarierten Content-Type und den Magic-Byte-Prefix. Fuer jede Kombination ist das Orakel eine zweistufige Sonde: nimmt der Server die Datei an, und fuehrt das Zurueckfordern den Marker aus. Vergiss die Upload-Race-Condition nicht, in der eine Webshell zwischen Schreiben und dem Antivirus- oder Move-Schritt kurz erreichbar ist; eine enge Upload-dann-Request-Schleife kann dieses Fenster auf einem echten Ziel gewinnen und muss ausdruecklich getestet werden, damit der Fix (validieren, bevor die Datei je erreichbar ist) begruendet ist.
Einen Upload zur vollen Kompromittierung verketten
Ein einzelner ausgefuehrter Marker ist ein Fund, aber der Report sollte Wirkung zeigen. Im Lab gab der GIF/PHP-Polyglot eine minimale Kommando-Webshell; von dort hob eine Standard-Reverse-Shell (ein bash-TCP-Callback zur Angreifer-VM im isolierten Netz) den Fuss in Sekunden zu einer interaktiven www-data-Session. Die realistische Eskalation von dort ist kein Hollywood-Exploit, sondern langweilige Persistenz: ein beschreibbares, vom Webserver ausgeliefertes Verzeichnis, ein per frueherem Traversal erreichbarer welt-beschreibbarer Cron-Pfad oder ein geleaktes DB-Credential in einer Config-Datei, die die Shell nun lesen kann. Dokumentiere die Kette bis zum belegten Impact und hoere auf; das Ziel ist, das Severity-Rating zu begruenden, nicht eine Maschine zu pluendern, und der Snapshot laesst dich die ganze Kette fuer den Kunden nachspielen, ohne Schaden zu hinterlassen.
Ein konkretes Muster aus echten Engagements macht das greifbar. Beim erwaehnten Fintech-Kunden begann die Kette nicht mit RCE, sondern mit einem SVG, das convert zwang, eine interne URL zu holen; die Antwort enthielt ein Cloud-Metadata-Token, und erst dieses Token oeffnete den naechsten Schritt. Die Lehre fuer den Report: bewerte einen Upload-Bug nie isoliert, sondern entlang der Kette, die er ermoeglicht, denn ein 'nur' gespeichertes SVG kann in Wahrheit ein SSRF-, Credential-Leak- und Datei-Read-Primitive in einem sein. Genau deshalb steht in jedem meiner Findings nicht bloss der Payload, sondern die vollstaendige Wirkungskette mit dem konkreten Punkt, an dem eine der oben genannten Verteidigungsregeln sie gebrochen haette.
Verteidigungsregeln, die wirklich wirken
Defensiv sind die Regeln, die sich einen Platz im Report verdienen, langweilig und wirksam. Erzeuge einen zufaelligen Dateinamen (UUIDv7) und verwirf den Client-Namen ganz; speichere ausserhalb des Document Root; liefere ueber einen Handler, der Content-Disposition: attachment und Content-Type: application/octet-stream erzwingt; validiere den MIME-Typ mit libmagic server-seitig, nicht den deklarierten Header; reprozessiere Bilder und entferne Metadaten; und blockiere doppelte Endungen am Webserver selbst, etwa indem du jede Anfrage ablehnst, deren Pfad .php, .phtml oder .phar vor einem weiteren Punkt enthaelt. Fuege einen Virenscan und ein Groessenlimit hinzu und stell das Ganze hinter die Shift-Left-Gates aus AppSec Shift-Left, damit eine Regression im CI faellt, nicht in Produktion.
Checkliste
Vor der Freigabe eines Upload-Features: Dateiname server-generiert und nie Nutzereingabe spiegelnd; Endung per Allowlist, nicht Blacklist; MIME per Inhalt geprueft, nicht per Header; Speicher ausserhalb des Webroot und mit Attachment-Disposition ausgeliefert; Bilder reprozessiert und Metadaten entfernt; Webserver fuehrt nichts im Upload-Pfad aus; Path Traversal im Dateinamen neutralisiert; Groessen- und Ratenlimits vorhanden; und ein Viren- oder Content-Scanner laeuft asynchron. Jedes 'Nein' auf dieser Liste ist ein Fund, und jeder Fund bekommt eine konkrete Remediation-Zeile, kein vages 'Input sanitizen'.
FAQ
Reicht das Blockieren von .php? Nein; eine Blacklist ist die schwaechstmoegliche Kontrolle, besiegt durch Gross-Klein-Mischung, alternative Handler (.phtml, .phar, .pht), nachgestellte Punkte und Server-Pfad-Parsing-Eigenheiten. Die dauerhafte Kontrolle ist eine Allowlist von Endungen kombiniert mit inhaltsbasierter MIME-Validierung und einem Webserver, der im Upload-Verzeichnis nie Code ausfuehrt. Kann ich SVG-Uploads sicher erlauben? Nur mit grosser Vorsicht: SVG ist XML und kann Skript und externe Referenzen tragen, also liefere es mit restriktiver Content-Security-Policy und Attachment-Disposition oder rastere es beim Empfang zu PNG und verwirf das Original. SVG als harmloses Bild zu behandeln ist, wie ImageTragick und Stored XSS immer wieder landen.
Sollten Uploads auf demselben Host liegen, der die App ausliefert? Bevorzuge eine getrennte Storage-Domain ohne Codeausfuehrung und einen abgeriegelten Ausliefer-Handler, damit selbst ein erfolgreicher Schreibvorgang nicht zu RCE auf dem App-Host wird. Und wie teste ich ohne Schaden? Snapshotte die VM, arbeite nur im autorisierten Scope, nutze harmlose Marker-Payloads, die den Bug belegen (eine eindeutige Zeichenkette, die eigene VM-ID), statt echte Daten zu exfiltrieren, und setze nach jedem Lauf zurueck; den Schreibvorgang zu belegen genuegt, /etc/passwd auf fremder Box zu lesen ist unnoetig und illegal.
Fazit
Datei-Upload bleibt gefaehrlich, weil er an der Naht dreier Systeme sitzt, Anwendung, Webserver und Speicherschicht, und eine Luecke in einem davon genuegt. Greif ihn an, wie dieses Lab es tut, Klasse fuer Klasse, mit klarem Orakel je Fall, und die Verteidigungsregeln schreiben sich selbst: server-generierte Namen, Allowlist-Endungen, inhaltsbasierte MIME-Pruefungen, reprozessierte Bilder, keine Ausfuehrung im Upload-Pfad und neutralisiertes Traversal. Halte jedes Experiment im autorisierten Scope mit einem Snapshot als Beleg, liefere die gehaerteten Regeln durchs CI, und das bescheidene Avatar-Formular hoert endgueltig auf, ein 40-Minuten-Pfad zur Shell zu sein.


