Post

Rediscovering CVE-2021-3156 in sudo v1.8.31 with AFL++

This post contains my attempt to rediscover CVE-2021-3156 using fuzzing with AFL++. This post also shows how much good input corpus is necessary for vulnerability finding as without the 2 inputs given, this bug would not have been found by the fuzzer.

Finding Vulnerability leading to CVE-2021-3156

CVE Description

Sudo before 1.9.5p2 contains an off-by-one error that can result in a heap-based buffer overflow,
which allows privilege escalation to root via "sudoedit -s" and a command-line argument that ends
with a single backslash character.

Introduction

In this blog we will try to find the vulnerability leading to CVE-2021-3156. Originally this vulnerability was found using code audit by Qualys, but we will try to discover it using fuzzing.

NOTE: For this blog we will be using sudo v1.8.31.

Setup

We will try to find vulnerability using the AFL++ fuzzer. But, in order to understand how to setup sudo for fuzzing let’s disect the CVE decription first.

From the CVE description we can see that the vulnerability occurs when we use the sudoedit binary as opposed to sudo. Well it can be blahblahedit as long as last 4 characters are edit. We will look into why this is the case later in the blog. We also know that sudoedit requires a -s option followed by some characters that causes the crash.

Do you see the problem here? AFL++ works best with applications that read from a file or stdin. But, here we need to fuzz the command line arguments. Fortunately, AFL++ provides code for doing exactly that: argv_fuzzing.

Just add the following lines in sudo.c to enable argv fuzzing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "sudo.h"
#include "sudo_plugin.h"
#include "sudo_plugin_int.h"
#include "argv-fuzz-inl.h" // Include header file

/*
 * Local variables
 */
struct plugin_container policy_plugin;
struct plugin_container_list io_plugins = TAILQ_HEAD_INITIALIZER(io_plugins);

...

int
main(int argc, char *argv[], char *envp[])
{
    AFL_INIT_ARGV();  // enable argv_fuzzing
    int nargc, ok, status = 0;
    char **nargv, **env_add;

One thing to point out is that we will be fuzzing all the cmdline args including argv[0] and we will let the fuzzer do mutations to found the correct name that invokes sudoedit specific functionality.

Now that we have setup argv_fuzzing, let’s test it out.

1
2
# argv_fuzzing requires NULL seperated args 
echo -ne "sudo\x00-l\x00" | ./sudoedit

We expect the output to be:

1
2
3
4
5
Matching Defaults entries for faran on faran:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
 
User faran may run the following commands on faran:
    (ALL : ALL) ALL

But the output is:

1
2
sudoedit: Only one of the -e, -h, -i, -K, -l, -s, -v or -V options may be specified
usage: sudoedit [-AknS] [-C num] [-g group] [-h host] [-p prompt] [-T timeout] [-u user] file ..

Why does sudoedit still run when argv[0] is sudo?

It turns out that by default, sudo uses HAVE___PROGNAME to get the program name and not from argv[0] as shown in progname.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
oid
initprogname(const char *name)
{
# ifdef HAVE___PROGNAME
    extern const char *__progname;

    if (__progname != NULL && *__progname != '\0')
	progname = __progname;
    else
# endif
    if ((progname = strrchr(name, '/')) != NULL) {
	progname++;
    } else {
	progname = name;
    }
...

We need to remove the HAVE___PROGNAME and then it will take argv[0] as as program name. The program name is set at the very start of main() in sudo.c.

1
2
3
4
5
6
7
8
9
10
11
12
int
main(int argc, char *argv[], char *envp[])
{
    int nargc, ok, status = 0;
    char **nargv, **env_add;
    
    ...

    initprogname(argc > 0 ? argv[0] : "sudo");

    /* Crank resource limits to unlimited. */
    unlimit_sudo();

Now, the programs sudo and sudoedit will work as expected.

Before we compile the target and run the fuzzer there is one more thing to take care of. AFL++ is root owned and so is sudo. sudo behaves very differently when we a root user runs it. That is not what we want. We want to fuzz sudo being a non-root user just as sudo is used in normal case scenario.

The way sudo determines the user type is in the get_user_info() function inside sudo.c.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static char **
get_user_info(struct user_details *ud)
{
    char *cp, **user_info, path[PATH_MAX];
    unsigned int i = 0;
    mode_t mask;
    struct passwd *pw;
    
    ...

    if (user_info == NULL)
	goto oom;

    ud->pid = getpid();
    ud->ppid = getppid();
    
    ...

    ud->uid = getuid(); // hardcode to 1000
    ud->euid = geteuid();
    ud->gid = getgid(); // hardcode to 1000
    ud->egid = getegid();

The user information is get using getuid(), geteuid(), getgid(), getegid(). We hardcode the uid and gid to 1000 so that we always run sudo being a normal user.

We are finally ready to run the fuzzer.

Compilation

We compile the code using gcc version. clang version gives error while compilation.

1
2
CC=afl-gcc-fast ./configure --disable-shared
make -j8

Input

We use 2 inputs:

1
2
echo -en "sudo\x00-l\x00" > in/c1
echo -en "sudoedit\x00abc\x00" > in/c2

Running Fuzzer

We can launch as many instances as we want, although 2-4 instances are fine.

1
2
3
4
afl-fuzz -i in/ -o out -M main -- ./sudo
afl-fuzz -i in/ -o out -S s1 -- ./sudo
afl-fuzz -i in/ -o out -S s2 -- ./sudo
afl-fuzz -i in/ -o out -S s3 -- ./sudo

After almost 30 minutes we start getting crashes. Let’s see one crash file:

1
2
3
4
5
6
7
8
9
10
00000000  73 9b 73 75 64 33 17 3a  87 30 64 73 75 73 75 64  |s.sud3.:.0dsusud|
00000010  6f 73 75 64 6f 65 64 69  74 00 2d 73 6e 00 26 04  |osudoedit.-sn.&.|
00000020  00 26 26 26 26 30 38 86  73 78 4e 2f 0a 5c 00 30  |.&&&&08.sxN/.\.0|
00000030  73 7d 73 25 25 25 25 25  25 25 25 25 25 55 25 25  |s}s%%%%%%%%%%U%%|
00000040  25 78 02 30 30 74 64 73  75 73 75 64 64 6f 5c 78  |%x.00tdsusuddo\x|
00000050  30 73 6e 64 6f 1c 78 30  30 35 30 ff ff ff 00 ff  |0sndo.x0050.....|
00000060  3d 00 ff 3d 00 2d 73 6e  00 26 26 26 26 26 26 26  |=..=.-sn.&&&&&&&|
00000070  01 7f ff 3d 00 23 73 73  00 0b 01 7f ff ff 00 31  |...=.#ss.......1|
00000080  27 0a 0a 0a                                       |'...|
00000084

You might be wondering why did it crash at name s�sud3:�0dsusudosudoedit. Remember at the start of the blog I said it only checks the last 4 characters and if they are edit, the sudoedit functionality gets executed. Lets see this in code. This code lies in parse_args() function of parse_args.c file.

1
2
3
4
5
6
7
 /* First, check to see if we were invoked as "sudoedit". */
    proglen = strlen(progname);
    if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
	progname = "sudoedit";
	mode = MODE_EDIT;
	sudo_settings[ARG_SUDOEDIT].value = "true";
    }

As the last 4 characters of argv[0] are edit, progname = "sudoedit"; is set. As all the command line arguments are separated by 00. Next arg is -s. And the argument after that is what causes the crash sn&&&&&08�sxN/\. From the CVE description we know that argument ending with \ causes the crash.

1
2
malloc(): invalid size (unsorted)
Aborted

Conclusion

I hope you learned somethinng new from this blog. I know I learned a lot of things while trying to recreate this CVE.

This post is licensed under CC BY 4.0 by the author.