Volver al blog

Síguenos y suscríbete

Cómo construir el sistema de test WAF

Christian Peron

Security Researcher

Para ayudar a nuestros clientes a proteger la seguridad de sus sitios y aplicaciones, sin dejar de ofrecer a sus usuarios experiencias fiables online, hemos creado un firewall de aplicaciones web (WAF) integral y muy fácil de configurar. En nuestra última entrada, explicamos parte de la tecnología en la que se basa nuestro WAF, cómo elegimos los conjuntos de reglas y cómo pusimos en práctica nuestros hallazgos. Para ofrecer una solución integral que proteja realmente tu infraestructura es esencial someterla continuamente a pruebas. La tecnología y las amenazas evolucionan constantemente, así que también deben hacerlo los conjuntos de reglas para garantizar una visibilidad y una mitigación adecuadas de los métodos de ataque que van surgiendo.

En esta entrada, explicaremos cómo garantizamos la calidad en la implementación de WAF a nuestros clientes, sometiéndola continuamente a prueba utilizando nuestro testing framework o marco para pruebas WAF (FTW, por sus siglas en inglés), y profundizaremos en los hallazgos y contribuciones realizados en la comunidad OWASP CRS con FTW.

Objetivos de diseño de Fastly

Para nosotros, construir un sistema de seguridad de calidad a prueba de retos es fundamental porque nuestro objetivo es garantizar la precisión de nuestras contramedidas basadas en WAF. La implementación WAF de Fastly se distingue por estar completamente integrada en nuestra plataforma de almacenamiento de caché Varnish, que optamos por crear por varias razones:

  • Integración: aprovechar nuestra plataforma de gestión de configuración existente

  • Flexibilidad: compatibilidad con la identificación de ataques web para múltiples protocolos

  • Simplicidad: ofrecer menos puntos de fallo

  • Rendimiento: integración directa en los nodos de caché

La integración con nuestra plataforma existente permite realizar cambios de configuración y visibilidad en tiempo real en el WAF de Fastly. Nuestro WAF está diseñado para adaptarse fácilmente según el cliente o las configuraciones de instancias WAF, como los umbrales de anomalía, lo cual es primordial, puesto que la susceptibilidad al riesgo varía de un cliente a otro.

Este enfoque nos aporta también flexibilidad para identificar y mitigar amenazas en cualquier protocolo compatible con nuestra plataforma, incluidos HTTP/HTTP2/TLS (tanto IPv4 como IPv6).

Reducir la complejidad en el entorno de cliente reduce los "nudos en el cable"; ya hay suficientes sistemas intermediarios, CDN, balanceadores de carga, etc. No queremos introducir uno más desviando el tráfico a través de un mecanismo WAF aparte.

Fuentes de las reglas WAF de calidad

Un WAF sin reglas de alta calidad no resulta demasiado útil. Por eso, actualmente Fastly dispone de tres fuentes de reglas:

  • Conjunto de reglas principales (CRS) de OWASP

  • Reglas internas de Fastly

  • Proveedores privados

Mantenemos nuestras propias reglas de identificación de ataques para vulnerabilidades críticas que nos consta que se están aprovechando en la realidad. No hay incremento del umbral de anomalía en este caso: si las solicitudes coinciden, se rechazan. El WAF de Fastly también incorpora reglas que escribe y mantiene nuestro socio Trustwave.

Diseño de la cadena de herramientas para la integración de las reglas ModSecurity

En nuestra entrada anterior, tratamos el diseño y la implementación del WAF de Fastly y hablamos de cómo seleccionamos e integramos las CRS de OWASP como un componente esencial del servicio. Hemos integrado directamente en nuestra plataforma de edge cloud la funcionalidad que permite la compatibilidad con estas reglas.

Hemos tenido que crear un analizador de reglas ModSecurity y una cadena de herramientas de generación VCL para traducir la regla ModSecurity (formato SecRule) al bloque de código VCL que asigna las funciones de transformación ModSecurity a nuestros equivalentes de tiempo de ejecución Varnish. Analicemos la siguiente regla CRS de OWASP, que comprueba si el sitio utiliza UTF-8 y pide validación de los datos cifrados:

#
# Check UTF encoding
# We only want to apply this check if UTF-8 encoding is actually used by the site, otherwise
# it will result in false positives.
#
# -=[ Rule Logic ]=-
# This chained rule first checks to see if the admin has set the TX:CRS_VALIDATE_UTF8_ENCODING
# variable in the crs-setup.conf file.
#
SecRule TX:CRS_VALIDATE_UTF8_ENCODING "@eq 1" \
  "phase:request,\
   rev:'2',\
   ver:'OWASP_CRS/3.0.0',\
   maturity:'6',\
   accuracy:'8',\
   t:none,\
   msg:'UTF8 Encoding Abuse Attack Attempt',\
   id:920250,\
   tag:'application-multi',\
   tag:'language-multi',\
   tag:'platform-multi',\
   tag:'attack-protocol',\
   tag:'OWASP_CRS/PROTOCOL_VIOLATION/EVASION',\
   severity:'WARNING',\
   chain"
   SecRule REQUEST_FILENAME|ARGS|ARGS_NAMES "@validateUtf8Encoding" \
     "setvar:'tx.msg=%{rule.msg}',\
      setvar:tx.anomaly_score=+%{tx.warning_anomaly_score},\
    setvar:tx.%{rule.id}-OWASP_CRS/PROTOCOL_VIOLATION/EVASION-%{matched_var_name}=%{matched_var}"

Nota: Hemos simplificado este fragmento de VCL de conversión posterior para mostrar las funciones pertinentes (p. ej., el uso de variables para contar puntuaciones de anomalía y establecer umbrales). El VCL real contiene muchos más datos que no se muestran; este VCL, tal cual, no funcionaría.

[..]
  if (!waf.validateUtf8Encoding(arg_key) ||
      !waf.validateUtf8Encoding(arg_val)) {
    set waf.anomaly_score += ...;
    set waf.rule_id = 920250;
    [..]
    set waf.severity = 4;
  }
  return (...);
}
[..]    

Para garantizar la compatibilidad con el conjunto de reglas (CRS) de OWASP y ModSecurity en general tuvimos que tener en cuenta diferentes aspectos a la hora de implementar este código:

  • Transformaciones: ModSecurity ofrece una serie de transformaciones que permiten a los escritores de reglas normalizar los datos de peticiones, lo que reduce el número de variantes de regla. Por ejemplo, puedes utilizar t:urlDecodeUni para descodificar URL sin necesidad de usar una regla para las formas de URL codificadas y descodificadas. Echa un vistazo aquí para obtener más información sobre las transformaciones ModSecurity.

  • Colecciones y variables: ModSecurity representa ciertos componentes de la petición HTTP como colecciones iterables (básicamente una lista). Por ejemplo, en una petición puede haber varios encabezados. Para cada encabezado de la petición existe un par clave-valor y la lista de pares clave-valor representa una colección. ModSecurity itera cada elemento de la colección y realiza las comparaciones.

  • Cadena de reglas: ModSecurity permite encadenar reglas de manera eficaz, de modo que los escritores de reglas puedan implementar operaciones "AND" lógicas para crear reglas más complejas.

Había una serie de partes móviles que debíamos tener en cuenta para el diseño de la implementación de nuestro WAF y teníamos que asegurarnos también de que el código escrito para traducir las reglas formateadas SecRule a VCL funcionaba correctamente. De lo contrario, corríamos el riesgo de introducir vulnerabilidades relacionadas con la evasión WAF en la implementación.

Por ejemplo, una transformación no idéntica en la implementación podía resultar en que los datos transformados fueran diferentes y, por tanto, no coincidieran con el patrón específico. Era evidente que necesitábamos un marco de pruebas: algo que estuviera en continua ejecución mientras actualizábamos las reglas, hacíamos cambios en la cadena de herramientas, etc. También era necesario que las operaciones de seguridad e ingeniería pudieran escribir código y reglas rápidamente a la vez que gestionaban el riesgo asociado con regresiones y errores de programación no deseados.

Marco de pruebas de WAF (FTW)

Zack Allen y Chaim Sanders (encargados del mantenimiento de la organización GitHub CRS-support) empezaron a trabajar en el marco de pruebas en colaboración con el equipo de Fastly. Este marco está escrito en Python y puede utilizarse como un módulo de Python integrado en el código propio de cada uno. También puede utilizarse de forma independiente, con el marco de pruebas py.test de Python.

En su núcleo, el FTW carga una especificación YAML de una solicitud HTTP (que contiene cargas útiles de ataque u otros elementos de ataques a aplicaciones web) y la traduce a una solicitud HTTP. Podemos crear baterías de pruebas completas para asegurarnos de que las reglas detectan los ataques.

Aunque existen muchas herramientas que nos permiten construir solicitudes, tener que compilarlas en archivos por lotes y analizar manualmente las solicitudes no es una solución demasiado elegante. Además, muchas de estas herramientas no nos permiten construir solicitudes no conformes, necesarias para probar ciertas reglas como los ataques basados en protocolos.

Para controlar los datos asociados a las solicitudes, necesitábamos un testing framework que nos permitiera un control granular sobre las propias solicitudes. La flexibilidad para interpretar los aciertos y los fallos de forma explícita (lo que nos permitía evitar el uso de herramientas externas al testing framework para procesar los datos de respuesta) era también primordial.

Estas son las descripciones simplificadas de algunos de los campos YAML utilizados en un test WAF:

  • Meta: los metadatos relacionados con el test, como el autor, la descripción de la prueba, un indicador de si la regla está habilitada, etc.

  • Test: cada prueba tiene un título y descripciones opcionales seguidas de una o más fases. Esta regla tiene una sola fase, pero se pueden definir más si queremos mover la aplicación a un estado determinado antes de la entrega de la carga útil.

  • Input: donde se definen la mayoría de las cargas útiles de ataque, en forma de URI, encabezados o contenido del cuerpo del mensaje POST.

  • Output: se usa para comprobar si se ha detectado una carga útil de ataque en particular. En este ejemplo, comprobamos si en un archivo de registro aparece el patrón con el id "942120".

Esta es una prueba FTW de ejemplo extraída del repositorio de regresiones de CRS de OWASP:

---
  meta:
    author: "Christian S.J. Peron"
    description: None
    enabled: true
    name: 942120.yaml
  tests:
  -
    test_title: 942120-1
    desc: "Injection of a SQL operator"
    stages:
    -
      stage:
        input:
          dest_addr: 127.0.0.1
          headers:
            Host: localhost
          method: GET
          port: 80
          uri: "/?var=blahblah&var2=LIKE%20NULL"
          version: HTTP/1.0
        output:
          log_contains: id "942120"

Nota: Hay otras especificaciones de output, por ejemplo, si el servidor HTTP devuelve el código de error 400, se puede hacer que el test ordene al marco que valide el código de error devuelto por el daemon web.

Es muy útil en el modo de bloqueo de WAF, cuando queremos confirmar que la carga útil de un ataque específico se ha identificado y se ha rechazado. Cuando utilizamos la configuración de pruebas en nuestro sistema de pruebas, se crea una solicitud en el cable con este aspecto:

20:36:56.956718 IP localhost.56762 > localhost.http: Flags [P.], seq 1:104, ack 1, win 342, options [nop,nop,TS val 1066948 ecr 1066948], length 103
E.....@.@.*............Pj.Vmg......V.......
..G...G.GET /?var=blahblah&var2=LIKE%20NULL HTTP/1.0
Host: localhost
X-Request-Id: 1503088616.96_942120-1          

Punto de partida

Dado que el CRS de OWASP se diseñó para ejecutarse en la implementación del WAF ModSecurity, decidimos utilizar ModSecurity como base para nuestra comparación. Las fuentes de nuestras pruebas relacionadas con CRS están en el proyecto OWASP-CRS regressions.

El CRS de OWASP se diseñó principalmente para funcionar con Apache y ModSecurity, aunque hemos trabajado para incluir la compatibilidad con otros servidores HTTP como Nginx. En todos los casos se trata de implementaciones completas de servidor HTTP, que realiza una función diferente a Varnish, el acelerador HTTP sobre el que está construida Fastly.

Como se trata de servidores HTTP, funcionan de forma distinta a Varnish. Por ejemplo, las peticiones con encabezados Host: no válidos no se procesarían en el WAF de Fastly, puesto que el encabezado Host enruta la petición al servicio pertinente (que puede no tener activado el WAF). Por tanto, denegar un trabajo de CI para reglas diseñadas para detectar encabezados Host mal formados o no válidos no sería correcto en nuestro contexto.

En otros casos, las pruebas de regresión que se envían con el CRS pueden incluir pruebas diseñadas para variantes más antiguas de la regla, y los cambios en las reglas podrían producir el fallo de estas pruebas. Por tanto, necesitábamos una manera de crear una línea base que representara qué pruebas se esperaba que fallasen y cuáles no.

Creamos un archivo Chef cookbook/Vagrant para automatizar el aprovisionamiento y la configuración de Apache, ModSecurity y la versión del CRS de OWASP 3.0.2:

#
# apache2+mod_security vagrant box
#
Vagrant.configure(2) do |config|
  config.ssh.forward_agent = true
  config.vm.define 'modsec0' do |modsec_conf|
    modsec_conf.vm.box = 'ubuntu/trusty64'
    modsec_conf.berkshelf.enabled = true
    modsec_conf.berkshelf.berksfile_path = './Berksfile'
    modsec_conf.vm.network 'private_network', ip: '192.168.50.75'
    modsec_conf.vm.provider 'virtualbox' do |v|
      v.memory = 512
      v.cpus = 2
    end
    modsec_conf.vm.provision :chef_solo do |chef|
      chef.add_recipe('waf_testbed::default')
    end
  end
end

Puedes encontrar la guía paso a paso waf_testbed de Fastly en GitHub aquí.

Diseño de la herramienta de ejecución de pruebas con FTW

Para garantizar la calidad de la traducción de reglas, necesitábamos pruebas para cada regla y, en algunos casos, múltiples comprobaciones para verificar que las reglas con varias colecciones se habían traducido a VCL correctamente. Se nos presentaba, asimismo, otro reto: teníamos que asegurarnos de que la regla que deseábamos probar era la regla que se atascaba en el WAF; comprobar simplemente los códigos de error HTTP no era suficiente, ya que la solicitud podía haber sido rechazada por una regla no relevante para el test WAF.

Por ejemplo, la prueba de inyección de objetos PHP desencadena múltiples reglas, pero teníamos que estar seguros de que la regla escrita para identificar la inyección de objetos detectaba la carga útil. Fastly utiliza X-Request-Id, una marca de tiempo junto con la regla (e ID de la prueba) que desencadenará la carga útil. Mantenemos el seguimiento de una serie de atributos para asegurarnos de poder vincular una instancia de prueba con una entrada del registro de WAF específica:

  • Marca de tiempo del momento en que se envió una petición

  • ID de la regla que estamos probando

  • ID de la prueba dentro de la regla que hemos utilizado

  • Si la prueba ha fallado o no (por ejemplo, ¿hemos visto un restablecimiento de TCP?)

  • Código de respuesta HTTP

Utilizamos el marco FTW como un módulo para nuestra herramienta de ejecución de pruebas, pero primero era necesario definir una función que se cargara en las configuraciones YAML utilizando el código extraído de algunas de las funciones de utilidad FTW. El primer argumento de la función get_rulesets (tomado de un ejemplo de FTW) es la ruta/directorio que contiene los archivos YAML y el segundo, una marca que indica si debemos ejecutar recursivamente o no (muy útil si tienes directorios anidados que también contengan pruebas). Por último, es necesario devolver una lista de objetos ruleset.Ruleset, que es lo que el ejecutor de pruebas FTW necesita para realizar las pruebas:

def get_rulesets(ruledir, recurse):
    """
    List of ruleset objects extracted from the yaml directory
    """
    yaml_files = []
    if os.path.isdir(ruledir) and recurse != 0:
        for root, dirs, files in os.walk(ruledir):
            for name in files:
                filename, file_extension = os.path.splitext(name)
                if file_extension == '.yaml':
                    yaml_files.append(os.path.join(root, name))
    if os.path.isdir(ruledir):
        yaml_files = util.get_files(ruledir, 'yaml')
    elif os.path.isfile(ruledir):
        yaml_files = [ruledir]
    extracted_files = util.extract_yaml(yaml_files)
    rulesets = []
    for extracted_yaml in extracted_files:
        rulesets.append(ruleset.Ruleset(extracted_yaml))
    return rulesets

Aunque no se han incluido en estos snippets, el sistema de pruebas de Fastly incluye unas cuantas operaciones adicionales que se producen durante la ejecución en CI:

  • Un modo de controlar el alcance de las pruebas muy granular, representado como un archivo de configuración.

  • Un mecanismo que identifique las pruebas que se espera que van a fallar y que evite que las pruebas de FTW fallidas activen errores de CI irrecuperables.

El siguiente snippet explica cómo ejecutar los test una vez que disponemos de los objetos de conjunto de reglas:

testfiles = get_rulesets(co.rule_path, co.recurse)
for tf in testfiles:
    for test in tf.tests:
        ruleid = test.test_title.split("-")[0]
        now = time.time()
        # Get some unique tag to associated with the test
        # We use seconds since UNIX epoch, rule ID and test ID
        # For example: 1503088616.96_942120-1
        logtag = get_some_tag(now, test.test_title)
        runner = testrunner.TestRunner()
        for stage in test.stages:
            odict = stage.output.output_dict
            headers = stage.input.headers
            if not "X-Request-Id" in headers.keys():
                stage.input.headers["X-Request-Id"] = logtag
            try:
                hua = http.HttpUA()
            except:
                print "failed to initialize UA object"
                sys.exit(1)
            try:
                runner.run_stage(stage, None, hua)
            except Exception as e:
                # handle exception(s)   

Pruebas continuas

Administramos todas las reglas utilizadas por la implementación de WAF de Fastly con el sistema de control de revisión git (en GitHub), y hemos establecido la comprobación continua de cualquier cambio en nuestro conjunto de reglas. Como subproducto de la configuración de nuestro trabajo de CI, también podemos identificar cualquier regresión en la cadena de herramientas o en el código WAF dentro de los propios motores de almacenamiento de caché de Varnish. Un ingeniero crea una rama del repositorio de reglas y hace un cambio, como solucionar una vulnerabilidad de evasión, una optimización del rendimiento, etc. El ingeniero crea una pull request, a partir de la cual todo se automatiza.

El proceso es el siguiente:

  1. Se desencadenan los trabajos de CI.

  2. Se lanza el contenedor.

  3. El motor de almacenamiento en caché se aprovisiona en el contenedor.

  4. Se comprueba la cadena de herramientas VCL.

  5. Se extraen las expresiones regulares de los conjuntos de reglas y se comprueba si hay condiciones de denegación de servicio con expresiones regulares (reDoS).

  6. Se aprovisiona un servidor "de origen" (básicamente un respondedor HTTP inútil que responde incondicionalmente con 200 y una configuración de control de caché para Varnish, de manera que Varnish no almacena nada en caché) y se configura la integración de registro local.

  7. Los conjuntos de reglas se convierten a VCL y, a efectos de nuestro trabajo de CI, se aumentan con algunas configuraciones de origen y registro. Además, Varnish se configura para registrar el encabezado X-Request-ID junto con los datos de WAF.

  8. El VCL de WAF se compila y se carga en Varnish.

  9. Se lanza el código de CI de WAF de Fastly y se ejecuta en nuestro conjunto de pruebas (incluidas las pruebas configuradas en las regresiones de CRS de OWASP).

  10. Los registros de Varnish se contrastan con el diario de CI de Fastly para determinar qué reglas han detectado correctamente la carga útil y cuáles no.

Hallazgos y contribuciones

Como resultado de este trabajo hemos encontrado varios errores de programación que hemos comunicado a la comunidad. La identificación de los errores de programación propiamente dicha es, por lo general, una combinación de los fallos en las pruebas del FTW y la investigación realizada por los equipos de seguridad e ingeniería de Fastly, que utilizan el FTW para las siguientes operaciones:

  • Identificación de errores y validación de correcciones en las propias reglas, que ya hemos comunicado.

  • Detección de errores en nuestra cadena de herramientas VCL.

  • Identificación de discrepancias entre las operaciones de transformación de ModSecurity y de WAF de Fastly.

  • Verificación y demostración de vulnerabilidades específicas y parches para corregir vulnerabilidades.

Hallazgos y contribuciones en relación con las reglas de OWASP

Es primordial que las reglas funcionen según su diseño: trabajamos en la comunidad de regresiones de CRS/CRS de OWASP para garantizar que los problemas que identificamos se comunican a los proyectos y se corrigen. La mayoría de los problemas que encontramos pueden conducir a evasiones, mientras que otros pueden provocar falsos negativos/positivos en los análisis.

Este es el resumen de los problemas que nos encontramos:

  • Falta de transformaciones. Hemos observado fallos en las pruebas de una serie de reglas. Algunos autores desarrollaron reglas asumiendo la ejecución previa de determinadas operaciones de descodificación. Por ejemplo, Apache y Nginx realizan automáticamente operaciones de descodificación antes de entrar en el código ModSecurity. Por ese motivo, en algunos casos, las reglas no especificaban ciertas operaciones de descodificación, pues se asumía que ya se habían realizado de antemano. En otros casos, los datos se descodificaban por duplicado, porque la regla especificaba la operación de descodificación, pero los datos ya se habían descodificado en una plataforma subyacente. Para seguir el debate y los cambios: PR 578 y PR 590.

  • Transformaciones que se producen en reglas incorrectas dentro de una cadena. Se ejecutaba una operación tolower en una colección o variable utilizada por una segunda regla de la cadena, pero la segunda regla de la cadena era la que hacía la comparación con el patrón, por lo que no ejecutaba la operación tolower. PR 804

  • Reglas que no distinguen entre mayúsculas y minúsculas. Hay una regla que busca http:// pero no encuentra coincidencias con HTTP:// en ataques de inclusión de archivos remotos (RFI). PR 726

  • Omisión de reglas debido a la falta de esquemas de URL. Por ejemplo, especificamos ftp:// y http:// pero no especificamos file:// cuando sí deberíamos haberlo hecho. PR 726

  • Falta de los atributos logdata y msg. Esto dificulta mucho la detección de falsos positivos, cuya mayoría se manifestó como pruebas de FTW fallidas. PR 798

  • Vulnerabilidad de fijación de sesión no detectada por la comunidad. Concretamente, se detectó el uso de operadores incorrectos en conjuntos de reglas encadenadas que creaba una vulnerabilidad de evasión. PR 480

Gracias al FTW detectamos más incidencias en nuestra cadena de herramientas que generalizamos así:

  • Clasificación o reordenación incorrectas de las especificaciones de transformación de una regla.

  • Errores por los que se estaban aplicando incorrectamente las transformaciones en una cadena de reglas.

  • Reordenación de reglas de tal modo que el gráfico de ejecución era distinto al especificado en el conjunto de reglas, p. ej., en las reglas de salto. Para saber más, echa un vistazo a la documentación de la acción SkipAfter.

  • Análisis léxico de las reglas: patrones incompletos o incorrectos, de modo que solo ciertas partes de los patrones o de los encabezados se incluían en la versión VCL de la regla.

Con vistas al futuro

Este artículo no habría sido posible sin las contribuciones de nuestros compañeros Eric Hodel y Federico Schwindt, que identificaron muchas de las evasiones de WAF descritas aquí (consulta los PR para obtener más información). El repositorio de FTW de Fastly se encuentra aquí: https://github.com/fastly/ftw. No dudes en echar un vistazo al código y enviarnos los PR que consideres oportunos. Como contribuidor comercial de CRS, esperamos poder colaborar en el futuro con la organización CRS-support en GitHub, que dispone de una versión mantenida por la comunidad de las utilidades relacionadas con CRS y FTW.

A medida que desarrollamos nuestra batería de test WAF y adoptamos nuevas versiones de CRS, vamos descubriendo nuevos errores de programación e incidencias en las reglas; se trata simplemente de una consecuencia (bienvenida) de nuestro proceso continuo de pruebas. La versión actual de CRS es 3.0.2, sin embargo, seguiremos estudiando las futuras versiones del conjunto de reglas, sometiéndolas a nuestra batería de pruebas y encontrando los eventuales problemas que comunicaremos a la cadena en nuestro continuo esfuerzo por construir herramientas y métodos de investigación que evadan nuestro WAF y, en definitiva, por ofrecer una plataforma más segura a nuestros clientes. No te lo pierdas. En nuestra siguiente entrada analizaremos una herramienta que hemos desarrollado que muta las cargas útiles de ataque para evadir los controles de WAF.