Эксплуатация MIME Sniffing

В каждом запросе обычно есть заголовок ответа, называемый Content-Type, который сообщает браузеру MIME-тип ответа, такой как text/html или application/json.

Но что, если заголовка Content-Type нет? Браузер попытается определить тип на основе содержимого файла. Даже если заголовок Content-Type присутствует, браузер все равно может интерпретировать его как другой тип.

Это поведение, при котором MIME-тип выводится из содержимого файла, называется MIME sniffing. Давайте вместе исследуем эту функцию!

MIME Sniffing 101

Мы можем легко отправить ответ без заголовка Content-Type, используя Express:

const express = require('express');
const app = express();
app.get('/', (req, res) => {
  res.write('<h1>hello</h1>')
  res.end()
});
app.listen(5555, () => {
  console.log('Server is running on port 5555');
});

Если вы откроете эту веб-страницу в браузере, вы увидите, что текст "hello" становится больше и жирным, что указывает на то, что браузер отображает ответ как веб-страницу:

Теперь давайте посмотрим на второй пример, где мы изменяем <h1> на <h2>:

Почему это вдруг отображается как простой текст?

Теперь в третьем примере у нас есть тот же <h1>, но с дополнительным текстом:

Снова он отображается как простой текст вместо HTML.

Вы можете подумать, что механизм MIME sniffing в браузере — это загадка, как черный ящик, о том, как он работает. К счастью, мы тестируем в Chrome, и Chromium имеет открытый исходный код.

Код, отвечающий за MIME sniffing в Chromium, можно найти в файле net/base/mime_sniffer.cc. В начале кода объясняется, как это работает:

Итак, что заставляет его считать полезную нагрузку "HTML payload"? Это проверяется дальше в коде:

Он проверяет, соответствует ли строка в начале ответа, после удаления пробелов, перечисленным выше шаблонам HTML. Общие начала HTML, такие как <!DOCTYPE html и <html, включены в этот список. Это также объясняет, почему только пример <h1>hello</h1> отображается как HTML в наших предыдущих тестах.

Согласно исходному коду, похоже, что Chromium не учитывает расширение файла или другие факторы в URL. Он полностью полагается на содержимое файла. Давайте проведем еще один тест, чтобы это подтвердить:

Я не буду включать изображение здесь, но независимо от того, что URL — это test.html, результат все равно отображается как простой текст. А как насчет других браузеров? Давайте попробуем открыть его в Firefox:

Мы можем увидеть, что Firefox действительно отображает его как HTML! Следовательно, мы можем сделать вывод, что Firefox учитывает расширение файла в URL при выполнении MIME sniffing.

Exploiting MIME Sniffing for Attacks

Из нашего предыдущего исследования мы подтвердили факт: если у ответа нет установленного Content-Type и мы можем контролировать содержимое, мы можем использовать MIME sniffing, чтобы заставить браузер интерпретировать файл как веб-страницу.

Например, предположим, что есть функция загрузки изображений, которая проверяет только расширение файла, но не содержимое. Мы можем загрузить файл с именем a.png, но содержимое будет <script>alert(1)</script>. Если сервер не добавляет автоматически Content-Type при обслуживании этого изображения, это становится уязвимостью XSS.

Однако большинство серверов в настоящее время автоматически добавляют Content-Type. Возможно ли это все еще?

Да, мы можем объединить это с другими незначительными проблемами.

Apache HTTP Server — это широко используемый сервер, и знаменитый стек LAMP (Linux + Apache + MySQL + PHP) использует этот сервер.

Apache HTTP Server имеет своеобразное поведение: если имя файла содержит только точку (.), он не выведет Content-Type. Например, a.png автоматически определит MIME-тип на основе расширения файла и выведет image/png, но если имя файла будет ..png, он не выведет Content-Type.

Таким образом, если бэкенд использует Apache HTTP Server для обработки загрузок файлов, мы можем загрузить, казалось бы, валидное изображение с расширением ..png, но при открытии в браузере оно будет отображаться как веб-страница, становясь уязвимостью XSS.

Согласно Apache HTTP Server, это ожидаемое поведение, и вы можете обратиться к @YNizry для получения дополнительных деталей.

Content Types that can execute JavaScript

Кроме HTML-файлов, какие еще типы файлов могут выполнять JavaScript?

В исследовании, проведенном BlackFan в 2020 году: Content-Type Research предоставлен исчерпывающий список: Content-Type that can be used for XSS.

Из списка мы видим, что кроме самых распространенных типов контента HTML, XML и SVG есть и другие, которые могут выполнять JavaScript.

Особый интерес представляют файлы SVG, потому что многие веб-сайты имеют функциональность загрузки изображений, и SVG считается форматом изображения. Поэтому некоторые веб-сайты разрешают загрузку SVG. Однако из этого исследования мы можем узнать, что разрешение загрузки SVG эквивалентно разрешению загрузки HTML, поскольку SVG может выполнять JavaScript!

Например, febin сообщил о уязвимости в программном обеспечении с открытым исходным кодом Mantis Bug Tracker в 2022 году: CVE-2022-33910: Stored XSS via SVG file upload. Эта уязвимость возникла потому, что пользователи могли загружать файлы при создании нового репорта, и разрешенный формат файла был SVG. Поэтому файл SVG мог быть загружен, и когда другие пользователи открывали его, скрытый код внутри выполнялся.

Content Types that can be loaded as scripts

Возьмем следующий фрагмент кода в качестве примера:

Вы когда-нибудь задумывались, какой должен быть тип контента URL, чтобы браузер загрузил его как скрипт?

Например, если это image/png, это не сработает, и вы увидите следующее сообщение об ошибке в браузере:

Refused to execute script from 'http://localhost:5555/js' because its MIME type ('image/png') is not executable.

Наиболее распространенный тип, text/javascript, очевидно, работает нормально. Но есть ли другие?

Из десяти перечисленных ниже типов контента только два из них не сработают. Можете угадать, какие два?

  1. application/zip

  2. application/json

  3. application/octet-stream

  4. text/csv

  5. text/html

  6. text/json

  7. text/plain

  8. huli/blog

  9. video/mp4

  10. font/woff2

Ответ будет раскрыт позже, но сначала давайте рассмотрим "действительные MIME-типы JavaScript", указанные в исходном коде Chromium: /third_party/blink/common/mime_util/mime_util.cc:

Все вышеперечисленные — это действительные MIME-типы JavaScript, и вы можете увидеть множество типов из прошлого, таких как jscript или livescript.

Кроме действительных MIME-типов JavaScript, согласно спецификации, есть только четыре типа, которые не сработают:

  1. audio/*

  2. image/*

  3. video/*

  4. text/csv

Кроме этих, все остальные типы действительны. Таким образом, среди упомянутых выше вариантов только text/csv и video/mp4 не сработают, в то время как остальные будут работать! Да, даже text/html и application/json будут работать, и даже xss/blog будет работать.

Если вы хотите ужесточить этот механизм и разрешить загрузку только MIME-типов JavaScript, вы можете добавить заголовок в ответ: X-Content-Type-Options: nosniff. После добавления этого заголовка ни один из 10 ранее упомянутых примеров не будет работать, и вы увидите следующее сообщение об ошибке при загрузке:

Refused to execute script from 'http://localhost:5555/js' because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled.

strict MIME type это функция, включаемая добавлением этого заголовка.

То же самое касается таблиц стилей. После включения этой функции только MIME-тип text/css будет признан действительным, и все остальные приведут к ошибке.

Итак, что произойдет, если вы продолжите включать эту функцию? Это создаст дополнительный риск безопасности.

Предположим, вы случайно нашли уязвимость XSS на веб-сайте. Однако проблема в том, что CSP веб-сайта — это script-src 'self';, что означает, что он не позволяет загружать внешние скрипты, и встроенные скрипты также заблокированы. Как вы можете обойти CSP в этом случае?

Если веб-сайт предоставляет функцию загрузки файлов, которая принимает файлы, отличные от изображений, видео и CSV-файлов, и не проверяет содержимое, предположим, что он принимает только ZIP-файлы, тогда вы можете загрузить файл, содержащий код JavaScript.

Таким образом, вы можете использовать <script src="/uploads/files/my.zip"></script>, чтобы загрузить скрипт и успешно обойти CSP. Причина, по которой это работает, заключается в поведении, упомянутом ранее — пока MIME-тип не является одним из тех немногих типов, его можно загрузить как скрипт.

Вот почему вы видите, что многие веб-сайты добавляют этот заголовок, чтобы предотвратить такое поведение.

Заключение

В этой статье мы рассмотрели множество интересных аспектов, связанных с MIME-типами, а также изучили много исходного кода Chromium. Лично я считаю, что исходный код Chromium написан очень понятно, включает комментарии и ссылки на спецификации, поэтому вам не нужно отдельно искать спецификации. Это как убить двух зайцев одним выстрелом. Наконец, мы поняли цель и функцию заголовка X-Content-Type-Options: nosniff. Я уверен, что многие люди видели этот заголовок раньше, но не знали, для чего он нужен, и теперь вы это знаете.

Дополнительные ссылки:

Last updated