CVE-2021-3156 sudo heap-overflow 漏洞分析
1 月26 日的时候, 有文章披露了 sudo 代码中存在 堆缓冲区溢出,于是花了漫长的时间尝试写相关利用, 本文以学习笔记为主。
完整利用可见:
https://gist.github.com/WinMin/9607a076d847f5768f372988762638f9
The Qualys Research Team has discovered a heap overflow vulnerability in sudo, a near-ubiquitous utility available on major Unix-like operating systems. Any unprivileged user can gain root privileges on a vulnerable host using a default sudo configuration by exploiting this vulnerability.
Sudo is a powerful utility that’s included in most if not all Unix- and Linux-based OSes. It allows users to run programs with the security privileges of another user. The vulnerability itself has been hiding in plain sight for nearly 10 years. It was introduced in July 2011 (commit 8255ed69) and affects all legacy versions from 1.8.2 to 1.8.31p2 and all stable versions from 1.9.0 to 1.9.5p1 in their default configuration.
Successful exploitation of this vulnerability allows any unprivileged user to gain root privileges on the vulnerable host. Qualys security researchers have been able to independently verify the vulnerability and develop multiple variants of exploit and obtain full root privileges on Ubuntu 20.04 (Sudo 1.8.31), Debian 10 (Sudo 1.8.27), and Fedora 33 (Sudo 1.9.2). Other operating systems and distributions are also likely to be exploitable.
漏洞分析
这里以 sudo 1.8.31 版本作为分析目标。 ubuntu 20.04.1 作为分析环境
PoC:
1 |
|
漏洞产生的代码位于 plugins/sudoers/sudoers.c
的 set_cmnd
函数
首先通过 854 处,为 sudoedit
-s
后的字符长度分配内存空间, 即 user_args
, 当代码处理到 866 处的时候, 如果参数为如下结构,即
1 | pwndbg> p NewArgv[0] |
第一次拷贝 会将 B
拷贝到 user_args
里,然后 from ++
当 B
拷贝完, from[0] == '\\'
, 且from[1]
不为空的时候, 此时 from ++ , 然后又进到这个 while 循环, from 后面的数据
1 | pwndbg> p from[0] |
这个时候 from 后面的数据为环境变量设置的数据, 即这里此时 from[0]
为 X/X
。最终结果就是 user_args
被越界
漏洞利用
三种利用思路
原作者的提到了, 他们通过随机添加 LC_*
等环境变量来风水堆布局, 产生了数十种 crash 样本,其中有三种利用思路,
(1)通过覆写 sudo_hook_entry
结构体
该部分代码位于 `src/hooks.c` 107行 , 总体思路为 通过堆溢出,劫持函数指针getenv_fn 的低两位, 通过爆破的方法将函数劫持到 `execv` 来执行我们的程序。 该思路已经有公开的利用代码, 可见[Github](https://github.com/lockedbyte/CVE-Exploits/tree/master/CVE-2021-3156)
(2) 通过覆写 service_user
结构体
该部分代码位于 glibc 源代码中的 `glibc-2.31/nss/nsswitch.c` 的330 行,
1 | 327 static int |
我们通过覆盖 ni->name
,让程序去 ___libc_dlopen
加载我们编写的 libc 库, 在加上 __attribute__ ((constructor))
的魔术方法,来让加载 libc 后第一时间执行我们的代码,
(3)通过覆写 def_timestampdir
结构体
将def_timestampdir覆盖为一个不存在的目录。然后我们可以与sudo的`ts_mkdirs()`竞争,创建一个指向任意文件的符号链接。并且尝试打开这个文件,向其中写入一个struct timestamp_entry。我们可以符号链接将其指向/etc/passwd,然后以root打开他,然后实现任意用户的注入从而root
这个类似的利用似乎也有 [Github](https://github.com/r4j0x00/exploits/blob/master/CVE-2021-3156/exploit.c)
编写利用
这里简单描述一下,我之前调试编写利用第二种方法的过程
首先我们知道了 sudo
代码会根据环境变量中的 LC*
来分配释放堆布局
in setlocale(), we malloc()ate and free() several LC environment variables (LC_CTYPE, LC_MESSAGES, LC_TIME, etc), thereby creating small holes at the very beginning of Sudo’s heap (free fast or tcache chunks);
其次,我们需要明确我们的目标是,我们要让分配的 user_args
结构体 与 service_user
结构体两者间的距离越近越好,因此,我们通过(fuzz 和 手动调试的方法来风水堆布局。
那么如何判断两者间的距离呢? 首先我们对分配 user_args
代码处下断, 即 b sudoers.c:854
, 然后对使用 service_user
处下断,,即b nsswitch.c:330
由于我们的利用是通过 execve
来执行 sudoedit
, 因此我们调试的是我们编写利用程序的子进程,因此还需要设置下 gdb 的调试模式
1 | catch exec |
这样就行了, 我将以上东西集成到一个 gdb 调试脚本中,
1 | catch exec |
然后挂上调试器 gdb exploit -x gdbscript
, 查看两者偏移,
1 | In file: /home/swpwn/Desktop/CVE-2021-3156/sudo-SUDO_1_8_31/plugins/sudoers/sudoers.c |
这里的 tcachebins
是我们即将分配的 user_args
chunk,具体分配是哪个, 取决于 user_args
的大小, 然后再 c 一下
1 | In file: /home/swpwn/glibc-2.31/nss/nsswitch.c |
获取 ni
的地址, 与上面的 tcachebins
进行比较, 越近越好, 我最初的利用脚本两者偏移最小为 0x700 左右,然后中间一路覆盖过去
将 ni->name
覆盖为 “X/X” ,其余内容以 \0
覆盖,这里会涉及一个问题,那么就是如何传入\00
字符呢? 我们知道 参数和环境变量都是不允许写入 \x00
的,否则将被截断。通过阅读代码和调试我们最终发现 我们可以单独的 \\
字符来作为一个 \x00
字符。
最后提及一下,非源码调试的方法,因为当我编写利用后,在本地执行是成功了,但是换了一个机器,即非编译的 sudo 的程序执行的时候却,失败了,这个时候发现自己编译的和系统自带还是不一样的,于是我又写了一个不是自己编译的利用。
当非源码调试的时候,由于漏洞函数是位于 sudoers.so
中,该 so 库并不是一开始就加载的,我们没法在没有符号 和 没有加载的情况下直接下断,所以我们在我们的 payload
设置一些特殊的字符, 比如 0xdeadbeaf
比如我这里设置一个单独的设置 args 参数为 ”BBBBBB”
以及我们再选择对 libc 中的 __libc_dlopen_mode
函数下断,因为我们最终的目的是 dlopen 我们的目标 so 程序,以及下到这个,也相当于到了 nss_load 函数附近了。
但是对这个 __libc_dlopen_mode
可能需要 glibc 的调试符号,可以通过 apt install libc6-dbg
来安装,以及下断需要开启 Pending Breakpoints
功能
1 | # cat gdbscript |
执行 gdb ./exploit -x gdbscript
c 一次, 断到 __libc_dlopen_mode
这是第一次 sudo 在执行 set_cmnd
之前 getpwuid
的 nss_load 操作
再 c 一次 , 这个就是 set_cmnd
之后就的 nss_load , 此时就是溢出之后的, 我们可以通过 search BBBB
1 | pwndbg> search "BBBB" |
来看我们分配的堆的位置
以及查看此时的寄存器
1 | LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA |
0x5555555891e0 地址为要被覆盖的目标
1 | pwndbg> p/x 0x5555555891e0 - 0x555555588bc0 |
通过这样的方法来查看两者的偏移
最后的利用见 https://gist.github.com/WinMin/9607a076d847f5768f372988762638f9
Reference
https://visualgdb.com/gdbreference/commands/set_stop-on-solib-events
https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt