XSS неспроста стоит в верхней части списка опасностей OWASP TOP 10. Любой толковый программист о них знает. Но это не мешает статистике: восемь из десяти веб-приложений имеют XSS-уязвимости. А если вспомнить личный опыт пентестов банков, то более реальной представляется картина «десять из десяти». Кажется, тема изъезжена от и до, однако есть подвид XSS, который по разным причинам потерялся. Это — DOM Based XSS. И как раз о нем я сегодня пишу.
Смещение фокуса
Атаки на клиентов представляют одну из основных проблем в безопасности веба. XSS, Clickjacking, CSRF — все они направлены именно против обычных пользователей, а не против серверных компонентов систем. И если раньше получить приличный профит можно было, эксплуатируя уязвимости в серверной части (и проникая внутрь корпоративной сети), то теперь фокус хакеров смещается на клиентскую часть. По этой причине XSS’ки, к которым многие относятся со скепсисом, могут сослужить хорошую службу.
То, что можно пропустить
Вначале внесу два пояснения, связанных с данной статьей.Первое. Главная цель — познакомить с DOM Based XSS тех людей, которые пока обходили этот тип уязвимостей стороной. Рассказать о тонкостях эксплуатации, а также поделиться мыслями о том, как правильно поставить процесс выявления подобных уязвимостей. Это своего рода ликбез. Поэтому в зависимости от твоих познаний можешь перескочить тот или иной кусок.Теперь второе. С полгода назад Владимир Кочетков написал на Хабре отличную статью «Вся правда об XSS, или почему межсайтовое выполнение сценариев не является уязвимостью?». В ней шла речь о том, что XSS — это именно атака, а не тип уязвимости. Помнится, это разожгло ряд ожесточенных споров и «крестовых походов», что очень повеселило… Но я буду называть XSS и атакой, и уязвимостью, хотя верно утверждение «XSS — тип атаки». Так будет проще, хотя правильно понимать смысл, конечно, важно.
Азбука XSS
Не могу не напомнить, для чего нам нужны XSS’ки. Нет, не для того, чтобы выполнить наш JavaScript-код у пользователя. Для этой цели мы можем просто затащить его на полностью подконтрольный нам сайт (http://evil.com).
Задача — выполнить НАШ JavaScript-код в браузере пользователя в контексте атакуемого домена (например, в контексте gmail.com). То есть цель — обойти Same Origin Policy, потому что на SOP и стоит почти вся браузерная безопасность.
Далее, что XSS-ка нам даст? Конечно, в простейшем случае мы просто получаем сессионные куки пользователя. Но на самом деле мы можем делать все, что может JavaScript: контролировать, что отображается на странице и что посылается на сервер, эмулировать действия пользователя, красть данные из формочек… Важно понять, что XSS в зависимости от ситуации и умения может стать сильным оружием.Теперь о классификации. Обычно выделяют «хранимые» («stored XSS» или «Type 2») и «отраженные» («reflected XSS» или «Type 1»). В хранимых мы посылаем XSS-ку, и она хранится на сервере, а дальше мы на эту страницу отправляем пользователей. В «отраженных» наша XSS-ка возвращается в теле ответа от сервера на конкретный запрос с самой XSS.Но здесь чего-то не хватает. И как ты, наверное, догадался, это — тема сегодняшней статьи DOM Based XSS (или Type 0). По разным причинам (часть из которых будет описана далее) данный вид XSS’ок малоизвестен, даже в наших кругах… Возможно, это связано с тем, что сканерами их не часто можно насканить. Но давай перейдем к теории.
Что такое DOM Based XSS?
Чтобы ответить на вопрос, необходимо прежде понять, а что же такое DOM. Начну издалека, с любимой темы — XML.Для XML есть два основных вида парсеров. Первый — SAX (Simple API for XML) — это тип парсеров с последовательной обработкой документов. Он читает элемент и генерит ивенты. Требует мало ресурсов, но очень прост. Второй — DOM (Document Object Model) — полностью загружает весь документ в память и представляет его в виде дерева. Но что еще важнее — он позволяет полностью манипулировать им. Можно добавлять, удалять, изменять структуру, сами элементы (узлы) и их атрибуты. А при чем здесь XML? При том, что с некоторых пор HTML является подвидом XML.В общем, данная концепция и используется в браузерах. Весь полученный HTML-документ от сервера представляется в виде DOM-дерева в браузере, а кроме того, имеется возможность менять его, используя стандартный API через тот или иной язык. В нашем случае это в основном JavaScript.DOM состоит из вложенных друг в друга в иерархическом порядке объектов, которые называются узлами (nodes). Каждый узел в структуре представляет располагающийся на странице HTML-элемент. Корневой элемент — document.Значение, хранящееся в узлах, — текст. К тому же у узлов есть атрибуты, к которым также можно обратиться. На рис. 1 представлен простейший HTML-файл, а также иерархия, которую создает у себя браузер. Поподробнее можно почитать про DOM и попробовать на примерах с JavaScript здесь: goo.gl/suiZE.
Рис. 1. Простейшее DOM-дерево
И, как уже было сказано, мы имеем возможность из JavaScript манипулировать DOM’ом. А что это нам дает? В определенных случаях, используя эти методы (если данные некорректно фильтруются), мы можем модифицировать DOM атакуемого сайта и добиться выполнения нашего JavaScript-кода в контексте атакуемого сайта. То есть суть — та же XSS. Простейший пример:
<body>
<script>document.write(location.href);</script>
</body>
Получив такую HTML’ку, браузер исполнит JavaScript-код и допишет в тело страницы (document.write) строку, взяв ее значение из location.href. Проблема здесь в том, что хакер может контролировать значение location.href и вставить свой JavaScript, который также будет исполнен. То есть если эта страница — test.html, то, чтобы добавить свой код, нам надо, чтобы наша жертва перешла по следующему URL (см. рис. 2):
http://victim.com/test.html#<script>alert(document.cookie);</script>
Рис. 2. DOM Based XSS
Рис. 2.1. Классическая DOM Based XSS
Здесь важно отметить, что в Firefox данный пример не заработает. Для IE и Chrome необходимо именно перейти по ссылке, а не просто написать в адресной строке скрипт, так как во втором случае все будет заURLенкожено перед исполнением кода (станет вида «%3Cscript%3Ealert(1);%3C/script%3E»).Зато второй пример будет рабочим для всех:
<body>
<script>
var l = location.hash.slice(1);
eval(l);
</script>
</body>
Эксплуатация:
http://victim.com/test_eval.html#alert(document.cookie)
Вариант XSS чуть более нестандартный:
<body>
<p>Hello my window name is:
<script>document.write(window.name);</script>
</p>
</body>
Эксплуатация (открываем страницу жертвы с нашей, чтобы мы могли контролировать window.name) — рис. 3:
<script>window.open("http://victim.com/test_window.html", "<script>alert('XSS')</scr" + "ipt>", "", false);</script>
Надеюсь, стало понятно, откуда растут ноги для DOM XSS.
Рис. 3. DOM Based XSS из window.name
Терминология?
Сама атака, как ни странно, очень бородатая. Как минимум в 2005 году Амит Клейн (Amit Klein, goo.gl/OOb3U) написал осмысленную идею о третьем виде XSS’ок, хотя сами DOM XSS уже находили и до этого. В его работе был представлен некий список того, откуда могут прийти данные от пользователя (рис. 4) и какие опасные функции могут привести к XSS'ке (рис. 5). Но, как ни странно, тема была развита и переосмыслена за последние годы — во многом благодаря таким людям, как Стефано Ди Паола (Stefano Di Paola) и Марио Хайдерих (Mario Heiderich).
Рис. 4. Откуда…
Рис. 5. Куда…
Самое главное, выработалась некая терминология — то, что мы контролируем и можем передать странице, называется «source», а итог — то куда данные приходят, опасные функции, с помощью которых мы можем произвести атаку и проэксплуатировать нашу XSS’ку, — называются «sink». Русских аналогов терминов даже пытаться искать не буду.И если sink’и относительно не сильно изменились (дополнились), то понимание source’ов сильно разрослось, что несколько меняет понимание атаки (ее классификацию), но об этом чуть позже.Здесь важно понимать, что есть что в принципе. Подробностей слишком много. А потому одним из значимых ресурсов при копании DOM XSS будет проект domxsswiki (goo.gl/yycvJ), на котором представлен список основных source и sink, а также тонкости их в контексте различных браузеров.Так вот, о новой классификации, которую спецы из Aspect Security недавно представили (в качестве троллинга, наверное) — см. рис. 6. Классификация эта точна и подчеркивает суть DOM XSS. Не важно, откуда получаются входные данные от злоумышленника (из конкретного ответа сервера, от клиента, со статической части страницы) — важно, что они используются в критичных функциях клиентской частью. Например, представь ситуацию, что мы можем отправить свой никнейм на сервер и он будет где-то там храниться, — потенциал для Stored XSS есть. Но если нам помешает фильтрация — мы, казалось бы, уже и не можем ничего сделать. А если наш никнейм используется где-то еще, но уже в контексте клиентской стороны и при этом используется где-то для модификации DOM’а? Получается, мы имеем вторую попытку для XSS’ки (теперь уже DOM XSS), так как, возможно, нам и не понадобятся те символы, которые нужны были для Stored XSS, но были отфильтрованы на сервере.
Рис. 6. Новая классификация?
Рис. 7. Новая версия типов source меняет классификацию
Рис. 8. Откуда (новая версия)…
В чем суть этой части? В том, чтобы дать тебе понять, что есть важные общие понятия, но DOM XSS — это очень специфичная и нетривиальная во многом вещь.
Специфика DOM XSS
Итак, после общих размышлений на тему и нескольких примеров, что мы можем выделить специфичного в DOM XSS?Во-первых, DOM XSS — это прежде всего проблема клиентской стороны веб-приложения. Уточню: это не проблема клиента, а проблема клиентской части приложения. Это некорректная фильтрация/использование данных, полученных из недоверенных источников, в клиентской части веб-приложения, то есть в основном в JavaScript.У этого пункта есть несколько последствий. DOM XSS может быть на «любой» странице, даже на обычной HTML’ке, если там используется JavaScript.Раньше поиск уязвимостей концентрировался на скриптах, на страницах, где мы могли вводить какие-то данные, а также страницах, где мы получали итог, — при этом статические странички были неинтересны как таковые. Теперь же даже «статика» может принести уязвимость.Достаточно часто для DOM XSS нам вообще не надо посылать XSS’ку на сервер. Три приведенных выше примера — тому подтверждение. Для первых двух примеров важно отметить, что браузеры (согласно стандартам) не отправляют на сервер то, что находится после символа «#». Это Fragment identifier — специальная часть URI-схемы, используемая изначально, для создания ссылок на части документа. Пример с вики: «http://www.example.org/foo.html#bar» ссылается на элемент с id=bar на странице foo.html. Его фишка в том, что он не отправляется на сервер, но доступен из JavaScript. Такой идентификатор постоянно используется в web 2.0 сайтах (сервис Gmail тому пример).Так вот «http://victim.com/test.html#» из первого примера заставит сделать браузер запрос к test.html, но без XSS’ки, а в JS будет полная строка. И никакие средства серверной защиты (фильтрация пользовательских данных, всевозможные WAF или IPS) не сработают. Проблема лежит в основном на клиентской части веб-приложения. Это первое замечание. Во-вторых, мы, как правило, не можем использовать стандартные техники и средства, которыми пользуемся для выявления классических XSS’ок и SQL-инъекций, так как они рассчитаны именно на выявление серверных проблем. В-третьих, хотя мы, по сути, и имеем возможность доступа к исходникам (JavaScript-то доставляется клиенту), но правильно и глубоко искать такие уязвимости — очень нетривиальная задача. Тонкостей и хитростей — хоть лопатой копай :).
Трудности и фишечки
Итак, в продолжение последнего пункта, хочется привести показательную картинку из презентации Стефано Ди Паолы (рис. 9). Анализировать JavaScript — ужасное дело, особенно стандартными средствами. Да, Марио Хайдерих написал два регэкспа для выявления основных sink и source:
/((src|href|data|location|code|value|action)s*["']]*s*+?s*=)|((replace|assign|navigate|getResponseHeader|open(Dialog)?|showModalDialog|eval|evaluate|execCommand|execScript|setTimeout|setInterval)s*["']]*s*()/
/(locations*[[.])|([.[]s*["']?s*(arguments|dialogArguments|innerHTML|write(ln)?|open(Dialog)?|showModalDialog|cookie|URL|documentURI|baseURI|referrer|name|opener|parent|top|content|self|frames)W)|(localStorage|sessionStorage|Database)/
Но ведь надо пройтись по всему dataflow, чтобы понять, откуда и что берется, куда и как попадает… Мало этого, кроме поиска потенциальной DOM XSS-уязвимости, надо еще и написать под нее эксплойт. А браузеры — разные, и, что хуже, поведение их различно. Не говоря уж от том, что в браузерах есть средства противодействии отраженным XSS’кам — и их также приходится обходить.Если взять первый пример и значение в location.href, то в нем хранится URL (общее представление):
scheme://user:pass@host/path/to/page.ext/Pathinfo;semicolon?search.location=value#hash=value&hash2=value2
А браузеры по-разному urlencod’ят данные URL. Firefox, например, кодирует символы <> после #, а IE не кодирует.IE:
http://host/path/to/page.ext/test%3Ca%22'%0A%60=%20+%20%3E;test%3Ca%22'%0A%60=%20+%20%3E?test<a"'%0A`=%20+%20>;#test<a"'%0A`=%20+%20>;
FF:
http://host/path/to/page.ext/test%3Ca%22%27%0A%60=%20+%20%3E;test%3Ca%22%27%0A%60=%20+%20%3E?test%3Ca%22%27%0A%60=%20+%20%3E;#test%3Ca%22%27%0A%60=%20+%20%3E;
Потому первая атака будет годна только для IE, Chrome.В то же время, если бы уязвимая страница имела код
<body>
<script>document.write(location.hash);</script>
</body>
<!-- используется location.hash вместо location.href -->
то эксплойт бы заработал во всех браузерах, так как FF для этого объекта хранит значение в раскодированном виде.Далее, еще один пример браузерных триков. Есть, например, уязвимая страница, которая добавляет в скрипт только имя сервера из referer:
document.write('<script src="http://Host/image.gif?t=’+(referrer.split("/")[2])+’></script>');
Казалось бы, что тут можно сделать? Да, мы можем влиять на referer! Все, что нам необходимо, — заманить на наш сайт пользователя и редиректнуть его с необходимой нам страницы на уязвимую. Таким образом мы повлияем на поле referer.Но здесь вроде начинается облом… Ан нет. Стефано выяснил, что IE поддерживает спецсимволы в имени хоста. То есть можем создать поддомен у себя «.evil.com» или, как в примере Стефано, ««onreadystatechange=eval(name).attacker.com».Кроме браузерных штук и различностей нативного JavaScript-кода есть еще и всевозможные JS-фреймворки, которые используется более чем повсеместно. Тот же jQuery имеет множество оберток над стандартными sink’ами (см. рис. 10).
Рис. 9. Анализ JavaScript — неблагодарное занятие
Рис. 10. jQuery и другие фреймворки еще больше затрудняют анализ
Обходим фильтры
Надеюсь, понимание относительно DOM XSS начало появляться. Теперь косвенно коснемся защиты. Конечно, простейший вариант — отказаться от JS на клиентской стороне :). Но понятно, что это нереально. Следующий вариант — не использовать безопасные функции изменения DOM’а, а также внедрить фильтрование пользовательских данных… Но, как ты, наверное, заметил, DOM XSS — это та еще темка. Это как некая разноцветная бурлящая сущность, не имеющая особых границ. Потому и понимание толкового в массах нет, а потому и с точки зрения защиты ошибок допускается много.Не так давно прочитал отличную статью, которую мы сейчас и разберем. В ней описаны два примера «безопасного» изменения DOM’а за счет использования фильтрации пользовательских данных.Пример 1. Использование element.textContent, который применяется для задания/чтения значения текста какого-либо узла. Используется также для фильтрации HTML. Например:
var div = document.createElement('div');
div.innerHTML = 'Hello <a href="http://bob.com">Bob</a>!';
console.log(div.textContent);
// Hello Bob!;
Здесь div.textContent вырезал «Bob» при добавлении элемента. Вроде бы безопасно и XSS-ку добавить мы не можем? А вот и нет. У этого метода есть фича: он HTML-сущности (entity) преобразует обратно в HTML:
var div = document.createElement('div');
div.innerHTML =
'Hello <a><script>alert("!")</script></a>!';
console.log(div.textContent);
// Hello <script>alert("!")</script>!
То есть с небольшими махинациями мы можем достаточно просто внедрить XSS’ку. Если воспользоваться этим методом несколько в другой последовательности
var div = document.createElement('div');
div.textContent = '<span>Foo & bar</span>';
console.log(div.innerHTML)
// <span>Foo & bar</span>
то получится опять-таки вроде бы вполне безопасный результат. Автор отмечает, что document.createTextNode имеет аналогичное поведение. Символы <, >, & были заменены на соответствующие сущности. И этим методом также пользуются для «фильтрации». Но ты, наверное, заметил, что здесь в фильтрации отсутствует достаточно важный символ — кавычка. А этот факт из теории классических XSS напоминает нам о возможности эксплуатации XSS на основе ивентов (event), что и показывает на примере автор:
function escapeHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
};
var userWebsite = '" onmouseover="alert('derp')" "';
var profileLink = '<a href="' + escapeHtml(userWebsite) + '">Bob</a>';
var div = document.getElementById('target');
div.innerHtml = profileLink;
// <a href="" onmouseover="alert('derp')" "">Bob</a>
Как ни странно, проблема растягивается и «перетекает» в другие решения. Например, jQuery имеет те же особенности в .text(). Вдобавок в той же domxsswiki можно почерпнуть некоторую информацию о фильтрации.
Реальность
Пара примеров. Во-первых, классический вариант, который был найден в Twitter’е:XSS’ка, как ни странно, была тривиальной:
http://twitter.com/#!javascript:alert(document.domain);
Происходит подстановка псевдо-хэндера javascript в location и, как следствие, исполнение нашего кода.Современный пример с сайта AVG:
//display the correct tab based on the url (#name)
var pathname = $(location).attr('href');var urlparts = pathname.split("#");
Эксплуатация опять-таки тривиальна:
http://www.avg.com/eu-en/download#"><img src=x onerror=prompt(/xss/);>
Далее чуть более странный пример, когда вроде как уязвимость близка, но проэксплуатировать ее непросто. Данная уязвимость была найдена в Adobe Flex 3. Уязвимая страница — /history/historyFrame.html — до сих пор массово находится в Сети (в том числе на «мощных» порталах).
function processUrl()
{
var pos = url.indexOf("?");
url = pos != -1 ? url.substr(pos + 1) : "";
if (!parent._ie_firstload) {
parent.BrowserHistory.setBrowserURL(url);
try {
parent.BrowserHistory.browserURLChange(url);
} catch(e) { }
} else {
parent._ie_firstload = false;
}
}
var url = document.location.href;
processUrl();
document.write(url);
Если взглянуть на последние строчки, то кажется — XSS-ка вот здесь, на блюдечке. Но нет, есть проблема — проверки parent._ie_firstload в функции processUrl. Напрямую уязвимость не проэксплуатировать — яваскрипт просто не дойдет до нужного места.Так как у страницы нет такого объекта, как parent, то JavaScript вылетит на «parent.BrowserHistory.setBrowserURL(url);». Но мы можем схитрить и на нашем сайте создать страничку, которая будет содержать два фрейма:
<html>
<body>
<iframe name="_ie_firstload"></iframe>
<iframe src="http://www.vuln.site/app/history/historyFrame.html?#<script>alert('xss')</script>"></iframe>
</body>
</html>
Таким образом мы создаем фрейм, к которому код с уязвимой страницы обратится в результате проверки «if (!parent._ie_firstload)». И так как теперь некий объект уже существует, то проверка попадает на Else и функция удачно завершается, давая возможность запуститься DOM XSS.Но и у этого метода есть свои тонкости. Например, FF запрещает обращаться к parent с другого домена, а потому юзать ее, по опыту автора, можно было только против IE.Если тебя заинтересовала тема DOM XSS, то обязательно глянь другие примеры, чтобы набрать опыта: goo.gl/ZWei3, goo.gl/gZawa, goo.gl/XRwBT, goo.gl/plqs9.
Послесловие
Возможно, повторюсь, но, подводя итоги, хотелось бы сказать, что DOM Based XSS — это тот еще непонятный зверек. А чем больше непонятностей и тонкостей — тем больше багов. Особенно с учетом того, что JavaScript все больше и больше «перетягивает на себя одеяло», а веб становится все более динамичным. В общем, ученье — свет, а творить — чудесно :). Удачных ресерчев!
Впервые опубликовано в журнале „Хакер“ от 05/2013.
Подпишись на «Хакер»
P.S. Можете поделиться знаниями и интересными идеями, написав для ][? Дайте знать :). Мы платим гонорары, но это не должно быть главной мотивацией.
Автор: XakepRU