2022 年 1 月,Qualys 公开 CVE-2021-4034。漏洞在 polkit 的 pkexec 里,本地普通用户即可提权到 root。利用代码公开后很快被集成进各类自动化工具,成了渗透测试里最省事的一步。
它的特别之处是年龄。从 pkexec 诞生算起,这个问题存在了十多年,几乎覆盖所有主流 Linux 发行版。
背景:pkexec 是什么#
polkit 是 Linux 上的授权框架,桌面环境用它来判断“这个用户能不能挂载磁盘、能不能改网络配置”。pkexec 是它的命令行前端,作用类似 sudo:以另一个用户(通常是 root)身份执行命令,但走 polkit 的规则,而不是 sudoers 文件。
因为要让普通用户能触发,pkexec 被安装成 setuid root 程序。这个设定决定了影响面:只要机器上有 pkexec 且带 setuid 位,本地用户就多了一条通往 root 的路径。
服务器上通常用不到它。桌面发行版默认装 polkit,服务器版也常因为依赖关系被带进来,所以它不像 sudo 那样天天被用到,加固清单里也很少提。一个没人记得的 setuid 程序,就这样躺在机器上十多年。
漏洞原理:参数数量为零的边界#
点到为止。pkexec 启动时会重新拼装参数与环境变量,为后续的授权检查做准备。当它以“参数个数为 0”的方式被调用时,内部对 argv 的索引假设失效,把本不该写入的位置当成了可写空间。
结果是越界写,同时让攻击者能够重新引入本应被清理掉的环境变量。其中一个被利用的是 GCONV_PATH,它会让动态链接器去加载指定路径下的字符集转换模块。攻击者只要让这个路径指向自己的目录,就能以 root 权限加载自己的共享库。
整条链路不依赖内核漏洞,也不需要竞态,稳定得可怕。这也是它比很多“需要拼运气”的提权洞更危险的地方。
这类洞为什么能藏这么久?因为它不是内存破坏的典型形态,静态分析很难把“argv 索引假设”判定为缺陷;正常调用时参数永远大于零,测试也覆盖不到。直到有人手工构造出零参数调用,问题才暴露出来。
影响范围#
| 环境 | 状态 | 怎么查 |
|---|---|---|
| Debian / Ubuntu | 受影响,已发补丁 | dpkg -l policykit-1 |
| RHEL / CentOS / Rocky | 受影响,已发补丁 | rpm -q polkit |
| 容器镜像内含 pkexec | 可能带旧版本 | 镜像内看 /usr/bin/pkexec |
| 最小化安装、无桌面 | 多数不受影响 | command -v pkexec |
Debian 系后来把 polkit 拆成了 polkitd、pkexec 等包,按 policykit-1 检索的资产清单会漏项。具体的包名与修复版本请以发行版安全公告为准。
另一个容易漏的地方是模板镜像。基础镜像里带着旧版 pkexec,之后建出的几十台机器都会继承,但没人会在业务清单里登记它。
云主机的公共镜像也要问一句。镜像厂商的构建时间、基线版本、是否附带桌面组件,决定了它有没有把旧版 pkexec 带进来。买机器时顺手跑一次 command -v pkexec,比事后补救便宜。
检测:先找 setuid,再对版本#
# 1. 找出带 setuid 位的 pkexec
find / -xdev -perm -4000 -name pkexec 2>/dev/null
# 2. 确认包版本
rpm -q polkit 2>/dev/null || dpkg -l | grep -E 'polkit|pkexec'
# 3. 看有没有异常调用痕迹(需提前开启 audit)
ausearch -m EXECVE -ts recent 2>/dev/null | grep -i pkexec | tail
第一条要加 -xdev,否则会扫进挂载的网络盘和容器层,慢且容易误报。如果机器上根本没有 pkexec,输出为空就是最好的结果。
第二条用于判断补丁状态。发行版常回填补丁不改版本号,Debian/Ubuntu 可以看 apt changelog policykit-1,RHEL 系看 rpm -q --changelog polkit | head -40。
第三条用于失陷排查。pkexec 提权成功后会拉起一个 root shell,audit 里能看到 pkexec 后面跟着异常的参数。没有 audit 的环境,退而求其次看 /var/log/auth.log 里的 pkexec 记录,以及 root 的 last 登录时间。
再补两项手工检查:cat /etc/ld.so.preload 是否被写入陌生路径,/tmp、/dev/shm 下是否有近期出现的 .so 文件。提权洞常被当作持久化的第一步。
批量环境可以用一条命令出一张表,交给配置管理工具收集:find / -xdev -perm -4000 -name pkexec -printf '%p %m\n'。把结果和包版本拼在一起,就能按“有 setuid 且版本旧”筛出优先级最高的机器。
修复与缓解:升级,或者直接拿掉 setuid#
两条路,选一条就能闭环。
- 升级 polkit 到发行版修复版本,然后重启 polkit 服务。这是唯一“修好”的路径,桌面环境必须走这条。
- 立刻止血:去掉 pkexec 的 setuid 位,或者直接删掉二进制。绝大多数服务器根本不需要 pkexec。
# 方案 A:升级(Debian/Ubuntu 示例)
sudo apt-get update
sudo apt-get install --only-upgrade policykit-1
# 方案 B:临时移除 setuid 位(可逆,先记录原权限)
stat -c '%a %n' /usr/bin/pkexec # 通常是 4755
sudo chmod 0755 /usr/bin/pkexec
# 需要恢复时
sudo chmod 4755 /usr/bin/pkexec
批量下发前先在测试机跑一遍。有的运维脚本、桌面自启项、监控 agent 会调用 pkexec,去掉 setuid 后这些会静默失败。变更单里记下原权限和回滚命令。
用 Ansible、Salt 这类工具推的时候,把“当前权限”写进采集结果,而不是无脑 chmod。否则某台机器本来就没装 pkexec,任务会直接报错。
走升级路线时注意 polkit 是常驻服务,升级后要重启它,否则内存里还是旧代码。重启一般不影响已登录的桌面会话,但正在进行的授权请求会失败,挑低峰做。
验证#
find / -xdev -perm -4000 -name pkexec应无输出(走方案 B 时)。- 以普通用户执行
pkexec id,应提示未授权或直接失败,而不是弹出认证框后成功。 - 走升级路线时,
pkexec --version与包版本一致,systemctl status polkit正常,桌面登录、挂载 U 盘这类操作不受影响。 - 复查
ld.so.preload、sudoers、authorized_keys 是否被改动。提权成功后攻击者第一件事通常是留后门。 - 有条件的话,用 auditd 监控 pkexec 的 execve 事件,持续观察一周。
结论#
- PwnKit 是本地提权,前提是攻击者已经有一个低权限 shell;它决定的是“进来之后能不能变 root”。
- 判断是否受影响只看两件事:pkexec 在不在、带不带 setuid 位。
- 服务器场景下删 setuid 位比等补丁更快,也更彻底。
- 容器镜像与模板镜像最容易漏,资产清单要单列一项。
- 修完要查持久化痕迹,别只打个补丁就收工。
完