A few weekends ago Google hosted it's annual Capture The Flag (CTF) competition: a set of computer security challenges involving reverse-engineering, cryptography, web technologies, and much more.
The aim of a CTF is to solve challenges by exploiting vulnerabilities in the provided application, server etc. in order to find a "flag", usually an unguessable string, which can be traded for points. Teams try to find flags and receive points during the limited competition time window, so they rise up the leaderboard. Generally top teams can receive prizes, or are invited to a finals round (which happens in the Google CTF).
In this post I will cover 5 of my top web security take-aways from the Google CTF web challenges. I won't go into full detail for every challenge, but instead focus on the vulnerabilities themselves and what you can do to prevent similar security holes in your applications.
If you are interested in full challenge write-ups I recommend you check out the CTFTime Writeups. Google also post past challenges and solutions on the Google CTF GitHub.
So let's check out some vulnerabilities!
1. Avoid Writing Custom Sanitizer Code
Google's beginner challenge for the CTF involved creating "pastes" which could then be shared with another user.
Most challenges involving user inputted content which is then reflected back to the user, and potentially other users, is almost certainly a cross-site scripting [OWASP 7 - XSS] challenge. Indeed, being a beginner challenge Google gave a fairly big clue in the page source with a comment including a backlog ticket number for fixing an XSS bug:
In this instance, the pasted content gets passed through the DOMPurify library's sanitize()
method, which in this case does not have a known vulnerability. The reference to /source
combined with our pasted contents being added to the note
variable hints at attacking the server code, which for this challenge was provided.
It is in the server source code that we find the Googlers have created their own custom sanitizer method:
/* Who wants a slice? */
const escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1)
.replace(/</g, '\\x3C').replace(/>/g, '\\x3E');
The intent is clear: our note is to be written into a double quoted string using ejs templates, so first off a quick way to ensure strings are escaped (and therefore can't close off a set of quotes and perform XSS) is to use JSON.stringify()
which will add backslashes to quotes (i.e. \"
) in any passed string. Indeed if we copy this function into a JS REPL (e.g. Node prompt or developer tools console) we can see a payload of - " -
becomes - \" -
.
The .slice(1, -1)
operation then removes the first and last character from the output of JSON.stringify()
, which for a string are double quotes. The last two replaces then escape all triangle brackets characters so to prevent you closing / adding script tags.
At first this might seem like a neat trick for escaping inputs - it certainly seems to work for any payload you can paste into the challenge's website, and it's neat and short. Unfortunately it's made a fundamental flaw in a key assumption about the user's input: that it will always be a string.
Passing an array (e.g. ['- " -']
) to the above method you'll instantly notice a difference. Instead of the first and last characters being a double quote, they are now square brackets which leaves a pair of unescaped double quotes as the first and last characters of the remaining string.
This means passing a payload of ["; alert('xss'); //"]
would allow us to bypass this custom sanitizer and execute an XSS attack. Passing an array is possible because the Express server has the extended bodyParser.urlencoded()
middleware enabled, allowing us to pass malicious payload in a POST body by using the extended syntax content[]=; alert('xss'); //
.
Ultimately this is a manifestation of OWASP 8 - Insecure Deserialization. An insecure parser of a payload allowing attackers to perform a secondary XSS attack. π₯
Suggestions
- Where possible, always use well tested third party sanitizer libraries which cover all possible inputs. Avoid custom sanitizers as it is very easy miss something.
- Reduce the allowed Accept types to a know allowlist for API endpoints to reduce the scope of user payloads. For example, don't use unnecessary or overscoped body parsing middleware.
- Validate user payloads for type and contents and consider returning with
400 Bad Request
like responses for invalid payloads. Using libraries such as express-validator can help make this simple.
Funnily enough, we find that the log-me-in web challenge suffers from very similar issues to the beginner challenge. Extended body parser syntax allows the attacker to bypass sanitization and leads to an SQL Injection [OWASP 1]. I add this note to encourage you to still be cautious with well maintained third party libraries, as they may still have gotchas despite seeming to perform sanitization. Specific payload validation will always harden your server's endpoints. π₯
2. Beware Of document.referrer
A gotcha that caught even the Google CTF creators out is the existence of the document.referrer property.
This property is set to either:
- An empty string in the case of direct navigation;
- The URL of the page where you navigated from, similar to the Referer header.
- The same value as the
href
of the parent window'sdocument.location
when inside an iframe.
In the case of the Tech Support challenge, the last property setting meant that an iframe vulnerable to XSS leaked the credentials of the admin user, as the frame inherited the parent window's href
in this referrer
property [OWASP 3 - Sensitive Data Exposure]. π’
Suggestions
- Avoid plaintext (or otherwise) credentials in any part of the URL, ideally for all pages, but especially for any public facing pages or pages containing iframes with a public interface.
- Educate your users about credential security and management best practices.
3. Avoid User Inputted HTML If Can!
The least solved web challenge with only 10 completions was the Safe HTML Paste challenge. This challenge was remarkably similar to the beginner Pasteurized challenge mentioned above, and allowed you to paste arbitrary content, view it and share it with an admin user.
Unlike the beginner challenge the server code was off-limits and appeared to be rock-solid. What this CTF demonstrated was how difficult it is to correctly sanitize arbitrary HTML, and how even a popular and well maintained library such as the Google Closure Library can have weaknesses. Further still, it demonstrates how easy it is to use a library that has a well documented vulnerability and patched version and yet be using an outdated and vulnerable version! [OWASP 9 - Using Components with Known Vulnerabilities]
The attack is well documented in this write-up and this research if you are interested to go through the DOM mutation details. π
Suggestions
- Avoid user inputted HTML content when possible.
- Always use the latest versions and patches of third party libraries.
- Regularly audit your libraries and their dependencies using tools like retire.js or snyk.
Β 4. Self-XSS Should Not Be Ignored
Coming back to the Tech Support challenge, the intended vulnerability path had a very interesting message - self-XSS when paired with cross-site request forgery (CSRF) can lead to dangerous session-hijacking.
In the challenge we find that the lack of CSRF controls on the login allows us to force the victim to join our session in a frame which subsequently runs a self-XSS.
Given the logged in frame is running in the victim's context, the self-XSS is granted privileged access to sibling frames allowing the attacker to manipulate, or in this case scrape, pages already open with the victim's previous session.
This kind of vulnerability is sufficiently open that you don't even require a third-party domain to send leaked data to! See the video below of an attack on the challenge that uses the self-XSS to store the flag in the attacker's address field.
Suggestions
- Enable CSRF protection on all forms, or ideally at least on any authentication / login flows.
- Close out any self-XSS vulnerabilities to prevent paired / secondary attacks.
- Enable strict content security policies (CSP) to prevent inline script execution without CSRF protection (e.g. nonce tokens).
5. Prototype Pollution Is A Real Issue
Similar to the Tech Support, the All The Little Things challenge also had an unintended solution.
One of the issues of this challenge was that user inputted content (via the window.name
property) was able to pollute the prototype of a heavily relied upon object by using the __proto__
property.
Prototype pollution can be a serious problem, especially in server-side authentication flows where attackers can attack to mutate the prototype to escalate their privileges. Several well known libraries such as Lodash have also been caught out as recently as this year making this a very current and real issue.
In the case of this challenge, it was interesting to see the vulnerability exposed client-side and is yet another clear warning for website maintainers to always sanitize and validate any user input, no matter how inconspicuous!
Suggestions
Getting similar to previous ones now!
- Where possible, always use well tested third party sanitizer libraries which cover all possible inputs. Avoid custom sanitizers as it is very easy miss something.
- Always use the latest versions and patches of third party libraries. Be sure to regularly audit your libraries and their dependencies using tools like retire.js or snyk.
- Where performing custom object assignment, merging or otherwise, ensure that you are deny-listing malicious keys such as
__proto__
,constructor
, and any variations thereof which might allow an attacker to change the intended property values of an object.
6. Parantheses-less XSS Attacks In Strict CSP
The final learning point from the Google CTF was the discovery of parentheses-less XSS attacks. I recommend you check out the below Medium article by the challenge creator @terjanq.
Arbitrary Parentheses-less XSS. against strict CSP policies | by terjanq | Medium
terjanq γ» γ»
Medium
Ultimately what we learn is that even in a setting as restricted as a JSONP callback, where almost all characters have been restricted, it is still possible to execute arbitrary XSS. In fact, there are several different attack payloads depending on the situation that can be used - check out this GitHub repo of example payloads.
What this shows is that even in restricted content security policy (CSP) situations, even the smallest XSS vulnerability can be exploited and escalated to an arbitrary attack.
Suggestions
- Ensure no XSS exploits possible on your site (to the best of your ability!). Check out tools like OWASP ZAP to help with discovering issues - be sure to always have the permission of the target site first before running any penetration tools!
- Perform strict validation on potential user input. In this challenge, restricting the allowed JSONP callback values to a defined enum of strings would have prevented the exploit.
That's all for now folks! Hope it made for interesting reading. π
Did you take part in the Google CTF this year? If so, what was your favourite challenge? What security points did you learn? I would love to hear your comments, ideas and suggestions - drop a note in the section below.
Till next time, stay secure! π€
Top comments (1)
Great writeup Craig. Really enjoyed it.