2021 年 1 月 26 日,Qualys Research Labs在 sudo 发现了一个缺陷。sudo 解析命令行参数的方式时,错误的判断了截断符,从而导致攻击者可以恶意构造载荷,使得sudo发生堆溢出,该漏洞在配合环境变量等分配堆以及释放堆的原语下,可以致使本地提权。
环境版本
• ubuntu 20.04
• sudo-1.8.31p2
采用下述命令进行编译安装
cd ./sudo-SUDO_1_8_31p2
mkdir build
./configure --prefix=/home/pwn/sudo CFLAGS=”-O0 -g"
make && make install
./sudoedit -s '\' 11111111111111111111111111111111111111111111111111111111111111111111
执行上述POC执行sudoedit会出现malloc():invalid size的字样,这是典型的堆溢出后导致的异常。
源码分析
set_cmnd函数
File: plugins\sudoers\sudoers.c
800: static int
801: set_cmnd(void)
802: {
...
819: if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
845:
846:
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 = '';
使用POC的例子对漏洞进行说明
漏洞原理图
【---- 帮助网安学习,以下所有学习资料免费领!领取资料加 [email protected]:yj009991,备注 “安全脉搏” 获取!】
① 网安学习成长路径思维导图
② 60 + 网安经典常用工具包
③ 100+SRC 漏洞分析报告
④ 150 + 网安攻防实战技术电子书
⑤ 最权威 CISSP 认证考试指南 + 题库
⑥ 超 1800 页 CTF 实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP 客户端安全检测指南(安卓 + IOS)
因此漏洞点在于在进入set_cmnd函数时需要对转义字符进行转义,但是函数却没有判断转义字符作为参数末尾的情况,即\ + \x00
parse_args函数用于反转义,即参数中若存在转义字符,会在每个转义字符之前增加一个\
File: src\parse_args.c
592: if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
593: char **av, *cmnd = NULL;
594: int ac = 1;
595:
596: if (argc != 0) {
597:
598: char *src, *dst;
599: size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) +
600: strlen(argv[argc - 1]) + 1;
601:
602: cmnd = dst = reallocarray(NULL, cmnd_size, 2);
603: if (cmnd == NULL)
604: sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
605: if (!gc_add(GC_PTR, cmnd))
606: exit(1);
607:
608: for (av = argv; *av != NULL; av++) {
609: for (src = *av; *src != ''; src++) {
610:
611: if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
612: *dst++ = '\\';
613: *dst++ = *src;
614: }
615: *dst++ = ' ';
616: }
617: if (cmnd != dst)
618: dst--;
619: *dst = '';
620:
621: ac += 2;
622: }
这也是为什么set_cmnd函数需要对参数进行转义,因此若先经过parse_args函数进行反转义,后经过set_cmnd函数进行转义,那么sudo是不会出现漏洞情况的
绕过检验
那么如何绕过set_cmnd函数直接进入parse_args函数,才是漏洞能够被成功触发的关键因素
首先是如何才能过进入set_cmnd函数,sudo会经过两重检测
sudo_mode需要具有MODE_RUN、MODE_EDIT或者MODE_CHECK的标志位
sudo_mode需要具有MODE_SHELL或者MODE_LOGIN_SHELL的标志位
File: plugins\sudoers\sudoers.c
...
819: if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
858: if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
想要获得MODE_SHELL的标志位,则需要设置-s参数,此时通过 SET(flags, MODE_SHELL),将flag设置上MODE_SHELL,并且默认的mode是为NULL,因此设置-s参数可以使得flag即设置MODE_SHELL又设置MODE_RUN。
File: src\parse_args.c
479: case 's':
480: sudo_settings[ARG_USER_SHELL].value = "true";
481: SET(flags, MODE_SHELL);
482: break;
...
534: if (!mode)
535: mode = MODE_RUN;
536: }
但是若使用sudo -s,那么就会导致flag即设置MODE_SHELL又设置MODE_RUN,就会进入parse_args函数的流程,该流程会把所有非字母数字的字符前方增加一个'\',那么就会导致我们无法构造'' + '\x00'的漏洞字符,因此想要漏洞利用成功,我们不需要程序进入set_cmd函数,但是不能进入parse_args函数
File: src\parse_args.c
592: if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
...
608: for (av = argv; *av != NULL; av++) {
609: for (src = *av; *src != ''; src++) {
610:
611: if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
612: *dst++ = '\\';
613: *dst++ = *src;
614: }
...
622: }
在parse_args函数的开头,会检测是以sudo还是以sudoedit进行调用,若使用sudoedit调用,那么会直接给mode设置上MODE_EDIT,从而绕过了mode==NULL时,需要将flag设置为MODE_RUN,因此使用sudoedit -s,可以使得flag即设置MODE_EDIT又设置MODE_SHELL
File: src\parse_args.c
...
265: proglen = strlen(progname);
266: if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
267: progname = "sudoedit";
268: mode = MODE_EDIT;
269: sudo_settings[ARG_SUDOEDIT].value = "true";
270: }
想要进入set_cmnd第二条路径就是flag设置为MODE_EDIT | MODE_SHELL,这样的输入就能够绕过parse_args函数而禁止进入set_cmd函数,这也是为什么sudo的堆溢出,需要使用sudoedit -s触发,而不是sudo -s
File: plugins\sudoers\sudoers.c
...
819: if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
858: if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
漏洞利用分析
由于程序存在一个明显的堆溢出漏洞,因此需要梳理一下堆溢出如何进行利用。
• 找到一个堆块,该堆块的值会影响程序执行的流程,这里称之为可利用堆块。
• 找到可以随意控制堆块位置的操作,将漏洞函数申请的堆块部署在可利用堆块的上方,当堆溢出触发时,可以将可利用堆块的值被改写成我们预期的值。
可利用堆块
nss是用于解析和获取不同类型的名称信息,例如如何通过用名称去获取用户信息,在sudo需要获取用户信息时则需要调用nss。
在使用nss去获取信息时,其实是通过不同的动态链接库去执行相应的行为,而这些库的文件名则存在于/etc/nsswitch.conf的配置文件中
例如想要查询passwd文件则需要用到libnss_files.so与libnss_systemed.so
那么如何加载这些动态链接库则需要依赖于nss_load_library函数,而且这些相关信息都被存放在service_user结构体中,而该结构体是存放在堆内存中的。
接着得先研究该结构体的值是否会影响程序的执行流程,代码如下。
File: nsswitch.c
327: static int
328: nss_load_library (service_user *ni)
329: {
330: if (ni->library == NULL)
331: {
332:
337: static name_database default_table;
338: ni->library = nss_new_service (service_table ?: &default_table,
339: ni->name);
340: if (ni->library == NULL)
341: return -1;
342: }
343:
344: if (ni->library->lib_handle == NULL)
345: {
346:
347: size_t shlen = (7 + strlen (ni->name) + 3
348: + strlen (__nss_shlib_revision) + 1);
349: int saved_errno = errno;
350: char shlib_name[shlen];
351:
352:
353: __st***y (__st***y (__st***y (__st***y (shlib_name,
354: "libnss_"),
355: ni->name),
356: ".so"),
357: __nss_shlib_revision);
358:
359: ni->library->lib_handle = __libc_dlopen (shlib_name);
上述代码有个非常关键的点在于,程序会使用__libc_dlopen打开shalib_name指定的动态链接库,而shalib_name是通过ni->name进行一系列的拼接得到,而ni->name则是存放在结构体service_user *ni中的,该结构体又是存放在堆内存中的。那么我们就找到了关键的值ni->name,它是能够完成修改程序执行流程的关键变量。
举个例子,例如我们将ni->name修改为X/test,那么最后拼接的结果会得到libnss_X/test.so,那么如果我们在当前目录下新建一个libnss_X并且在该目录中创建一个test.so的动态链接库,那么sudo就会加载并执行我们动态链接库中的代码。至此我们找到利用的第一个关键因素,可利用堆块。
布置堆块的操作
由于我们已经找到了可利用的堆块,如果能够将堆溢出的堆块部署在可利用堆块的上方,在利用堆溢出修改ni->name,即可完成任意代码执行的效果。
在sudo的main函数中,会执行setlocate函数。setlocale 是一个用于设置程序的区域设置(locale)的函数,在许多编程语言和操作系统中都有对应的实现。
区域设置是指程序在运行时所采用的语言、地区、日期格式、货币符号等相关信息的集合。通过设置区域设置,程序可以根据不同的地区和语言环境来适应本地化需求。
export [email protected]
而在setlocal函数中涉及十分多的堆块分配与释放的操作,当调用setlocal(LC_ALL,"")时,程序会通过环境变量设置的值去搜索区域设置的值,而环境变量的搜索则依靠_nl_find_locale函数。
_nl_find_locale函数
File: locale\findlocale.c
101: struct __locale_data *
102: _nl_find_locale (const char *locale_path, size_t locale_path_len,
103: int category, const char **name)
104: {
...
184:
197: mask = _nl_explode_name (loc_name, &language, &modifier, &territory,
198: &codeset, &normalized_codeset);
199: if (mask == -1)
200:
201: return NULL;
202:
205: locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category],
206: locale_path, locale_path_len, mask,
207: language, territory, codeset,
208: normalized_codeset, modifier,
209: _nl_category_names_get (category), 0);
210:
211: if (locale_file == NULL)
212: {
213:
215: locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category],
216: locale_path, locale_path_len, mask,
217: language, territory, codeset,
218: normalized_codeset, modifier,
219: _nl_category_names_get (category), 1);
220: if (locale_file == NULL)
221:
222: return NULL;
223: }
}
_nl_make_l10nflist**函数**
_nl_make_l10nflist会根据我们传入的值进行堆块的分配。
File: intl\l10nflist.c
150: struct loaded_l10nfile *
151: _nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list,
152: const char *dirlist, size_t dirlist_len,
153: int mask, const char *language, const char *territory,
154: const char *codeset, const char *normalized_codeset,
155: const char *modifier,
156: const char *filename, int do_allocate)
157: {
...
165:
166: abs_filename = (char *) malloc (dirlist_len
167: + strlen (language)
168: + ((mask & XPG_TERRITORY) != 0
169: ? strlen (territory) + 1 : 0)
170: + ((mask & XPG_CODESET) != 0
171: ? strlen (codeset) + 1 : 0)
172: + ((mask & XPG_NORM_CODESET) != 0
173: ? strlen (normalized_codeset) + 1 : 0)
174: + ((mask & XPG_MODIFIER) != 0
175: ? strlen (modifier) + 1 : 0)
176: + 1 + strlen (filename) + 1);
177:
...
292: }
setlocale**函数**
setlocale函数总体操作则是读取环境变量的值获取区域设置的值,根据区域设置的值分配堆块大小,若其中存在不符合区域值的规范,则会将所有先前申请的堆块都释放掉。
File: locale\setlocale.c
334: while (category-- > 0)
335: if (category != LC_ALL)
336: {
337: newdata[category] = _nl_find_locale (locale_path, locale_path_len,
338: category,
339: &newnames[category]);
340:
...
364: else
365: {
366: newnames[category] = __strdup (newnames[category]);
367: if (newnames[category] == NULL)
368: break;
369: }
...
393: if (category != LC_ALL && newnames[category] != _nl_C_name
394: && newnames[category] != _nl_global_locale.__names[category])
395: free ((char *) newnames[category]);
因此可以通过区域值去控制堆块的大小,接着在最后设置一个错误的区域值去控制堆块的位置,至此我们找到可控制堆块的操作。
LC_IDENTIFICATION = [email protected] #若长度为0x10,则malloc(0x10) LC_MEASUREMENT = [email protected],#若长度为0X20,则malloc(0x20) LC_TELEPHONE = XXXX #不符合区域值的规范,则会调用free()
exp的分析
由于我们需要控制server_user的堆块,因此需要知道该堆块的大小为多少,通过调试可知是0x40的堆块,因此利用setlocate多释放几个0x40的堆块,那么server_user就会使用到我们所释放的堆块。
紧接着将漏洞堆块分配到server_user堆块的上方,由于server_user的堆块是我们自己构建的,因此只需要在释放该堆块的同时也释放漏洞堆块即可,并且漏洞堆块的申请可是根据参数的长度所设置的
将设置区域值的函数设置为堆块分配与释放的原语,使用@后面的字符控制堆块的大小
使用错误的区域值进行堆块的释放
最后就是如何填充到可利用堆块,这里使用堆溢出,并且在环境变量中构造填充字符串,使得漏洞堆块可以覆盖掉可利用堆块的内容值,但这里需要注意的是,我们需要将ni->library中用\x00填充,而\x00是无法直接输入到环境变量中的,因此需要再次观察漏洞函数是如何拷贝字符的。根据代码分析可知,只要''后紧跟着'\x00',那么我们就能将\x00的值直接拷贝的堆内存中。紧接着将ni->name修改为我们认为构造的动态链接库即可。
File: plugins\sudoers\sudoers.c
866: if (from[0] == '\\' && !isspace((unsigned char)from[1]))
867: from++;
868: *to++ = *from++;
869: }
设置多个环境变量使得内存存在多个'' + '\x00',从而使用'\x00'去覆盖堆的内存值。
演示效果如下
漏洞的修复则是将MODE_EDIT的标志位进行了额外的判断,并且在''后面增加了对''的校验
--- a/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700
+++ b/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700
@@ -547,7 +547,7 @@
- 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)) {
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] != '' &&
+ !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 = '';
Sudo堆溢出攻击流程
首先利用setlocate作为堆块分配与释放的原语,构造出适合的堆布局确保server_user堆块尽可能贴近漏洞代码开辟出来的堆块。
其次利用堆溢出将server_user堆块的ni->name值覆盖,覆盖的值为恶意构造的动态链接库名。
最后等待动态链接库被加载执行。
Sudo堆溢出利用的限制
由于sudo堆溢出依赖堆的布局,因此不同版本的sudo或者操作系统都会影响漏洞的利用。
本文作者:合天网安实验室
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/202423.html