针对CVE-2021-3156(Sudo中的堆缓冲区溢出)的技术分析和漏洞利用开发,包括三个在主要Linux发行版上实现本地权限提升的有效漏洞利用。
Qualys Security Advisory
Baron Samedit:Sudo 中的堆缓冲区溢出 (CVE-2021-3156)
概述 分析 利用 致谢 时间线
我们在 Sudo (https://www.sudo.ws/) 中发现了一个堆缓冲区溢出漏洞。该漏洞:
可由任何本地用户(普通用户和系统用户,sudoers 和非 sudoers)利用,无需认证(即攻击者无需知道用户密码);
于 2011 年 7 月引入(提交 8255ed69),影响 1.8.2 至 1.8.31p2 的所有旧版本以及 1.9.0 至 1.9.5p1 的所有稳定版本,均处于默认配置。
我们为这个漏洞开发了三种不同的利用方式,并在 Ubuntu 20.04 (Sudo 1.8.31)、Debian 10 (Sudo 1.8.27) 和 Fedora 33 (Sudo 1.9.2) 上获得了完整的 root 权限。其他操作系统和发行版也可能被利用。
如果 Sudo 以 "shell" 模式执行命令(shell -c command):
通过 -s 选项(设置 Sudo 的 MODE_SHELL 标志);
或通过 -i 选项(设置 Sudo 的 MODE_SHELL 和 MODE_LOGIN_SHELL 标志);
那么,在 Sudo 的 main() 函数开头,parse_args() 会重写 argv(第 609-617 行),将命令行参数连接起来(第 587-595 行),并用反斜杠转义所有元字符(第 590-591 行):
之后,在 sudoers_policy_main() 中,set_cmnd() 将命令行参数连接到堆缓冲区 "user_args" 中(第 864-871 行)并取消转义元字符(第 866-867 行),"用于 sudoers 匹配和日志记录":
不幸的是,如果某个命令行参数以单个反斜杠字符结尾,那么:
在第 866 行,"from[0]" 是反斜杠字符,而 "from[1]" 是参数的 null 终止符(即不是空格字符);
在第 867 行,"from" 递增后指向 null 终止符;
在第 868 行,null 终止符被复制到 "user_args" 缓冲区,且 "from" 再次递增,指向 null 终止符之后的第一个字符(即超出参数的边界);
第 865-869 行的 "while" 循环会读取并复制越界字符到 "user_args" 缓冲区。
换句话说,set_cmnd() 存在堆缓冲区溢出漏洞,因为复制到 "user_args" 缓冲区的越界字符并未包含在缓冲区大小计算中(第 852-853 行)。
然而理论上,命令行参数不可能以单个反斜杠结尾:如果设置了 MODE_SHELL 或 MODE_LOGIN_SHELL(第 858 行,到达易受攻击代码的必要条件),则 MODE_SHELL 已被设置(第 571 行),并且 parse_args() 已经转义了所有元字符,包括反斜杠(即,它用第二个反斜杠转义了每个单个反斜杠)。
但实际上,set_cmnd() 中易受攻击的代码和 parse_args() 中的转义代码被略有不同的条件包围:
对比:
那么,我们的问题是:能否设置 MODE_SHELL 以及 MODE_EDIT 或 MODE_CHECK(以到达易受攻击的代码),但不设置默认的 MODE_RUN(以避免转义代码)?
答案似乎是否定的:如果设置 MODE_EDIT(-e 选项,第 361 行)或 MODE_CHECK(-l 选项,第 423 和 519 行),则 parse_args() 会从 "valid_flags" 中移除 MODE_SHELL(第 363 和 424 行),并在指定无效标志(如 MODE_SHELL)时退出并报错(第 532-533 行):
但我们发现了一个漏洞:如果以 "sudoedit" 而非 "sudo" 执行 Sudo,则 parse_args() 会自动设置 MODE_EDIT(第 270 行),但不会重置 "valid_flags",而 "valid_flags" 默认包含 MODE_SHELL(第 127 和 249 行):
因此,如果执行 "sudoedit -s",则同时设置 MODE_EDIT 和 MODE_SHELL(但不设置 MODE_RUN),从而避免转义代码,到达易受攻击的代码,并通过以单个反斜杠结尾的命令行参数溢出基于堆的 "user_args" 缓冲区:
perl -e 'print "A" x 65536'
malloc(): corrupted top size
Aborted (core dumped)从攻击者的角度来看,这个缓冲区溢出非常理想:
我们可以控制被溢出 "user_args" 缓冲区的大小(第 852-854 行中连接的命令行参数的大小);
我们可以独立控制溢出本身的大小和内容(最后一个命令行参数之后紧跟着第一个环境变量,这些变量未包含在第 852-853 行的大小计算中);
我们甚至可以向被溢出的缓冲区写入 null 字节(每个以单个反斜杠结尾的命令行参数或环境变量都会向 "user_args" 写入一个 null 字节,第 866-868 行)。
例如,在 amd64 Linux 上,以下命令分配一个 24 字节的 "user_args" 缓冲区(32 字节的堆块),并将下一个堆块的大小字段覆盖为 "A=a\0B=b\0"(0x00623d4200613d41),fd 字段覆盖为 "C=c\0D=d\0"(0x00643d4400633d43),bk 字段覆盖为 "E=e\0F=f\0"(0x00663d4600653d45):
--|--------+--------+--------+--------|--------+--------+--------+--------+-- | | |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.| --|--------+--------+--------+--------|--------+--------+--------+--------+-- size <---- user_args buffer ----> size fd bk
由于 Sudo 在其 main() 函数的最开始调用了本地化函数:
并将翻译字符串(通过 gettext() 函数和 _() 宏)传递给格式字符串函数,例如: