Grammarly is the unicorn company that announced its open bug bounty program last September. Since that time, many security researchers posted their submissions and got paid well. Some of Grammarly’s issues are also useful for others. Like the recent XSS, that also bypasses an AWS WAF.
The recent XSS report is a bit different among others. First of all, it was submitted by Frans Rosen, one of the top HackerOne hackers. He is the 6th for the all-time rank.
Secondly, the report was paid for $3000 unlike tons of $50-100 XSSes on a platform.
The report title is: “Config override using non-validated query parameter allows at least reflected XSS by injecting configuration into state” https://hackerone.com/reports/1082847
This security issue is an excellent example of in-depth JavaScript research that Frans did.
Moreover, for some reason, an XSS payload that Frans used to validate this Grammarly vulnerability bypasses AWS WAF.
Let’s understand why.
The payload look like a very usual XSS for the first glance:
https://app.grammarly.com/docs/new?config={%22account%22:{%22subscription%22:%22javascript:alert(document.domain)//%22},%22api%22:{%22redirect%22:%22javascript:alert(document.domain)//%22}}
It consists of a JSON-encoded config parameter that vulnerable application parse. JSON attributes subscription
and redirect
contains an ordinary XSS sample of alert() function call. And if we will send this request to check how does the mod_security WAF detects it, we will get the following:
2021/03/03 01:10:44 [error] 41#41: *1 [client 172.17.0.1] ModSecurity: Access denied with code 403 (phase 2). Matched "Operator `Ge' with parameter `5' against variable `TX:ANOMALY_SCORE' (Value: `28' ) [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "80"] [id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 28)"] [data ""] [severity "2"] [ver "OWASP_CRS/3.2.0"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "172.17.0.1"] [uri "/docs/new"] [unique_id "161473384455.381362"] [ref ""], client: 172.17.0.1, server: localhost, request: "GET /docs/new?config={%22account%22:{%22subscription%22:%22javascript:alert(document.domain)//%22},%22api%22:{%22redirect%22:%22javascript:alert(document.domain)//%22}} HTTP/1.1", host: "localhost:8080"
At the same time, AWS WAF will pass this payload with no blocking:
But, if we will remove the JSON object and use the same payload in a plain text way, AWS WAF will block it:
An attentive reader may notice that we modified the payload by adding onerror
attribute to make this payload work in HTML attribute injection way. That’s true, but now we can add the JSON prefix back and see what happens:
It passed! We basically found that adding JSON prefixes before payloads makes them invisible for AWS WAF. Let’s understand what happened and why.
We initially decided that the reason for this bypass is behind the JSON parser. As we already discussed recently in a post WAF JSON decoding capability required to protect against API threats like CVE-2020-13942 Apache Unomi RCE
But it was not even close since AWS WAF has no JSON parsing capabilities at all.
After minifying Grammarly payload, we will get that the following request will be blocked by AWS WAF:
But adding just one additional double quote into there bypass the WAF:
The reason is not in JSON-like prefix, but the double-quote itself. We can easily remove the bracer charter and get the same bypass result with the payload ""onerror=javascript:alert('I-LOVE-AWS-WAF!')
.
It seems irrational but adding any numbers of double quotes before an XSS payloads bypass AWS WAF. We tested on 1500 rules enabled
Please leave a comment if you have an idea why. Thanks for reading!