CVE-2021-3156漏洞深度剖析:从堆溢出原理到本地提权实战复现 📅 2026/7/6 3:51:07 1. 项目概述一次对sudo堆溢出漏洞的深度剖析与实战复现最近在整理内部安全审计的案例库时我又把那个经典的、影响深远的CVE-2021-3156漏洞翻了出来。这个漏洞在安全圈里有个更响亮的名字——“Baron Samedit”。它之所以令人印象深刻不仅因为其影响范围几乎覆盖了所有主流Linux发行版更因为其漏洞原理之巧妙利用条件之宽松堪称近年来本地权限提升漏洞的“教科书级”案例。简单来说这是一个存在于sudo命令行工具中的堆缓冲区溢出漏洞允许任何本地普通用户在无需密码验证的情况下直接获取目标系统的root最高权限。想象一下一个攻击者只需要在终端里敲入一行特定的命令就能瞬间从“访客”变成“管理员”这对系统安全的破坏是颠覆性的。今天我就带大家从头到尾亲手复现一遍这个漏洞。我们的目标不仅仅是让漏洞“跑起来”拿到那个象征着最高权限的“#”提示符更重要的是我们要深入理解它为什么会发生漏洞的根源在哪里以及在实际的渗透测试或安全研究中我们该如何系统地分析、验证和利用这类内存破坏漏洞。无论你是刚入门的安全爱好者还是有一定经验的运维工程师通过这次动手实践你都能对Linux系统安全、二进制漏洞分析有一个更立体、更深刻的认识。整个过程我们会在一个精心配置的Ubuntu 20.04虚拟机中进行确保环境可控、过程清晰。2. 漏洞原理深度解析从代码到崩溃的链条要成功复现并理解一个漏洞盲目操作是行不通的。我们必须先弄清楚它的“病根”在哪里。CVE-2021-3156的根源在于sudo在解析命令行参数时一个关于转义字符处理的逻辑错误最终导致了堆缓冲区溢出。2.1 漏洞触发点与错误逻辑漏洞的核心函数位于sudoers策略插件中具体是sudoers_policy_main函数及其调用的set_cmnd函数。sudo在处理以反斜杠\结尾的命令行参数时本意可能是为了进行某些特殊的转义或拼接处理。但在某些特定的代码路径下特别是当sudo被调用执行类似sudoedit这样的操作时负责拼接参数到堆内存缓冲区user_args的代码错误地判断了缓冲区剩余空间。关键的错误逻辑在于当遇到参数末尾的反斜杠时代码会错误地认为需要为后续可能添加的空白字符如空格预留位置或者错误地进行了字符转义处理比如将\和下一个字符一起处理。但在实际的内存拷贝操作如memcpy或strlcpy中这个预留或计算的长度超出了目标缓冲区user_args的实际分配大小。更具体地说在拼接参数到user_args缓冲区时用于计算源字符串长度的逻辑和用于计算目标缓冲区偏移的逻辑出现了不一致。例如假设我们执行一个命令sudoedit -s \非常多的A。这里-s是一个触发特定代码路径的标志。漏洞代码在解析这个以反斜杠结尾的参数时可能会错误地计算需要拷贝的字符串长度。它可能认为需要拷贝整个参数字符串包括反斜杠及其之后的填充字符但在计算目标缓冲区的写入位置时却没有正确考虑反斜杠带来的额外影响或者错误地进行了多次字符追加最终导致向user_args缓冲区写入了超过其容量的数据这就是堆缓冲区溢出。2.2 堆内存布局与溢出影响理解堆溢出必须对堆内存管理有个基本概念。user_args缓冲区是通过malloc函数在堆上动态分配的一块内存。堆是一块进程可用的自由内存区域其中不仅存放着我们申请的数据缓冲区还夹杂着堆管理器如glibc的ptmalloc2用于维护堆块结构的关键元数据例如块的大小size、前后块指针等。当溢出发生时多余的数据就像洪水一样淹没了为user_args分配的内存边界开始覆盖其后紧邻的堆内存区域。如果后面是另一个重要的数据结构或者堆管理器元数据那么这些数据就会被篡改。攻击者的终极目标就是精心控制溢出数据的内容和长度让被覆盖的数据恰好是能够劫持程序执行流的关键信息比如函数返回地址、函数指针等。在CVE-2021-3156的利用中攻击者通过溢出覆盖了堆上的某些关键数据结构最终实现的效果是绕过所有权限检查直接以root身份执行任意命令。其利用链可能涉及覆盖sudoers模块中的策略相关数据或者通过堆风水Heap Feng Shui技术来操控堆布局使溢出数据恰好覆盖一个函数指针当程序后续调用这个指针时就跳转到了攻击者控制的shellcode或ROP链上。注意不同Linux发行版、不同版本的glibc库其堆内存分配策略、堆块结构可能略有差异。因此针对某个特定版本如Ubuntu 20.04编写成功的利用代码在CentOS 8上可能就需要调整。这也是我们复现时要指定环境的原因。3. 实验环境搭建与漏洞版本确认“工欲善其事必先利其器”。一个干净、可控的实验环境是安全研究的基础它能防止我们的操作影响宿主系统也方便进行快照和回滚。3.1 虚拟机与系统准备我强烈建议使用虚拟机进行所有漏洞复现工作。这里我选择VirtualBox和Ubuntu 20.04.2 LTS。为什么是20.04因为该版本默认搭载的sudo版本正在漏洞影响范围内且系统稳定资料丰富。创建虚拟机在VirtualBox中新建一台虚拟机分配至少2核CPU、2GB内存和20GB磁盘空间。网络模式选择“NAT”或“仅主机网络”即可无需连接外网。安装系统从Ubuntu官网下载20.04.2 LTS的ISO镜像挂载到虚拟机并完成安装。在安装过程中创建一个普通用户例如test并为其设置密码。切记不要给这个用户分配sudo权限我们就是要用这个普通账号来提权。系统更新可选但重要安装完成后先不要急着操作。我们可以先更新一下软件列表但千万不要升级sudo包。可以运行apt update然后使用apt list --upgradable | grep sudo查看sudo是否有更新。如果有我们暂时不安装它。我们的目的就是保留有漏洞的版本。安装必要工具漏洞分析和利用需要一些基础工具。以root身份安装系统时设置的那个账号执行apt install -y gcc make git gdbgcc和make用于编译利用代码git用于拉取代码gdb是强大的调试器是我们分析漏洞的“眼睛”。3.2 确认漏洞存在环境准备好后第一件事就是验证我们的系统是否确实存在漏洞。有两种快速验证的方法方法一检查sudo版本在终端中运行sudo --version查看输出的第一行。受影响的sudo版本范围是1.8.2到1.9.5p1不含补丁版本。例如Ubuntu 20.04初始安装的sudo版本可能是1.8.31这个版本就在漏洞范围内。方法二使用漏洞检测PoC有一个非常简单的命令可以检测漏洞是否存在。注意这个命令本身不会提权只会触发一个可能导致崩溃的行为用于验证。在普通用户test的shell下执行sudoedit -s \ perl -e print A x 65536或者它的一个变种sudoedit -s \ python3 -c print(A*1000)如果系统存在漏洞你很可能会看到类似于“段错误核心已转储”(segmentation fault) 的错误信息或者sudo命令报错退出。这表明我们精心构造的超长参数确实触发了sudo程序的异常处理导致了崩溃。这是漏洞存在的强烈暗示但并非百分之百确定。有些打了补丁的系统也可能因为其他原因报错。更可靠的检测方法是使用社区成熟的检测脚本。我们可以从GitHub上获取git clone https://github.com/blasty/CVE-2021-3156.git cd CVE-2021-3156 make ./sudo-hax-me-a-sandwich运行这个程序它会自动检测并告诉你系统是否易受攻击以及可能的利用技术需要指定目标编号。如果显示“VULNERABLE”那么恭喜或者说担忧你的环境准备好了。实操心得在虚拟机中操作前务必创建一个系统快照。取个名字叫“Clean Ubuntu with Vulnerable sudo”。这样无论我们在复现过程中把系统搞崩溃多少次都可以一键恢复到干净状态极大地提升了实验效率。4. 漏洞利用代码分析与编译检测到漏洞后下一步就是利用它。我们不会使用黑箱工具而是选择一个开源的、有详细注释的利用代码进行学习和编译。这里我以blasty发布的CVE-2021-3156仓库中的利用代码为例。4.1 获取与阅读利用代码我们已经克隆了仓库。关键文件是hax.c和lib.c。打开hax.c我们可以看到利用程序的主体结构。它通常包含以下步骤参数构造程序会构造一个特殊的命令行参数数组其核心就是那个以反斜杠结尾后面跟着一大串填充字符如‘A’的参数。这个填充的长度需要精确计算以控制溢出数据的多少。堆布局操控Heap Spray/Feng Shui为了稳定地让溢出数据覆盖到目标地址利用代码可能会先执行一系列内存分配操作来“塑造”堆内存的布局使得user_args缓冲区之后的位置恰好是攻击者想要覆盖的目标数据结构。这可能包括反复分配和释放一些特定大小的内存块。触发漏洞最终利用程序通过execve系统调用执行/usr/bin/sudoedit程序并将构造好的恶意参数传递给它。权限获取如果利用成功sudoedit进程会在内部因溢出而执行流被劫持转而执行攻击者预设的代码可能是启动一个root shell。由于sudoedit本身是以root权限运行的通过setuid位因此获得的shell也就具备了root权限。4.2 编译利用程序在CVE-2021-3156目录下我们已经运行过make。Makefile文件会指导编译过程。通常编译命令很简单make这将会生成一个或多个可执行文件例如sudo-hax-me-a-sandwich检测用和真正的利用程序可能名字类似exploit。如果make默认只编译了检测工具我们可能需要查看Makefile或仓库的README来编译利用本体。有时利用代码就集成在检测工具里通过参数选择利用模式。编译过程通常很顺利。如果遇到错误最常见的问题是缺少32位库如果利用代码是32位的。在64位Ubuntu上可以安装sudo apt install gcc-multilib注意这里我们暂时用实验环境中的root权限来安装构建依赖这是合理的环境准备步骤。4.3 理解利用的适配性编译成功后不要急着运行。这个利用代码很可能不是“万能”的。正如之前原理部分提到的堆布局高度依赖于glibc版本不同版本的堆管理算法可能不同。系统架构64位和32位的内存地址、堆块结构不同。sudo的编译选项是否启用了堆栈保护Canary、地址空间布局随机化ASLR等缓解措施。开源利用代码通常会内置多个“目标”target对应不同的发行版和版本。运行利用程序时可能需要指定目标编号例如./exploit 0这里的0可能代表 Ubuntu 20.04 (x86_64)。我们需要仔细阅读项目的README.md或代码注释找到适合我们实验环境Ubuntu 20.04 x86_64的目标ID。注意事项在真实渗透测试中拿到一个公开的漏洞利用代码Exploit后直接对生产环境使用是极其危险和不专业的。首先可能因为环境差异导致利用失败留下崩溃日志。其次可能触发未预期的行为导致系统不稳定。正确的做法是先在尽可能模拟目标环境相同OS、相同版本的测试机上验证和调试理解其原理必要时进行适配修改。5. 实战复现步骤与过程记录现在我们来到了最关键的动手环节。请确保你当前使用的是我们创建的普通用户test。5.1 步骤一身份确认与环境检查首先确认我们是非root用户并且没有sudo权限。whoami # 输出应为 test sudo -l # 系统会提示 test 不在 sudoers 文件中此事将被报告。这模拟了一个攻击者最初获得的普通shell访问权限。5.2 步骤二运行漏洞利用程序进入我们编译好利用程序的目录。假设利用程序名为exploit且我们已知目标ID为1具体ID请根据你使用的利用代码文档确定。cd ~/CVE-2021-3156 ./exploit 1执行这一刻需要屏住呼吸观察。如果利用成功你可能会看到程序输出一些调试信息然后命令行提示符会神奇地从$变成#。这意味着你已经在一个拥有root权限的shell中了你可以执行以下命令验证# 提示符变成了 # whoami # 输出 root id # 输出 uid0(root) gid0(root) groups0(root)恭喜漏洞复现成功你刚刚完成了一次本地权限提升。5.3 步骤三失败情况分析与调试然而安全研究之路很少一帆风顺。一次成功的背后可能是无数次的失败。如果你的利用程序崩溃了或者提权没有成功别灰心这正是学习的好机会。常见的失败原因和排查思路如下目标ID不对这是最常见的原因。利用代码中的偏移、地址可能是为特定版本硬编码的。解决方法重新查阅利用代码的文档或注释尝试其他可能的目标ID。有些利用脚本带自动检测功能可以尝试不加参数运行./exploit看是否有提示。ASLR地址空间布局随机化现代Linux系统默认启用ASLR这会使堆、栈、库的基地址每次运行都发生变化导致硬编码的地址失效。公开的利用代码通常已经采用了某种技术来绕过ASLR例如通过堆喷射Heap Spray用大量可控数据填充内存提高跳转地址命中的概率或者利用信息泄漏。如果失败可以尝试临时关闭ASLR进行测试仅限实验环境# 在当前shell会话中关闭ASLR echo 0 | sudo tee /proc/sys/kernel/randomize_va_space然后再次运行利用程序。切记测试完毕后要重新打开ASLR值为2。环境差异即使同是Ubuntu 20.04安装的软件包多少、系统运行时间长短都会影响堆的初始状态。可以尝试重启虚拟机在一个“干净”的进程环境下再次运行利用。利用代码本身问题有些公开的利用代码可能在某些边缘情况下不稳定。可以尝试搜索其他研究者发布的不同的利用代码GitHub上有很多进行对比测试。5.4 步骤四使用调试器GDB深入观察对于想深究的同学我们可以使用GDB来动态跟踪漏洞触发过程。这需要以root权限调试一个setuid程序需要一些技巧。首先我们需要编译一个带调试符号的sudo。或者更简单的方法利用Linux的ptrace_scope设置。在实验环境中我们可以先切换到root用我们刚刚获得的权限或者用安装系统时的root账号然后# 允许普通用户调试任何进程仅用于实验 echo 0 /proc/sys/kernel/yama/ptrace_scope # 切换回test用户 su - test然后在一个终端窗口运行待调试的sudo命令先构造好会导致崩溃的参数在另一个终端用gdb附加进程。# 终端1触发漏洞 sudoedit -s \ python3 -c print(A*1000) # 此时sudo进程会挂起或崩溃# 终端2查找sudo进程PID并附加 ps aux | grep sudoedit gdb -p PID在GDB中你可以设置断点在可疑的函数上如set_cmnd单步执行观察内存和寄存器的变化亲眼目睹溢出是如何发生的。这步操作需要对C语言、汇编和GDB调试有一定基础是提升二进制漏洞分析能力的必经之路。6. 漏洞修复与安全加固建议成功复现漏洞证明了其危害性。那么如何防御呢作为一个负责任的安全从业者我们不仅要会“攻”更要懂“防”。6.1 官方补丁与升级修复漏洞最根本、最有效的方法是升级到已修复的sudo版本。各大Linux发行版在漏洞披露后都迅速发布了安全更新。Ubuntu/Debian:sudo apt update sudo apt upgrade sudoRHEL/CentOS 7/8:sudo yum update sudo或sudo dnf update sudoFedora:sudo dnf update sudo升级后再次运行漏洞检测脚本或命令应该显示“NOT VULNERABLE”不易受攻击或不再产生段错误。6.2 临时缓解措施如果因为某些原因无法立即升级可以考虑以下临时缓解措施但它们可能影响正常功能需谨慎评估移除sudo的setuid位极端措施这等于废掉了sudo的核心功能不推荐。chmod u-s /usr/bin/sudo使用权限限制工具如通过pam_limits或systemd的SystemCallFilter对sudo进程可以调用的系统调用进行过滤但这需要深厚的内核和安全知识。最务实的建议是立即升级。对于企业环境应建立完善的漏洞响应和补丁管理流程确保此类高危漏洞能在尽可能短的时间内被修复。6.3 深度防御与安全实践一次漏洞复现也是一次安全意识的洗礼。我们可以从中总结出更普适的安全最佳实践最小权限原则严格限制普通用户的sudo权限。使用visudo编辑/etc/sudoers文件为用户或组分配具体的、最小化的命令权限而不是通用的ALL(ALL:ALL) ALL。定期更新与漏洞扫描建立操作系统和关键软件如sudo, openssl, sshd的定期更新机制。使用诸如lynis,OpenSCAP等安全扫描工具进行合规性检查。启用安全特性确保系统的安全特性如ASLR、NXDEP、Stack Canaries等都是开启状态。它们能极大增加漏洞利用的难度。代码审计与模糊测试对于自行开发或使用的关键开源软件条件允许时可进行代码安全审计或引入模糊测试Fuzzing以发现潜在的类似内存安全问题。7. 复现过程中的常见问题与排查实录在带领团队内部复现和教学的过程中我记录下了一些高频出现的“坑”这里分享给大家希望能帮你节省时间。问题1编译利用代码时报错 “fatal error: xxx.h: No such file or directory”原因缺少必要的开发库。解决根据缺失的头文件名称安装对应的-dev或-devel包。例如缺openssl/ssl.h就安装libssl-dev。可以尝试apt search 库名来查找包名。问题2运行利用程序后系统卡死或虚拟机崩溃原因利用程序可能触发了不可恢复的内存错误导致内核恐慌Kernel Panic或关键进程崩溃。解决这是实验中的正常风险。直接恢复之前创建的虚拟机快照即可。这也提醒我们一定要在虚拟机中操作并提前做快照。问题3漏洞检测脚本说存在漏洞但利用程序总是失败排查思路确认目标ID这是首要怀疑对象。仔细阅读利用代码的说明尝试所有可能的目标ID。检查ASLR运行cat /proc/sys/kernel/randomize_va_space如果输出是2说明全ASLR开启。尝试临时关闭设为0再测试。检查环境变量有些利用代码依赖于特定的环境变量来布局内存。确保你是直接在干净的shell中运行没有在复杂的终端环境如tmux, screen或IDE集成终端中运行这些环境可能会设置额外的变量。尝试其他利用代码GitHub上搜索“CVE-2021-3156 exploit”可能会有多个实现其对环境的适应性可能不同。问题4GDB附加进程时提示“Operation not permitted”原因即使以root运行gdb默认策略也可能禁止调试非子进程。ptrace_scope设置是关键。解决如前所述临时设置echo 0 /proc/sys/kernel/yama/ptrace_scope。或者在启动要调试的程序时直接使用gdb --args sudoedit ...的方式启动。问题5升级sudo后某些自定义的sudoers配置不工作了原因极少数情况下新版本sudo的语法或行为可能有细微变化。解决检查/etc/sudoers文件的语法是否正确可以使用visudo -c命令检查。仔细阅读新版本sudo的发行说明Changelog看是否有不兼容的变更。通常这种情况很少见。整个复现过程从环境搭建到最终提权成功就像完成一次精密的外科手术。它要求你对目标系统、漏洞原理和利用工具有清晰的认识。失败是常态而每一次对失败原因的排查都是对计算机系统理解的一次深化。当你最终看到那个#提示符时收获的不仅仅是一次成功的提权更是一套分析、应对二进制漏洞的方法论。记住我们研究漏洞的最终目的是为了更好地修复它、防御它。