CVE-2020-15172 (published 2020-09-15T19:15Z)

The Act module for Red Discord Bot before commit 6b9f3b86 is vulnerable to Remote Code Execution. With this exploit, Discord users can use specially crafted messages to perform destructive actions and/or access sensitive information. Unloading the Act module with `unload act` can render this exploit inaccessible.

Severity: MEDIUM
CVSS v2 base score: 6.5 (AV:N/AC:L/Au:S/C:P/I:P/A:P)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-502
cwe_name: Deserialization of Untrusted Data
description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
extended_description: 
            It is often convenient to serialize objects for communication or to save them for later use. However, deserialized data or code can often be modified without using the provided accessor functions if it does not use cryptography to protect itself. Furthermore, any cryptography would still be client-side security -- which is a dangerous security assumption.
            Data that is untrusted can not be trusted to be well-formed.
            When developers place no restrictions on "gadget chains," or series of instances and method invocations that can self-execute during the deserialization process (i.e., before the object is returned to the caller), it is sometimes possible for attackers to leverage them to perform unauthorized actions, like generating a shell.
         
         

Fix information:
repo_url: https://github.com/zephyrkul/FluffyCogs
commit_details: repo_url: https://github.com/zephyrkul/FluffyCogs
author: zephyrkul
author_date: 2020-09-09 08:02:49-06:00
msg: [act] don't trigger on disabled cmds
repository: repo_url: https://github.com/zephyrkul/FluffyCogs
repo_name: Zephyrkul/FluffyCogs
description: Cogs for Red V3
repo_language: Python
owner: zephyrkul
file_changes: filename: act.py
change_type: ModificationType.MODIFY
diff: @@ -1,16 +1,19 @@
-import itertools
+import functools
 import random
+import re
 from typing import Union
 
 import aiohttp
 import discord
 import inflection
-from redbot.core import Config, checks, commands
+from redbot.core import bot, Config, checks, commands
 from redbot.core.i18n import get_locale
 from redbot.core.utils.chat_formatting import italics
 
 from .helpers import *
 
+fmt_re = re.compile(r"{(?:0|user)(?:\.([^\{]+))?}")
+
 
 class Act(commands.Cog):
     """
@@ -26,7 +29,7 @@ class Act(commands.Cog):
     async def red_delete_data_for_user(self, *, requester, user_id):
         pass  # No data to delete
 
-    def __init__(self, bot):
+    def __init__(self, bot: bot.Red):
         super().__init__()
         self.bot = bot
         self.config = Config.get_conf(self, identifier=2_113_674_295, force_registration=True)
@@ -34,7 +37,7 @@ class Act(commands.Cog):
         self.config.register_guild(custom={})
         self.try_after = None
 
-    async def initialize(self, bot):
+    async def initialize(self, bot: bot.Red):
         # temporary backwards compatibility
         key = await self.config.tenorkey()
         if not key:
@@ -42,8 +45,20 @@ class Act(commands.Cog):
         await bot.set_shared_api_tokens("tenor", api_key=key)
         await self.config.tenorkey.clear()
 
+    @staticmethod
+    def repl(target: discord.Member, match: re.Match):
+        if attr := match.group(1):
+            print(attr)
+            if attr.startswith("_") or "." in attr:
+                return str(target)
+            try:
+                return str(getattr(target, attr))
+            except AttributeError:
+                return str(target)
+        return str(target)
+
     @commands.command(hidden=True)
-    async def act(self, ctx, *, target: Union[discord.Member, str] = None):
+    async def act(self, ctx: commands.Context, *, target: Union[discord.Member, str] = None):
         """
         Acts on the specified user.
         """
@@ -87,7 +102,8 @@ class Act(commands.Cog):
             action.insert(iverb + 1, target.mention)
             message = italics(" ".join(action))
         else:
-            message = message.format(target, user=target)
+            assert isinstance(message, str)
+            message = fmt_re.sub(functools.partial(self.repl, target), message)
 
         # add reaction gif
         if self.try_after and ctx.message.created_at < self.try_after:
@@ -131,7 +147,7 @@ class Act(commands.Cog):
 
     @commands.group()
     @checks.is_owner()
-    async def actset(self, ctx):
+    async def actset(self, ctx: commands.Context):
         """
         Configure various settings for the act cog.
         """
@@ -139,7 +155,7 @@ class Act(commands.Cog):
     @actset.group(aliases=["custom"], invoke_without_command=True)
     @checks.admin_or_permissions(manage_guild=True)
     @commands.guild_only()
-    async def customize(self, ctx, command: str.lower, *, response: str = None):
+    async def customize(self, ctx: commands.Context, command: str.lower, *, response: str = None):
         """
         Customize the response to an action.
 
@@ -148,13 +164,19 @@ class Act(commands.Cog):
         """
         if not response:
             await self.config.guild(ctx.guild).clear_raw("custom", command)
+            await ctx.tick()
         else:
             await self.config.guild(ctx.guild).set_raw("custom", command, value=response)
-        await ctx.tick()
+            await ctx.send(
+                fmt_re.sub(functools.partial(self.repl, ctx.author), response),
+                allowed_mentions=discord.AllowedMentions(users=False),
+            )
 
     @customize.command(name="global")
     @checks.is_owner()
-    async def customize_global(self, ctx, command: str.lower, *, response: str = None):
+    async def customize_global(
+        self, ctx: commands.Context, command: str.lower, *, response: str = None
+    ):
         """
         Globally customize the response to an action.
 
@@ -170,7 +192,7 @@ class Act(commands.Cog):
     @actset.group(invoke_without_command=True)
     @checks.admin_or_permissions(manage_guild=True)
     @commands.guild_only()
-    async def ignore(self, ctx, command: str.lower):
+    async def ignore(self, ctx: commands.Context, command: str.lower):
         """
         Ignore or unignore the specified action.
 
@@ -189,7 +211,7 @@ class Act(commands.Cog):
 
     @ignore.command(name="global")
     @checks.is_owner()
-    async def ignore_global(self, ctx, command: str.lower):
+    async def ignore_global(self, ctx: commands.Context, command: str.lower):
         """
         Globally ignore or unignore the specified action.
 
@@ -205,7 +227,7 @@ class Act(commands.Cog):
 
     @actset.command()
     @checks.is_owner()
-    async def tenorkey(self, ctx):
+    async def tenorkey(self, ctx: commands.Context):
         """
         Sets a Tenor GIF API key to enable reaction gifs with act commands.
 
@@ -225,20 +247,14 @@ class Act(commands.Cog):
         await ctx.maybe_send_embed("\n".join(instructions))
 
     @commands.Cog.listener()
-    async def on_message(self, message):
-        if message.author.bot:
+    async def on_command_error(
+        self, ctx: commands.Context, error: commands.CommandError, unhandled_by_cog: bool = False
+    ):
+        if ctx.command == self.act:
             return
-
-        ctx = await self.bot.get_context(message)
-        if ctx.prefix is None or not ctx.invoked_with.replace("_", "").isalpha():
+        if isinstance(error, commands.UserFeedbackCheckFailure):
+            # UserFeedbackCheckFailure inherits from CheckFailure
             return
-
-        if ctx.valid and ctx.command.enabled:
-            try:
-                if await ctx.command.can_run(ctx):
-                    return
-            except commands.errors.CheckFailure:
-                return
-
-        ctx.command = self.act
-        await self.bot.invoke(ctx)
+        elif isinstance(error, (commands.CheckFailure, commands.CommandNotFound)):
+            ctx.command = self.act
+            await ctx.bot.invoke(ctx)

programming_language: Python