Preámbulo
Primero, diría que no es la forma correcta de abordar el problema. Es un poco como decir " no deberías asesinar personas porque de lo contrario irás a la cárcel ".
Del mismo modo, no cita su variable porque de lo contrario está introduciendo vulnerabilidades de seguridad. Usted cita sus variables porque está mal no hacerlo (pero si el temor a la cárcel puede ayudar, ¿por qué no?).
Un pequeño resumen para aquellos que acaban de subirse al tren.
En la mayoría de los shells, dejar una expansión variable sin comillas (aunque eso (y el resto de esta respuesta) también se aplica a la sustitución de comandos ( `...`
o $(...)
) y la expansión aritmética ( $((...))
o $[...]
)) tiene un significado muy especial. La mejor manera de describirlo es que es como invocar algún tipo de operador implícito split + glob¹ .
cmd $var
en otro idioma se escribiría algo como:
cmd(glob(split($var)))
$var
primero se divide en una lista de palabras de acuerdo con reglas complejas que involucran el $IFS
parámetro especial (la parte dividida ) y luego cada palabra resultante de esa división se considera como un patrón que se expande a una lista de archivos que coinciden (la parte global ) .
Como ejemplo, si $var
contiene *.txt,/var/*.xml
y $IFS
contiene ,
, cmd
se llamaría con varios argumentos, siendo el primero cmd
y los siguientes los txt
archivos en el directorio actual y los xml
archivos en /var
.
Si quisieras llamar cmd
solo con los dos argumentos literales cmd
y *.txt,/var/*.xml
escribirías:
cmd "$var"
que estaría en tu otro idioma más familiar:
cmd($var)
¿Qué queremos decir con vulnerabilidad en un shell ?
Después de todo, se sabe desde los albores del tiempo que los scripts de shell no deben usarse en contextos sensibles a la seguridad. Seguramente, OK, dejar una variable sin comillas es un error, pero eso no puede hacer tanto daño, ¿verdad?
Bueno, a pesar del hecho de que alguien le diría que los scripts de shell nunca deberían usarse para CGIs web, o que afortunadamente la mayoría de los sistemas no permiten scripts de shell setuid / setgid hoy en día, una cosa que es shellshock (el error bash explotable remotamente que hizo que los titulares en septiembre de 2014) revelados es que los shells todavía se usan ampliamente donde probablemente no deberían: en CGI, en scripts de enlace de cliente DHCP, en comandos sudoers, invocados por (si no como ) comandos setuid ...
A veces sin saberlo. Por ejemplo, system('cmd $PATH_INFO')
en un script php
/ perl
/ python
CGI invoca un shell para interpretar esa línea de comando (sin mencionar el hecho de que en cmd
sí mismo puede ser un script de shell y su autor puede nunca haber esperado que se llame desde un CGI).
Tienes una vulnerabilidad cuando hay un camino para la escalada de privilegios, es decir, cuando alguien (llamémosle el atacante ) puede hacer algo que no debe hacer.
Invariablemente, eso significa que el atacante proporciona datos, que los datos están siendo procesados por un usuario / proceso privilegiado que inadvertidamente hace algo que no debería estar haciendo, en la mayoría de los casos debido a un error.
Básicamente, tienes un problema cuando tu código con errores procesa datos bajo el control del atacante .
Ahora, no siempre es obvio de dónde provienen esos datos , y a menudo es difícil saber si su código alguna vez podrá procesar datos no confiables.
En lo que respecta a las variables, en el caso de un script CGI, es bastante obvio, los datos son los parámetros CGI GET / POST y cosas como cookies, ruta, host ... parámetros.
Para un script setuid (que se ejecuta como un usuario cuando es invocado por otro), son los argumentos o las variables de entorno.
Otro vector muy común son los nombres de archivo. Si obtiene una lista de archivos de un directorio, es posible que el atacante haya plantado archivos allí .
En ese sentido, incluso cuando se le solicite un shell interactivo, podría ser vulnerable (al procesar archivos en, /tmp
o ~/tmp
por ejemplo).
Incluso a ~/.bashrc
puede ser vulnerable (por ejemplo, bash
lo interpretará cuando se invoque ssh
para ejecutar un ForcedCommand
like en git
implementaciones de servidor con algunas variables bajo el control del cliente).
Ahora, un script no puede ser llamado directamente para procesar datos no confiables, pero puede ser llamado por otro comando que lo haga. O su código incorrecto puede ser copiado en scripts que lo hagan (por usted 3 años más adelante o uno de sus colegas). Un lugar donde es particularmente crítico es en las respuestas en los sitios de preguntas y respuestas, ya que nunca sabrá dónde pueden terminar las copias de su código.
Abajo a los negocios; ¿qué tan malo es?
Dejar una variable (o sustitución de comando) sin comillas es, con mucho, la fuente número uno de vulnerabilidades de seguridad asociadas con el código de shell. En parte porque esos errores a menudo se traducen en vulnerabilidades, pero también porque es muy común ver variables sin comillas.
En realidad, al buscar vulnerabilidades en el código de shell, lo primero que debe hacer es buscar variables sin comillas. Es fácil de detectar, a menudo un buen candidato, generalmente fácil de rastrear a los datos controlados por el atacante.
Hay un número infinito de formas en que una variable sin comillas puede convertirse en una vulnerabilidad. Solo daré algunas tendencias comunes aquí.
Divulgación de información
La mayoría de las personas se toparán con errores asociados con variables no citadas debido a la parte dividida (por ejemplo, es común que los archivos tengan espacios en sus nombres hoy en día y el espacio está en el valor predeterminado de IFS). Mucha gente pasará por alto la
parte glob . La parte glob es al menos tan peligrosa como la
parte dividida .
La aplicación de globo realizada sobre una entrada externa no desinfectada significa que el atacante puede hacerle leer el contenido de cualquier directorio.
En:
echo You entered: $unsanitised_external_input
si $unsanitised_external_input
contiene /*
, eso significa que el atacante puede ver el contenido de /
. No es gran cosa. Sin embargo, se vuelve más interesante con lo /home/*
que le brinda una lista de nombres de usuario en la máquina /tmp/*
, /home/*/.forward
para obtener sugerencias sobre otras prácticas peligrosas, /etc/rc*/*
para servicios habilitados ... No es necesario nombrarlos individualmente. Un valor de /*
/*/* /*/*/*...
solo enumerará todo el sistema de archivos.
Denegación de vulnerabilidades de servicio.
Llevando el caso anterior demasiado lejos y tenemos un DoS.
En realidad, cualquier variable sin comillas en el contexto de la lista con entrada no higiénica es al menos una vulnerabilidad DoS.
Incluso los scripters de shell expertos suelen olvidarse de citar cosas como:
#! /bin/sh -
: ${QUERYSTRING=$1}
:
es el comando no-op. ¿Qué podría salir mal?
Eso está destinado a asignar $1
a $QUERYSTRING
if no $QUERYSTRING
estaba configurado. Esa es una manera rápida de hacer que un script CGI sea invocable desde la línea de comandos también.
Sin $QUERYSTRING
embargo, todavía se expande y, como no se cita, se invoca el operador split + glob .
Ahora, hay algunos globos que son particularmente caros de expandir. El /*/*/*/*
primero es lo suficientemente malo, ya que significa enumerar directorios de hasta 4 niveles inferiores. Además de la actividad del disco y la CPU, eso significa almacenar decenas de miles de rutas de archivos (40k aquí en una máquina virtual de servidor mínima, 10k de los cuales directorios).
Ahora /*/*/*/*/../../../../*/*/*/*
significa 40k x 10k y
/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*
es suficiente para poner de rodillas incluso a la máquina más poderosa.
Pruébelo usted mismo (aunque esté preparado para que su máquina se bloquee o cuelgue):
a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'
Por supuesto, si el código es:
echo $QUERYSTRING > /some/file
Entonces puedes llenar el disco.
Simplemente haga una búsqueda en google en shell cgi o bash cgi o ksh cgi , y encontrará algunas páginas que le muestran cómo escribir CGI en shells. Observe cómo la mitad de los que procesan los parámetros son vulnerables.
Incluso el propio David Korn
es vulnerable (mira el manejo de las cookies).
hasta vulnerabilidades de ejecución de código arbitrario
La ejecución de código arbitrario es el peor tipo de vulnerabilidad, ya que si el atacante puede ejecutar cualquier comando, no hay límite en lo que puede hacer.
Esa es generalmente la parte dividida que conduce a esos. Esa división da como resultado que se pasen varios argumentos a los comandos cuando solo se espera uno. Mientras que el primero de ellos se usará en el contexto esperado, los otros estarán en un contexto diferente, por lo que potencialmente se interpretarán de manera diferente. Mejor con un ejemplo:
awk -v foo=$external_input '$2 == foo'
Aquí, la intención era asignar el contenido de la
$external_input
variable de shell a la foo
awk
variable.
Ahora:
$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux
La segunda palabra resultante de la división de $external_input
no se asigna foo
sino que se considera como awk
código (aquí, que ejecuta un comando arbitrario:) uname
.
Esto es especialmente un problema para los comandos que se pueden ejecutar otros comandos ( awk
, env
, sed
(GNU uno), perl
, find
...) sobre todo con las variantes de GNU (que aceptan opciones después de argumentos). A veces, no sospecharías que los comandos puedan ejecutar otros como ksh
, bash
o zsh
's [
o printf
...
for file in *; do
[ -f $file ] || continue
something-that-would-be-dangerous-if-$file-were-a-directory
done
Si creamos un directorio llamado x -o yes
, entonces la prueba se vuelve positiva, porque estamos evaluando una expresión condicional completamente diferente.
Peor aún, si creamos un archivo llamado x -a a[0$(uname>&2)] -gt 1
, con todas las implementaciones de ksh al menos (que incluye la sh
mayoría de los Unices comerciales y algunos BSD), que se ejecuta uname
porque esos shells realizan una evaluación aritmética en los operadores de comparación numéricos del [
comando.
$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux
Lo mismo bash
para un nombre de archivo como x -a -v a[0$(uname>&2)]
.
Por supuesto, si no pueden obtener una ejecución arbitraria, el atacante puede conformarse con un daño menor (lo que puede ayudar a obtener una ejecución arbitraria). Cualquier comando que pueda escribir archivos o cambiar permisos, propiedad o tener algún efecto principal o secundario podría ser explotado.
Se pueden hacer todo tipo de cosas con nombres de archivos.
$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done
Y terminas haciendo ..
grabable (recursivamente con GNU
chmod
).
Las secuencias de comandos que realizan el procesamiento automático de archivos en áreas públicamente grabables como /tmp
deben escribirse con mucho cuidado.
Qué pasa [ $# -gt 1 ]
Eso es algo que encuentro exasperante. Algunas personas se molestan en preguntarse si una expansión en particular puede ser problemática para decidir si pueden omitir las citas.
Es como decir Oye, parece que $#
no puede estar sujeto al operador split + glob, pidamos al shell que lo divida + glob . O bien , vamos a escribir un código incorrecto solo porque es poco probable que se solucione el error .
Ahora, ¿qué tan improbable es? OK, $#
(o $!
, $?
o cualquier sustitución aritmética) solo puede contener dígitos (o -
para algunos) por lo que la parte glob está fuera. Sin embargo, para que la parte dividida haga algo, todo lo que necesitamos es $IFS
contener dígitos (o -
).
Con algunos proyectiles, $IFS
puede heredarse del entorno, pero si el entorno no es seguro, se acabó el juego de todos modos.
Ahora si escribes una función como:
my_function() {
[ $# -eq 2 ] || return
...
}
Lo que eso significa es que el comportamiento de su función depende del contexto en el que se llama. O, en otras palabras, se $IFS
convierte en una de las entradas. Estrictamente hablando, cuando escribe la documentación API para su función, debería ser algo como:
# my_function
# inputs:
# $1: source directory
# $2: destination directory
# $IFS: used to split $#, expected not to contain digits...
Y el código que llama a su función debe asegurarse de $IFS
que no contenga dígitos. Todo eso porque no tenía ganas de escribir esos 2 caracteres de comillas dobles.
Ahora, para que ese [ $# -eq 2 ]
error se convierta en una vulnerabilidad, necesitaría de alguna manera que el valor de $IFS
estar bajo el control del atacante . Posiblemente, eso normalmente no sucedería a menos que el atacante lograra explotar otro error.
Sin embargo, eso no es desconocido. Un caso común es cuando las personas olvidan desinfectar los datos antes de usarlos en expresiones aritméticas. Ya hemos visto anteriormente que puede permitir la ejecución de código arbitrario en algunos shells, pero en todos ellos, permite
al atacante dar a cualquier variable un valor entero.
Por ejemplo:
n=$(($1 + 1))
if [ $# -gt 2 ]; then
echo >&2 "Too many arguments"
exit 1
fi
Y con un $1
valor con (IFS=-1234567890)
, esa evaluación aritmética tiene el efecto secundario de la configuración IFS y el siguiente [
comando falla, lo que significa que se omite la comprobación de demasiados argumentos .
¿Qué pasa cuando no se invoca el operador split + glob ?
Hay otro caso en el que se necesitan comillas alrededor de variables y otras expansiones: cuando se usa como patrón.
[[ $a = $b ]] # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac
no probar si $a
y $b
son los mismos (excepto con zsh
) pero si $a
coincide con el patrón en $b
. Y hay que citar $b
si usted desea comparar como cadenas (lo mismo en "${a#$b}"
o "${a%$b}"
o "${a##*$b*}"
donde $b
debe ser citado si no es para ser tomado como un patrón).
Lo que esto significa es que [[ $a = $b ]]
puede devolver cierto en los casos en que $a
es diferente de $b
(por ejemplo, cuando $a
es anything
y $b
es *
) o puede devolver false cuando son idénticos (por ejemplo, cuando ambos $a
y $b
son [a]
).
¿Puede eso crear una vulnerabilidad de seguridad? Sí, como cualquier error. Aquí, el atacante puede alterar el flujo de código lógico de su script y / o romper las suposiciones que está haciendo su script. Por ejemplo, con un código como:
if [[ $1 = $2 ]]; then
echo >&2 '$1 and $2 cannot be the same or damage will incur'
exit 1
fi
El atacante puede pasar por alto el cheque al pasar '[a]' '[a]'
.
Ahora, si no se aplica esa coincidencia de patrón ni el operador de división + glob , ¿cuál es el peligro de dejar una variable sin comillas?
Tengo que admitir que sí escribo:
a=$b
case $a in...
Allí, las citas no perjudican pero no son estrictamente necesarias.
Sin embargo, un efecto secundario de omitir citas en esos casos (por ejemplo, en las respuestas de preguntas y respuestas) es que puede enviar un mensaje incorrecto a los principiantes: que puede estar bien no citar variables .
Por ejemplo, pueden comenzar a pensar que si a=$b
está bien, entonces también export a=$b
lo estaría (lo cual no está en muchos shells, ya que está en argumentos para el export
comando en el contexto de la lista) o env a=$b
.
¿Qué hay de zsh
?
zsh
solucionó la mayoría de esas torpezas de diseño. En zsh
(al menos cuando no está en el modo de emulación sh / ksh), si desea dividir , globular o emparejar patrones , debe solicitarlo explícitamente: $=var
dividir y $~var
glob o para que el contenido de la variable sea tratado como un patrón.
Sin embargo, la división (pero no globalización) todavía se realiza implícitamente en la sustitución de comandos sin comillas (como en echo $(cmd)
).
Además, un efecto secundario a veces no deseado de no citar variables es la eliminación de vacíos . El zsh
comportamiento es similar a lo que puede lograr en otros shells al deshabilitar el globbing por completo (con set -f
) y dividir (con IFS=''
). Todavía en:
cmd $var
No habrá división + glob , pero si $var
está vacío, en lugar de recibir un argumento vacío, cmd
no recibirá ningún argumento.
Eso puede causar errores (como lo obvio [ -n $var ]
). Posiblemente eso pueda romper las expectativas y suposiciones de un script y causar vulnerabilidades, pero no puedo encontrar un ejemplo no muy descabellado en este momento).
¿Y cuando se hace necesario la división + glob operador?
Sí, generalmente es cuando quieres dejar tu variable sin comillas. Pero luego debe asegurarse de sintonizar sus operadores de división y glob correctamente antes de usarlo. Si solo desea la parte dividida y no la parte glob (que es el caso la mayor parte del tiempo), entonces necesita deshabilitar globbing ( set -o noglob
/ set -f
) y corregir $IFS
. De lo contrario, también causará vulnerabilidades (como el ejemplo CGI de David Korn mencionado anteriormente).
Conclusión
En resumen, dejar una variable (o sustitución de comando o expansión aritmética) sin comillas en shells puede ser muy peligroso, especialmente cuando se realiza en contextos incorrectos, y es muy difícil saber cuáles son esos contextos incorrectos.
Esa es una de las razones por las que se considera una mala práctica .
Gracias por leer hasta ahora. Si se te pasa por la cabeza, no te preocupes. No se puede esperar que todos entiendan todas las implicaciones de escribir su código de la manera en que lo escriben. Es por eso que tenemos
recomendaciones de buenas prácticas , para que se puedan seguir sin necesariamente entender por qué.
(y en caso de que aún no sea obvio, evite escribir código confidencial de seguridad en shells).
¡Y por favor cite sus variables en sus respuestas en este sitio!
¹En ksh93
y pdksh
y derivados, la expansión de llaves también se realiza a menos que el globbing esté deshabilitado (en el caso de ksh93
versiones hasta ksh93u +, incluso cuando la braceexpand
opción está deshabilitada).