One of the lesser-known Google projects is Google Crisis Map.
It was created to help people find and use critical emergency information. [source]
Although it is still working, it doesn’t seem to be used much anymore.
Since it’s an older project (created in 2012) and not updated often, it’s a great target to look for vulnerabilities.
It’s hosted on the google.org domain, which doesn’t have as big a severity as google.com (for client-side vulnerabilities), but it’s still a domain owned by Google.
Logging in
If you go to the project’s home page (google.org/crisismap), you’ll get redirected to the default map “Weather and Events”. This isn’t very interesting for us since the only thing we can do is view the map.
There is a way to manage and create new maps. It can be accessed if we add .maps
at the end of the URL: google.org/crisismap/.maps
Once you open this page, you’ll need to log in with your Google account to continue. Now you should see a dashboard with a list of maps. There are three default maps for every account.
For some reason, if you publish one of these maps on your own domain, everyone will see that in the dashboard under the “Published Map” field.
Creating a map
If you click on the red “Create Map” button, you’ll most likely see a message that the gmail.com domain can’t be used for creating new maps.
This means we need to log in using an email with our custom domain. We can do this either by logging in with a GSuite account or an email that uses a domain other than gmail.com. After that, we can create a new map.
After clicking the “Continue” button, we’ll get redirected to a page where we can edit the newly created map.
Finding the XSS
First, we’ll add a new layer to the map.
A dialog for creating a new layer will pop up.
We’ll enter anything as the “Title”.
Now if we enter javascript:alert(document.domain)
into the “Source URL” field, it’ll show an error:
Invalid URL – please include a protocol (e.g. http:// or https://)
This means it checks if the URL is valid before it allows you to save the new layer. The deobfuscated JavaScript code that validates the URL looks like this:
if (url && !url.toLowerCase().match("^\\s*(http://|https://|docs://|$)")) {
showError("Invalid URL - please include a protocol (e.g. http:// or https://)");
}
But this is only validation on the client-side before the actual save request is sent to the backend.
Modifying the request
We can use a web debugging proxy like Fiddler or Burp Suite to modify the request and send the modified version instead.
First, we need to change the “Source URL” to a valid URL, e.g. https://example.com
.
We’ll click the “OK” button and click “Save” to send the save request. Then we’ll modify the request. This is what the request looks like:
POST https://google.org/crisismap/.api/maps/1234
{
"id": "1234",
"title": "Untitled map",
"base_map_type": "GOOGLE_ROADMAP",
"layers": [{
"id": "1",
"title": "Test layer",
"visibility": "DEFAULT_ON",
"type": "KML",
"source": {
"kml": {
"url": "https://example.com"
}
}
}]
}
We’ll replace https://example.com
with javascript:alert(document.domain)
and send this modified request.
Testing the XSS
The request is now sent and saved, so we’ll reload the page.
Open “Layers” and click on “Download KML”.
After we click on the download link, the XSS is fired and the alert box pops up with the domain name!
How to fix this
Why did this happen? The URL validation happened only on the frontend and not in the backend. That means this could be fixed by validating the URL in the backend as well.
But this is not the way Google decided to fix it. Instead of checking the URL when saving it in the backend, the URL is now validated before displaying in the DOM.
So if the URL isn’t valid, it won’t be used as the link. It’ll use a meaningless value like about:invalid
instead.
<a href="about:invalid#zClosurez">Download KML</a>
The impact
Okay, so we have a link that points to a javascript:
URI containing the payload. The link is on a page to manage the map. And you must log in and have permission to access the page.
Clearly, this is self-XSS since only we are able to get this XSS executed.
Now how do we get from self-XSS to a real XSS?
Increasing the severity
Every map we create can be published to be viewed by the public. If you’re logged in via an email with the domain example.com
, you can publish the map to the URL https://google.org/crisismap/example.com/test
.
Anyone can open this URL and view the map we’ve created. To get the XSS working, the user would have to open or be navigated to this page, open the “Layers” view and then click the “Download KML” link.
This means it’d no longer be self-XSS, but it’s still too many steps the user would have to make for this XSS to be useful.
Clickjacking
If we take a look at the response HTTP headers, we can see that google.org doesn’t send the X-Frame-Options
header.
The
— MDN web docsX-Frame-Options
HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a<frame>
,<iframe>
,<embed>
or<object>
. Sites can use this to avoid clickjacking attacks, by ensuring that their content is not embedded into other sites.
The (intentional) lack of this HTTP header on google.org means we can embed the published map into an iframe
on our own website.
<iframe src="https://google.org/crisismap/example.com/test"></iframe>
This is how it’ll look like. In order to fire the XSS the user now doesn’t have to even leave our website. But they’d still need to click on two places in the iframe
(“Layers” > “Download KML“).
The iframe
is loaded on our website – that means we can use CSS and JavaScript to manipulate it.
The first thing that came to my mind was to put black DIVs around the point where we want the user to click. Then detect a click and move the DIV to the second point.
This worked well but it still requires the user to click on two different locations.
But a more efficient solution would be to position the iframe
absolutely with CSS so the user doesn’t have to move the cursor at all.
Below is was a live demo. Unfortunately, Google Crisis Map has been discontinued so the live demo won’t work anymore. You can see how it looked like in the video.
It scales the iframe
50× and moves it to the position we want the user to click. First to the “Layers” tab. After a click, it moves over the link with the payload.
It was possible to execute the XSS by clicking two times somewhere in the iframe
. We could even overlay the iframe with an opaque div with pointer-events
disabled, so the user would have no idea they are clicking in the iframe.
For the sake of the sample and the fact that this vulnerability is already fixed, the link goes to https://
and not javascript:
URI.
Conclusions
There are several things to be taken from here.
- Never trust user input. Always check (on the backend) if it’s valid before saving it. Make sure to properly sanitize it depending on the context it will be in.
- Don’t allow other domains to embed your website in an
iframe
by correctly setting theX-Frame-Options
header. - When looking for vulnerabilities, try to find the highest possible severity of the vulnerability.
For example, if you find an XSS, try making it into an account takeover by finding incorrectly configured cookies or endpoints. - Don’t be afraid to look for older projects that still fit into the scope of the Bug Bounty program.
Timeline | |
---|---|
2018-12-09 | Vulnerability reported |
2018-12-10 | Priority changed to P1 |
2018-12-10 | Looking into it |
2018-12-10 | Nice catch |
2018-12-11 | Reward issued |