1. 项目概述:从一次“意外”的权限提升说起
那天下午,我正在一台很久没维护的Ubuntu 18.04测试服务器上调试一个脚本,一个再普通不过的 sudo 命令,后面跟着一个带反斜杠转义的空格参数。屏幕闪烁了一下,没有返回预期的“command not found”,而是直接弹出了一个全新的、拥有root权限的shell。那一刻,我愣住了,随即意识到,我无意中触发了那个在安全圈里掀起轩然大波的漏洞——CVE-2021-3156,江湖人称“Baron Samedit”。这不是演习,这是一个存在于 sudo 这个几乎每个Linux/Unix系统管理员和开发者都无比信赖的核心工具中的“核弹级”本地提权漏洞。它允许任何被授权使用 sudo 的用户(甚至在某些特定配置下,是任何本地用户),通过精心构造的命令参数,绕过所有权限检查,直接获取系统的最高控制权(root shell)。这个漏洞的影响范围之广,几乎涵盖了2011年之后的所有主流Linux发行版(包括Ubuntu、Debian、CentOS/RHEL、Fedora等)和macOS Big Sur。复现并理解这个漏洞,不仅是一次有趣的技术探险,更是深入理解系统底层安全机制、缓冲区溢出原理以及 sudo 内部工作流程的绝佳机会。无论你是安全研究员、运维工程师还是对系统底层感兴趣开发者,跟着我一起拆解这个漏洞,你收获的将远不止一个“exp”(漏洞利用程序)。
2. 漏洞核心原理深度拆解:当转义字符遇上堆溢出
要复现一个漏洞,首先要彻底理解它为何会发生。CVE-2021-3156的根源在于 sudo 在解析命令行参数时,对反斜杠( \ )转义字符的处理逻辑存在严重缺陷,导致了堆缓冲区溢出。我们得钻进 sudo 的源代码里看看。
2.1 sudo 的参数解析与转义逻辑
sudo 的一个基本功能是允许用户以其他用户身份(通常是root)执行命令。当你执行 sudo -u root /bin/bash 时, sudo 会进行权限验证,通过后调用 execve 系统调用以root身份启动一个新的 bash 进程。问题出在 sudo 的一个特殊模式:当使用 -s 或 -i 选项时, sudo 会启动一个shell。在这个模式下,为了正确地处理用户提供的参数并将其传递给这个新shell, sudo 需要对这些参数进行“去转义”处理。
想象一下这个命令: sudo -s 。当你直接在终端输入这个时, sudo 会启动root的shell。但漏洞的触发点在于,如果你在 -s 或 -i 之后,传递一个以反斜杠结尾的命令参数,事情就开始变得诡异了。例如: sudo -s '\' 。注意,这里最后一个字符是反斜杠,并且后面有一个空格。在正常的shell解析中,反斜杠会转义其后的空格,使得 '\' (反斜杠+空格)被整体视为一个参数传递给 sudo 。
sudo 内部有一个函数(在 plugins/sudoers/policy.c 的 set_cmnd 函数中)负责拼接这些命令行参数,构建最终要执行的命令字符串。它的逻辑大致是:遍历用户提供的参数,计算每个参数的长度,然后分配一个足够大的堆内存缓冲区,把这些参数连同空格一起拷贝进去。
2.2 关键缺陷:长度计算与内存拷贝的“错位”
漏洞的核心就发生在这个“计算长度”和“实际拷贝”的两个阶段,它们对反斜杠的处理不一致。
- 长度计算阶段 :在计算参数
“\”(一个反斜杠加一个空格)的长度时,代码会进行“去转义”处理。它识别到反斜杠转义了后面的空格,因此认为这个空格是参数的一部分,而不是参数的分隔符。所以,在计算长度时,它正确地认为这个参数包含两个字符:反斜杠和空格。 - 内存拷贝阶段 :这是致命的一步。当代码真正将这个参数的内容拷贝到预先分配好的堆缓冲区时,它又进行了一次“去转义”处理。它看到反斜杠后面是空格,于是决定“这个空格是被转义的,属于参数内容,我不需要为它额外处理,但反斜杠本身是转义符,我不应该把它拷贝进去”。于是,它只拷贝了空格这一个字符,而“跳过”了反斜杠。
这就产生了一个严重的“账实不符”:计算长度时算了2个字符( \ 和空格),但实际只拷贝了1个字符(空格)。本应为这个参数预留的2字节空间,实际上只写入了1字节的数据。
2.3 堆缓冲区溢出的形成
由于这种不一致发生在每一个以反斜杠结尾的参数处理过程中,如果我们提供大量这样的参数,例如: sudo -s ‘\‘ ‘\‘ ‘\‘ ... (重复数百次) 那么,“计算的总长度”和“实际写入的总数据量”之间的差值就会累积得越来越大。这个差值,就成为了溢出的“弹药”。
sudo 的堆内存管理器(例如glibc的 malloc )根据“计算的总长度”分配了一块内存。而实际拷贝的数据量小于这个长度,照理说不会溢出。但关键在于, sudo 在拷贝完所有用户参数后,还会在缓冲区的末尾添加一个NULL字符( \0 )作为字符串结束符。添加这个NULL字符的偏移位置,是基于“计算的总长度”来确定的。
因为实际数据没填满缓冲区,代码会越过已写入的数据区域,在更远的地方写入这个NULL字符。这个写入操作,就落在了分配的内存区域之外,覆盖了堆上相邻的其他关键数据——这就是一次典型的“堆缓冲区溢出”。
注意 :这里有一个非常精妙的点。单纯的溢出不一定能导致代码执行。CVE-2021-3156的利用之所以稳定,是因为它溢出覆盖的目标非常精准。在特定的glibc版本和
sudo编译选项中,溢出会覆盖堆内存中用于管理堆块本身的“元数据”,比如size域或fd/bk指针(在glibc的ptmalloc中)。通过精心控制溢出内容和堆布局(通过调整参数个数和长度),攻击者可以篡改这些元数据,最终实现“任意地址写”或直接劫持程序控制流,将其导向攻击者放置在内存中的shellcode或利用现有gadget链(ROP)来启动一个shell。
2.4 漏洞触发的必要条件
理解原理后,我们可以总结出触发漏洞的几个关键条件,这在后续复现和环境准备中至关重要:
-
sudo版本 :影响范围极广,从1.8.2到1.9.5p1之间的多个版本均受影响。主流的Linux发行版在2021年初的默认仓库中提供的sudo


1349

被折叠的 条评论
为什么被折叠?



