Hacked Discord - Bookmarklet Strikes Back
Discord accounts are getting hacked. This is my analysis of how most recent bookmarklet attacks work, with guidelines on what Discord can do to mitigate these attacks.
For the past couple of months, I've been hearing about increasing numbers of account takeover attacks in the Discord community. Discord has somehow become a de facto official messenger application among the cryptocurrency community, with new channels oriented around NFTs, popping up like mushrooms.
Hacking Discord accounts has suddenly become a very lucrative business for cybercriminals, who are going in for the kill, to make some easy money. They take over admin accounts in cryptocurrency-oriented communities to spread malware and launch further social engineering attacks. My focus is going to be purely on Discord account security, which should be of concern to everyone using Discord.
In recent weeks I thought the attackers are using some new reverse-proxy phishing techniques to hijack WebSocket sessions with similar tools to Evilginx, but in reality the hacks, I discovered, are much easier to execute than I anticipated.
In this post I will be explaining how the attacks work, what everyone can do to protect themselves and more importantly what Discord can do to mitigate such attacks.
Please bear in mind that this post covers my personal point of view on how I feel the mitigations should be implemented and I am well aware that some of you may have much better ideas. I encourage you to contact me @mrgretzky or at email@example.com if you feel I missed anything or was mistaken.
Criticism is welcomed!
The Master Key
When you log in to your Discord account, either by entering your account credentials on the login screen, or by scanning a QR code with your Discord mobile app, Discord will send you your account token, in form of a string of data.
This token is the only key required to access your Discord account.
From now on I will refer to that token as the master token, since it works like a master key for your Discord account. That single line of text, consisting of around 70 characters, is what the attackers are after. When they manage to extract the master token from your account, it is game over and third parties can now freely access your account, bypassing both the login screen and any multi-factor authentication you may've set up.
Now that we know what the attackers are after, let's analyze the attack flow.
Attacker's main goal is to convince you in any way possible to reveal your account token, with the most likely approach being social engineering. First, attackers will create a story to set themselves up as a people you can trust, who will resolve your pressing issue, like unban your account on a Discord channel or elevate your community status.
In this thread you can read how attackers managed to convince the target to do a screen share from their computer with Chrome DevTools opened, on the side. DevTools allowed them to extract the master token, through revealing the contents of Discord's LocalStorage.
Discord now purposefully renames the
token variable, containing the master token's value, from the storage. This was done to prevent attackers from stealing master tokens, by convincing the targets to open LocalStorage through screen share.
Discord also shows a pretty warning, informing users about the risks of pasting unknown code into the DevTools console.
The renaming of
localStorage object and concealment of
token variable did not fix the issue. Attackers figured out they can force Discord window to reload and then retrieve the data before implemented mitigations are executed.
Attacker's code retrieves the victim's Discord master token and sends it to the attacker.
To try it out, open Discord website, go to the console tab in Chrome DevTools (Control+Shift+I) and paste this code into it, which is something you should never do when asked 😉. After you press Enter, you should see the popup box with your master token value in it.
Attackers will exfiltrate the token through Discord WebHooks. This is done to bypass current Content Security Policy rules as obviously
discord.com domain is allowed to be connected to from Discord client.
Another much harder approach, the attackers can take, is deploying malware onto target computer. Once you install malware on your PC the consequences can be far greater than just losing your Discord account. Just wanted to make note here that browser's LocalStorage will store the tokens in unencrypted form.
LocalStorage database files for Chrome reside in:
%LOCALAPPDATA%\Google\Chrome\User Data\<PROFILE>\Local Storage\leveldb\
Once the attacker retrieves your Discord master token, they can inject it into their browser's LocalStorage, with
token as a variable name. It can easily be done using LocalStorage Manager extension for Chrome.
Discord on reload will detect the valid token in its LocalStorage and allow full control over the hijacked account. All this is possible even with multi-factor authentication enabled on hijacked account.
There is unfortunately no definitive fix to this problem, but there are definitely ways to increase the costs for attackers and make the attacks much harder to execute.
Store Token as an
Modern web services rely heavily on communication with REST APIs and Discord is no different. REST APIs, to conform with the standard, must implement a stateless architecture, meaning that each request from the client to the server must contain all of the information necessary to understand and complete the request.
The session token, included with requests to REST API, is usually embedded within the
Authorization HTTP header. We can see that Discord app does it the same way:
For the web application to be able to include the session token within the
Authorization header server-side can also be allowed, but it should be optional. For reasons unknown to me, majority of REST API developers ignore token authorization via cookies altogether.
Server should be sending the authorization session token value as a cookie with
HttpOnly flag, in
Set-Cookie HTTP header.
Storing the session token with
The request, which sends authentication token as a cookie, could look like this:
I've noticed that Discord's functionality does not rely solely on interaction with its API, but the major part of its functionality is handled through WebSocket connections.
The WebSocket connection is established with
gateway.discord.gg endpoint, instead of the main
If the session token cookie was delivered as a response to request delivered to domain
discord.com, it would not be possible to set a cookie for domain
discord.gg due to security boundaries.
To counter that, Discord would either need to implement some smart routing allowing Discord clients to use WebSocket connections through
discord.com domain or it would have to implement authorization using one-time token with
discord.gg endpoint, once the user successfully logs in, to have
discord.gg return a valid session token as a cookie, for its own domain.
Right now Discord permits establishing WebSocket connection through
gateway.discord.gg to anyone and the authorization token validation happens during internal WebSocket messages exchange.
Ephemeral Session Tokens
Making a strict requirement for tokens to be delivered as
That's why I'd make authentication reliant on two types of tokens:
- Authentication token stored only as a cookie with
HttpOnlyflag, which will be used to authenticate with REST API and to initiate a WebSocket connection.
- Session token generated dynamically with short expiration time (few hours), accompanied by a refresh token used for creating new session tokens. Session tokens will be used only in internal WebSocket communication. WebSocket connections will allow to be established only after presenting a valid authentication token as a cookie.
If you wish to learn more about ephemeral session tokens and refresh tokens, I recommend this post.
But, wait! Attacker controls the session anyway!
Me and @buherator exchanged opinions about possible mitigations and he made a good point:
HttpOnly flag, since attacker's requests can have them included automatically with
withCredentials set to
No matter how well the tokens are protected, Discord client needs access to all of them, which means attacker executing code, in context of the app, will be able to forge valid client requests, potentially being able to change user's settings or sending spam messages to subscribed channels.
I've been thinking about it for few days and reached a conclusion that implementing the mitigations I mentioned would still be worth it. At the moment the attack is extremely easy to pull off, which is what makes it so dangerous. Ability to forge packets, impersonating a target user is not as bad as being able to completely recreate victim's session within a Discord client remotely.
With token cookie protected with
HttpOnly flag, the attacker will only be able to use the token to perform actions, impersonating the hacked user, but they will never be able to exfiltrate the token's value, in order to inject it into their own browser.
In my opinion this will still vastly lower the severity of the attack and will force the attackers to increase the complexity of the attack in technical terms, requiring knowledge of Discord API, in order to perform the attacker's tasks, once the user's session is hijacked.
Another thing to note here is that the attacker will remain in charge of the user's hijacked session only until the Discord app is closed or reloaded. They will not be able to spawn their own session to freely impersonate the hacked user at any time they want. Currently the stolen master token gives the attacker lifetime access to victim's account. Token is invalidated and recreated only when user changes their password.
It is important to note that the attackers are only able to exfiltrate the master token value, using
XMLHttpRequest and Discord's Webhooks, by sending it to Discord channels they control. Using WebHooks allows them to comply with Discord's Content Security Policy.
If it weren't for WebHooks, attackers would have to figure out another way to exfiltrate the stolen tokens. At the time of writing the article, the
connect-src CSP rules for
discord.com, which tell the browser the domains it should only allow Discord app to connect to, are as follows:
connect-src 'self' https://discordapp.com https://discord.com https://connect.facebook.net https://api.greenhouse.io https://api.github.com https://sentry.io https://www.google-analytics.com https://hackerone-api.discord.workers.dev https://*.hcaptcha.com https://hcaptcha.com https://geolocation.onetrust.com/cookieconsentpub/v1/geo/location ws://127.0.0.1:* http://127.0.0.1:*;
There seem to be other services allowed, which could be used to exfiltrate the tokens through them, like GitHub API, Sentry or Google Analytics.
Not sure if Discord client needs access to WebHooks through
XMLHttpRequest, but if it doesn't, it may've been a better choice for Discord to host WebHook handlers on a different domain than
discord.com and control access to them with additional CSP rules.
There is also one question, which remains unanswered:
Should web browsers still support bookmarklets?
As asked by @zh4ck in his tweet, which triggered my initial curiosity about the attacks.
Bookmarklets were useful in the days when it was convenient to click them to spawn an HTML popup for quickly sharing the URL on social media. These days I'm not sure if anyone still needs them.
I have no idea if letting bookmarks start with
I hope you liked the post and hopefully it managed to teach you something.
I am constantly looking for interesting projects to work on. If you think my skills may be of help to you, do reach out, through the contact page.
Until next time!
- Chrome Inspector hack https://twitter.com/LittlelemonsNFT/status/1477923368053706755
- Bookmarklet Discord hack https://twitter.com/Serpent/status/1485002655953211392
- Bookmarklet Discussion https://twitter.com/zh4ck/status/1562734600941891589
- LocalStorage Manager: https://chrome.google.com/webstore/detail/localstorage-manager/fkhoimdhngkiicbjobkinobjkoefhkap