赞
踩
sudo是Linux比较常用的命令,它允许普通用户获得临时的root身份执行特权指令,保障了Linux系统的安全。
【环境】:ubuntu18.04
【版本】:sudo-1.8.27
输入以下POC:
sudoedit -s '\' `perl -e 'print "A" x 65536'`
首先通过输入命令查看当前系统使用的版本
sudo --version
下载合适的sudo,解压并编译。
需要使用root特权启动gdb,并挂载sudoedit:
设置命令行参数,然后执行,等待崩溃,打印调用栈
根据公开的资料报道,该漏洞属于堆溢出,那么我需要找到致使溢出的代码段。依照调用栈,我首先审计位于 s u d o . c \textcolor{orange}{sudo.c} sudo.c的 m a i n \textcolor{cornflowerblue}{main} main函数源码
...
/* Fill in user_info with user name, uid, cwd, etc. */
if ((user_info = get_user_info(&user_details)) == NULL)
exit(EXIT_FAILURE); /* get_user_info printed error message */
/* Disable core dumps if not enabled in sudo.conf. */
if (sudo_conf_disable_coredump())
disable_coredump(false);
/* Parse command line arguments. */
sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
sudo_debug_printf(SUDO_DEBUG_DEBUG, "sudo_mode %d", sudo_mode);
...
代码注释也写的很明白了,重点看 p a r s e _ a r g s \textcolor{cornflowerblue}{parse\_args} parse_args函数,他会对我们输入的命令以及参数做一些预处理。
... /* 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"; } /* Load local IP addresses and masks. */ if (get_net_ifs(&cp) > 0) sudo_settings[ARG_NET_ADDRS].value = cp; /* Set max_groups from sudo.conf. */ i = sudo_conf_max_groups(); if (i != -1) { if (asprintf(&cp, "%d", i) == -1) sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); sudo_settings[ARG_MAX_GROUPS].value = cp; } /* Returns true if the last option string was "-h" */ #define got_host_flag (optind > 1 && argv[optind - 1][0] == '-' && \ argv[optind - 1][1] == 'h' && argv[optind - 1][2] == '\0') /* Returns true if the last option string was "--" */ #define got_end_of_args (optind > 1 && argv[optind - 1][0] == '-' && \ argv[optind - 1][1] == '-' && argv[optind - 1][2] == '\0') /* Returns true if next option is an environment variable */ #define is_envar (optind < argc && argv[optind][0] != '/' && \ strchr(argv[optind], '=') != NULL) /* Space for environment variables is lazy allocated. */ memset(&extra_env, 0, sizeof(extra_env)); /* XXX - should fill in settings at the end to avoid dupes */ for (;;) { /* * Some trickiness is required to allow environment variables * to be interspersed with command line options. */ if ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) {//解析参数 switch (ch) { case 'A': SET(tgetpass_flags, TGP_ASKPASS); break; #ifdef HAVE_BSD_AUTH_H case 'a': if (*optarg == '\0') usage(1); sudo_settings[ARG_BSDAUTH_TYPE].value = optarg; break; #endif case 'b': SET(flags, MODE_BACKGROUND); break; case 'C': if (strtonum(optarg, 3, INT_MAX, NULL) == 0) { sudo_warnx(U_("the argument to -C must be a number greater than or equal to 3")); usage(1); } sudo_settings[ARG_CLOSEFROM].value = optarg; break; #ifdef HAVE_LOGIN_CAP_H case 'c': if (*optarg == '\0') usage(1); sudo_settings[ARG_LOGIN_CLASS].value = optarg; break; #endif case 'D': /* Ignored for backwards compatibility. */ break; case 'E': /* * Optional argument is a comma-separated list of * environment variables to preserve. * If not present, preserve everything. */ if (optarg == NULL) { sudo_settings[ARG_PRESERVE_ENVIRONMENT].value = "true"; SET(flags, MODE_PRESERVE_ENV); } else { parse_env_list(&extra_env, optarg); } break; 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 'g': if (*optarg == '\0') usage(1); runas_group = optarg; sudo_settings[ARG_RUNAS_GROUP].value = optarg; break; case 'H': sudo_settings[ARG_SET_HOME].value = "true"; break; case 'h': if (optarg == NULL) { /* * Optional args support -hhostname, not -h hostname. * If we see a non-option after the -h flag, treat as * remote host and bump optind to skip over it. */ if (got_host_flag && !is_envar && argv[optind] != NULL && argv[optind][0] != '-') { sudo_settings[ARG_REMOTE_HOST].value = argv[optind++]; continue; } if (mode && mode != MODE_HELP) { if (strcmp(progname, "sudoedit") != 0) usage_excl(1); } mode = MODE_HELP; valid_flags = 0; break; } /* FALLTHROUGH */ case OPT_HOSTNAME: if (*optarg == '\0') usage(1); sudo_settings[ARG_REMOTE_HOST].value = optarg; break; case 'i': sudo_settings[ARG_LOGIN_SHELL].value = "true"; SET(flags, MODE_LOGIN_SHELL); break; case 'k': sudo_settings[ARG_IGNORE_TICKET].value = "true"; break; case 'K': sudo_settings[ARG_IGNORE_TICKET].value = "true"; if (mode && mode != MODE_KILL) usage_excl(1); mode = MODE_KILL; valid_flags = 0; 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; case 'n': SET(flags, MODE_NONINTERACTIVE); sudo_settings[ARG_NONINTERACTIVE].value = "true"; break; case 'P': sudo_settings[ARG_PRESERVE_GROUPS].value = "true"; break; case 'p': /* An empty prompt is allowed. */ sudo_settings[ARG_PROMPT].value = optarg; break; #ifdef HAVE_SELINUX case 'r': if (*optarg == '\0') usage(1); sudo_settings[ARG_SELINUX_ROLE].value = optarg; break; case 't': if (*optarg == '\0') usage(1); sudo_settings[ARG_SELINUX_TYPE].value = optarg; break; #endif case 'T': /* Plugin determines whether empty timeout is allowed. */ sudo_settings[ARG_TIMEOUT].value = optarg; break; case 'S': SET(tgetpass_flags, TGP_STDIN); break; case 's': sudo_settings[ARG_USER_SHELL].value = "true"; SET(flags, MODE_SHELL); break; case 'U': if (*optarg == '\0') usage(1); list_user = optarg; break; case 'u': if (*optarg == '\0') usage(1); runas_user = optarg; sudo_settings[ARG_RUNAS_USER].value = optarg; break; case 'v': if (mode && mode != MODE_VALIDATE) usage_excl(1); mode = MODE_VALIDATE; valid_flags = MODE_NONINTERACTIVE; break; case 'V': if (mode && mode != MODE_VERSION) usage_excl(1); mode = MODE_VERSION; valid_flags = 0; break; default: usage(1); } } else if (!got_end_of_args && is_envar) { /* Insert key=value pair, crank optind and resume getopt. */ env_insert(&extra_env, argv[optind]); optind++; } else { /* Not an option or an environment variable -- we're done. */ break; } } argc -= optind; argv += optind; ...
以上代码就是根据输入的命令和参数去设置一些标志位。调试的时候我给出的命令和参数就是poc,所以执行完这段代码, m o d e = M O D E _ E D I T \textcolor{orange}{mode=MODE\_EDIT} mode=MODE_EDIT, f l a g = M O D E _ S H E L L \textcolor{orange}{flag=MODE\_SHELL} flag=MODE_SHELL,记住这一点!因为这是访问到漏洞代码的先决条件。
有了以上这两个标志,就会执行到下面的代码段
... /* * For shell mode we need to rewrite argv */ if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { char **av, *cmnd = NULL; int ac = 1; 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); 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++) { /* 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 */ } ...
上面这段代码将会拼接输入的所有参数,并将其中的元字符进行转义。继续单步跟踪,跟踪到 s u d o e r s _ p o l i c y _ m a i n \textcolor{cornflowerblue}{sudoers\_policy\_main} sudoers_policy_main里面调用了 s e t _ c m n d \textcolor{cornflowerblue}{set\_cmnd} set_cmnd,发生了堆溢出其关键代码段如下
... /* set user_args */ if (NewArgc > 1) { char *to, *from, **av; size_t size, n; /* 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) { sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(-1); } 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'; } else { ...
NewArgc和NewArgv是全局变量,其值来源于 p a r s e _ a r g s \textcolor{cornflowerblue}{parse\_args} parse_args预处理后的结果。本代码段在获取size过程中没有问题,问题出现在下面给to赋值的这段代码中。请注意这两个地方:
...
for (to = user_args, av = NewArgv + 1; (from = *av); av++)
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
...
假设,当前 N e w A r g s [ 1 ] = ‘ 反 斜 杠 + 空 格 ’ \textcolor{orange}{NewArgs[1]=‘反斜杠+空格’} NewArgs[1]=‘反斜杠+空格’, N e w A r g s [ 2 ] = ‘ A A A A . . . ’ \textcolor{orange}{NewArgs[2]=‘AAAA...’} NewArgs[2]=‘AAAA...’,而 a v = N e w A r g s [ 1 ] \textcolor{orange}{av=NewArgs[1]} av=NewArgs[1]指向则根据if中的判断,条件满足,所以 f r o m + + \textcolor{orange}{from++} from++,使得这一轮while循环中,复制了一遍 N e w A r g s [ 2 ] \textcolor{orange}{NewArgs[2]} NewArgs[2]中的内容,然后到下一轮for循环时,av指向了 N e w A r g s [ 2 ] \textcolor{orange}{NewArgs[2]} NewArgs[2],if条件不满足,while中又复制一遍 N e w A r g s [ 2 ] \textcolor{orange}{NewArgs[2]} NewArgs[2]的内容,导致堆溢出。
再来看没有溢出前to指向的堆情况
溢出之后
发现next chunk(当前情况下是top chunk)的size域被覆盖了,所以后面如果有涉及到 m a l l o c \textcolor{cornflowerblue}{malloc} malloc操作且大小不在空闲堆列表中,需要从top chunk切割时自然就会报错了。
目前已知能够控制输入的参数精确覆盖堆中的各个域,但是我们仅有一次溢出的机会,没办法泄露,没办法在一个进程中重复利用,另外sudo开启了保护:
开启了ASLR、DEP和GS,所以想只利用一次溢出就能劫持程序流,我觉得很极限了,看看程序中有没有什么可利用的结构体吧,通过堆溢出覆盖掉其中的函数指针,这样就能劫持成功了。可问题来了:
只要堆中存在这种结构体,我可以通过控制输入参数,申请和释放合适大小的chunk,理论上就能溢出覆写结构体了。怎么去找这种结构体呢?我觉得比较高效的就是fuzz了。但目前我还在研究怎么实现。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。