XSSing Google Code-in thanks to improperly escaped JSON data

Google Code-in is an online programming competition for students hosted by Google that takes place every year.

When I was signing up for a second time, I put a payload into all the text fields. I didn’t expect anything to happen, but when I clicked the submit button, all the payloads were executed. And the payloads continued executing on every page I visited. This alone didn’t mean much as it would only classify as an Self-XSS, but meant that this didn’t have to be the only place it was unescaped. I submitted this bug to the support email and also to Google VRP in case it turns out to be a real issue.

In Google Code-in you can submit tasks for review and also can add comments to them. And as usual, I put the payload in the comment. Surprisingly, when I added the comment, the payload worked once again. And it stayed there even after I reloaded the page. I sent an update to Google and they fixed it the following day.

Now let’s take a look what happened with the payload.

They used script elements with type application/json generated on backend to pass user data to the client-side.

<script type="application/json">
{"someData": true, "text": "hello world", "user": 123}
</script>

In the comment and other fields I used a simple payload like this:

"'><script src=x></script>{{1-1}}

When a new comment is sent, it’s also added to the JSON object which holds the comments of a task as well as some other data.
So when the comment was added, the JSON would look something like this:

<script type="application/json">
    {
        "someData": true,
        "comments": [{
            "id": 123,
            "text": "\"'><script src=x></script>{{1-1}}"
        }]
    }
</script>

As you can see, the double quote is escaped correctly and it’s a perfectly valid JSON.
Except… they forgot to escape one important thing.

As written in the HTML4 documentation:

The first occurrence of the character sequence “</” (end-tag open delimiter) is treated as terminating the end of the element’s content. In valid documents, this would be the end tag for the element.

This means as soon as the HTML parser sees </script>, it assumes it is the end of that element.

Google Code-in XSS

 

We get even more info in the appendix of the documentation:

When script or style data is the content of an element (SCRIPT and STYLE), the data begins immediately after the element start tag and ends at the first ETAGO (“</”) delimiter followed by a name start character ([a-zA-Z]); note that this may not be the element’s end tag. Authors should therefore escape “</” within the content.

 

How to prevent this from happening, from the chapter Restrictions for contents of script elements:

The easiest and safest way to avoid the rather strange restrictions described in this section is to always escape “<!--” as “<\!--“, “<script” as “<\script“, and “</script” as “<\/script” when these sequences appear in literals in scripts (e.g. in strings, regular expressions, or comments), and to avoid writing code that uses such constructs in expressions. Doing so avoids the pitfalls that the restrictions in this section are prone to triggering: namely, that, for historical reasons, parsing of script blocks in HTML is a strange and exotic practice that acts unintuitively in the face of these sequences.

 

This still wouldn’t be enough to get a working XSS on the page (in modern browsers) since they have set up content security policy, which I wrote about bypassing it in a separate article. In a nutshell, CSP allows you to whitelist allowed sources of scripts, styles, and other resources to mitigate XSS attacks. This means a <script> element just like that wouldn’t be able to get through CSP and therefore wouldn’t be executed.

Fortunately, Google Code-in uses Angular on their frontend. This means CSP doesn’t apply to it. Expressions like {{1-1}} get easily evaluated (one example is XSS in McDonalds.com). Since Angular 1.6, Google removed the expression sandbox completely, which means we can access the document with no problem just like this:

{{constructor.constructor('alert("xss")')()}}

 

Now we have a working payload that gets executed every time someone (in this case mentors or site admins) open the comments page.

 

Timeline:

30.10.2018: Vuln reported
31.10.2018: Fixed (by the dev team)
01.11.2018: Closed
21.11.2018: Reopened and accepted, Priority changed to P2
11.12.2018: Reward issued
12.12.2018: Marked as fixed

ThomasOrlita.cz
Follow me on Twitter: @ThomasOrlita

Leave a Reply