Published 16 years ago
(updated 13 years ago)
En la edición anterior de SG prometí que en esta columna trataría temas relativos a la seguridad en cómputo y cómo escribir código más confiable y más robusto. Las vulnerabilidades más comunes son también las más fáciles de explotar para un atacante (y utilizando algunas prácticas base, son las más fáciles de evitar o corregir). Casi todas las vulnerabilidades se originan por falta de validación (o exceso de confianza) en los datos proporcionados por el usuario.Prácticamente, la totalidad de los sistemas web que desarrollemos procesarán datos provenientes de terceros: ya sea mostrando o grabando lo expresado en formas HTML;determinando el flujo de nuestra aplicación a través de rutas y parámetros ; «galletas» HTTP o incluso -considerando la tendencia de migración hacia un esquema de «cloud computing»- tomando resultados de procedimientos remotos en sistemas no controlados por nosotros. A cada paso debemos emplear datos en los que no confiamos. Esta puerta de entrada permite a un
atacante una amplia variedad de modalidades de intrusión. En general, podemos hablar de ellas como inyección de código interpretado y en esta ocasión hablaremos específicamente de inyección de SQL.
En el desarrollo de sistemas debemos partir siempre del principio de mínima confianza: No se debe confiar en ningún dato proveniente de fuera de nuestro sistema, independientemente de quién sea el usuario. Esto es especialmente importante cuando requerimos que un elemento cruce entre las principales barreras de las diversas capas de nuestro sistema.
Tomemos como primer ejemplo al sistema de gestión de contenido que usa SG. Si quisieran leer la edición anterior de esta columna, a la que hice referencia hace algunas líneas, pueden encontrarla en:
http://www.sg.com.mx/content/view/825
Todos hemos analizado URLs, y resultará obvio que «825» corresponda al ID de la nota en la base de datos, y que los componentes «content» y «view» indican la operación que el sistema debe realizar ante una solicitud. Ahora bien, ¿a qué me refiero con que cruzamos las barreras entre las capas?
Enfoquémonos en el ID. Al analizar el URL, el ID es un pedazo de texto (formalmente es una cadena que es recibida como parte del método GET, uno de los métodos definidos para el protocolo HTTP). El servidor web Apache que recibe mi solicitud interpreta este método GET y encuentra encuentra –utilizando mod_rewrite, indicado por el archivo htaccess– que el contenido indicado por la ruta /content/view/* debe ser procesado por el archivo index.php, que a su vez es manejado por el lenguaje PHP. El archivo index.php corresponde en este caso al sistema Joomla, que reconoce la ruta, convierte al ID en su representación numérica y lo utiliza para pedir a la base de datos le entregue los datos relacionados con determinado artículo. Entonces, aquí podemos reconocer los siguientes puntos principales de manipulación de la solicitud:
1. Apache recibe una solicitud HTTP, y la reescribe (via mod_rewrite),
indicando «content», «view» y «825» como parámetros a index.php.
2. PHP analiza, separa y estructura los parámetros recibidos para ser
utilizados por Joomla.
3. Joomla solicita el artículo 825 a la base de datos.
La variabilidad de los primeros pasos es en realidad menor, pero al solicitar a la base de datos el artículo «825» (y este es el caso más sencillo de todos) deben pasar muchas cosas. Primero que nada, «825»
es una cadena de caracteres. PHP es un lenguaje débilmente tipificado (los números se convierten en cadenas y viceversa automáticamente según sea requerido), pero una base de datos maneja tipos estrictamente. Como atacante, puedo buscar qué pasa si le pido al sistema algo que no se espere, por ejemplo «825aaa». En este caso (¡felicidades!), el código PHP que invoca a la base de datos sí verifica que el tipo de datos sea correcto: Hace una conversión a entero, y descarta lo que sobra. Sin embargo (y no doy URLs por razones obvias), en muchas ocasiones esto me llevaría a recibir un mensaje como el siguiente:
Warning: pg_execute() [ function.pg-execute]: Query failed: ERROR: invalid input syntax
for integer: “825aaa” in /home/(...)/index.php on line 192
Esto indicaría que uno de los parámetros fue pasado sin verificación de PHP al motor de base de datos, y fue éste el que reconoció al error. Ahora, esto no califica aún como inyección de SQL (dado que el motor
de bases de datos supo reaccionar ante esta situación), pero estamos prácticamente a las puertas. Podemos generalizar que cuando un desarrollador no validó la entrada en un punto, habrá muchos otros en que no lo haya hecho. Este error en particular nos indica que el código contiene alguna construcción parecida a la siguiente:
SELECT * FROM articulo WHERE id = $id_art
La vulnerabilidad aquí consiste en que el programador no tomó en cuenta que $id_art puede contener cualquier cosa enviada por el usuario. ¿Cómo puede un atacante aprovecharse de esto?
Presentaré a continuación algunos ejemplos, evitando enfocarme a ningún lenguaje en específico.
Lo importante es cómo tratamos al SQL generado. Para estos ejemplos, cambiemos un poco el caso de uso: En vez de ubicar recursos, hablemos acerca de una de las operaciones más comunes: La identificación de
un usuario vía login y contraseña. Supongamos que el mismo sistema del código recién mencionado utiliza la siguiente función para validar a sus usuarios:
$data = $db->fetch(“SELECT id FROM usuarios
WHERE login = ‘$login’ AND passwd = ‘$passwd’”);
if ($data) { $uid = $data[0];
} else { print “<h1>Usuario inválido!</h1>”;
}
Aquí pueden apreciar la práctica muy cómoda y común de “interpolar” variables dentro
de una cadena. Muchos lenguajes permiten construir cadenas donde se expande el contenido de determinadas variables. En caso de que su lenguaje favorito no maneje esta característica, concatenar las sub-cadenas y las variables nos lleva al mismo efecto. Imaginemos ahora que el usuario ingresara el login «fulano’;--». La clave de este ataque es confundir a la base de datos para aceptar comandos generados por el usuario. El ataque completo se limita a cuatro caracteres: «‘;--». Al cerrar la comilla e indicar (con el punto y coma) que termina el comando, la base de datos entiende que la solicitud se da por terminada y lo que sigue es otro comando. La sentencia quedaría de la siguiente forma:
SELECT id FROM usuarios WHERE login = ‘fulano’;--’
AND PASSWD = ‘’
Podríamos enviarle más de un comando consecutivo que concluyera de forma coherente, pero lo más sencillo es utilizar el doble guión indicando que inicia un comentario. De este modo, logramos vulnerar la seguridad del sistema, entrando como un usuario cuyo login conocemos, aún desconociendo su contraseña.
Siguiendo con ejemplos similar y considerando que típicamente el ID del administrador de un sistema es el más bajo (ID=1), entonces imaginemos el resultado de los siguientes nombres de usuario falsos:
ninguno’ OR id = 1;--
‘; INSERT INTO usuarios (login, passwd) VALUES
(‘fulano’, ‘de tal’); --
‘; DROP TABLE usuarios; --
Podemos ver un ejemplo similar en la famosa tira cómica de Bobby Tables (http://imgs. xkcd.com/comics/exploits_of_a_mom.png). ¿Y qué podemos hacer? Protegerse de inyección de SQL es sencillo, pero hay que hacerlo en prácticamente todas nuestras consultas, y convertir nuestra manera natural de escribir código en una segura.
La regla de oro es nunca cruzar fronteras incorporando datos no confiables, y esto no sólo es muy sencillo sino que muchas veces (específicamente cuando iteramos sobre un conjunto de valores efectuando la misma
consulta para cada uno de ellos) hará los tiempos de respuesta de nuestro sistema sensiblemente mejores. La respuesta es separar la preparación de la ejecución de las consultas. Al preparar una consulta, nuestro motor de bases de datos la compila y prepara las estructuras necesarias para recibir los parámetros a través de «placeholders», marcadores que serán substituídos por los valores que indiquemos en una solicitud posterior.
Volvamos al ejemplo del login/contraseña:
$query = $db->prepare(‘SELECT id FROM usuarios
WHERE login = ? AND PASSWD = ?’);
$data = $query->execute($login, $passwd);
Los símbolos de interrogación son enviados como literales a nuestra base de datos, que
sabe ya qué le pediremos y prepara los índices para respondernos. Podemos enviar contenido arbitrario como login y password, ya sin preocuparnos de si el motor lo intentará interpretar.
Revisar todas las cadenas que enviamos a nuestra base de datos puede parecer una tarea tediosa, pero ante la facilidad de encontrar y explotar este tipo de vulnerabilidades, bien vale la pena. En las referencias a continuación podrán leer mucho más acerca de la anatomía de las inyecciones SQL, y diversas maneras de explotarlas incluso cuando existe cierto grado de validación.
Acerca del Autor
Gunnar Wolf es administrador de sistemas para el Instituto de Investigaciones Económicas de la UNAM; entusiasta y promotor del Software Libre, desarrollador del proyecto Debian GNU/Linux desde el 2003, miembro externo del Departamento de Seguridad en Cómputo de DGSCA-UNAM desde 1999.
atacante una amplia variedad de modalidades de intrusión. En general, podemos hablar de ellas como inyección de código interpretado y en esta ocasión hablaremos específicamente de inyección de SQL.
En el desarrollo de sistemas debemos partir siempre del principio de mínima confianza: No se debe confiar en ningún dato proveniente de fuera de nuestro sistema, independientemente de quién sea el usuario. Esto es especialmente importante cuando requerimos que un elemento cruce entre las principales barreras de las diversas capas de nuestro sistema.
Tomemos como primer ejemplo al sistema de gestión de contenido que usa SG. Si quisieran leer la edición anterior de esta columna, a la que hice referencia hace algunas líneas, pueden encontrarla en:
http://www.sg.com.mx/content/view/825
Todos hemos analizado URLs, y resultará obvio que «825» corresponda al ID de la nota en la base de datos, y que los componentes «content» y «view» indican la operación que el sistema debe realizar ante una solicitud. Ahora bien, ¿a qué me refiero con que cruzamos las barreras entre las capas?
Enfoquémonos en el ID. Al analizar el URL, el ID es un pedazo de texto (formalmente es una cadena que es recibida como parte del método GET, uno de los métodos definidos para el protocolo HTTP). El servidor web Apache que recibe mi solicitud interpreta este método GET y encuentra encuentra –utilizando mod_rewrite, indicado por el archivo htaccess– que el contenido indicado por la ruta /content/view/* debe ser procesado por el archivo index.php, que a su vez es manejado por el lenguaje PHP. El archivo index.php corresponde en este caso al sistema Joomla, que reconoce la ruta, convierte al ID en su representación numérica y lo utiliza para pedir a la base de datos le entregue los datos relacionados con determinado artículo. Entonces, aquí podemos reconocer los siguientes puntos principales de manipulación de la solicitud:
1. Apache recibe una solicitud HTTP, y la reescribe (via mod_rewrite),
indicando «content», «view» y «825» como parámetros a index.php.
2. PHP analiza, separa y estructura los parámetros recibidos para ser
utilizados por Joomla.
3. Joomla solicita el artículo 825 a la base de datos.
La variabilidad de los primeros pasos es en realidad menor, pero al solicitar a la base de datos el artículo «825» (y este es el caso más sencillo de todos) deben pasar muchas cosas. Primero que nada, «825»
es una cadena de caracteres. PHP es un lenguaje débilmente tipificado (los números se convierten en cadenas y viceversa automáticamente según sea requerido), pero una base de datos maneja tipos estrictamente. Como atacante, puedo buscar qué pasa si le pido al sistema algo que no se espere, por ejemplo «825aaa». En este caso (¡felicidades!), el código PHP que invoca a la base de datos sí verifica que el tipo de datos sea correcto: Hace una conversión a entero, y descarta lo que sobra. Sin embargo (y no doy URLs por razones obvias), en muchas ocasiones esto me llevaría a recibir un mensaje como el siguiente:
Warning: pg_execute() [ function.pg-execute]: Query failed: ERROR: invalid input syntax
for integer: “825aaa” in /home/(...)/index.php on line 192
Esto indicaría que uno de los parámetros fue pasado sin verificación de PHP al motor de base de datos, y fue éste el que reconoció al error. Ahora, esto no califica aún como inyección de SQL (dado que el motor
de bases de datos supo reaccionar ante esta situación), pero estamos prácticamente a las puertas. Podemos generalizar que cuando un desarrollador no validó la entrada en un punto, habrá muchos otros en que no lo haya hecho. Este error en particular nos indica que el código contiene alguna construcción parecida a la siguiente:
SELECT * FROM articulo WHERE id = $id_art
La vulnerabilidad aquí consiste en que el programador no tomó en cuenta que $id_art puede contener cualquier cosa enviada por el usuario. ¿Cómo puede un atacante aprovecharse de esto?
Presentaré a continuación algunos ejemplos, evitando enfocarme a ningún lenguaje en específico.
Lo importante es cómo tratamos al SQL generado. Para estos ejemplos, cambiemos un poco el caso de uso: En vez de ubicar recursos, hablemos acerca de una de las operaciones más comunes: La identificación de
un usuario vía login y contraseña. Supongamos que el mismo sistema del código recién mencionado utiliza la siguiente función para validar a sus usuarios:
$data = $db->fetch(“SELECT id FROM usuarios
WHERE login = ‘$login’ AND passwd = ‘$passwd’”);
if ($data) { $uid = $data[0];
} else { print “<h1>Usuario inválido!</h1>”;
}
Aquí pueden apreciar la práctica muy cómoda y común de “interpolar” variables dentro
de una cadena. Muchos lenguajes permiten construir cadenas donde se expande el contenido de determinadas variables. En caso de que su lenguaje favorito no maneje esta característica, concatenar las sub-cadenas y las variables nos lleva al mismo efecto. Imaginemos ahora que el usuario ingresara el login «fulano’;--». La clave de este ataque es confundir a la base de datos para aceptar comandos generados por el usuario. El ataque completo se limita a cuatro caracteres: «‘;--». Al cerrar la comilla e indicar (con el punto y coma) que termina el comando, la base de datos entiende que la solicitud se da por terminada y lo que sigue es otro comando. La sentencia quedaría de la siguiente forma:
SELECT id FROM usuarios WHERE login = ‘fulano’;--’
AND PASSWD = ‘’
Podríamos enviarle más de un comando consecutivo que concluyera de forma coherente, pero lo más sencillo es utilizar el doble guión indicando que inicia un comentario. De este modo, logramos vulnerar la seguridad del sistema, entrando como un usuario cuyo login conocemos, aún desconociendo su contraseña.
Siguiendo con ejemplos similar y considerando que típicamente el ID del administrador de un sistema es el más bajo (ID=1), entonces imaginemos el resultado de los siguientes nombres de usuario falsos:
ninguno’ OR id = 1;--
‘; INSERT INTO usuarios (login, passwd) VALUES
(‘fulano’, ‘de tal’); --
‘; DROP TABLE usuarios; --
Podemos ver un ejemplo similar en la famosa tira cómica de Bobby Tables (http://imgs. xkcd.com/comics/exploits_of_a_mom.png). ¿Y qué podemos hacer? Protegerse de inyección de SQL es sencillo, pero hay que hacerlo en prácticamente todas nuestras consultas, y convertir nuestra manera natural de escribir código en una segura.
La regla de oro es nunca cruzar fronteras incorporando datos no confiables, y esto no sólo es muy sencillo sino que muchas veces (específicamente cuando iteramos sobre un conjunto de valores efectuando la misma
consulta para cada uno de ellos) hará los tiempos de respuesta de nuestro sistema sensiblemente mejores. La respuesta es separar la preparación de la ejecución de las consultas. Al preparar una consulta, nuestro motor de bases de datos la compila y prepara las estructuras necesarias para recibir los parámetros a través de «placeholders», marcadores que serán substituídos por los valores que indiquemos en una solicitud posterior.
Volvamos al ejemplo del login/contraseña:
$query = $db->prepare(‘SELECT id FROM usuarios
WHERE login = ? AND PASSWD = ?’);
$data = $query->execute($login, $passwd);
Los símbolos de interrogación son enviados como literales a nuestra base de datos, que
sabe ya qué le pediremos y prepara los índices para respondernos. Podemos enviar contenido arbitrario como login y password, ya sin preocuparnos de si el motor lo intentará interpretar.
Revisar todas las cadenas que enviamos a nuestra base de datos puede parecer una tarea tediosa, pero ante la facilidad de encontrar y explotar este tipo de vulnerabilidades, bien vale la pena. En las referencias a continuación podrán leer mucho más acerca de la anatomía de las inyecciones SQL, y diversas maneras de explotarlas incluso cuando existe cierto grado de validación.
Acerca del Autor
Gunnar Wolf es administrador de sistemas para el Instituto de Investigaciones Económicas de la UNAM; entusiasta y promotor del Software Libre, desarrollador del proyecto Debian GNU/Linux desde el 2003, miembro externo del Departamento de Seguridad en Cómputo de DGSCA-UNAM desde 1999.
- Log in to post comments