Jueves, 5 de Junio de 2008 por climens

Protección de QueryString en ASP clásico contra inyección SQL

Hace un mes comenté que nos habían atacado por SQL, pero la cosa ha ido a más durante este tiempo y los ataques han proliferado y han ido variando ligeramente con el tiempo siendo en algunos casos menos destructivos pero más efectivos.
El problema son las aplicaciones legadas, en mi caso hechas en ASP clásico, en las que no hay ni tiempo ni ganas de revisar y cambiar todo lo que habría que cambiar para proteger adecuadamente estas aplicaciones (algunas datan del 2000).
A esto se une la propia inutilidad del IIS6 (por no hablar del IIS5 que también anda por ahí) para filtrar adecuadamente las QueryStrings contra ciertos patrones.
Para el ataque en cuestión, una primera aproximación es protección a través de TSQL, denegando el permiso de acceso a sysobjects y syscolumns al usuario que accede a la base de datos desde la aplicación ASP:

USE [database]
DENY SELECT ON ..sysobjects TO [login]
DENY SELECT ON ..syscolumns TO [login]

Esto puede ser suficiente para la oleada de ataques actuales pero no siempre es posible cambiar esos permisos ya que no se puede acceder con una cuenta con privilegios a la base de datos.
Entonces queda la protección por código, a través de algún archivo básico que se incluya en todas las páginas (si la aplicación es más o menos seria seguro que tiene alguno de estos). Casi seguro que al código hay acceso de algún modo para poder realizar actualizaciones. Si ni SQL, ni código, ni nada pues entonces solo hay que esperar que esté todo bien protegido de serie.
La protección por ASP es bastante simple y limitada pero sirve para el caso que nos ocupa como solución de emergencia sin tener que cambiar demasiadas cosas. Se basa en recoger los parámetros de la URL y buscar una serie de cadenas que seguramente aparecen en un ataque y improbablemente en una página bien formada, pero allá cada uno:

const ERR_BAD_QUERY = 1 ' El valor que se desee realmente

if request.servervariables("QUERY_STRING") <> "" then
    dim testQueryString, vDangerousStrings, dangerousString

    testQueryString = request.servervariables("QUERY_STRING")

    vDangerousStrings = split("CAST(,DECLARE%20,VARCHAR(,EXEC(",",")

    for each dangerousString in vDangerousStrings
        if instr(1, testQueryString, dangerousString, vbTextCompare) <> 0 then
            err.raise vbObjectError + ERR_BAD_QUERY, "AppName", "Los parámetros ("&dangerousString&") de la URL no son correctos: " & testQueryString
        end if
    next
end if

Con esto, cuando intenten atacar, fallará estrepitosamente y si están adecuadamente configurados los errores personalizados en el IIS para el error HTTP 500 aparecerá una bonita página al atacante. En mi caso, cada error manda un mail a una cuenta de monitorización.
Espero que a alguien le haya servido, aunque repito, no es una solución demasiado elegante ni infalibre. Además, no protege los parámetros pasados por POST, aunque de momento los ataques no están funcionando de ese modo, podrían hacerlo en algún momento.
Para implementar una solución más completa, estos consejos para la detección XSS y SQL Injection pueden ser bastante útiles.

Actualización 06/06/2008:
Después de hacer algunas pruebas de rendimiento, usar expresiones regulares no es realmente más lento que usar instr, así que allá va una versión mejorada:

const ERR_BAD_QUERY = 1 ' El valor que se desee realmente

if request.servervariables("QUERY_STRING") <> "" then
    dim re, testQueryString, vDangerousStrings, dangerousString

    set re = new regexp
    re.ignorecase = true

    testQueryString = request.servervariables("QUERY_STRING")

    vDangerousStrings = split("CAST(%20)*\(,DECLARE(%20)+,VARCHAR(%20)*\(,EXEC(%20)*\(",",")

    for each dangerousString in vDangerousStrings
        re.pattern = dangerousString

        if re.test(testQueryString) then
            set vMatches = re.execute(testQueryString)
            err.raise vbObjectError + ERR_BAD_QUERY, "AppName", "Los parámetros ("&vMatches(0)&") de la URL no son correctos: " & testQueryString
        end if
    next

    set re = nothing
end if

Con esto se flexibilizan las cadenas a buscar de modo que se adapten a variaciones del ataque que añadan espacios entre los comandos TSQL, que seguirían siendo perfectamente válidos para el intérprete de SQL Server.

Compartir | meneame | fresqui | del.icio.us | digg | technorati
Tags: , , , | Sin comentarios

Miércoles, 20 de Febrero de 2008 por climens

Wapiti, auditoría web sencilla

WapitiDe un tiempo a esta parte habían empezado a llegar a una cuenta de correo que tenemos configurada para recibir los informes de error de las distintas aplicaciones web una serie de errores bastante sospechosos. Eran todo URLs con cadenas de texto en parámetros numéricos que hacían fallar la página. Esto se producía evidentemente por nuestra pobre comprobación de los parámetros que producía que en algunos casos el valor numérico en cuestión llegase tal cual a una consulta en la base de datos, fallando estrepitosamente.

Lo curioso del caso es que solamente ocurría en algunas webs concretas y siguiendo siempre el mismo patrón: claramente usaban una herramienta que buscaba posibles vectores de inyección SQL o similares. Era un mecanismo bastante burdo pero seguro que en algún sitio algo encontraron. Además las IPs eran variadas así que posiblemente se debía a la aparición de alguna herramienta en algún foro de script kiddies.

Y me dije: yo quiero hacer lo mismo. Así que me puse a buscar y encontré algunas herramientas, siendo la más interesante Wapiti, un fuzzer sencillo hecho en Python con las siguientes funcionalidades:

  • Detección de errores en la gestión de ficheros (fopen, includes...)
  • Inyección SQL
  • Inyección XSS
  • Inyección LDAP
  • Ejecución de comandos (eval(), system()...)
  • Inyección CRLF (HTTP splitting)

Además muestra un mensaje de alerta cada vez que se produce un error 500, con lo que es ideal para analizar aplicaciones hechas en ASP (ehem).

Así pues hice un testado exhaustivo con esta herramienta a nuestro código base y la verdad que encontré cosas interesantes, como un error de lo más ingenuo que daba paso a un bonito ataque XSS o unas cuantas entradas sin proteger que proporcionaban posibles ataques SQL jugando con los parámetros. Otros errores no eran tan evidentes y se basaban en jugar con los datos enviados por POST, que por cierto para las pruebas encontré imprescindible la extensión Live HTTP Headers que permite ver el tráfico HTTP y luego repetir una llamada modificando las cabeceras (con calculadora automática del Content-Length, afortunadamente).

He de decir que esto evidentemente no es una herramienta de revisión de código pero si me tengo que poner a revisar toda nuestra base de código antiguo apaga y vámonos. Más vale un poco de protección que ninguna protección, por lo menos la valla un poco más alta que la del vecino.

Posiblemente aun queden muchos agujeros que tapar pero en general ya solemos usar protección para todos los parámetros que entran como comprobar que los números son valores numéricos y que las cadenas de texto son como deben ser y no tienen gato encerrado (ver el XSS FAQ para un método bastante competo de protección).

En resumen, que a lo largo de los años se ha descuidado muchas veces la seguridad de aplicaciones web (las prisas, la poca importancia atribuida, el desconocimiento, etc) y cada vez hay más y mejores técnicas para sacar provecho de nuestros fallos, por lo que cualquier comprobación no está de menos.

Actualización 08/03/2008: Kriptópolis se hace eco de un artículo interesante llamado "El mito de la seguridad web total". Interesante lectura.

Compartir | meneame | fresqui | del.icio.us | digg | technorati
Tags: , , , | 6 comentarios