Глава 4. DOM для хакеров

Где моё окно?

В этой главе, чтобы получше разобраться в DOM и, надеюсь, обучить вас новым приёмам, мы намерены взломать его. Во- первых, цель данного раздела получить через DOM объект window. Зачем нам window? Это окно, будучи глобальным объектом, очень важно для покидания песочницы JavaScript, поскольку оно позволяет вам получать доступ к глобально определённым функциям, таким как eval, которые делают для вас возможным исполнять произвольный JavaScript, который впоследствии способен обходить песочницы. Для объекта окна существует большое число псевдонимов: frames, globalThis, parent, self, top. Когда ваш сайт не заключён в кадр (frame), тогда parent и top будут указывать на свой объект window, когда же он заключён в кадр, тогда top будет указывать на самое верхнее окно, причём вне зависимости от того пересекает ли оно первоначальное или нет. Как и следовало ожидать, "parent" указывает на предка текущей странице в кадре (frame). Также имеется способ получения доступа к объекту window из узла DOM, document.defaultView хранит ссылку на текущий объект window. Попробуйте из консоли браузера:


1 document.defaultView.alert(1337)//вызывает alert с 1337
 	   

Приведённый выше код вызывает функцию оповещения применяя свойство defaultView объекта document для получения ссылки на window. Свойство defaultView доступно только для объекта document, хотя имеется хитрость, позволяющая получить сам объект document из узла DOM, а затем получить доступ к defaultView из такого document. Мы можем воспользоваться свойством ownerDocument и, как подразумевает его называние, вы можете получить используемый данным узлом DOM объект document:


1 let node = document.createElement('div');
2 node.ownerDocument.defaultView.alert(1337)
 	   

Многие песочницы были взломаны с применением данного знания, поскольку обычно доступ к объекту "window" блокирован; данный обходной манёвр позволяет вам снова получать к нему доступ.

Ещё одним способом получения объекта window являются события, когда вы в узле DOM пользуетесь неким событием, его обработчик определяется при помощи некого аргумента события, что сделано с той целью, дабы обойти такой факт, как то, что во времена, в кои был широко популярен Internet Explorer, он пользовался глобальной переменной с названием "event". Современные браузеры определяют событие как аргумент обработчика таким образом, что при доступе к этому событию оно превращается в локальную переменную. Занятно, что объект глобального события присутствует и в наши дни, но он устарел. В Chrome имеется свойство пути для каждого события ошибки и это некий массив объектов, которые привели к созданию данного события. Самый последний элемент в данном массиве это собственно объект window, а потому вы можете получить window просто считав его. Для этого сгодится функция pop():


1 <img src onerror=event.path.pop().alert(1337)>
 	   

В прочих браузерах вы можете воспользоваться принятым в качестве стандарта методом composedPath(), который возвращает массив эквивалентов имеющемуся в Chrome свойству path. Это означает, что вы можете выполнить вызов composedPath и получить самый последний элемент его массива, а он будет содержать объект window во всех браузерах:


1 <img src onerror=event.composedPath().pop().alert(1337)>
 	   

Помните, что я сказал что всякий обработчик добавляет аргумент к объекту event? Для элементов SVG имеется особый случай. Браузер добавляет не "event", а вместо него "evt", что означает, что вы можете получать доступ к объекту window при помощи данного аргумента в точности как при методе path или composedPath():


1 <svg><image href=1 onerror=evt.composedPath().pop().alert(1337)>
 	   

Чтобы найти это я не читал исходный код Chrome, вместо этого я просмотрел код обработчика события и обнаружил там это определение. Если вы запустите это в Chrome, вы обнаружите как именно определена функция обработчика:


1 <svg><image href=1 onerror=alert(onerror)>
 	   

Что имеет результатом следующий код:


1 function onerror(evt) {//ссылка на событие предоставляется при помощи evt
2     alert(onerror)
3 }
 	   

Итак, evt указывает на событие. Кажется, это относится к SVG, но интересно, есть ли ещё прочие? Стоит затратить некоторое время чтобы обнаружить есть ли ещё что- нибудь, потому как это полезно когда песочница JavaScript выполняет для вас защиту от доступа к объекту event.

В завершение данного раздела я расскажу вам об объекте Error и о том, как вы можете пользоваться prepareStackTrace для получения доступа к объекту window в Chrome. Применяя обратный вызов prepareStackTrace вы можете настроить трассировку стека, что очень удобно для разработчиков. Основная идея состоит в том, что вы предоставляете некую функцию с двумя аргументами: одним для сообщения об ошибке, а другой для некого массива объектов CallSite. Объект CallSite состоит из свойств и методов, относящихся к трассировке стека. Например, вы получили флаг isEval для определения вызывается ли текущий CallSite изнутри eval, но что нам интересно и в самом деле, так это то как получать объект window. К счастью, Chrome предоставляет нам для объекта CallSite полезный метод getThis(), а он вернёт объект window, когда "this" не определён самим исполняемым кодом. Давайте посмотрим на это в действии:


1 Error.prepareStackTrace=function(error, callSites){
2     callSites.shift().getThis().alert(1337);
3 };
4 new Error().stack
 	   

Мы определили свой метод обратного вызова, причём его функция просто получает первый объект CallSite из массива callSites, вызывает его функцию getThis(), которая возвращает объект window, а тот затем применяется для доступа к alert() и её вызову. Обратный вызов prepareStackTrace выполняется только при доступе к свойству стека объекта Error.

Область действия события HTML

Когда в неком элементе HTML выполняется событие JavaScript, браузер ограничивает эту исполняемую функцию самим элементом и соответствующим объектом document. Это означает, что вы можете применять ярлыки, просто указывая свойства текущего объекта или объекта document без полного пути к такому свойству. Фактически, браузер делает следующее:


1 with(document) {
2     with(element) {
3         //исполняется event
4     }
5 }
 	   

Помните свойство document defaultView? На самом деле мы можем применять его внутри события самостоятельно. А потому вы можете получать доступ к объекту window просто применяя данное свойство:


1 <img/src/onerror=defaultView.alert(1337)>
 	   

Это работает по причине указанного выше оператора "with". Наш браузер исполняет данное событие и ищет свойство defaultView в своём элементе изображения, но не может найти его там, поэтому он теперь проверяет объект document и обнаруживает, что оно там имеется, а потому свойство document.defaultView доступно и возвращается. Если вы выполните перечисление объекта document, вы обнаружите какие именно свойства вам доступны. Вы можете выполнить это при помощи индивидуального перечисления или просто воспользоваться в консоли браузера console.dir(document).

По причине установленной области действия вы также можете применять и прочие функции DOM, здесь мы можем создать некий сценарий, добавить в конец какой-то код и присоединить его к document без указания полного пути к document:


1 <img/src/onerror=s=createElement('script');s.append('alert(1337)');appendChild(s)>
 	   

Обратите внимание, что в данном случае применяется именно appendChild(), поскольку append вызовет исключительную ситуацию если вы не укажете полный путь, по крайней мере в Chrome. Функция append() принимает строку или узел. Функция appendChild() же фактически будет выполняться для нашего объекта изображения, document обладает методом appendChild(), но изображение более приоритетно, поэтому данный сценарий будет добавлен к объекту image, а не document.

Затирание DOM

Затирание (clobbering) DOM это метод, пользующийся преимуществом шаблона кодирования, который проверяет глобальную переменную и когда она не существует, следует иным путём кода. Основная идея состоит в том, что вы затираете не существующую глобальную переменную неким элементом DOM, чаще всего элементом привязки (ancor). Представьте что у вас имеется следующий код:


1 let url = window.currentUrl || 'http:///example.com';
 	   

Данный образец кода на первый взгляд выглядит безобидным, но в действительности window.currentUrl управляется не только через глобальную переменную , но также и через элемент DOM. На заре веб проектирования сайты достаточно часто применяли атрибуты id элементов form для ссылки на сам элемент благодаря функциональной особенности в Internet Explorer и Netscape, которая позволяла атрибуту id или name данного элемента form превращаться в глобальную переменную в качестве некого ярлыка для разработчиков. Данная функциональная возможность делает возможным затирание DOM. Вы наверняка обращали внимание на код, который выглядит примерно следующим образом:


1 <form id=searchForm>
2 </form>
3 <script>
4 searchForm.submit()
5 </script>
 	   

Ваш браузер создаёт глобальную переменную с названием "searchForm" и допускает её применение для обращения к имеющейся форме без применения метода getElementById(). Кроме того, вы можете применять атрибут "name" чтобы проделывать то же самое с единственным важным отличием, а именно: при использовании атрибута "name" вы также определяете свойство соответствующего объекта document:


1 <form id=x></form><form name=y></form>
2 <script>
3 alert(x)//[object HTMLFormElement]
4 alert(typeof document.x)
5 alert(y)//[object HTMLFormElement]
6 alert(document.y)//[object HTMLFormElement]
7 </script>
 	   

Обе приведённые выше формы, как вы можете наблюдать, создают глобальные переменные, причём эти глобальные переменные "x" и "y" затёрты элементами формы. Вторая строка кода JavaScript проверяет имеется ли в document свойство "x", но оно не определено, поскольку затёртые свойства не добавляют свойства в document. Самая последняя строка показывает, что document.y был затёрт элементом формы. Только определённые элементы способны применять атрибут name для затирания глобальных переменных, а именно: embed, form, iframe, image, img и object.

Элементы првязки превращают затирание DOM в ещё более занятное, ибо они позволяют вам применять атрибут "href" для изменения имеющегося значения затираемого объекта. Обычно, когда вы применяете некий элемент формы для затирания переменной, вы получаете значение toString самого объекта, скажем, [objectHTMLFormElement], однако при помощи привязки значением toString будет сама привязка "href":


1 <a href="clobbered:1337" id=x></a>
2 <script>
3 alert(x);//clobbered:1337
4 alert(typeof x);//object
5 </script>
 	   

Как вы можете видеть, глобальный "x" содержит строку "clobbered:1337" при доступе к нему как к строке. Обратите внимание на то, что я произнёс при доступе как к строке, "x" по- прежнему выступает элементом привязки, а это всего лишь toString данного объекта привязки, который возвращает значение "href" данного элемента. Имеется ещё кое что, о чём надлежит помнить при попытке затирания DOM, это то, что вы можете получать значение лишь известных атрибутов HTML. Например, вы не можете применять "x" "x.notAnAttribute", в то время как "x.lite" проходит. Я поставил перед собой много лет назад цель нарушить это правило, и для его обхода можно применять коллекции. Collections DOM это подобные массивам объекты, которые содержат элементы HTML. Я обнаружил, что вы можете использовать несколько элементов с одним и тем же id или name и это создаёт коллекцию. Затем вы можете применять иные id или name (в зависимости от того что вы использовали изначально) для уничтожения второго свойства. Это, скорее всего, лучше иллюстрируется примером:


1 <a id=x>
2 <a id=x name=y href=clobbered:1337>
3 <script>
4 alert(x.y)//clobbered:1337
5 </script>
 	   

В приводимом выше примере имеются две совместно используемых привязки атрибута id с одним и тем же значением "x", это формирует коллекцию DOM, кроме того, вторая привязка обладает атрибутом name, а поскольку это коллекция DOM, вы можете ссылаться на элементы в такой коллекции по имени или индексу, в данном случае мы ссылаемся на вторую привязку по имени "y". В то же время вполне можно также применять и индекс:


1 <a id=x>
2 <a id=x name=y href=clobbered:1337>
3 <script>
4 alert(x[1])//clobbered:1337
5 </script>
 	   

Приведённый выше код получает коллекцию DOM с "x" и получает второй элемент в коллекции (коллекции индексируются начиная с нуля), что позволяет нам затереть x[1] значением, которое мы можем контролировать.

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


1 <a id=x>
2 <a id=x name=y href=clobbered:1>
3 <a id=x name=y href=clobbered:2>
4 
5 <script>
6 alert(x[2])//clobbered:2
7 </script>
 	   

Приводимый выше код создаёт коллекцию с тремя привязками и третья привязка индексируется при помощи 2, поскольку, напомним, коллекции индексируются с нуля. Если я изменю третью привязку на обладание именем атрибута name равным "z", это не сработает, потому как значение атрибута name не создаёт коллекции. Если вам требуется затирать на глубину трёх уровней, вам придётся применять различные элементы, такие как формы.


1 <form id=x name=y><input id=z></form>
2 <form id=x></form>
3 <script>
4 alert(x.y.z)
5 </script>
 	   

Однако имеется проблема: как я уже упоминал ранее, вы не способны управлять методом toString отличающихся от привязки элементов, поэтому, в данном случае "z" будет эквивалентно "[object HTMLInputElement]". Для ьтого чтобы затирать свойства с более чем тремя уровнями в глубину, вам придётся применять допустимые атрибуты HTML , которые к тому же и допустимые свойства DOM:


1 <form id=x name=y><input id=z value=1337></form>
2 <form id=x></form>
3 <script>
4 alert(x.y.z.value)//1337
5 </script>
 	   

Существует одно исключение из данного правила с применением iframe. при помощи iframe вы можете применять атрибуты strdoc и name. Здесь происходит то, что window затёртого iframe обладает затёртым значением, что означает, что вы способны связывать iframe с прочими элементами, чтобы создавать столько уровней, сколько пожелаете. Единственным недостатком является то, что iframe, вероятно, блокируется фильтром HTML.

Это лучше проиллюстрировать примером. Сначала мы создадим iframe и воспользуемся его атрибутом strdoc для создания элемента внутри этого iframe:


1 <iframe name=foo srcdoc="<a id=bar href=clobbered:1337></a>"></iframe>
2 <script>
3 alert(foo)//[object Window]
4 alert(foo.bar)//undefined
5 </script>
 	   

Приведённый выше пример показывает, что "foo" был затёрт при помощи объекта window, соответствующего iframe, а поскольку это тот же самый оригинал, это делает для нас возможным дальнейшее затирание, применяя внутреннюю часть своего iframe. Но почему "foo.bar" не определён? Это обусловлено тем, что для построения srcdoc в iframe требуется некоторое время, а времени недостаточно для прорисовки содержимого этого кадра и затирания его свойства при помощи соответствующего элемента привязки. К счастью, я нашёл обходной путь: когда вы вводите импорт стиля из разных источников, это создаёт достаточную задержку для прорисовки соответствующего элемента внутри iframe:


1 <iframe name=foo srcdoc="<a id=bar href=clobbered:1337></a>"></iframe>
2 <style>
3 @import 'https://garethheyes.co.uk';
4 </style>
5 <script>
6 alert(foo)//[object Window]
7 alert(foo.bar)//clobbered:1337
8 </script>
 	   

Применяя данную методику вы способны затирать столько свойств сколько пожелаете при том условии, что для их отображения хватает времени. Однако существует проблема: для указания вложенных атрибутов iframe у вас имеется ограничение на применение одинарных и двойных кавычек, а после того как конкретная кавычка была применена, вы не сможете воспользоваться ею снова. Подходящее решение состоит в применении HTML кодирования, после чего вы сможете закодировать содержимое "srcdoc" столько раз, сколько потребуется. Да, вы не ослышались, внутри "srcdoc" вы можете применять объекты HTML для построения HTML!

Давайте попробуем затереть пять свойств. Сначала нам требуется iframe с атрибутом name и со значением самого первого свойства name и заключённым в двойные кавычки "srcdoc". Затем другой iframe для второго свойства с атрибутом "srcdoc" в одинарных кавычках для создания ещё одного вложенного iframe, который создаёт значение третьего свойства. После этого мы создаём "srcdoc" без кавычек для создания своих затирающих привязок. Поскольку мы применяем вложенные "srcdoc", нам следует закодировать в HTML значение количества раз, которое вкладывается iframe. Чтобы дать время для построения iframe, нам снова требуется соответствующий блок style, а после этого мы способны затирать a.b.c.d.e!


1 <iframe name=a srcdoc="
2 <iframe srcdoc='<iframe name=c srcdoc=<a/id=d&amp;amp;#x20;name=e&amp;amp;#x20;href=\
3 clobbered:1337&amp;amp;gt;<a&amp;amp;#x20;id=d&amp;amp;gt; name=d>' name=b>"></ifram
4 e>
5 <style>@import '//garethheyes.co.uk';</style>
6 <script>
7 alert(a.b.c.d.e)//clobbered:1337
8 </script>
 	   

  Эксплуатация фильтра

Существуют и иные способы эксплуатации затирания DOM, контроль значения — это лишь один из способов употребления. Вы можете затереть свойство атрибутов узла DOM, чтобы обмануть фильтр и заставить его вообще не удалять атрибуты. Рассмотрим следующий пример, в котором показан элемент формы с тремя атрибутами. Сценарий перебирает атрибуты от последнего к первому и проверяет, начинается ли он с "on" и, если да, то атрибут удаляется:


1  <form id=x onclick=alert(1) onmouseover=alert(2)>
2  <input>
3  </form>
4  <script>
5  for(let i=document.getElementById('x').attributes.length-1;i>=0;i--) {
6      let attribute = document.getElementById('x').attributes[i];
7      if(!/^on/i.test(attribute.name)){
8      continue;
9          }
10     document.getElementById('x').removeAttribute(attribute.name);
11 }
12 </script>
 	   

Данный сценарий удаляет необходимые атрибуты перебирая их при помощи свойства attributes, к сожалению, данной свойство можно затереть, предоставив некому дочернему узлу название "attributes". Тогда происходит то, что данный сценарий применяет затёртый ввод в качестве свойства атрибутов, а длина этого элемента не определена, а потому цикл ничего не будет повторять. Это позволит злоумышленнику проникать вовнутрь вредоносных событий и нарушать установленную фильтрацию.

Вы не ограничены attributes, можно затирать и иные свойства DOM, такие как tagName/ nodeName, допустим у вас имеется список фильтра блокирования, удаляющий определённые теги, скажем, form. Вы можете внедрить в форму элемент ввода, который обладает атрибутом с названием tagName или nodeName. Когда фильтр получает доступ к tagName или nodeName он вместо них обращается к затёртым вводимым данным, а, следовательно, возвращает неверное значение:


1 <form id=x>
2 <input name=nodeName>
3 </form>
4 <script>
5 alert(document.getElementById('x').nodeName)//[object HTMLInputElement]
6 </script>
 	   

Небезопасны даже такие свойства как parentNode. У вас имеется возможность его затирания, как и всех прочих, что будет возвращать неверный parentNode для элемента формы. Если бы у вас имелся фильтр, который выполнял бы обход элементов с применением свойств DOM, таких как parentNode, nextSibling, previousSibling и тому подобных, ваш фильтр совершал бы свой обход по неверным элементам и отфильтровывал бы неверные узлы DOM.

  Затирание document.getElementById()

Именно этим недавно обнаруженным мной методом вы способны фактически затирать результаты вызова document.getElementById(). Когда у вас имеется элемент со значением id равным "x", а также другой элемент с таким же id, в большинстве случаев возвращается getElementById() самый первый элемент. Однако я обнаружил, что когда вы пользуетесь элементом <html> или <body>, вы можете изменять установленный порядок DOM и эти элементы будут сливать воедино соответствующие атрибуты таких повторяющихся тегов, что заставит getElementById() возвращать тег <html> или <body> в зависимости от того какой из них вы применяете:


1 <div id="x"></div>
2 <body id="x">
3 <script>
4 alert(document.getElementById('x'))
5 </script>
 	   

Это можно употреблять когда некий сайт защищён CSP {Cryptographic Service Provider}, а у вас имеется инъекция HTML, которая происходит после всех тех элементов, которые вы желаете употребить. Применяя данный метод вы можете затереть имеющиеся узлы и изменить результаты getElementById() чтобы воспользоваться своим элементом и, возможно, получить XSS в зависимости от того что делает данный сайт. Я обнаружил эту методику во время тестирования хорошо известного крупного сайта и они применяли в начале своего дерева DOM невидимый элемент div рядом с тегом body, а также применяли это для управления CDN {Content Delivery Network} домена, который позднее применялся в сценарии исполнителя службы, который после этого применял вызов importScript() внутри соответствующего исполнителя службы.

  Затирание document.querySelector()

В точности этот же метод можно применять для затирания результатов document.querySelector(), когда сайт применяет его для поиска первого элемента с определённым названием класса, тогда DOM будет переупорядочен и вместо этого будут возвращаться <html> или <body>:


1 <div class="x"></div>
2 <body class="x">
3 <script>
4 alert(document.querySelector('.x'))
5 </script>
 	   

Выводы

В этой главе мы рассмотрели множество занимательных вещей. Сначала мы нашли разные способы получения объекта window из узла DOM. Затем мы рассмотрели область действия событий DOM и то, как всякое событие обладает не только доступом к области действия своего элемента, но и к области действия document. Мы закончили разделом о затирании DOM и объяснили различные возможные атаки, а также узнали насколько сложно написать фильтр, который безопасно обходит DOM.