De betrouwbare oplossing voor OS command injection is om helemaal te stoppen met het doorgeven van door gebruikers beïnvloede strings aan een shell. Betere escaping is de zwakkere optie. Het praktische werk bestaat uit het vinden van alle plekken waar uw code een proces start, en vervolgens per plek bepalen of die aanroep überhaupt moet bestaan.
Command injection ontstaat wanneer een applicatie onveilige gebruikersdata doorgeeft aan een systeemshell. De geïnjecteerde commando's draaien meestal met de rechten van de applicatie. CWE-78 voegt daar een onprettig detail aan toe: omdat de applicatie de commando's uitvoert, kan de activiteit lijken voort te komen uit de applicatie of de eigenaar ervan.
Waarom escaping het verkeerde eerste antwoord is
De PHP-functie escapeshellarg() plaatst een waarde tussen enkele aanhalingstekens, zodat de shell er één argument van maakt. Daardoor wordt & echo "hello" niet als tweede commando uitgevoerd. Het voorkomt echter niet dat de aanvaller bepaalt welk ene argument het programma ontvangt.
Elke OS command injection is ook een argument injection. In de OWASP cheat sheet toont escape("--help") dat aan curl wordt doorgegeven nog steeds de helptekst. Afhankelijk van het programma loopt de impact uiteen van het lekken van informatie tot remote code execution.
Een opmerking van een lezer op de PHP-handleidingspagina voor escapeshellarg() laat dezelfde tekortkoming zien met tar. Elke bestandsnaam wordt correct ge-escaped, maar een waarde die met een koppelteken begint, wordt nog steeds als optie gelezen, en tar heeft een optie waarmee een ander commando wordt uitgevoerd. Het toevoegen van -- vóór de bestandsnamen lost dit op, maar de opmerking wijst erop dat niet elk programma deze scheidingsmarkering ondersteunt.
Voor Windows vermeldt de handleiding dat escapeshellarg() dubbele aanhalingstekens gebruikt en procenttekens, uitroeptekens en dubbele aanhalingstekens door spaties vervangt. Een opmerking van een lezer voegt toe dat het resultaat veilig is, maar met verlies van gegevens. Hoe dan ook is dit een reden om de shell te vermijden, geen oplossing.
Vind de aanroepen
Voer deze zoekopdrachten uit over elke repository. Pas de bestandstypen aan op uw eigen indeling:
rg -n 'shell=True|os\.system|os\.popen' --type py
rg -n "subprocess\.(run|Popen|call|check_output)\(\s*[f\"']" --type py
rg -n '\b(system|exec|shell_exec|passthru|popen|proc_open)\s*\(' --type php
rg -n '`[^`]*\$' --type php
rg -n 'Runtime\.getRuntime\(\)\.exec|ProcessBuilder' --type java
rg -n 'ProcessStartInfo|Process\.Start|UseShellExecute' --type cs
rg -n '\bsystem\s*\(|popen\s*\(|exec[lv]p?e?\s*\(' --type c
Het tweede Python-patroon vangt een enkele string die als commando wordt doorgegeven. Zonder shell=True werkt dat alleen als de string uitsluitend een programmanaam bevat, dus elke treffer vraagt om handmatige controle. Het PHP-patroon voor backticks levert veel ruis op, omdat het ook backticks in commentaar en SQL vindt. Zoek ook in shellscripts, deploymentscripts en cron-wrappers, die net zo vaak commando's uit variabelen opbouwen als applicatiecode.
| Taal | Zoek naar | Vervang door |
|---|---|---|
| PHP | system(), exec(), shell_exec(), passthru(), popen(), proc_open(), de backtick-operator | Ingebouwde functies zoals mkdir() |
| Python | shell=True; os.system; os.popen; een enkele commandostring | subprocess.run met een lijst |
| Java | Runtime.exec(String) | ProcessBuilder met het programma en elk argument als afzonderlijk element |
| .NET | cmd.exe, powershell.exe, sh -c, een samengestelde Arguments-string | ProcessStartInfo met ArgumentList |
| C | system() | Een library-aanroep |
Neem ook hardgecodeerde commando's mee. De OWASP community write-up beschrijft een bevoorrecht CGI-programma dat cd /var/yp && make uitvoert, zonder gebruikersinvoer in de string. Het gebruikte geen absoluut pad voor make, waardoor een aanvaller $PATH kon wijzigen en zijn eigen binary kon laten uitvoeren met de rechten van het programma.
De treffers beoordelen
Een ruwe lijst met treffers is nog geen lijst met bevindingen. Volg voor elke treffer de waarde terug en vraag u af of een request die kan bereiken. Route- en queryparameters, formuliervelden, cookies en HTTP-headers zijn de voor de hand liggende bronnen. Geüploade bestandsnamen, waarden uit een database waarin gebruikers kunnen schrijven, berichten uit queues en omgevingsvariabelen worden het vaakst vergeten.
Verdeel de treffers over drie stapels: bereikbaar via een niet-geauthenticeerd request, alleen bereikbaar na inloggen, en helemaal niet bereikbaar. Pak de eerste stapel als eerste aan. Houd de derde op de lijst, want een latere refactoring kan haar zonder dat iemand het merkt alsnog met een request verbinden.
Vervang de aanroep door een API
Ingebouwde libraryfuncties kunnen niet zo worden gemanipuleerd dat ze iets anders doen dan hun bedoelde taak. Gebruik mkdir() in plaats van system("mkdir /dir_name"). Gebruik de Java mail-API in javax.mail.* in plaats van mail via een shell aan te roepen. CWE-78 geeft hetzelfde advies voor het ontwerp: gebruik library-aanroepen in plaats van externe processen.
Wanneer u een proces moet starten
Voor sommige tools bestaat geen library-equivalent. Gebruik dan twee lagen tegelijk: een argumentenlijst zonder shell, en validatie.
De Python subprocess-documentatie stelt dat een reeks argumenten over het algemeen de voorkeur heeft, omdat de module het escapen en quoten afhandelt. De documentatie bevat een waarschuwing om de sectie Security Considerations te lezen voordat u shell=True gebruikt. Ook wordt een volledig gekwalificeerd pad voor het uitvoerbare bestand aanbevolen, waarmee de PATH-lookup verdwijnt die in het hierboven genoemde make-voorbeeld wordt misbruikt.
import re, subprocess
if not re.fullmatch(r"[a-z0-9]{3,10}", name):
raise ValueError("rejected")
subprocess.run(["/usr/bin/tar", "cf", "out.tar", "--", name], check=True)
Het patroon weigert al een koppelteken aan het begin, dus -- lijkt overbodig. Het is defense in depth: als iemand de regex later versoepelt, voorkomt de scheidingsmarkering nog steeds dat name als optie wordt gelezen.
Gebruik in .NET een vast, volledig gekwalificeerd pad in FileName, zet UseShellExecute = false en voeg elk argument toe via ArgumentList. Geef nooit niet-vertrouwde invoer door aan cmd.exe, powershell.exe of sh -c. Het scheiden van argumenten is geen validatie, zowel in Java als in .NET.
Valideer vervolgens de waarden. Controleer het commando aan de hand van een lijst met toegestane commando's, en controleer elk argument tegen toegestane waarden of een patroon met een maximale lengte, waarbij metatekens en witruimte buiten beschouwing blijven.
Bij curl is -- niet genoeg, omdat latere argumenten nog steeds als URL-operanden worden behandeld en extra transfers mogelijk blijven. Geef precies één gevalideerde URL mee, gebruik --globoff en pas SSRF-beveiligingen toe.
Draai de applicatie met de laagste rechten die nodig zijn en geef taken met één enkele functie een eigen, geïsoleerd account. Een sandbox zoals chroot, AppArmor of SELinux helpt, maar CWE-78 beoordeelt de effectiviteit ervan als beperkt. Het beperkt waar een aanvaller bij kan, maar neemt de kwetsbaarheid niet weg.
Waar u kunt beginnen
- Voer de bovenstaande zoekopdrachten uit over elke repository, inclusief deploymentscripts, CI-jobs en cron-wrappers. Noteer elke treffer met de invoer die erbij komt, en sorteer de lijst op de vraag of een request de treffer kan bereiken.
- Vraag u bij elke treffer af of een library of ingebouwde functie hetzelfde doet. Is dat zo, vervang dan de aanroep en verwijder het starten van het proces.
- Herschrijf de overige aanroepen zodat ze een argumentenlijst gebruiken, geen shell en een absoluut pad naar het uitvoerbare bestand. Voeg allowlist-validatie en de
---scheidingsmarkering toe waar het programma die ondersteunt. - Laat de processen draaien onder een speciaal account met lage rechten, zodat een over het hoofd geziene aanroep minder mogelijkheden biedt.
- Test elke resterende aanroep met een waarde als
bob.txt;sleep 5en met een waarde die met een koppelteken begint. Meet de responstijd in plaats van op uitvoer te wachten. De waarde met het koppelteken moet worden geweigerd of als bestandsnaam worden behandeld. Voor een externe controle probeert een penetratietest beide, of ons team veilige softwareontwikkeling kan de code samen met u beoordelen.
