CVE-2019-18634 OOB write – analysis and development of a working PoC

CVE-2019-18634 is a vulnerability in sudo prior to version 1.8.26, but then discovered to be possible to exploit in versions after 1.8.26 until 1.8.30. This means the only way to be full patched is using sudo version 1.8.31.

The vulnerability is an out-of-bounds write, that allows an attacker to enter more bytes than the buffer size should accept, thus leading to a buffer overflow vulnerability in which will be possible to write internal program data that will help attackers to escalate privileges and get root!

The most important fact to know about this vulnerability is that it needs pwfeedback enabled at /etc/sudoers.

The pwfeedback module is used to print an asterisk for each character sent to the password prompt, making the password input a bit more comfortable.

A good way to avoid exploitation with old versions is disabling pwfeedback module in the sudoers file.

In versions prior to 1.8.26 triggering the overflow is easy, as specified https://www.sudo.ws/alerts/pwfeedback.html.

code:

$ perl -e 'print(("A" x 100 . chr(0)) x 50)' | sudo -S -k id

But due to a new code added in version 1.8.26 the bug is a bit more complex to exploit, forcing us to use a pty pseudo-terminal:

$ socat pty,link=/tmp/pty,waitslave exec:"perl -e 'print((\"A\" x 100 . chr(0x15)) x 50)'" &
$ sudo -S -k id < /tmp/pty

Now we will see how the out-of-bounds vulnerability can be triggered and why does it occur.

The sudo_conversation() function located in conversation.c:46 calls tgetpass() (at tgetpass.c:84).

In tgetpass.c:98, we can see how it reads from SUDO_ASKPASS environment variable:

Our main target will be to enter the conditional at tgetpass.c:114, that way we will be entering in the function sudo_askpass() at tgetpass.c:238.

As we can see in the code in sudo_askpass() it sets the privileges to the user_details.uid value, then it will be a target to be overflowed during the OOB-write.

Finally we can see it uses execl() function to execute the binary specified in SUDO_ASKPASS.

We can see how the buffer overflow starts if we go to tgetpass.c:308, which is the function getln() used for reading the password.

In tgetpass.c:326 we can see it compares one input char with the sudo_term_kill value, which will be 0x15. If we enter the sudo_term_kill character we will enter in the loop. The loop substracts from cp (the base pointer that changes each time we add a char) 1 byte by one until reaching a value equal to buf. The problem in the loop is the write syscall in tgetpass.c:328 as if failing (returning -1), it breaks out of the loop. And after the loop, we find that the left value is restored with the bufsiz value. So if the loop failed, we have the posibility to write bufsiz bytes after buf+bufsize, and if repeating the sudo_term_kill character each n bytes (bufsiz), we would be reaching a non-size-defined input as we can enter the number of bytes we would like.

How can we make write() fail? If using a pty pseudo-terminal we can make it read-only, making write fail when trying to write into it.

At this point, the easiest way to get something useful of this bug is overwriting internal program values, like the user_details.uid to get what we want…root privileges!

The user_details struct:

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;
};

Finally, we will end up with a wrong password message once the overflow has been triggered with those modified values.

Then the SUDO_ASKPASS specified binary will be executed as root, we can implement a callback detector in our exploit to launch a root shell instead of exploiting again sudo. Also we can use another program located in /tmp that pops a shell as root.

Now we need to know the memory order on it:

static char buf[SUDO_CONV_REPL_MAX + 1]

static const char *askpass;

static volatile sig_atomic_t signo[NSIG];

extern int tgetpass_flags;

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;
};

Our target will be set user_details.uid to 0, so the final privileges for our binary will be root. And also set TGP_ASKPASS properly (0x04). And make sure we use 0x15 each n bytes to keep the overflow active.

Knowing the offsets, it is very easy to trigger the BOF, all we have to do is prepare a pseudo-terminal, and prepare the payload with the overwrites.

Finally the binary specified in SUDO_ASKPASS will get executed as root.

You can find the full-working proof-of-concept (PoC) here.

Leave a Reply