当前位置:   article > 正文

CVE-2021-3156 sudo堆溢出分析与利用_sudo提权cve-2021-3156分析漏洞

sudo提权cve-2021-3156分析漏洞

CVE-2021-3156

0x0 简介

sudoLinux比较常用的命令,它允许普通用户获得临时的root身份执行特权指令,保障了Linux系统的安全。

0x1 漏洞复现

【环境】:ubuntu18.04

【版本】:sudo-1.8.27

​ 输入以下POC:

sudoedit  -s '\' `perl -e 'print "A" x 65536'`
  • 1

在这里插入图片描述

0x2 漏洞分析

(1)环境搭建

首先通过输入命令查看当前系统使用的版本

sudo --version
  • 1

下载合适的sudo,解压并编译。

(2)调试

需要使用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);
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

代码注释也写的很明白了,重点看 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;
...
  • 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
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226

以上代码就是根据输入的命令和参数去设置一些标志位。调试的时候我给出的命令和参数就是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 */
	}
...
  • 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

上面这段代码将会拼接输入的所有参数,并将其中的元字符进行转义。继续单步跟踪,跟踪到 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 {
...
  • 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

NewArgcNewArgv是全局变量,其值来源于 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]))
...
  • 1
  • 2
  • 3
  • 4

假设,当前 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切割时自然就会报错了。

(3)利用

目前已知能够控制输入的参数精确覆盖堆中的各个域,但是我们仅有一次溢出的机会,没办法泄露,没办法在一个进程中重复利用,另外sudo开启了保护:

在这里插入图片描述

开启了ASLRDEPGS,所以想只利用一次溢出就能劫持程序流,我觉得很极限了,看看程序中有没有什么可利用的结构体吧,通过堆溢出覆盖掉其中的函数指针,这样就能劫持成功了。可问题来了:

  • 程序很大,结构很复杂
  • 哪些结构体在哪些时候会创建,又在哪些时候被释放
  • 如何保证能够准确溢出到结构体中的函数指针

只要堆中存在这种结构体,我可以通过控制输入参数,申请和释放合适大小的chunk,理论上就能溢出覆写结构体了。怎么去找这种结构体呢?我觉得比较高效的就是fuzz了。但目前我还在研究怎么实现。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/720325
推荐阅读
相关标签
  

闽ICP备14008679号