[翻译]CVE-2021-3156:Sudo中基于堆的缓冲区溢出 (Baron Samedit)

简述


Qualys研究小组在sudo中发现了一个堆溢出漏洞,sudo是一个几乎无处不在的实用程序,可用于主要的类Unix操作系统。通过利用此漏洞,任何未经授权的用户都可以使用默认sudo配置在易受攻击的主机上获得root权限。

Sudo是一个强大的实用程序,它包含在大多数基于Unix和Linux的操作系统中。它允许用户以另一个用户的安全权限运行程序。近10年来,这个漏洞本身一直隐藏在人们的视线中。它于2011年7月引入(commit 8255ed69),在默认配置中影响从1.8.2到1.8.31p2的所有旧版本和从1.9.0到1.9.5p1的所有稳定版本。

成功利用此漏洞可使任何未经授权的用户在易受攻击的主机上获得根用户权限。Qualys安全研究人员已经能够独立验证该漏洞,开发多种漏洞变体,并在Ubuntu 20.04(Sudo 1.8.31)、Debian 10(Sudo 1.8.27)和Fedora 33(Sudo 1.9.2)上获得完整的root权限。其他操作系统和发行版也可能被利用。

Qualys研究团队确认该漏洞后,Qualys立即进行了负责任的漏洞披露,并协调sudo的作者和开源发行版公布该漏洞。

披露时间表

漏洞验证视频

请见附件
CVE-2021-3156.part1.rar (7 MB) CVE-2021-3156.part2.rar (4.4 MB)

技术细节

  • 如果在“shell”模式下执行 Sudo 以运行命令(shell-c 命令)
  • 通过-s选项,设置Sudo的MODE_SHELL标志
  • 通过-i 选项设置 Sudo 的 MODE _ shell 和 MODE _ login _ shell 标志; 然后,在 Sudo 的 main ()的开头,parse _ args ()通过串联所有命令行参数(第587-595行)和用反斜杠转义所有元字符(第590-591行)来重写 argv
------------------------------------------------------------
571     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { 
572         char **av, *cmnd = NULL; 
573         int ac = 1; 
... 
581             cmnd = dst = reallocarray(NULL, cmnd_size, 2); 
... 
587             for (av = argv; *av != NULL; av++) { 
588                 for (src = *av; *src != '\0'; src++) { 
589                     /* quote potential meta characters */ 
590                     if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$') 
591                         *dst++ = '\\'; 
592                     *dst++ = *src; 
593                 } 
594                 *dst++ = ' '; 
595             } 
... 
600             ac += 2; /* -c cmnd */ 
... 
603         av = reallocarray(NULL, ac + 1, sizeof(char *)); 
... 
609         av[0] = (char *)user_details.shell; /* plugin may override shell */ 
610         if (cmnd != NULL) { 
611             av[1] = "-c"; 
612             av[2] = cmnd; 
613         } 
614         av[ac] = NULL; 
615  
616         argv = av; 
617         argc = ac; 
618     } 
------------------------------------------------------------

稍后,在sudoers\u policy\u main()中,set\cmnd()将命令行参数连接到基于堆的缓冲区“user\u args”(第864-871行)中,并取消对元字符(第866-867行)的scape,“用于sudoers匹配和日志记录目的”:

------------------------------------------------------------
819     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { 
... 
852             for (size = 0, av = NewArgv + 1; *av; av++) 
853                 size += strlen(*av) + 1; 
854             if (size == 0 || (user_args = malloc(size)) == NULL) { 
... 
857             } 
858             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { 
... 
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                 } 
... 
884             } 
... 
886     } 
------------------------------------------------------------

不幸的是,如果命令行参数以单个反斜杠字符结尾,则:

  • 在第866行,“ from [0]”是反斜杠字符,“ from [1]”是参数的 null 结束符(即,不是空格字符) ;
  • 在第867行,“ from”递增,并指向null 结束符;
  • 在第868行,null 结束符被复制到“ user _ args”缓冲区,“ from”再次递增并指向 null 结束符之后的第一个字符(即超出参数的边界) ;
  • 第865-869行的“ while”循环读取超出界限的字符并将其复制到“ user _ args”缓冲区。

换句话说,set _ cmnd ()容易受到基于堆的缓冲区溢出的影响,因为复制到“ user _ args”缓冲区的界外字符没有包含在其大小中(计算在 lines852-853)。

然而,理论上,任何命令行参数都不能以单个反斜杠字符结束: 如果设置了 MODE _ shell 或 MODE _ login _ shell (第858行,这是到达易受攻击代码的必要条件) ,那么设置了 MODE _ shell (第571行) ,并且 parse _ args ()已经转义了所有元字符,包括反斜杠(即,它用第二个反斜杠对每个反斜杠进行转义)。
但实际上,set_cmnd()中的弱势代码和parse_args()中的转义代码周围的条件略有不同:

------------------------------------------------------------
819     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { 
... 
858             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { 
------------------------------------------------------------

对比:

------------------------------------------------------------
571     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { 
------------------------------------------------------------

我们的问题是: 我们是否可以设置 MODE _ shell 和 MODE _ edit 或 MODE _ check (以达到易受攻击的代码) ,而不设置默认的 MODE _ run (以避免转义代码) ?

答案似乎是否定的: 如果我们设置 MODE _ edit (- e 选项,第361行)或 MODE _ check (- l 选项,第423和519行) ,然后 parse _ args ()从“ valid _ flags”(第363和424行)中删除 MODE _ shell,如果我们指定无效的标志如 MODE _ shell (第532-533行) ,则退出时将出现错误:

------------------------------------------------------------
358                 case 'e': 
... 
361                     mode = MODE_EDIT; 
362                     sudo_settings[ARG_SUDOEDIT].value = "true"; 
363                     valid_flags = MODE_NONINTERACTIVE; 
364                     break; 
... 
416                 case 'l': 
... 
423                     mode = MODE_LIST; 
424                     valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST; 
425                     break; 
... 
518     if (argc > 0 && mode == MODE_LIST) 
519         mode = MODE_CHECK; 
... 
532     if ((flags & valid_flags) != flags) 
533         usage(1); 
------------------------------------------------------------ 

但我们发现了一个漏洞:如果我们以 "sudoedit "而不是 "sudo "的方式执行Sudo,那么parse_args()会自动设置MODE_EDIT(第270行),但不会重置 "valid_flags",而且 "valid_flags "默认包括MODE_SHELL(第127行和249行)

------------------------------------------------------------
127 #define DEFAULT_VALID_FLAGS     (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL) 
... 
249     int valid_flags = DEFAULT_VALID_FLAGS; 
... 
267     proglen = strlen(progname); 
268     if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 
269         progname = "sudoedit"; 
270         mode = MODE_EDIT; 
271         sudo_settings[ARG_SUDOEDIT].value = "true"; 
272     } 
------------------------------------------------------------

因此,如果我们执行“ sudoedit-s”,然后我们同时设置 MODE _ edit 和 MODE _ shell (但不设置 MODE _ run) ,我们就避免了转义代码,达到了易受攻击的代码,并且通过一个以一个反斜杠字符结尾的命令行参数溢出了基于堆的缓冲区“ user _ args”:

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

从攻击者的角度来看,这种缓冲区溢出是理想的,原因如下:

1)攻击者控制可以溢出的“ user _ args”缓冲区的大小(串联的命令行参数的大小,在第852-854行) ;

2)攻击者独立控制溢出本身的大小和内容(我们最后的命令行参数后面跟着我们的第一个环境变量,这些变量不包括在第852-853行的大小计算中) ;

3)攻击者甚至可以向溢出的缓冲区写入空字节(每个命令行参数或者以一个反斜杠结尾的环境变量向“ user _ args”写入一个空字节,第866-868行)。

例如,在 amd64 Linux 上,下面的命令分配一个24字节的“ user _ args”缓冲区(一个32字节的堆块) ,并用“ a = a 0B = b0”(0x00623d4200613d41)覆盖下一个块的大小字段,用“ c = c 0D = d 0”(0x00643d4400633d43)覆盖其 fd 字段及其 bk 字段“ e = e0f = f0”(0x00663d4600653d45) :

------------------------------------------------------------
env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\' 
------------------------------------------------------------

--|--------+--------+--------+--------|--------+--------+--------+--------+-- 
  |        |        |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.| 
--|--------+--------+--------+--------|--------+--------+--------+--------+-- 

              size  <---- user_args buffer ---->  size      fd       bk 

解决方案

  • 鉴于该漏洞的攻击面很广,Qualys建议用户立即应用该漏洞的补丁。
  • Qualys客户可以在漏洞知识库中搜索CVE-2021-3156,以确定所有易受此漏洞影响的QID和资产。
  • 如果您不是客户,请开始您的免费Qualys VMDR试用版,以获得对CVE-2021-3156的QIDs(检测)的完整访问权限,这样您就可以识别您的易受攻击的资产。

Qualys 覆盖范围

QID 374891: Sudo Heap-based Buffer Overflow 漏洞。

该QID在vulnsigs版本VULNSIGS-2.5.90-4和Linux Cloud Agent manififest版本lx_manifest-2.5.90.4-3中可用。

仪表版

通过 VMDR Dashboard,您可以实时跟踪这个漏洞、它们所影响的主机、它们的状态和总体管理。通过为仪表板小部件启用趋势,您可以使用“ Baron Samedit | Heap-based buffer overflow Sudo”仪表板跟踪环境中的这些漏洞趋势。

查看和下载 Baron Samedit 仪表板

供应商参考资料

常见问题解答(FAQs)

哪些版本容易受到攻击?

以下版本的 sudo 易受攻击:

  • 从1.8.2到1.8.31p2的所有旧版本
  • 从1.9.0到1.9.5p1的所有稳定版本

如何测试我是否有易受攻击的版本?

  • 要测试系统是否易受攻击,请以非 root 用户登录系统。
  • 运行命令“sudoedit -s /”
  • 如果系统易受攻击,它将响应以“sudoedit:”开头的错误
  • 如果系统打了补丁,它将以一个以“usage:”开头的错误响应

1.8.2之前的版本容易受到攻击吗?

没有。见上文解释。
是否需要一个本地用户来利用这个漏洞?
是的,但是这个用户不需要是特权用户或成为sudoers列表的一部分。例如,即使是账户 "nobody "也可以利用这个问题。
为什么把这个漏洞命名为 “Baron Samedit”?
这是对Baron Samedi和sudoedit的戏称。
Qualys 研究团队是否会发布此漏洞的利用代码?
不会。

补充

TL;DR

这个洞其实非常的简单,关键在于在-s的情况下去尝试对输入的args做一些处理,其中出问题的阶段在于里面有一个所谓的escape meta chars 的东西,我还没看懂这个操作在干嘛... 但是不影响我们理解这个洞.

如果你输入sudoedit -s '\' somedata, 他会把尝试把-s后面的args弄到一块内存上去,所以它会遍历这些args,来一个一个的拷贝.

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';

里面有个if就是前面那个所谓的escape过程的,它如果碰到\ 字符,它会往后看一下是不是空格,如果不是空格,它会跳过拷贝这个\. 但是问题就来了,如果刚好这个arg只有\这一个字符,那么挨着它的是一个\0,它确实也不是空格。 所以可以跳,然后拷贝这个\0. 接着这个\0如果后面还有内容(不是\0),那它会继续拷贝. 它在拷贝之前会根据args的总长度去申请目标内存,这个内存大小是fixed. 继续拷贝额外的内容,就导致了溢出...

留个坑,我想想怎么玩....

1 Like

嗯嗯感觉Qualys研究小组是手滑发现的之后深入分析了一波 :joy: