Note
Timeline of events
- 2022/08/22 22:23 (UTC): Private disclosure through hackerone with high severity
- 2022/08/23 22:08 (UTC): Acknowledgement by Discord
- 2022/08/24 19:57 (UTC): Remediation by Discord + escalation of severity to critical
- 2022/08/24 21:11 (UTC): Confirmed remediation
- 2022/08/24 21:30 (UTC): Resolution of the incident report and bounty reward
- 2022/08/24 21:38 (UTC): Request for disclosure
- 2022/09/23 21:39 (UTC): Report disclosure
Execution of guild-based application commands that are not deployed to target server causing unredeemable privilege escalation.
Note
I'm writing this article, blog, gist, call it what you want, with the general public and a frequent Discord user in mind. If you are just interested in the juicy technical details: Sorry for all the fluff! Maybe the TL;DR sections and paragraphs preceding code blocks will be enough!
TL;DR: I have a bot on two servers, but only one server has the /pet command.
Cerberus is a small Discord app project of mine to further my understanding of redis and prisma. You pet the hell hound regularly, gain its trust and are in turn rewarded with a silly story line of discovering the personality of the different heads.
As such, the bot features a /pet command which was just deployed to the discord.js server as a fun community interaction (I would later discover that people really liked petting fictional hell beasts - who knew!).
Along with this "gaming" side, cerberus also helped me scan for certain join and name patterns in a massive Discord community of a wildly popular influencer I moderated for at the time.
Why is this relevant? Because it meant that the /pet command was not on that second server, but just deployed in the first. This will become important soon.
TL;DR: I can use the command in the server it is not deployed to.
Discord introduces command mentions. Using the </command_name:command_id> syntax, you can embed app commands in regular Discord messages. Users interacting with this embedded element, will then open up the slash command UI and be prompted to enter options.
I added the command mention for my app's /pet command to its description, since it adds a fun discovery venue and flavour to the app's description. Note that the app's profile description can be prompted by clicking on the app's name in the member list or on messages it sent.
Curious about the rejection behaviour I clicked on the command mention in the app's profile description in the influencer's server (the one without the command!).
The command went through and I got a response. Odd.
Note
While command mentions lead to the discovery, I don't think the vulnerability I discovered was connected to this update at all, meaning it might have been present much longer than I originally anticipated. According to sources familiar with the code base, there were no updates to the affected code paths for months at the time.
TL;DR: Discord does not validate the command origin and "Oh shit, this is a problem."
So I could use a command in a server it was not deployed to. Not a big deal for /pet, a command with a response that just the command author can see and does absolutely nothing with the server. Besides, if the app is added to the server, of course its commands can be used...
Well, no. Let me explain.
Discord apps can deploy commands globally, so they can be used in every server the app is in, or to one specific community (more recently also user accounts, but this was way before that time). Some Discord bots use this for a more modular approach to commands. In a mixed bot this could mean that some people may choose to just deploy the "fun" command module with a virtual pet, fishing, and meme commands while another community chooses to use the powerful "moderation" module with commands that can manage and ban server members.
Now, what If I can use "moderation" commands in the "fun" community?
And... What if I didn't even need any permission to do that?
Oh no.
Caution
Not every Discord app is a goober you can pet and have fun with. Apps are used as powerful moderation tools with a lot of permissions! It's a common (horrible) practice to give all apps in a server a "Bot" role, to make handling permissions easy and comfortable for the community administrators. Unfortunately many communities just enable the ADMINISTRATOR permission for this role following the "This bot is so popular, surely the bot dev is not a danger" credo. Thoughts like "besides, this bot can just do fun silly things, what's the harm in giving it more permissions than it really needs" also dominate the space.
To be clear: I recommend not giving ANY app this permission and employ you to be very careful with the set of permission you give every app. Least privilege should be the default! Everything should precisely have the permission it needs to do what it's here for. Not a single bit more.
Whenever a Discord uses an application command, it's really just an authenticated HTTP POST request to the /interactions endpoint. It looks something like this:
{
"type": 2,
"application_id": "",
"guild_id": "guild1",
"channel_id": "guild1-channel",
"session_id": "",
"data": {
"version": "",
"id": "",
"guild_id": "guild2",
"name": "",
"type": 1,
"options": [],
"application_command": {
"id": "",
"application_id": "",
"version": "",
"default_permission": true,
"default_member_permissions": null,
"type": 1,
"name": "",
"description": "",
"guild_id": "guild3"
},
"attachments": []
},
"nonce": ""
}Tip
I'm using the command payload from my original report. The structure has changed a bit since, if you look at command execution today!
Now, one would expect that the guild_id "guild1" and data.guild_id "guild2" and data.application_command.guild_id "guild3" would always be the same. After all, you should only be able to use a command in the guild it is deployed in.
However in my case, guild_id "guild1" was different. It was the influencer server, the one without the command! This is an issue.
Commands can also have permission restrictions. The developer can define "default permissions" and server staff can then configure who can and cannot use specific commands via the community server's settings.
But I could use a command in a server it wasn't even deployed to. So where exactly was that permission check? Well... In the server the command is in.
TL;DR: I can ban you from a server the bot is deployed to, even if that server doesn't have a /ban command, given the app has the command in a server i can use it in.
To proof impact i thought about (and reported on) the following setup:
- Create 2 test guilds, EVIL and REGULAR
- Setup EVIL: grant the user the
ADMINISTRATORpermission (for simplicity) - Setup REGULAR: users have the permission to use slash commands (crucially, no elevated permissions, they are regular users and can use benign, fun commands)
- The test application needs to be in both guilds, EVIL and REGULAR
- Deploy a slash command to EVIL only, copy the ID of the command
- Post a message in the format
</exploit:id> - Click the embedded command (optionally fill in options) and execute it despite it not being deployed to the server.
Note
As briefly mentioned above, this really has nothing to do with the command mention, it just serves as a simple interface to interact with and demonstrate the exploit. This can be done without ever interacting with the server (sending the message with the command interaction) by crafting a payload. So steps 6 and 7 can be skipped. Such a payload can be seen below. I have omitted all keys that are irrelevant for understanding what's happening.
{
"guild_id": "REGULAR",
"channel_id": "REGULAR_CHANNEL",
"data": {
"guild_id": "EVIL",
"application_command": {
"guild_id": "EVIL"
},
},
}Discord does not validate that the various guild_id keys are the same. As long as the app is in REGULAR and has permission, i can use any commands that i deployed to EVIL (to come back to the previous example, choosing to opt into all the moderation features) in the context of REGULAR.
Note
While I didn't notice it right away, this also implies that I can redirect the permission check of commands into a server i control!
The permission check uses data.guild_id. Which I can control and can differ from the top level guild_id.
Estimating the exact impact of this is difficult, but it seems to apply to all apps with a command that does something that can be abused and have the permission to do so. I never tried exploiting this, nor have i told anyone what's going on before Discord fixed the issue. I just executed the proof of concept on a willing participant in two empty servers.
Suffice to say, Discord staff found it had enough impact to warrant raising the severity to "Critical" and awarding me with a 3,500$ bug bounty as well as the golden bug hunter badge.
TL;DR: Principle of least privilege!
I mentioned it above already. Principle of least privilege is king. Falling victim to the "not deployed variant" of this or similar exploits can easily be prevented by not giving apps (or users) permissions they do not need.
If the app does not do moderation tasks, it does not need moderation privileges.
Keep in mind that this is not just true for exploits in the platform API, as I have talked about here.
Anyone with the bot token to any bot on your server can do anything on your server that that bot can theoretically do on your server.
Tokens can be compromised.
Bot owners and their compliance may be bought.
If you are working at a high enough risk level, maybe paying a developer to build and maintain bespoke tools in-house and making the apps private is a thing you should consider.
Tip
I want to clarify again, that any app with permissions and a command to use it could've been exploited by this.
I mention private, bespoke apps here since that would prevent anyone from inviting the app to another server to redirect the permission checks to!
Think twice about who you give permissions to and if they really need them.
TL:DR: This is where I yap on for a bit, thanks for reading!
Why did it take me ages to write this article? Was Discord annoying with the disclosure? Did someone forbid me to speak about this and cause bad PR?
No.
I'm just very self-conscious about being a noob in the security industry and have difficulties understanding that even my report of an incident may be of value to someone (literally anyone).
It took years of people continuously asking where I got my golden bug hunter badge (and sticking around for the story, showing genuine interest, not just the clout chasing idolisation of badges and badge holders (which is another can of worms I'm not getting into here)) for me to actually write this down.
No, I don't think I'm some big shot security researcher.
I had enough fundamental knowledge about how the platform works and happened to stumble upon some odd behavior I applied critical thinking to.
It was a ton of luck and coincidence.
I'm really just a bit of a nerd with a lot of anxiety.
Thanks to DD for acting as a lab rat for my proof of concept and the very necessary rain check.
Thanks to everyone at discord.js that answered all my dumb questions over the years I've spent in the community as member, helper, moderator and admin. You are what got me into programming!
Thanks Discord and all involved staff for the quick acknowledgement, remediation and frictionless bounty reward! With how much bad i hear about disclosures taking ages, this was all said and done in 2 days!
PS: If you asked me about how I got my badge, this article existing is your fault.
PPS: No AI was involved in the creation of either this text or any other deliberation I made during the process (and it's sad that that's no longer the default expectation).