‹ARES offensive division vulnerability research

Pesquisa de VulnerabilidadeVulnerability Research

Uma historinha sobre um XSS no chat.mozilla.org (CVE-2021-21320)A short story about an XSS in chat.mozilla.org (CVE-2021-21320)

A validação de origem do postMessage vinha de input do usuário, e o blob enviado caía direto no createObjectURL sem checar o content-type — o suficiente pra transformar um preview de upload em XSS.The postMessage origin check was driven by user input, and the blob it received went straight into createObjectURL with no content-type validation — enough to turn an upload preview into XSS.


CVE ID
CVE-2021-21320
Severity
Medium · CVSS 6.1
Affected
matrix-react-sdk ≤ v3.14.0 (chat.mozilla.org)
Vendor
Matrix.org / Mozilla
Disclosure
Coordenada · corrigida · $500 de bountyCoordinated · patched · $500 bounty
Researcher
Guilherme Keerok (@duph0use)

No mês passado, eu e uns amigos fundamos a @duph0use, uma casa onde a gente passou o último mês fazendo bug bounty, pesquisando e trabalhando. Em algum momento por lá, comecei a procurar bugs na Mozilla — o que me levou a achar 3 XSSes.Last month some friends and I founded @duph0use, a house where we spent the past month doing bug bounties, researching, and working. At some point while I was there I started hunting for bugs in Mozilla, which led me to find 3 XSSes.

Neste post vou mostrar só um desses achados.In this post I'll show only one of those findings.

ConteúdoContents

Achando o alvoFinding the target

Enquanto navegava pelos produtos da Mozilla, acabei caindo no chat.mozilla.org, que é um webchat mantido pelo Matrix.org.While I was poking around Mozilla's products, I ended up on chat.mozilla.org, a webchat maintained by Matrix.org.

Como esse chat também é open source, comecei olhando o código-fonte e descobri uma funcionalidade de upload de arquivos. Tentei várias coisas diferentes nela, mas não cheguei a lugar nenhum. Só que essa mesma funcionalidade tinha também um preview das imagens/arquivos depois do upload — e isso era interessante.Since the chat is also open source, I started by reading through the source and found a file upload feature. I tried a bunch of different things against it but got nowhere. However, the same feature also had a preview of the images/files after upload, which was interesting.

O preview num iframeThe preview inside an iframe

O preview dos arquivos era feito dentro de um iframe. No cenário comum, um usuário normal clica no botão de upload e sobe algum arquivo; esse arquivo vira um blob, e na hora do upload o conteúdo desse blob é enviado pro servidor.The file preview was rendered inside an iframe. In the common scenario, a regular user clicks the upload button and uploads some file; that file gets turned into a blob, and at upload time the content of that blob is sent to the server.

O que tornou esse ataque possível foi que a aplicação enviava um postMessage pro iframe pra criar o blob. E esse mesmo iframe era usado pra dar preview nas imagens.What made this attack possible is that the application sent a postMessage to the iframe to create the blob. And that same iframe was used to preview the images.

Como dá pra ver no código a seguir, a validação da origem do postMessage era feita a partir de input do usuário:As you can see in the code below, the postMessage origin validation was driven by user input:

src/usercontent/index.js#L1

const params = window.location.search.substring(1).split('&');
let lockOrigin;
for (let i = 0; i < params.length; ++i) {
    const parts = params[i].split('=');
    if (parts[0] === 'origin') lockOrigin = decodeURIComponent(parts[1]);
}

Em seguida, a próxima parte do código recebia o postMessage e mandava os dados dele pro remoteRender:Then the next part of the code received the postMessage and passed its data to remoteRender:

src/usercontent/index.js#L47

window.onmessage = function(e) {
    if (e.origin === lockOrigin) {
        if (e.data.blob) remoteRender(e);
        else remoteSetTint(e);
    }
};

O blob sem checagem de content-typeThe blob with no content-type check

Na linha 49, a mensagem pegava um blob enviado pelo usuário (e.data.blob) e, sem verificar o content-type, passava o conteúdo dele pro createObjectURL na linha 22:On line 49, the message took a blob sent by the user (e.data.blob) and, without verifying the content-type, passed its content into createObjectURL on line 22:

src/usercontent/index.js#L22

a.href = window.URL.createObjectURL(data.blob);

A única coisa um pouco chata nesse XSS é a necessidade de interação do usuário, causada pelo atributo download (linha 19). Mas é trivial:The only slightly annoying thing about this XSS is the user-interaction requirement, caused by the download attribute (line 19). But it's trivial:

src/usercontent/index.js#L19

 a.download = data.download;

Por causa disso, o usuário precisa clicar com o botão direito do mouse sobre a imagem e então abrir ela em outra aba. Pra resolver isso, criei uma imagem simples dizendo "abra esta imagem em uma nova aba" e passei essa imagem via postMessage no atributo imgSrc (linha 40).Because of that, the user has to right-click the image and then open it in another tab. To get them to do this, I created a simple image saying "open this image in a new tab" and passed it via postMessage in the imgSrc attribute (line 40).

src/usercontent/index.js#L40

img.src = data.imgSrc;

A PoC finalThe final PoC

Esta é a minha PoC final:This is my final PoC:

<html>
<head>
</head>
<body>
<center>Click anywhere in the page</center>
<script>
        let x;
        onclick = () => {
            x = open("https://chat.mozilla.org/usercontent/?origin=https%3a%2f%2f"+document.domain);
            setTimeout(() => {
                const payload = new Blob(["<script>alert(1337)<\/script>"], { type : "text/html" });
                x.postMessage({ blob: payload, imgSrc: "https://i.imgur.com/CMq55u9.png", auto: true}, "*");
            }, 2500);
        }
</script>
</body>
</html>
NotaNote

Repare no truque da origem: o iframe lê o origin da própria query string (?origin=https://…) e usa esse valor como lockOrigin. Como o atacante controla a URL que abre o iframe, ele simplesmente passa a própria origem ali — e a checagem e.origin === lockOrigin passa numa boa.Notice the origin trick: the iframe reads origin from its own query string (?origin=https://…) and uses that value as lockOrigin. Since the attacker controls the URL that opens the iframe, they just pass their own origin there — and the e.origin === lockOrigin check passes happily.

Aqui você pode ver o vídeo da PoC:Here you can watch the PoC video:

https://www.youtube.com/watch?v=nmio2rTn38Q

TimelineTimeline

ReferênciasReferences

  1. matrix-react-sdk — src/usercontent/index.js (v3.14.0).matrix-react-sdk — src/usercontent/index.js (v3.14.0). https://github.com/matrix-org/matrix-react-sdk/blob/v3.14.0/src/usercontent/index.js
  2. Vídeo da PoC no YouTube.PoC video on YouTube. https://www.youtube.com/watch?v=nmio2rTn38Q