[LinuxFocus-icon]
Hogar  |  Mapa  |  Indice  |  Busqueda

Noticias | Arca | Enlaces | Sobre LF
Este artículo está disponible en los siguientes idiomas: English  Castellano  Deutsch  Francais  Nederlands  Portugues  Russian  Turkce  

convert to palmConvert to GutenPalm
or to PalmDoc

[imagen de los autores]
por Frédéric Raynal, Christophe Blaess, Christophe Grenier
<pappy(at)users.sourceforge.net, ccb(at)club-internet.fr, grenier(at)nef.esiea.fr>

Sobre el autor:

Christophe Blaess es un ingeniero aeronáutico independiente. Es un fan de Linux y realiza mucho de su trabajo con este sistema. Además, coordina la traducción de las páginas del manual publicadas en Linux Documentation Project.

Christophe Grenier es un estudiante de 5º curso en ESIEA, donde trabaja como administrador de sistemas. Siente pasión por la seguridad en computadoras.

Frédéric Raynal ha estado utilizando Linux durante muchos años ya que no contamina, no utiliza hormonas, ni GMO ni harina de engorde para animales... solamente contiene esfuerzo e ingenio.



Taducido al español por:
Gerard Farràs Ballabriga <gerard.farras(at)campus.uab.es>

Contenidos:

 

Evitando agujeros de seguridad al desarrollar una aplicación - Parte 6: Scripts CGI

[article illustration]

Resumen:

Obteniendo un fichero, ejecutando un script en Perl mal programado ... "Hay más de un camino para hacer esto!"

Artículos previos de esta serie :



 

Servidor Web, URI y problemas de configuración

 

Introducción (demasiado breve) sobre como trabaja un servidor web y como construir una URI

Cuando un cliente pide un archivo HTML, el servidor envía la página pedida (o un mensaje de error). El navegador interpreta el código HTML para formatear y visualizar el fichero. Para poner un ejemplo, escribiendo la URL (Uniform Request Locator) http://www.linuxdoc.org/HOWTO/HOWTO-INDEX/howtos.html, el cliente se conecta al servidor www.linuxdoc.org y pide la página /HOWTO/HOWTO-INDEX/howtos.html, utilizando el protocolo HTTP. Si la página existe, el servidor envía el archivo pedido. Con este modelo estático, si el archivo está presente en el servidor , éste es enviado "tal y como es" al cliente, de otra forma un mensaje de error es enviado (el bien conocido 404 - Not Found).

Desgraciadamente, esto no permite la interactividad con el usuario. De este modo cosas como e-negocios, e-reservas para vacaciones o e-loquesea no es posible.

Afortunadamente, hay soluciones para generar dinámicamente páginas HTML. Los scripts CGI (Common Gateway Interface) son una de ellas. En este caso, la URL para acceder a estas páginas es construida de forma ligeramente diferente:

http://<servidor><pathHaciaScript>[?[param_1=val_1][...][&param_n=val_n]]
La lista de argumentos es guardada en la variable de entorno QUERY_STRING. En este contexto, un script CGI no es nada más que un archivo ejecutable. Utiliza stdin (standard input) o la variable de entorno QUERY_STRING para obtener los argumentos que le pasan. Después de ejecutar el código, el resultado es mostrado en stdout (standard output) y luego, redirigido al cliente web. Casi todos los lenguajes de programación pueden ser usados para escribir un script CGI (un programa compilado en C, Perl, shell-scripts...).

Por ejemplo, permítame buscar qué es lo que los HOWTOs de www.linuxdoc.org conocen sobre ssh :

http://www.linuxdoc.org/cgi-bin/ldpsrch.cgi?

svr=http%3A%2F%2Fwww.linuxdoc.org&srch=ssh&db=1&scope=0&rpt=20
De hecho, esto es mucho más simple de lo que parece. Vamos a analizar esta URL :

Frecuentemente, los nombres de los argumentos y sus valores son suficientemente explícitos como para entender su significado. Además, el contenido de la página que muestra las respuestas puede ser significativas.

Ahora sabemos que el lado brillante de los scripts CGI es la habilidad que tiene un usuario para pasar argumentos... pero el lado oscuro es que un script mal programado abre un agujero de seguridad.

Probablemente habrás notado caracteres extraños en tu navegador o presentes dentro de la petición previa. Estos caracteres están en formato Unicode. La tabla 1 muestra el significado de algunos de estos códigos. Permíteme mencionar que algunos servidores IIS4.0 y IIS5.0 tienen una vulnerabilidad basada en estos caracteres.

 

Configuración de Apache con "SSI Server Side Include"

Server Side Include es una función parte de los servidores web. Permite integrar instrucciones dentro de las páginas web, incluir un fichero "tal y como es", o ejecutar un comando (shell o script CGI).

En el fichero de configuración del Apache httpd.conf, la instrucción "AddHandler server-parsed .shtml" activa este mecanismo. Frecuentemente, para evitar la distinción entre .html and .shtml, uno puede añadir la extensión .html. Evidentemente, esto ralentiza el servidor... Esto puede ser controlado a nivel de directorios con las instrucciones:



En el script adjunto LibroDeInvitados.cgi, el texto proporcionado por el usuario es incluido en un archivo HTML, sin la conversión de caracteres de '<' y '>' hacia los códigos HTML &lt; and &gt; . Suficiente para que una persona curiosa envie una de las siguientes instrucciones :

Con el primero,
LibroDeInvitados.cgi?email=pappy&texte=%3c%21--%23printenv%20--%3e
obtienes algunas líneas de información sobre el sistema :
DOCUMENT_ROOT=/home/web/sites/www8080
HTTP_ACCEPT=image/gif, image/jpeg, image/pjpeg, image/png, */*
HTTP_ACCEPT_CHARSET=iso-8859-1,*,utf-8
HTTP_ACCEPT_ENCODING=gzip
HTTP_ACCEPT_LANGUAGE=en, fr
HTTP_CONNECTION=Keep-Alive
HTTP_HOST=www.esiea.fr:8080
HTTP_PRAGMA=no-cache
HTTP_REFERER=http://www.esiea.fr:8080/~grenier/cgi/LibroDeInvitados.cgi?
 email=&texte=%3C%21--%23include+file%3D%22LibroDeInvitados.cgi%22--%3E
HTTP_USER_AGENT=Mozilla/4.76 [fr] (X11; U; Linux 2.2.16 i686)
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/X11R6/bin
REMOTE_ADDR=194.57.201.103
REMOTE_HOST=nef.esiea.fr
REMOTE_PORT=3672
SCRIPT_FILENAME=/mnt/c/nef/grenier/public_html/cgi/LibroDeInvitados.html
SERVER_ADDR=194.57.201.103
SERVER_ADMIN=master8080@nef.esiea.fr
SERVER_NAME=www.esiea.fr
SERVER_PORT=8080
SERVER_SIGNATURE=<ADDRESS>Apache/1.3.14 Server www.esiea.fr Port
8080</ADDRESS>

SERVER_SOFTWARE=Apache/1.3.14 (Unix)  (Red-Hat/Linux) PHP/3.0.18
GATEWAY_INTERFACE=CGI/1.1
SERVER_PROTOCOL=HTTP/1.0
REQUEST_METHOD=GET
QUERY_STRING=
REQUEST_URI=/~grenier/cgi/LibroDeInvitados.html
SCRIPT_NAME=/~grenier/cgi/LibroDeInvitados.html
DATE_LOCAL=Tuesday, 27-Feb-2001 15:33:56 CET
DATE_GMT=Tuesday, 27-Feb-2001 14:33:56 GMT
LAST_MODIFIED=Tuesday, 27-Feb-2001 15:28:05 CET
DOCUMENT_URI=/~grenier/cgi/LibroDeInvitados.shtml
DOCUMENT_PATH_INFO=
USER_NAME=grenier
DOCUMENT_NAME=LibroDeInvitados.html



La instrucción exec, suministra casi lo mismo que una shell :

LibroDeInvitados.cgi?email=ppy&texte=%3c%21--%23exec%20cmd="cat%20/etc/passwd"%20--%3e

No intentes "<!--#include file="/etc/passwd"-->", el path es relativo al directorio donde puedes encontrar el fichero HTML y no puede contener "..". El fichero de Apache error_log , contendrá un mensaje indicando un intento de acceso a un fichero prohibido . El usuario podrá ver el mensaje [an error occurred while processing this directive] en la página HTML.

SSI no es necesario frecuentemente y es mejor desactivarlo del servidor. Sin embargo, la causa del problema es la combinación entre la aplicación mal programada LibroDeInvitados y SSI.

 

Scripts en Perl

En esta sección, vamos a presentar los agujeros de seguridad relacionados con los scripts CGI escritos en Perl. Para hacerlo mas claro, no daremos todo el código sino solamente las partes requeridas para entender el problema.

Cada uno de nuestros scripts está programado siguiendo la plantilla siguiente :

#!/usr/bin/perl -wT
BEGIN { $ENV{PATH} = '/usr/bin:/bin' }
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};   # Hacemos %ENV más segura =:-)
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD>";
print "<TITLE>Comando remoto</TITLE></HEAD>\n";
&ReadParse(\%input);
# Podemos usar $input por ejemplo así:
# print "<p>$input{archivo}</p>\n";
# ########################################## #
# Principio de la descripción del problema   #
# ########################################## #



# #################################### #
# Fin de la descripción del problema   #
# #################################### #

form:
print "<form action=\"$ENV{ÑOMBRE_DEL_SCRIPT'}\">\n";
print "<input type=texto name=archivo>\n </form>\n";
print "</BODY>\n";
print "</HTML>\n";
exit(0);

# el primer argumento tiene que ser una referencia a un hash
# El hash será rellenado con datos.
sub ReadParse($) {
  my $in=shift;
  my ($i, $key, $val);
  my $in_primero;
  my @in_segundo;

  # Leer en texto
  if ($ENV{'REQUEST_METHOD'} eq "GET") {
    $in_first = $ENV{'QUERY_STRING'};
  } elsif ($ENV{'REQUEST_METHOD'} eq "POST") {
    read(STDIN,$in_primero,$ENV{'CONTENT_LENGTH'});
  }else{
    die "ERROR: Método de petición desconocido\n";
  }

  @in_segundo = split(/&/,$in_primero);

  foreach $i (0 .. $#in_segundo) {
    # Convertir los caracteres + en espacios
    $in_segundo[$i] =~ s/\+/ /g;

    # Partir entre la clave y el valor.
    ($key, $val) = split(/=/,$in_segundo[$i],2);

    # Convertir %XX de números hexadecimales a alfanuméricos
    $key =~ s/%(..)/pack("c",hex($1))/ge;
    $val =~ s/%(..)/pack("c",hex($1))/ge;

    # Asociar una clave con un valor
    # \0 es el separador múltiple
    $$in{$key} .= "\0" if (defined($$in{$key}));
    $$in{$key} .= $val;

  }
  return length($#in_segundo);
}

Después vamos a aprender más sobre los argumentos pasados al intérprete Perl (-wT). Empezamos limpiando las variables de entorno $ENV y $PATH y enviamos seguidamente la cabecera HTML (esto es parte del protocolo html implementado por el navegador cliente y el servidor. Aunque no podamos verlo desde el lado del cliente). La función ReadParse() lee los argumentos pasados al script. Esto puede hacerse más facilmente con módulos, pero así podemos ver el código entero. Ahora vamos a presentar algunos ejemplos.

 

El byte null

Perl considera cada carácter de la misma forma, cosa que difiere de las funciones en C, por ejemplo. En Perl, el carácter null al final de una cadena (string) es como cualquier otro. Qué implica eso ?

Vamos a añadir el siguiente código a nuestro script para crear showhtml.cgi  :

  # showhtml.cgi
  my $archivo= $input{archivo}.".html";
  print "<BODY>Archivo : $archivo<BR>";
  if (-e $archivo) {
      open(FILE,"$archivo") || goto form;
      print <FILE>;
  }


La función ReadParse() obtiene el único argumento : el nombre del archivo a mostrar. Para prevenir que se pueda leer algo más que ficheros HTML, añadimos la extensión ".html" al final del nombre del fichero. Pero, recuerda, el carácter null es como cualquiera otro...

De este modo, si nuestra petición es showhtml.cgi?archivo=%2Fetc%2Fpasswd%00 el archivo es llamado my $archivo = "/etc/passwd\0.html" y nuestros pasmados ojos estarán mirando algo que no parece HTML.

Qué es lo que pasa ? El comando strace nos muestra como Perl abre un fichero:

  /tmp >>cat >open.pl << EOF
  > #!/usr/bin/perl
  > open(FILE, "/etc/passwd\0.html");
  > EOF
  /tmp >>chmod 0700 open.pl
  /tmp >>strace ./open.pl 2>&1 | grep open
  execve("./open.pl", ["./open.pl"], [/* 24 vars */]) = 0
  ...
  open("./open.pl", O_RDONLY)             = 3
  read(3, "#!/usr/bin/perl\n\nopen(FILE, \"/et"..., 4096) = 51
  open("/etc/passwd", O_RDONLY)           = 3


El último open() presentado por strace corresponde a la llamada de sistema escrita en C. Como podemos ver la extensión .html ha desaparecido, y esto nos permite abrir /etc/passwd.

Este problema es solucionado con una expresión regular muy simple que borre todos los caracteres null:

s/\0//g;


 

Utilizando pipes (tuberías)

Aquí tenemos un script sin protección alguna que muestra un archivo especificado del árbol de directorios /home/httpd/ :

#pipe1.cgi

my $archivo= "/home/httpd/".$input{archivo};
print "<BODY>File : $archivo<BR>";
open(FILE,"$archivo") || goto form;
print <FILE>;


No vayais a reíros con este ejemplo! He visto cosas parecidas en muchos scripts.

El primer exploit es obvio :

pipe1.cgi?archivo=..%2F..%2F..%2Fetc%2Fpasswd
Suficiente para "subir" al árbol de directorios y acceder a cualquier fichero. Pero hay algo mucho más interesante : ejecutar el comando que tú escojas. En Perl, el comando open(FILE, "/bin/ls") abre el archivo binario "/bin/ls" ... pero open(FILE, "/bin/ls |") ejecuta dicho comando. Añadiendo un simple pipe | cambia la conducta de open().

Otro problema viene del hecho que la existencia del archivo no es testeada, esto nos permite ejecutar cualquier comando y además pasarle argumentos:

pipe1.cgi?archivo=..%2F..%2F..%2Fbin%2Fcat%20%2fetc%2fpasswd%20|
muestra el contenido del fichero de passwords.

Testeando la existencia del archivo da menos libertad a open :

#pipe2.cgi

my $archivo= "/home/httpd/".$input{archivo};
print "<BODY>File : $archivo<BR>";
if (-e $archivo) {
  open(FILE,"$archivo") || goto form;
  print <FILE>
} else {
  print "-e fallado: el fichero no existe\n";
}
Aquí el ejemplo anterior ya no funciona. El test "-e" falla ya que no encuentra el archivo "../../../bin/cat /etc/passwd |".

Vamos a probar el comando /bin/ls. Su conducta será la misma que antes. Eso es, si intentamos, por ejemplo de listar el contenido del directorio /etc , "-e" testea la existencia del fichero "../../../bin/ls /etc |", que no existe. Como no suministremos el nombre de un fichero "fantasma" no vamos a obtener nada interesante :(

Sin embargo, aún hay alguna alternativa. El fichero /bin/ls existe (en la mayoría de los sistemas) y pasaría el chequeo, pero si open() es llamado con este nombre de archivo se mostraría el archivo binario, pero no se ejecutaría. Debemos buscar una solución para poner una tubería (pipe) '|' al final del nombre, pero que no sea testeado con "-e". Ya conocemos la solución : el byte null. Si enviamos "../../../bin/ls\0|" , el test de existencia pasa ya que solo considera "../../../bin/ls", pero open() puede ver el pipe y luego ejecuta el comando. Así que la URL que suministra el contenido del directorio actual es:

pipe2.cgi?archivo=../../../bin/ls%00|
 

Salto de línea

El script finger.cgi ejecuta la instrucción finger en nuestra máquina :

#finger.cgi

print "<BODY>";
$login = $input{'login'};
$login =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g;
print "Login $login<BR>\n";
print "Finger<BR>\n";
$CMD= "/usr/bin/finger $login|";
open(FILE,"$CMD") || goto form;
print <FILE>


Este script utiliza, como mínimo, una protección útil: tiene en cuenta algunos caracteres extraños para prevenir que sean interpretados por la shell poniendo un '\' delante. Así, el punto y coma es cambiado a "\;" por la expresión regular. Pero la lista no contiene todos los caracteres importantes. Entre otros, el salto de línea '\n'.

En tu shell preferida puedes validar una instrucción pulsando la tecla RETURN o ENTER , que envía el carácter '\n'. En Perl, puedes hacer lo mismo. Ya hemos visto como la instrucción open() nos permite ejecutar un comando cuando la línea termina con un pipe '|'.

Para simular este comportamiento es suficiente con añadir un salto de línea y una instrucción , después de enviar el login al comando finger :

finger.cgi?login=kmaster%0Acat%20/etc/passwd


Hay otros caracteres interesantes para ejecutar varias instrucciones en una sola línea:


Estos no funcionan aquí ya que el script está protegido de ellos gracias a la expresión regular. Pero, vamos a buscar soluciones.  

La barra invertida y el punto y coma

El script finger.cgi previo evita problemas con algunos caracteres extraños. Así, la URL <finger.cgi?login=kmaster;cat%20/etc/passwd no resulta ya que el punto y coma es evitado. Sin embargo, hay un carácter que no está protegido: la barra invertida '\'.

Imaginemos por un momento un script que evite la ascensión en el árbol de directorios utilizando la expresión regular s/\.\.//g para desembarazarnos de "..". No importa! Las shells pueden manejar varios '/' a la vez (simplemente prueba cat ///etc//////passwd para quedar convencido).

Por ejemplo, en el script anterior pipe2.cgi, la variable $fichero es inicializada con el prefijo "/home/httpd/". Parece que usando esta expresión regular sería suficiente para evitar la ascensión en el árbol de directorios. Evidentemente, esta expresión protege a "..", pero qué pasa si nosotros protegemos el carácter '.' ? Eso es, la expresión regular no concuerda si el nombre del fichero es .\./.\./etc/passwd. En realidad, esta cadena funciona bien con la llamada system() (o con ` ... `), pero falla con open() o el test "-e".

Vamos atrás con el script finger.cgi. Utilizando el punto y coma la URL finger.cgi?login=kmaster;cat%20/etc/passwd no da el resultado esperado ya que el punto y coma es filtrado por la expresión regular. Eso es, la shell recibe la instrucción:

/usr/bin/finger kmaster\;cat /etc/passwd
Los siguientes errores son encontrados en los logs del servidor web :
finger: kmaster;cat: no such user.
finger: /etc/passwd: no such user.
Estos mensajes son idénticos a los que obtendrías si lo escribieses en una shell. El problema viene del hecho que la shell considera el carácter protegido ';' como parte de la cadena "kmaster;cat" .

Necesitamos separar las dos instrucciones, la primera para el script y la siguiente que queremos ejecutar. Debemos proteger ';' : <A HREF="finger.cgi?login=kmaster\;cat%20/etc/passwd"> finger.cgi?login=kmaster\;cat%20/etc/passwd</A>. La cadena "\; es cambiada en el script por "\\;", y luego, enviada a la shell. Éste lee lo siguiente :

/usr/bin/finger kmaster\\;cat /etc/passwd
La shell divide la cadena en dos:
  1. /usr/bin/finger kmaster\ que probablemente fallará... no sufras por eso ;-)
  2. cat /etc/passwd que mostrará el fichero de passwords.
La solución es simple : el carácter barra invertida '\' también debe ser filtrado.

 

Utilizando un carácter " no protegido

A veces, el parámetro es "protegido" con comillas. Hemos cambiado ligeramente el script previo finger.cgi para proteger la variable $login.

Sin embargo, si las comillas no son filtradas sera una consideración inútil. Suficiente con añadir una en nuestra petición. Así, la primera comilla " enviada, cierra la abierta por el script. Luego, escribes el comando, y la segunda comilla abriendo la última (que cerraría) del script.

El script finger2.cgi ilustra la idea :

#finger2.cgi

print "<BODY>";
$login = $input{'login'};
$login =~ s/\0//g;
$login =~ s/([<>\*\|`&\$!#\(\)\[\]\{\}:'\n])/\\$1/g;
print "Login $login<BR>\n";
print "Finger<BR>\n";
#Nueva (in)eficiente super protección :
$CMD= "/usr/bin/finger \"$login\"|";
open(FILE,"$CMD") || goto form;
while(<FILE>) {
  print;
}


La URL que ejecutará el comando se convierte en :

finger2.cgi?login=kmaster%22%3Bcat%20%2Fetc%2Fpasswd%3B%22
La shell recibe el comando /usr/bin/finger "$login";cat /etc/passwd"" y las comillas ya no serán un problema.

Recuerda que si quieres proteger los parámetros de tu script con comillas, debes filtrarlos igual que la barra invertida o el punto y coma.

 

Programando en Perl

 

Opciones de Warning y tainting

Cuando programemos en Perl es aconsejable la opción w o "use warnings;" (Perl 5.6.0 y siguientes) ya que nos informará sobre problemas potenciales como variables no inicializadas o expresiones/funciones obsoletas.

La opción T (taint mode) proporciona mayor seguridad. Este modo activa varios tests. El más importante concierne a una posible corrupción (tainting) de las variables. Las variables pueden estar limpias o posiblemente corruptas. Los datos que provienen del exterior del programa son considerados corruptos hasta que no hayan sido limpiadas. Las variables que pueden ser corruptas no se pueden asignar a objetos que serán usados en el exterior del programa (llamadas a otros comandos de la shell).

En taint mode, los argumentos de la línea de comandos, las variables de entorno, algunos resultados de llamadas de sistema (readdir(), readlink(), readdir(), ...) y los datos que provienen de archivos, son considerados sospechosos y por lo tanto son vigilados.

Para limpiar una variable debes pasarla a través de una expresión regular. Evidentemente, utilizar .* es inútil. El objetivo es forzarte a tener en cuenta los argumentos proporcionados. Debemos intentar utilizar siempre una expresión regular lo más específica posible.

No obstante, este modo no protege de todo: no se vigilan los argumentos pasados a system() o exec() como una lista de variables. Debemos ser muy cuidadosos cuando uno de nuestros scripts utilize estas funciones. Las instrucciones exec "sh", '-c', $arg; son consideradas como seguras, esté como esté $arg :(

También es recomendado "use strict;" al principio de tus programas. Así forzamos la obligación de declarar todas las variables; algunas personas pueden encontrarlo molesto pero es obligatorio cuando utilizemos mod-perl.

De este modo, tus scripts en Perl deben empezar así :

#!/usr/bin/perl -wT
use strict;
use CGI;
con Perl 5.6.0 :
#!/usr/bin/perl -T
use warnings;
use strict;
use CGI;


 

La llamada open()

Muchos programadores abren un fichero simplemente utilizando open(FILE,"$fichero") || .... Ya hemos visto los riesgos de este código. Para reducir este riesgo es suficiente con especificar el modo para abrirlo:

En efecto, no abras tus ficheros de forma mal especificada.

Antes de acceder a un fichero es recomendable chequear si éste existe. Esto no evita las condiciones de carrera presentadas en el artículo anterior, pero es útil respecto a algunos trucos como comandos con sus argumentos.

if ( -e $fichero ) { ... }

Desde la versión de Perl 5.6, hay una nueva sintaxis para la llamada open() : open(FILEHANDLE,MODO,LISTA). Con el modo '<' , el fichero se abre para lectura; con '>' , el fichero es truncado o creado si es necesario, y abierto para escritura. Hay un par de modos muy interesantes para la comunicación con otros procesos. Si el modo es '|-' o '-|', el argumento LISTA es interpretado como un comando y es respectivamente encontrado antes o después de la tubería.

Antes de Perl 5.6 y open() con tres argumentos, algunas personas utilizaban el comando sysopen().

 

Filtrando la entrada

Existen dos métodos : Podemos especificar los caracteres prohibidos, o definir explícitamente los carecteres permitidos utilizando expresiones regulares. Los programas de ejemplo te habrán convencido que es muy fácil olvidarse de filtrar caracteres que pueden ser potencialmente peligrosos, es por esta razón que el segundo método es recomendado.

Prácticamente, lo que hacemos es lo siguiente : primero, chequeamos si la petición contiene sólo caracteres permitidos. Luego, filtramos los caracteres considerados como peligrosos entre los permitidos.

#!/usr/bin/perl -wT

# filtro.pl

#  Las variables $seguro y $peligroso definen respectivamente
#  los caracteres sin riesgo y los arriesgados.
#  Es suficiente con añadir/quitar algunos para cambiar el filtro.
#  Solamente la entrada $input que contenga los caracteres incluídos en
#  las definiciones es válida.

use strict;

my $input = shift;

my $seguro = '\w\d';
my $peligroso = '&`\'\\|"*?~<>^(){}\$\n\r\[\]';
#Note:
#  '/', espacio y tab no son parte de las definiciones


if ($input =~ m/^[$seguro$peligroso]+$/g) {
    $input =~ s/([$peligroso]+)/\\$1/g;
} else {
    die "Hay caracteres no permitidos en la entrada $input\n";
}
print "input = [$input]\n";


Este script define dos conjuntos de caracteres :

Cualquier petición que contenga un caracter que no esté presente en uno de los dos conjuntos deberá ser immediatamente descartado.

 

Scripts PHP

No querría ser polémico pero creo que es mejor escribir scripts en PHP que no en Perl. Más exactamente, como administrador de sistemas, prefiero que mis usuarios escriban scripts con lenguaje PHP que no en Perl. Cualquiera que programe mal - o de forma insegura - en PHP puede dejar un agujero de seguridad igual de peligroso que en Perl. Si es así, porqué prefiero PHP? Pués porqué en este lenguaje puedes activar un Modo Seguro (Safe mode) cuando haya problemas de programación (safe_mode=on) o desactivar funciones peligrosas (disable_functions=...). Este modo impide acceder a ficheros que no sean propiedad del usuario, o cambiar variables de entorno sin que esté explícitamente permitido, ejecutar comandos, etc.

Por defecto el banner de Apache nos informa sobre la versión de PHP que estamos usando.

$ telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.localdomain.
Escape character is '^]'.
HEAD / HTTP/1.0

HTTP/1.1 200 OK
Date: Tue, 03 Apr 2001 11:22:41 GMT
Server: Apache/1.3.14 (Unix)  (Red-Hat/Linux) mod_ssl/2.7.1
        OpenSSL/0.9.5a PHP/4.0.4pl1 mod_perl/1.24
Connection: close
Content-Type: text/html

Connection closed by foreign host.
Suficiente con escribir expose_PHP = Off en /etc/php.ini para esconder dicha información :
Server: Apache/1.3.14 (Unix)  (Red-Hat/Linux) mod_ssl/2.7.1
OpenSSL/0.9.5a mod_perl/1.24


El fichero /etc/php.ini (PHP4) o /etc/httpd/php3.ini tiene muchos parámetros que permiten hacer el sistema más robusto. Por ejemplo, la opción "magic_quotes_gpc" añade unas comillas en los argumentos recibidos por los métodos GET, POST y vía cookies; esto soluciona algunos problemas de seguridad que nos hemos encontrado con Perl.

 

Conclusión

Entre los artículos de esta serie, éste es probablemente el más fácil de entender. Nos enseña vulnerabilidades que pueden ser explotadas cada día en la web. Hay muchas otras, frecuentemente relacionadas con mala programación (por ejemplo, un script que envía correo electrónico, que coge como argumento el campo From:, proporciona un buen lugar para enviar spam (correo no deseado). Los ejemplos son muy numerosos. En seguida que hay un script en un sitio web, habrá como mínimo una persona que intentará usarlo de forma fraudulenta.

Este artículo termina la serie sobre programación segura. Esperamos haberte mostrado los principales agujeros de seguridad presentes en muchas aplicaciones y que a partir de ahora tengas en cuenta el factor "seguridad" cuando diseñes y programes tus aplicaciones. Los problemas de seguridad son muchas veces olvidados debido al limitado propósito de la aplicación (uso interno, uso en una red privada, modelo temporal, etc.). Sin embargo, un módulo originariamente diseñado para un uso muy restringido puede convertirse en la base de una aplicación mucho mayor, y luego los cambios serán más caros.


 

Algunos caracteres unicode

Unicode Carácter
%00 \0 (final de cadena)
%0a \n (salto de carro)
%20 espacio
%21 !
%22 "
%23 #
%26 & (ampersand)
%2f /
%3b ;
%3c <
%3e >
Tab 1 : Correspondencia de caracteres en Unicode

 

Enlaces


 

El programa LibroDeInvitados.cgi defectuoso

#!/usr/bin/perl -w

# LibroDeInvitados.cgi

BEGIN { $ENV{PATH} = '/usr/bin:/bin' }
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};   # Hacemos %ENV más seguro =:-)
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD><TITLE>Libro De Visitas Peligroso
</TITLE></HEAD>\n";
&ReadParse(\%input);
my $email= $input{email};
my $texto= $input{texto};
$texto =~ s/\n/<BR>/g;

print "<BODY><A HREF=\"LibroDeInvitados.html\">
       GuestBook </A><BR><form
action=\"$ENV{'SCRIPT_NAME'}\">\n
      Email: <input type=texto name=email><BR>\n
      Texte:<BR>\n<textarea name=\"texto\" rows=15 cols=70>
      </textarea><BR><input type=submit value=\"Adelante!\">
      </form>\n";
print "</BODY>\n";
print "</HTML>";
open (FILE,">>LibroDeInvitados.html") || die ("No es posible la escritura\n");
print FILE "Email: $email<BR>\n";
print FILE "Texto: $texto<BR>\n";
print FILE "<HR>\n";
close(FILE);
exit(0);

sub ReadParse {
  my $in =shift;
  my ($i, $key, $val);
  my $in_first;
  my @in_second;

  # Read in text
  if ($ENV{'REQUEST_METHOD'} eq "GET") {
    $in_first = $ENV{'QUERY_STRING'};
  } elsif ($ENV{'REQUEST_METHOD'} eq "POST") {
    read(STDIN,$in_first,$ENV{'CONTENT_LENGTH'});
  }else{
    die "ERROR: Método de petición desconocido\n";
  }

  @in_second = split(/&/,$in_first);

  foreach $i (0 .. $#in_second) {
    # Convertir los + en espacios
    $in_second[$i] =~ s/\+/ /g;

    # Dividir entre la clave y el valor.
    ($key, $val) = split(/=/,$in_second[$i],2);

    # Convertir los números hexadecimales %XX a alfanuméricos
    $key =~ s/%(..)/pack("c",hex($1))/ge;
    $val =~ s/%(..)/pack("c",hex($1))/ge;

    # Asociar una clave con su valor.
    $$in{$key} .= "\0" if (defined($$in{$key}));
    $$in{$key} .= $val;

  }

  return length($#in_second);
}


 

Formulario de "talkback" para este artículo

Cada artículo tiene su propia página de "talkback". A través de esa página puedes enviar un comentario o consultar los comentarios de otros lectores
 Ir a la página de "talkback" 

Contactar con el equipo de LinuFocus
© Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL
LinuxFocus.org

Pinchar aquí para informar de algún problema o enviar comentarios a LinuxFocus
Información sobre la traducción:
fr --> -- : Frédéric Raynal, Christophe Blaess, Christophe Grenier <pappy(at)users.sourceforge.net, ccb(at)club-internet.fr, grenier(at)nef.esiea.fr>
fr --> en: Georges Tarbouriech <georges.t(at)linuxfocus.org>
en --> es: Gerard Farràs Ballabriga <gerard.farras(at)campus.uab.es>

2001-11-23, generated by lfparser version 2.21