CVE-2019-18634漏洞复现与分析
2020-04-24 10:23:40 Author: xz.aliyun.com(查看原文) 阅读量:314 收藏

漏洞概述

CVE-2019-18634是一个sudo 提权漏洞。影响sudo版本为[1.7.1,1.8.31)。在官方描述中只提及该漏洞可以在1.8.26前的版本中利用,实际上在1.8.26的版本中仍有利用的方式。具体地,如果pwfeedback配置选项在/etc/sudoers被启用,攻击者可以利用一个栈溢出漏洞(实际上溢出点在bss段)来获取root权限。这个配置选项在多数的Linux系统中都不是默认选项,但是在Linux Mint操作系统和Elementary OS上是默认开启的,因此漏洞的危害比较大,攻击者需要向getln传递一个超长字符串来触发攻击。

漏洞复现

下载sudo-1.8.25源码并编译

wget https://www.sudo.ws/dist/sudo-1.8.25.tar.gz
tar -zxvf ./sudo-1.8.25.tar.gz
cd ./sudo-1.8.25
./configure
make -j4
make install

系统默认的sudo位于/usr/bin/目录下,编译之后的sudo会放在/usr/local/bin/目录下,在环境变量中后者更靠前因此默认调用的是我们编译之后的binary。

╭─wz@wz-virtual-machine ~/Desktop/CTF/CVE-2019-18634/my_exp ‹hexo*› 
╰─$ which sudo                                                1 ↵
/usr/local/bin/sudo
╭─wz@wz-virtual-machine ~/Desktop/CTF/CVE-2019-18634/my_exp ‹hexo*› 
╰─$ ll /usr/bin/sudo
-rwsr-xr-x 1 root root 134K 2月   1 02:37 /usr/bin/sudo
╭─wz@wz-virtual-machine ~/Desktop/CTF/CVE-2019-18634/my_exp ‹hexo*› 
╰─$ ll /usr/local/bin/sudo
╭─wz@wz-virtual-machine ~/Desktop/CTF/CVE-2019-18634/my_exp ‹hexo*› 
╰─$ sudo --version
Sudo version 1.8.25
Sudoers policy plugin version 1.8.25
Sudoers file grammar version 46
Sudoers I/O plugin version 1.8.25

漏洞的复现环境为Ubuntu 16.04,默认没有开启pwfeedback配置,因此我们首先切换到root用户,修改/etc/sudoers,添加一行Defaults pwfeedback,然后使用sudo -l列出用户的可用权限,可以看到配置生效。

╭─wz@wz-virtual-machine ~/Desktop/CTF/CVE-2019-18634/my_exp ‹hexo*› 
╰─$ sudo -l
Matching Defaults entries for wz on wz-virtual-machine:
    pwfeedback, env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User wz may run the following commands on wz-virtual-machine:
    (ALL : ALL) ALL

首先拿POC进行测试,第一个POC为多个A*100+‘\x00’组成的字符串,-S参数指的是从stdin读取密码,成功使得sudo崩溃。这个poc适用于1.8.26以前的漏洞版本。

─wz@wz-virtual-machine ~/Desktop/CTF/CVE-2019-18634/my_exp ‹hexo*› 
╰─$ perl -e 'print(("A" x 100 . "\x{00}") x 50)' | sudo -S id                                                                  132 ↵
Password: 
[1]    31047 done                                        perl -e 'print(("A" x 100 . "\x{00}") x 50)' | 
       31048 illegal hardware instruction (core dumped)  sudo -S id

第二个POC的payload与第一个相似,只是末尾结束符从\x00改成了\x15。通过socat创建一个伪终端ptywaitslave参数使得当sudo/tmp/pty读取输入时,执行下述的命令向文件中输入payload。可以看到这个POC也成功使得sudo崩溃。

╭─wz@wz-virtual-machine ~/Desktop/CTF/CVE-2019-18634/my_exp ‹hexo*› 
╰─$ socat pty,link=/tmp/pty,waitslave exec:"perl -e 'print((\"A\" x 100 . chr(0x15)) x 50)'" &
[1] 30757
╭─wz@wz-virtual-machine ~/Desktop/CTF/CVE-2019-18634/my_exp ‹hexo*› 
╰─$ sudo -S id < /tmp/pty
Password: 
[1]  + 30757 done       socat pty,link=/tmp/pty,waitslave 
[2]    30883 illegal hardware instruction (core dumped)  sudo -S id < /tmp/pty
CWwait-slave
Blocks the open phase until a process opens the slave side of the pty. Usually, socat continues after generating the pty with opening the next address or with entering the transfer loop. With the wait-slave option, socat waits until some process opens the slave side of the pty before continuing. This option only works if the operating system provides the CWpoll() system call. And it depends on an undocumented behaviour of ptycqs, so it does not work on all operating systems. It has successfully been tested on Linux, FreeBSD, NetBSD, and on Tru64 with openpty.

漏洞分析

根据NVD的描述,漏洞位于getln函数,我们重新编译sudo,添加--enabble-asan参数,这个参数使得gcc在编译阶段对程序添加了额外的指令和数据,可以检测内存溢出。可以理解成开启沙箱保护的功能。

make clean
./configure --enable-asan
make -j4
make install

再使用第二个poc进行测试,沙箱可以检测出bss段的溢出,且给出了程序崩溃时的寄存器情况以及函数调用链,可以看到在tgetpass.c:178行调用getln,在tgetpass:345处产生了溢出。

根据源码,可以看出漏洞发生在程序使用getln函数获取密码时。参数buf位于bss段,bufsz为输入最大长度,这里是0xff,feedbackpwfeedback选项。

跟进去接着看,开始将buf赋值给cp指针,left表示剩余的输入字符数量。在没有开启pwfeedback的时候挨个拷贝输入字符到cp中直到遇到换行,*cp++ = c;

开启了该选项的情况下情况复杂一些,如果输入字符为sudo_term_kill,即删除所有字符就会回滚输入,将cp恢复到和buf一致的位置,并将剩余字节left重新赋值为bufsize。如果输入字符为sudo_term_erase,即删除单个字符,就将字符位置回滚1字节,left++

这里的逻辑看上去没有什么问题,但是我们从管道获取输入而非从终端获取输入,由于管道是单向的,因此在这个读管道中我们只能read而不能write,故write(fd, "\b \b", 3) == -1总是成立,在处理sudo_term_kill这个字符的时候直接Break跳出,重新赋值left为bufsiz,即0xff,又可以输入一轮0xff内的数据,通过*cp++ = c;覆写bss数据,直到溢出自己的0x100的数据边界,最终造成crash。

//tgetpass:178
pass = getln(input, buf, sizeof(buf), ISSET(flags, TGP_MASK));
//...
getln(int fd, char *buf, size_t bufsiz, int feedback)
{
    size_t left = bufsiz;
    ssize_t nr = -1;
    char *cp = buf;
    char c = '\0';
    debug_decl(getln, SUDO_DEBUG_CONV)

    if (left == 0) {
    errno = EINVAL;
    debug_return_str(NULL);     /* sanity */
    }

    while (--left) {
    nr = read(fd, &c, 1);
    if (nr != 1 || c == '\n' || c == '\r')
        break;
    if (feedback) {
        if (c == sudo_term_kill) {//调试为0x15
        while (cp > buf) {
            if (write(fd, "\b \b", 3) == -1)//vul
            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;//赋值部分
    }
    *cp = '\0';
    if (feedback) {
    /* erase stars */
    while (cp > buf) {
        if (write(fd, "\b \b", 3) == -1)
        break;
        --cp;
    }
    }

    debug_return_str_masked(nr == 1 ? buf : NULL);
}

漏洞利用

漏洞定位 && 前置知识

首先定位一下输入的位置,在IDA中打开sudo,查看getlen的调用定位到目标调用处,可以看到内存排布为buf[0x100]->askpass[0x20]->signo[0x104]->tgetpass_flags[0x1c]->user_details[0x68]

sudo_debug_ret = getln(v18, buf_6188, flagsa & 8, v19);
/*
bss:00000000002218E0 buf_6188        db 100h dup(?)          ; DATA XREF: tgetpass+412↑o
.bss:00000000002219E0 ; Function-local static variable
.bss:00000000002219E0 ; const char *askpass_6187
.bss:00000000002219E0 askpass_6187    dq ?                    ; DATA XREF: tgetpass+5D↑r
.bss:00000000002219E0                                         ; tgetpass+81↑r ...
.bss:00000000002219E8                 align 20h
.bss:0000000000221A00 ; volatile sig_atomic_t signo[65]
.bss:0000000000221A00 signo           dd 41h dup(?)           ; DATA XREF: tgetpass_handler+5↑o
.bss:0000000000221A00                                         ; tgetpass+1EA↑o
.bss:0000000000221B04                 public tgetpass_flags
.bss:0000000000221B04 ; int tgetpass_flags
.bss:0000000000221B04 tgetpass_flags  dd ?                    ; DATA XREF: sudo_conversation+5F↑r
.bss:0000000000221B04                                         ; parse_args:loc_11D90↑w ...
.bss:0000000000221B08                 align 20h
.bss:0000000000221B20                 public user_details_0
.bss:0000000000221B20 ; user_details user_details_0
.bss:0000000000221B20 user_details_0  user_details <?>        ; DATA XREF: get_user_info+41↑o
.bss:0000000000221B20                                         ; get_user_info+77↑w ...
.bss:0000000000221B88                 public list_user
.bss:0000000000221B88 ; const char *list_user
.bss:0000000000221B88 list_user       dq ?                    ; DATA XREF: main+87F↑r
*/
//tgetpass.c
static const char *askpass;
static char buf[SUDO_CONV_REPL_MAX + 1];

askpass变量定义在tgetpass.c,为静态变量,这个变量对于漏洞利用来说很关键,其作用是指定一个可执行程序,从这个程序中获取输入作为密码。检查TGP_ASKPASS这个flag之后选择启用该选项。从环境变量SUDO_ASKPASS中读取要执行的程序,调用sudo_askpass函数,其中fork出子进程来执行程序,在这之前会检查suid/uid/gid详情可以查看下列函数调用中的注释。之后调用getln获取输入,注意这里没有启用pwfeedback

/*
 * Like getpass(3) but with timeout and echo flags.
 */
char *
tgetpass(const char *prompt, int timeout, int flags,
    struct sudo_conv_callback *callback)
{
    struct sigaction sa, savealrm, saveint, savehup, savequit, saveterm;
    struct sigaction savetstp, savettin, savettou;
    char *pass;
    static const char *askpass;//定义
    static char buf[SUDO_CONV_REPL_MAX + 1];
    int i, input, output, save_errno, neednl = 0, need_restart;
    debug_decl(tgetpass, SUDO_DEBUG_CONV)

    (void) fflush(stdout);

    if (askpass == NULL) {
    askpass = getenv_unhooked("SUDO_ASKPASS");//从环境变量获取
    if (askpass == NULL || *askpass == '\0')
        askpass = sudo_conf_askpass_path();
    }

    /* If no tty present and we need to disable echo, try askpass. */
    if (!ISSET(flags, TGP_STDIN|TGP_ECHO|TGP_ASKPASS|TGP_NOECHO_TRY) &&
    !tty_present()) {//注意TGP_ASKPASS这个flag
    if (askpass == NULL || getenv_unhooked("DISPLAY") == NULL) {
        sudo_warnx(U_("no tty present and no askpass program specified"));
        debug_return_str(NULL);
    }
    SET(flags, TGP_ASKPASS);//启用askpass选项
    }

    /* If using a helper program to get the password, run it instead. */
    if (ISSET(flags, TGP_ASKPASS)) {
    if (askpass == NULL || *askpass == '\0')
        sudo_fatalx(U_("no askpass program specified, try setting SUDO_ASKPASS"));
    debug_return_str_masked(sudo_askpass(askpass, prompt));//调用sudo_askpass,askpass作为参数
    }
    //...
}
//
/*
 * Fork a child and exec sudo-askpass to get the password from the user.
 */
static char *
sudo_askpass(const char *askpass, const char *prompt)
{
    static char buf[SUDO_CONV_REPL_MAX + 1], *pass;
    struct sigaction sa, savechld;
    int pfd[2], status;
    pid_t child;
    //...

    if (pipe(pfd) == -1)
    sudo_fatal(U_("unable to create pipe"));

    child = sudo_debug_fork();//fork处子进程
    //...

    if (child == 0) {
    /* child, point stdout to output side of the pipe and exec askpass */
    if (dup2(pfd[1], STDOUT_FILENO) == -1) {//stdout复制到pfd[1]这个输出管道
        sudo_warn("dup2");
        _exit(255);
    }
    if (setuid(ROOT_UID) == -1)
        sudo_warn("setuid(%d)", ROOT_UID);//检查suid
    if (setgid(user_details.gid)) {
        sudo_warn(U_("unable to set gid to %u"), (unsigned int)user_details.gid);//检查gid
        _exit(255);
    }
    if (setuid(user_details.uid)) {
        sudo_warn(U_("unable to set uid to %u"), (unsigned int)user_details.uid);//检查uid
        _exit(255);
    }
    closefrom(STDERR_FILENO + 1);
    execl(askpass, askpass, prompt, (char *)NULL);//去执行askpass指定的程序
    sudo_warn(U_("unable to run %s"), askpass);
    _exit(255);
    }

    /* Get response from child (askpass). */
    (void) close(pfd[1]);
    pass = getln(pfd[0], buf, sizeof(buf), 0);//调用getln获取密码,注意pwfeedback参数为0,表示不启用该选项。
    (void) close(pfd[0]);

    //...

    debug_return_str_masked(pass);
}

signo定义为static volatile sig_atomic_t signo[NSIG];,存储程序运行中接收到的各种信号以便对其处理,这里我们置为\x00即可。

tgetpass_flags保存一些选项对应的flag。在sudo.h中可以找到宏及表示的含义。其中TGP_ASKPASS为4,因此我们要启用askpass需要设置此值为0x4

/*
 * Flags for tgetpass()
 */
#define TGP_NOECHO  0x00        /* turn echo off reading pw (default) */
#define TGP_ECHO    0x01        /* leave echo on when reading passwd */
#define TGP_STDIN   0x02        /* read from stdin, not /dev/tty */
#define TGP_ASKPASS 0x04        /* read from askpass helper program */
#define TGP_MASK    0x08        /* mask user input when reading */
#define TGP_NOECHO_TRY  0x10        /* turn off echo if possible */

user_details保存了用户的信息,包括uidpid

struct user_details {
    pid_t pid;
    pid_t ppid;
    pid_t pgid;
    pid_t tcpgid;
    pid_t sid;
    uid_t uid;
    uid_t euid;
    uid_t gid;
    uid_t egid;
    const char *username;
    const char *cwd;
    const char *tty;
    const char *host;
    const char *shell;
    GETGROUPS_T *groups;
    int ngroups;
    int ts_cols;
    int ts_lines;
};

利用思路

在之前的分析中我们提到了sudo_term_kill\x00,这导致我们如果想要溢出写user_details无法将signo覆写为零值,随后程序会认为产生异常信号而杀死进程。而在pty作为输入源的情况下这个值为\x15。因此我们首先确定要选择pty输入。其次,我们需要利用刚才分析的askpass的调用链来执行我们的程序以获得root shell。由于askpass里的getln没有启用pwfeedback,所以我们需要先溢出写user_details,在这个溢出写的过程中同时改掉tgetpass_flagsTGP_ASKPASS。因为sudo有三次输入密码的机会,在第一次输入密码失败后由于我们设置了askpass,第二次会调用askpass指向的外部程序。

在这个程序中,我们设置suid并起一个shell,即可在第二次处理时获得root shell

exp

这里使用的exp来自Plazmaz-CVE-2019-18634,其编写思路和我刚才描述的一致。注意在不同的环境下变量的偏移有所不同,最好拿IDA看一下在脚本中手动修改。

踩坑

这里的坑其实算是题外话,我之前调试的时候用的是开启了asan的binary,结果漏洞无法复现,在IDA中可以看到这里的赋值语句前面多了俩条件,使用gdb进行调试发现这里的$rdx+0x7FFF8000的前0x20都是0,因为cp_1 >> 3向右移动三位,因此对于0x20 << 3的范围内这里都是为0,故!(BYTE)v19总是为真,而到了0x100输入之后这里的值变为0xf9v9为地址,&7结果在[0,7]之间,必定小于0xf9(有符号比较),故后面无法再溢出,可以看到这个保护措施还是蛮到位的。

while ( 1 )
        {
          v9 = (unsigned __int8)cp_1 & 7;
          v19 = *(unsigned __int8 *)(((unsigned __int64)cp_1 >> 3) + 0x7FFF8000);
          if ( (char)v19 > (char)v9 || !(_BYTE)v19 )// 条件?注意前面是有符号比较,0xf9<0
          {
            *cp_1++ = v12;                      // 赋值部分
            goto LABEL_15;
          }
/*
.text:000000000007C6B1 loc_7C6B1:                              ; CODE XREF: getln+50E↓j
.text:000000000007C6B1                 mov     rdx, r14
.text:000000000007C6B4                 mov     rsi, r14
.text:000000000007C6B7                 shr     rdx, 3
.text:000000000007C6BB                 and     esi, 7
.text:000000000007C6BE                 movzx   edx, byte ptr [rdx+7FFF8000h]
.text:000000000007C6C5                 cmp     dl, sil
.text:000000000007C6C8                 jg      short loc_7C6D2
.text:000000000007C6CA                 test    dl, dl
.text:000000000007C6CC                 jnz     loc_7C87D
.text:000000000007C6D2
*/

总结

sudo作为特权系统软件,稍有不慎就可能被拿来提权,且用户态程序比内核简单许多,近些年已经爆出了诸多相关的漏洞,有很多因为配置问题在实际中几乎不可能被利用,而CVE-2019-18634的配置选项并非冷门,影响的范围较广,漏洞的原理较为简单,适合刚入门二进制漏洞的师傅们进行分析和调试。

参考

CVE-2019-18634 sudo 提权漏洞分析

Not hunter2: Buffer Overflow in Sudo via pwfeedback

sudo 历史漏洞回顾


文章来源: http://xz.aliyun.com/t/7622
如有侵权请联系:admin#unsafe.sh