Exploiting CVE-2019–14287 for OS Senior Design

James Luo
7 min readJan 1, 2020

Intro

CVE-2019–14287 is a sudo vulnerability that allows you to execute certain commands as root where it is not allowed. This bug was discovered in October 2019 by an Apple security researcher. Debian gave this bug a NVD severity of high This vulnerability affects systems with sudo version below 1.8.28. In the file lib/util/strtoid.c, they do a conversion from signed 64 bit to unsigned 32 bit. During this conversion, an overflow error gets introduced. The error results in the value 0xFFFFFFFF to be ignored in the system.

Context

Understanding why this vulnerability is considered to be a high severity according to Debian requires an understanding of why we use the sudoers file. The sudoers file is used to restrict certain actions that people who have sudo permission can do. This setup is common among most enterprise linux configurations. For example a typical school linux server would have an administrator who has full root control of the entire server, but some professors might have some sudo permissions to configure tools for their own class. The way an admin might want to setup this up would be to only allow the professors to run tools like pip package manager which requires sudo permissions at times. But however the professor should not be able to run commands like sudo rm -rf/* no preserve root.

All of this setup is done in a file known /etc/sudoers.d or /etc/sudoers. The setup will vary depending on the machine you are setting this up on. For Ubuntu we needed to create a file in /etc/sudoers.d/userName as shown in the figure below

Setup

The way the sudoers file is setup is designed so that we give the user a right to execute some sort of limited sudo. For the Ubuntu setup, the configuration allows the user hackman to execute all commands as root except spawning a bash shell. The key here is that the vulnerable user has to be given access to run their command as any arbitrary user. This is where the “ALL” keyword comes into play. If we have “ALL” for our user in our visudo file, then we will be able to run the designated command as root even if inside the file we are specifically don’t we aren’t allowed to. The reason that we need the “ALL” is that if, for instance, instead of “ (ALL, !root) ALL” we had “(!root) ALL”, then our exploit would not work. This is because in the system calls that sudo uses (setreuid and setresuid) to change privilege and users; they will actually return as root for sudo to run the function as. So, we need the “ALL” to let us run the command as any arbitrary user (in our case root) in order for our exploit to work, otherwise the visudo file will still blocked our attempt at running the command and say we are not allowed to run the command.

proof of expolit

The root cause of this exploit is a set of system calls that Sudo uses, namely setreuid and setresuid, that treat -1 as a special case input.

#if defined(HAVE_SETRESUID)
if (setresuid(details->uid, details->euid, details->euid) != 0) {
sudo_warn(U_("unable to change to runas uid (%u, %u)"),
(unsigned int)details->uid, (unsigned int)details->euid);
goto done;
}
#elif defined(HAVE_SETREUID)
if (setreuid(details->uid, details->euid) != 0) {
sudo_warn(U_("unable to change to runas uid (%u, %u)"),
(unsigned int)details->uid, (unsigned int)details->euid);
goto done;
}
#else
/* Cannot support real user ID that is different from effective user ID. */
if (setuid(details->euid) != 0) {
sudo_warn(U_("unable to change to runas uid (%u, %u)"),
(unsigned int)details->euid, (unsigned int)details->euid);
goto done;
}
#endif /* !HAVE_SETRESUID && !HAVE_SETREUID */

As seen from the code snippet above, these two system calls are used and if we take a look into the part of setresuid that causes this exploit.

// 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;
}

As you can see from this code snippet of the setresuid system call. If -1 is passed in as an input then the “set_user” function never actually gets called! So in this case the “return commit_creds(new)” is just going to return whatever the “new” object contains!

So, going deeper if we go into the “prepare_creds()” function which is where new is first instantiated we see that new by default is assigned values of 0. So in our exploit, -1 is going to return 0 (or root) to Sudo, and then Sudo will execute that command as root.

struct cred *prepare_creds(void)
{
struct task_struct *task = current;
const struct cred *old;
struct cred *new;

validate_process_creds();

new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;

kdebug("prepare_creds() alloc %p", new);

old = task->cred;
memcpy(new, old, sizeof(struct cred));

new->non_rcu = 0;
atomic_set(&new->usage, 1);
set_cred_subscribers(new, 0);
get_group_info(new->group_info);
get_uid(new->user);
get_user_ns(new->user_ns);

#ifdef CONFIG_KEYS
key_get(new->session_keyring);
key_get(new->process_keyring);
key_get(new->thread_keyring);
key_get(new->request_key_auth);
#endif

#ifdef CONFIG_SECURITY
new->security = NULL;
#endif

if (security_prepare_creds(new, old, GFP_KERNEL) < 0)
goto error;
validate_creds(new);
return new;

error:
abort_creds(new);
return NULL;
}

Patch

The patch for this exploit is very simple. They simply check to see if the value is -1, and if it is, then they will just return an error right there.

The Patch

MacOS

For MacOS all we need to do is add the user into a user group and then configure the sudoers file using visudo`in that file simple just add the user group and their permissions as shown in the configuration bellow.

For the MacOS setup, the All means that everyone in that user group can run all commands as anyone. By adding the ‘!root’, it prevents the user group 179 to execute any commands as root. With the proper setup the user would receive a message saying “Sorry username is not allowed to execute (command) as root”.

When we built this exploit on the MacOS we used a setup that will prevent any commands to be executed as sudo. When we run the payload “sudo -u#-1 echo hi” This commands executes without any issues. But when we try to check what user we are using “sudo -u#-1 whoami”, we are not root. But instead we become an arbitrary user as seen in figure 1. If we were to try to spawn a root shell using “sudo -u#-1 /bin/zsh” we spawn an unknown uid which has no power on the system as shown in figure 2.

Figure 1
Figure 2

When digging into the MacOS sudo code, we cross compared it with the Linux source code for sudo. When comparing the two OS kernels we found out that the MacOS version does not use the system call setreuid(), which is the root cause of the issue. As a result the MacOS kernel does not seem to be affected by this exploit.

Lessons Learned

During the investigation of this process we learned a ton about security, and how terminal commands really work with the OS. We also learned a ton looking into open-source codebases and trying to track the root cause down by jumping through different functions and variables till we get to what we were looking for. Since our PoC was just one line of “code”, it was really interesting to see how that affected the Sudo program itself. We learned that just a simple non-check can have huge consequences like our CVE.

In the case of this vulnerability, the people in charge of Sudo did not take into account the “-1” input of the system calls that they were using; and in turn did not think to check against this value in their code, resulting in this exploit.

Another issue that we could spend time investigating is the way this simple “-1” vulnerability work as it could lead to other issues of the same type in the system. The reason for that is because of how the patch was considered to be a hard coded value of “-1” and 4294967295. In the future we believe that there will be another CVE that can be related to this current one due to its nature.

Sources

https://www.sudo.ws/alerts/minus_1_uid.html(Sudo incident report)

http://man7.org/linux/man-pages/man2/setresuid.2.html(man page for setreuid)

https://programmer.ink/think/cve-2019-14287-linux-sudo-vulnerability-analysis.html(a break down of the file changes)

https://blog.0xbbc.com/2019/10/cve-2019-14287-local-privilege-escalation/ (a break down of how the exploit works and what code it hits)

https://security-tracker.debian.org/tracker/CVE-2019-14287(Debian CVE Alert)

https://opensource.apple.com/source/sudo/sudo-86.50.1/sudo/src/exec.c.auto.html(MacOS source code)

--

--

James Luo

Student studying computer science with interesting iOS and computer security Looking for a Full Time SWE position in the Bay Area https://jluo117.github.io