The sudo bug

Proper authorization is a crucial step in managing any organization, from smallest to largest. Usually, it is done by the Principle of Least Privilege, which says that any user should have just enough privileges necessary to perform their job, and nothing more.

In most Unix-based operating systems, the root user has the highest privileges. Anyone having access to this user account can virtually do anything in the system without questions asked. However, there is another way of running commands, which generally require root privileges, and this is done by the sudo keyword, which is short for Superuser do. Accounts that can run sudo commands are called the sudoers. Imagine what would happen if this command contained an exploitable bug?

Exactly this happened in January 2021, when a report was published about a heap-based buffer overflow in sudo, which was present in the command since July 2011. This article explains what happened, how it affects you, and what you should do about it.

CC2.5 image by xkcd

What happened?

In January 2021, Qualys released a report that they found a heap-based buffer overflow in sudo, which was introduced almost 10 years ago and affects all legacy versions from 1.8.2 to 1.8.31p2 and all stable versions from 1.9.0 to 1.9.5p1. They stated that the overflow was exploitable by any local user without authentication. The report contained three different exploits for the vulnerability, with which full root privileges could be obtained on Ubuntu 20.04, Debian 10, and Fedora 33 operating systems. We examined the vulnerability on a Ubuntu 20.04.1 machine, and it was, in fact, exploitable. But where is the bug exactly?

Sudo can be executed with several options that slightly modify its behavior.

  • The -s switch opens an interactive non-login shell. It does this by setting a flag called MODE_SHELL. This means that the current user becomes root (if they have the permission to do so), and the working directory stays the same. If you executed sudo -s in the directory /etc, you will stay there, but now as root.
  • The -i switch opens an interactive login shell. This option sets both MODE_SHELL and MODE_LOGIN_SHELL. The difference here is that regardless of where you executed sudo -i, you will be in the directory /root and as the user root.

These switches can be used to run commands in the name of the root user as well. Running the command sudo -s ls in the directory /etc will list the directory’s contents but does not change the user to root, while running sudo -i ls in the same directory will list the contents of /root, also without changing the current user. In both cases the MODE_SHELL flag is set, and thus the code part is reached, where all command-line arguments are concatenated, while the meta-characters in them are escaped with backslashes. Meta-characters are non-alphanumeric, and neither of the following: _, -, $.

Let’s take an example command-line input:

echo ?example

This would be escaped as echo \?example in the corresponding variable.

Are you still with me?

Later in the code, the meta-characters are unescaped, and the arguments are concatenated again into a heap-based buffer. This is done in the following code snippet:

864     for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
865         while (*from) {
866             if (from[0] == '\\' && !isspace((unsigned char)from[1]))
867                 from++;
868             *to++ = *from++;
869         }
870         *to++ = ' ';
871     }

And here is the edge-case.

  1. If an argument ends with a single backslash, then in line 866 it is in from[0], while the null terminator is in from[1].
  2. Line 867 increments from, so it points to the null terminator.
  3. Line 868 copies the null terminator to the user_args buffer, from is incremented again, so it now points after the null terminator, making it out of bounds.
  4. This way, the while loop copies out-of-bound characters into the user_args buffer.

Why doesn’t it stop, though?

Because the while loop only exits at a null byte, which would be the null terminator, but it was already skipped. So the loop will only stop at the next null byte in the memory, which’s location is unknown.

I hope you see the problem here. This code is vulnerable to a heap-based buffer overflow because the out-of-bounds characters copied into user_args were not included in its size.

Heap is the part of the memory which is used for dynamic memory allocation. For example, if you allocate memory for a variable with malloc, it will have a fixed size. If you try to insert something larger here, it will cause an overflow. This is called a heap-based buffer overflow.

At this point, you might ask the question that how on earth would a command-line argument end with a single backslash if it was escaped correctly earlier?

The catch is that the first code snippet I talked about is surrounded by the following condition:

571     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {

However, the vulnerable code with the potential buffer overflow has a slightly different condition:

819     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
858             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {

Note: the two checks are very far away from each other.

I wonder if there is a possibility to omit the escaping while still executing the input?

We need two things. First, we somehow need to set the MODE_SHELL flag along with MODE_EDIT or MODE_CHECK, to bypass the conditions on lines 819 and 858. Second, we need to reach the vulnerable code without setting MODE_RUN, so the escaping of meta-characters is omitted since the condition on line 571 is false. Given these two prerequisites, we can pass an argument to the vulnerable code that ends with a single unescaped backslash.

Typically, if the MODE_EDIT or MODE_CHECK flag is set, it automatically removes MODE_SHELL from the available flags, so setting both would end in an error, but the guys at Qualys found a loophole. They found that executing sudoedit -s automatically sets the MODE_EDIT flag but does not remove MODE_SHELL from the available flags. Additionally, it does not set MODE_RUN, so no escaping happens, but the vulnerable code snippet is reached, and thus buffer overflow can happen.

The following code snippet helps you check whether your system is susceptible.

sudoedit -s '\' `perl -e 'print "A" x 65536'`

If the output of this command is something like this:

malloc(): corrupted top size
Aborted (core dumped)

Then the overflow was successful.

That was quite a ride.

I promise we will not go deeper into it. The Qualys article shows that they developed three different exploits, which helped them obtain full root privileges on the platforms mentioned in this post earlier, but I will not cover them here. I just wanted to make their findings a little bit easier to understand for the developer community. The code snippets were taken from their article.

Scademy has you covered

Would you like to see the exploitation of the sudo bug in real life? We cover it in some of our courses related to C++ secure coding. If this is something you are interested in, check it out!

Let’s go to our next part.

How does this affect you, and what should you do?

It goes without saying that if you use any of the OS versions mentioned in this article, you should immediately update sudo to the latest version if you hadn’t already done so. As the vulnerability was fixed shortly after the article’s release, this should be a piece of cake.

The good news is that the article was released after the patch was ready, so most probably everyone who regularly updates their software already used the patched version when the report appeared in the media. Another good news is that Qualys was likely the first to find the vulnerability. When writing this post, I did not find any mentions of successful exploitations of the sudo bug.

Although, as we all know, exploitations happen without being in the media…

Secure coding lessons

So what are some of the critical lessons in this example? First, it is a good practice to always escape/filter user input right before its usage. Doing so would have prevented the different conditions used in the code for escaping and unescaping. When we look at the commit, which contains the patch for the sudo bug, we see that the developers introduced stricter flag checking. Nice!

Furthermore, the check for the end-of-word backslash (and the null terminator) was hardcoded into the condition.

973     if (from[0] == '\\' && from[1] != '\0' &&
974         !isspace((unsigned char)from[1])) {

Escaping and unescaping are still very far away from each other in the code, making it harder to maintain.

This leads to another lesson: never assume that input is well-formatted. The developers did a good job considering this one because they even put the lesson in the commit message. 😉

“Don’t assume the sudo front-end is sending reasonable mode flags. These checks need to be kept consistent between the sudo front-end and the sudoers plugin."

— millert

Closing thoughts

The sudo bug is a severe problem since it was found in the code, whose sole responsibility is managing privileged access in Linux systems. How paradoxical, as Linux is widely known for its outstanding security. The only reason that makes it less severe is that it is a local only attack, as stated in this post, which means that the attacker must have access to the vulnerable computer and be able to run programs on it. It is not remotely exploitable without authentication.

But still, the sudo bug is an excellent example of why security is an essential factor while developing software.

comments powered by Disqus