Die verlässliche Lösung gegen OS Command Injection besteht darin, benutzerbeeinflusste Strings gar nicht erst an eine Shell zu übergeben. Besseres Escaping ist die schwächere Option. Die praktische Arbeit besteht darin, jede Stelle zu finden, an der Ihr Code einen Prozess startet, und dann für jede einzelne zu entscheiden, ob sie überhaupt existieren muss.
Command Injection entsteht, wenn eine Anwendung unsichere Benutzerdaten an eine System-Shell übergibt. Die eingeschleusten Befehle laufen meist mit den Rechten der Anwendung. CWE-78 nennt dazu ein unangenehmes Detail: Da die Anwendung die Befehle ausführt, kann es so aussehen, als stamme die Aktivität von der Anwendung oder ihrem Betreiber.
Warum Escaping die falsche erste Antwort ist
Die PHP-Funktion escapeshellarg() setzt einen Wert in einfache Anführungszeichen, sodass die Shell ihn als ein einziges Argument behandelt. Dadurch wird verhindert, dass & echo "hello" als zweiter Befehl ausgeführt wird. Der Angreifer kann aber weiterhin bestimmen, welches einzelne Argument das Programm erhält.
Jede OS Command Injection ist zugleich eine Argument Injection. Im OWASP Cheat Sheet gibt escape("--help"), an curl übergeben, trotzdem den Hilfetext aus. Je nach Programm reicht die Auswirkung von Informationspreisgabe bis zu Remote Code Execution.
Ein Leserkommentar auf der PHP-Handbuchseite zu escapeshellarg() zeigt dieselbe Schwäche am Beispiel von tar. Jeder Dateiname wird korrekt escaped, ein Wert mit führendem Bindestrich wird aber dennoch als Option gelesen, und tar hat eine Option, die einen weiteren Befehl ausführt. Ein -- vor den Dateinamen behebt das, der Kommentar weist jedoch darauf hin, dass nicht jedes Programm dieses Trennzeichen unterstützt.
Unter Windows verwendet escapeshellarg() laut Handbuch doppelte Anführungszeichen und ersetzt Prozentzeichen, Ausrufezeichen und doppelte Anführungszeichen durch Leerzeichen. Ein Leserkommentar ergänzt, das Ergebnis sei sicher, aber verlustbehaftet. In beiden Fällen ist das ein Grund, die Shell zu meiden, und keine Lösung.
Die Aufrufe finden
Führen Sie diese Suchen über jedes Repository aus. Passen Sie die Dateitypen an Ihre Struktur an:
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
Das zweite Python-Muster erfasst einen einzelnen String, der als Befehl übergeben wird. Ohne shell=True funktioniert das nur, wenn der String ausschließlich ein Programm benennt, daher muss jeder Treffer manuell geprüft werden. Das PHP-Backtick-Muster ist verrauscht, weil es auch Backticks in Kommentaren und SQL findet. Durchsuchen Sie außerdem Shell-Skripte, Deploy-Skripte und Cron-Wrapper, die Befehle aus Variablen oft genauso häufig zusammenbauen wie Anwendungscode.
| Sprache | Wonach suchen | Ersetzen durch |
|---|---|---|
| PHP | system(), exec(), shell_exec(), passthru(), popen(), proc_open(), der Backtick-Operator | Eingebaute Funktionen wie mkdir() |
| Python | shell=True; os.system; os.popen; ein einzelner Befehls-String | subprocess.run mit einer Liste |
| Java | Runtime.exec(String) | ProcessBuilder, mit Programm und jedem Argument als eigenem Element |
| .NET | cmd.exe, powershell.exe, sh -c, ein zusammengebauter Arguments-String | ProcessStartInfo mit ArgumentList |
| C | system() | Ein Bibliotheksaufruf |
Beziehen Sie auch fest codierte Befehle ein. Der OWASP-Community-Beitrag beschreibt ein privilegiertes CGI-Programm, das cd /var/yp && make ausführt, ganz ohne Benutzereingabe im String. Es verwendete für make keinen absoluten Pfad, sodass ein Angreifer $PATH ändern und seine eigene Binärdatei mit den Rechten des Programms ausführen lassen konnte.
Treffer triagieren
Eine rohe Trefferliste ist keine Liste von Findings. Verfolgen Sie bei jedem Treffer den Wert rückwärts und fragen Sie, ob ihn eine Anfrage erreichen kann. Route- und Query-Parameter, Formularfelder, Cookies und HTTP-Header sind die offensichtlichen Quellen. Hochgeladene Dateinamen, Werte aus einer Datenbank, in die Benutzer schreiben können, Queue-Nachrichten und Umgebungsvariablen vergisst man gerne.
Sortieren Sie die Treffer in drei Stapel: erreichbar über eine nicht authentifizierte Anfrage, erst nach dem Login erreichbar und gar nicht erreichbar. Beheben Sie zuerst den ersten Stapel. Behalten Sie den dritten auf der Liste, denn ein späteres Refactoring kann ihn unbemerkt mit einer Anfrage verbinden.
Den Aufruf durch eine API ersetzen
Eingebaute Bibliotheksfunktionen lassen sich nicht dazu bringen, etwas anderes zu tun als ihre vorgesehene Aufgabe. Verwenden Sie mkdir() statt system("mkdir /dir_name"). Nutzen Sie die Java-Mail-API in javax.mail.*, statt mail über die Shell aufzurufen. CWE-78 gibt für das Design denselben Rat: Bibliotheksaufrufe statt externer Prozesse verwenden.
Wenn Sie einen Prozess starten müssen
Für manche Werkzeuge gibt es kein Bibliotheksäquivalent. Dann setzen Sie zwei Schichten gemeinsam ein: eine Argumentliste ohne Shell und eine Validierung.
Die Python-subprocess-Dokumentation sagt, eine Argumentsequenz sei in der Regel vorzuziehen, weil das Modul Escaping und Quoting übernimmt. Sie enthält den Hinweis, vor der Verwendung von shell=True den Abschnitt Security Considerations zu lesen. Außerdem empfiehlt sie einen vollqualifizierten Pfad für die ausführbare Datei. Das beseitigt die PATH-Suche, die das oben genannte make-Beispiel ausnutzt.
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)
Das Muster weist einen führenden Bindestrich bereits zurück, daher wirkt -- überflüssig. Es ist jedoch Defense in Depth: Wenn jemand den regulären Ausdruck später lockert, verhindert das Trennzeichen weiterhin, dass name als Option gelesen wird.
Verwenden Sie in .NET einen festen, vollqualifizierten Pfad in FileName, setzen Sie UseShellExecute = false und fügen Sie jedes Argument über ArgumentList hinzu. Übergeben Sie niemals nicht vertrauenswürdige Eingaben an cmd.exe, powershell.exe oder sh -c. Das Trennen von Argumenten ist keine Validierung, weder in Java noch in .NET.
Validieren Sie anschließend die Werte. Prüfen Sie den Befehl gegen eine Liste erlaubter Befehle und jedes Argument gegen erlaubte Werte oder ein Muster mit maximaler Länge, ohne Metazeichen und Leerraum.
Bei curl reicht -- nicht aus, weil spätere Argumente weiterhin als URL-Operanden gelten und weitere Transfers möglich sind. Übergeben Sie genau eine validierte URL, verwenden Sie --globoff und setzen Sie SSRF-Schutzmaßnahmen ein.
Betreiben Sie die Anwendung mit den geringsten benötigten Rechten und geben Sie Jobs mit nur einer Aufgabe ein eigenes, isoliertes Konto. Eine Sandbox wie chroot, AppArmor oder SELinux hilft, doch CWE-78 stuft ihre Wirksamkeit als begrenzt ein. Sie verringert, was ein Angreifer erreichen kann, beseitigt die Schwachstelle aber nicht.
Wo Sie anfangen
- Führen Sie die obigen Suchen über jedes Repository aus, einschließlich Deploy-Skripten, CI-Jobs und Cron-Wrappern. Listen Sie jeden Treffer mit der Eingabe auf, die ihn erreicht, und sortieren Sie die Liste danach, ob eine Anfrage ihn erreichen kann.
- Fragen Sie bei jedem Treffer, ob eine Bibliothek oder eingebaute Funktion dieselbe Aufgabe erledigt. Wenn ja, ersetzen Sie den Aufruf und entfernen Sie den Prozessstart.
- Schreiben Sie die verbleibenden Aufrufe so um, dass sie eine Argumentliste, keine Shell und einen absoluten Pfad zur ausführbaren Datei verwenden. Ergänzen Sie Allowlist-Validierung und, wo das Programm es unterstützt, das Trennzeichen
--. - Lassen Sie die Prozesse unter einem dedizierten Konto mit minimalen Rechten laufen, damit ein übersehener Fall weniger Angriffsfläche bietet.
- Testen Sie jeden verbleibenden Aufruf mit einem Wert wie
bob.txt;sleep 5und mit einem Wert, der mit einem Bindestrich beginnt. Messen Sie die Antwortzeit, statt auf eine Ausgabe zu warten. Der Wert mit führendem Bindestrich sollte abgewiesen oder als Dateiname behandelt werden. Für eine unabhängige Prüfung testet ein Penetrationstest beides, oder unser Team für sichere Softwareentwicklung prüft den Code gemeinsam mit Ihnen.
