前言
在一次学习研究漏洞中碰到了一个Linux提权漏洞CVE-2021-4034,引起了我的兴趣,便开始对这个漏洞细节展开分析,在学习的同时,我也想借此与各位师傅一起分享我的学习成果,我将详细分析介绍这一漏洞的工作原理、影响范围及其复现步骤。希望通过本篇文章的分享,能够帮助读者更全面地理解这个漏洞的风险,以期提高读者在安全防护与漏洞利用方面的认知
漏洞背景
此漏洞位于pkexec这一重要的组件中,它是Polkit的一部分,通过SUID二进制文件执行特定任务。
pkexec:一个允许以其他用户身份执行特定程序的程序(SUID 二进制)
在QUALYS通告中,可以明确以下几点:
- 当argc为0时触发该漏洞
- argv和envp在内存中是连续的
- 越界写操作使我们能够写入envp[0](引入一个新的环境变量)
因此,首先需要理解C语言中的参数是如何工作的:
- argv:这是一个以空字符终止的字符串数组,其元素是传递给程序的命令行参数。当通过命令行执行时,第一个(0)参数是程序本身
- argc:一个整数,表示传递给 main() 函数的参数数组 argv 的大小。数组 argv 的长度为 argc,且 argv[argc] == NULL
- envp:这个参数为函数提供对程序环境变量的访问,例如PATH变量
了解漏洞
为了能够完整的学习该漏洞,我将使用Ubuntu 18.04(2021年中)默认版本的pkexec,版本号为 0.105。
从相应的库中获取源代码:
git -c http.sslVerify=false clone https://gitlab.freedesktop.org/polkit/polkit.git
git checkout tags/0.105
这个漏洞问题出在 pkexec.c文件中,特别是处理参数的for循环。这个 for 循环检查传递给 pkexec 的每个参数,代码如下:
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;
}
opt_user = g_strdup (argv[n]);
}
else if (strcmp (argv[n], "--disable-internal-agent") == 0)
{
opt_disable_internal_agent = TRUE;
}
else
{
break;
}
}
如果我们传递的argc == 0会发生什么呢?由于n从1开始,for循环会立即终止,意味着n == 1。这样n的值会传播到537行的以下代码:
path = g_strdup (argv[n]);
g_strdup的目标将是envp[0],因为sizeof(argv) == 1,这个值为NULL(argv[0] == NULL)。
考虑到这一点,通过调用execve系统调用,我们能够控制argv[0]使其不再是程序名称,而是传递一个空数组。
为了理解这个参数处理,我做了一个微小的示例,使用了两个基本程序,彼此通过execve() 调用。
ubuntu@ubuntu:~/pwn/tests/args$ cat one.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]){
printf("Value of argc: %d\n", argc);
char **s = argv;
while(*s != NULL){
printf("Value: %s\n", *s);
s++;
}
return 0;
}
ubuntu@ubuntu:~/pwn/tests/args$ cat two.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]){
char *args[] = {NULL};
char *envp[] = {"1","2", NULL};
execve("./one", args, envp);
return 0;
}
编译并运行这个示例,很容易注意到,与直接从 shell 调用程序相比,使用execve时envp[0]在我们的控制之下。
重新引入环境变量
如我们所知,调用一个SUID二进制程序是特殊的,因为它会清理传递给它的环境变量,以避免使用诸如 LD_PRELOAD 环境变量的攻击,但这正是此漏洞的亮点。
在越界读取的循环之后,我们达到以下代码片段:
/* 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.
*/