One push too far / part 2
In part one of this article series I discussed a way for a website to abuse push notification permissions. However, the question remains as to how attackers would get hold of such permissions to begin with? One simple way is via a malicious site. But there is another, more ‘hackish’ way: hijacking the notifications permissions of a legitimate site via XSS.
Unsubscribe
As discussed earlier, an integral part of the notification mechanism is the authorization scheme, based on the mix of public/private keys and endpoint-URLs which are represented in the user’s subscription. As one would imagine, it is easy for a website to renew the user’s subscription for a notification service (i.e. change private/public keys and renew the URL). There could be many reasons as to why a website owner would choose to do so such as if the subscription URL was lost, rotation of private keys, etc. What is a bit surprising though, is that you can do it without proving any ownership of the current subscription.
In other words, once a user has given permission to receive notifications, JavaScript code (form the app’s domain, of course) can replace the public/private keys used to send notifications to the user without first proving ownership of the original key. According to Google, that is by design.
From a hacker’s perspective, Using XSS we could now unsubscribe the victim and then subscribing them again with our own key – and hijack the notification mechanism.
Persistency via reflected XSS
Imagine that we have detected a reflected XSS vulnerability in a website that uses notifications. Let’s see how we can leverage it to gain persistent access to the user’s browser.
First, we need to unsubscribe the user from the current (“legitimate”) subscription. This can be simply done as follows:
function unsubscribe(){ navigator.serviceWorker.ready.then(function(reg) { reg.pushManager.getSubscription().then(function(subscription) { subscription.unsubscribe().then(function(successful) { // You’ve successfully unsubscribed }).catch(function(e) { // Unsubscription failed }) })}); }
All we do here is release the current subscription (the original website can no longer send notifications). Next, we need to re-subscribe the user using our own key:
function reSubscribe(){ navigator.serviceWorker.getRegistrations().then(function(registrations) { for(let registration of registrations) { registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: applicationServerKey }).then(function(subscription) { console.log(subscription.toJSON()); });}}); }
And, well, that’s it.
You can now send notifications to the user, that appear to originate from the compromised site. Essentially, you have persistent access to the user’s browser, gained by exploiting a reflected XSS.
Weak vs strong hijack
Using the above hijacking method, we gain limited control over the victim’s browser. While we can send notifications, we cannot control what happens with these notifications once they arrive – this depends on the service worker that was registered by the original website (remember, the service worker must originate from the website’s domain).
This doesn’t necessarily harm our attack. For example, many websites will consume the ‘onClick’ event and will open a new tab in the user’s browser, leading him to a URL that was received as the notification payload. In this instance we can lead the user to our malicious site with a type of ‘phishing notification’.
However, in some instances, our attack can be stronger. If we can somehow store our own service-worker file on the targeted domain, even temporarily (for example by a variant of a reflected file download vulnerability), we can register our own service worker (replacing the original one) and decide for ourselves what happens when a notification arrives.
Mitigation ideas
From my perspective, the core issue is that the user cannot rely on the public key to validate the server’s identity. The key can be replaced without notifying the user, and without proving ownership of the original key. When I approached Google with the issue, I suggested managing notification keys in a similar way to managing SSL keys:
Initial public key should be received from the service provider (Google / Mozilla), after proving ownership of the relevant domain.Any subsequent public key for the domain can only be obtained after first proving ownership of the initial private key.For development processes, you could use ‘self-signed’ keys (not generated by the service provider), but in these cases the user will be prompted with a warning of a dangerous key.
Google rejected the idea as it adds overhead to both the service provider and the developer. Furthermore, they maintain the risk is not severe enough to introduce such a security mechanism to an already complicated notification solution. I’m not sure I agree. For the time being, however, it is the website’s responsibility to defend its users when deploying notifications, for example by periodically validating the subscription.
Further read
In the last part of this series I’ll discuss how you can leverage notifications to deploy a cool botnet With Google/Mozilla as your CNC.
Zohar Shachar, tech lead
Comentarios