Misc

Linux Character Devices: Exploring systemd-run and pkexec

In this blog post, we quickly look into issues involving character devices. As is typical for Linux, everything is a file, so character devices are referenced as files, such as pseudo terminals (pts) under /dev/pts/. man pty briefly introduces the topic. Essentially, it is used to connect a program, such as a terminal emulator, to a shell. In the end, a pty can read and write like a regular file. A colleague already brought up the topic of ptys and character devices. But more recently a Twitter post and the accompanying advisory piqued my interest.

The advisory says:

Systemd-run/run0 allocates user-owned pty’s and attaches the slave to high privilege programs without changing ownership or locking the pty slave.

So practically, this means that a high-privileged process uses a pty owned by a low-privileged user, posing a potential risk. Let’s take a closer look at this if this is indeed the case:

First, we start sleep 1000 with the systemd-run --pty command which gives us enough time to investigate.

$ systemd-run --pty sleep 1000

Next, we examine the running processes, their user ID (UID), and the associated terminal (TTY).

$ ps -aef 
UID          PID    PPID  C STIME TTY          TIME CMD
[...]
user       20910   19868  0 12:57 pts/7    00:00:00 systemd-run --pty sleep 1000
[...]
root       20931       1  0 12:57 pts/8    00:00:00 /usr/sbin/sleep 1000
[...]

Indeed, the sleep command is running as root, although we started it as a non-privileged user. In the next step, let’s investigate the pts of the privileged sleep command:

$ ls -lah /dev/pts/8
crw--w---- 1 user tty 136, 8 May  7 12:57 /dev/pts/8
$ file /dev/pts/8 
/dev/pts/8: character special (136/8)

Indeed, this file is owned by the user that issued the systemd-run --pty command. This file is not a regular file but a character device file. As expected for Linux, everything is a file. The next question would be what can be done with access to this file. Typical file operations are read and write. So let’s try this again with something more interactive:

$ systemd-run --pty bash
Running as unit: run-u297.service
Press ^] three times within 1s to disconnect TTY.
[root@system /]# id
uid=0(root) gid=0(root) groups=0(root)
[root@system /]# tty
/dev/pts/8
[root@system /]# # We wait for the user to write to the pty
[root@system /]# Hello from the normal user

If we read from the pty from another shell using cat /dev/pts/8, we can see what is typed. We can also write to the pty, shown in the last line. This behavior might look flawed, but it only shows the characters in the terminal.

$ echo "Hello from the normal user" > /dev/pts/8

So far, we have shown what is directly possible on the surface. But at this point, recalling what is required to reach this attack surface is essential. We already need to be a user on the system, and another user on the system is also using it. So, this is relevant in a post-exploitation scenario.

The advisory discusses two attack vectors: reading from the device and writing to the device. The first one is simpler but has a limited impact (information disclosure). The latter has a more severe impact (elevation of privileges) but requires more prerequisites (ptrace). This behavior is not exploitable on an adequately hardened system, although attackers might already have a sufficiently large attack surface and permissions to do their work.

This technique is as many others not new and others have chimed into the discussion. The isopenbsdsecu.re project gives a good historical overview on related issues. But it is always interesting to see issues resurface. Sometimes, this can be motivation to go out there and look for these kinds of issues ourselves; this is what we will do next!

What Happens for Sudo:

This issue sounds interesting. What happens if we use this approach, for example, with sudo?

$ sudo -i
[root@system ~]# tty
/dev/pts/6
[root@system ~]# ls -lah /dev/pts/6
crw--w---- 1 root tty 136, 6 May  7 13:50 /dev/pts/6

So we know we are connected to /dev/pts/5. We can see that /dev/pts/5 is owned by root. If we trace sudo, we can also see that the group is set via chown.

$ sudo strace sudo -i
[sudo] password for user: 
execve("/usr/sbin/sudo", ["sudo", "-i"], 0x7ffd44ed7708 /* 21 vars */) = 0
[...]
chown("/dev/pts/7", 0, 5)               = 0

However, as a side note, it is possible to attach to the parents’ pts (the shell from where sudo is called) to read the user’s password when they enter it after calling sudo.

Can we find other candidates?

The next step is to find further candidates where this is possible. At this point, I forgot the prerequisites I discussed previously and pursued classical elevation of privilege research (in this case, SUID binaries). To recall, I forgot that we need an interactive user to observe their behavior; with SUID binaries, we would attack ourselves as we already have elevated our privileges. Funnily, this quick heuristic approach yielded something similar to the abovementioned results.

I first asked myself how to identify processes that rely on a tty. My idea was to do this as automated as possible. Therefore, one idea might be to check which functions are used in the context of pty usage. Then, we could simply enumerate interesting binaries (SUID, as discussed, not really) and then check their imports for these functions. Looking at the source code of systemd-run quickly provided a possible candidate. Namely, isatty, which, according to the man page, test[s] whether a file descriptor refers to a terminal. So let’s over-approximate and check for all imports containing tty. There may be different functions used.

Let’s verify if we can identify this indeed with some simple commands:

$ objdump -T /usr/sbin/systemd-run | grep tty
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) isatty

Okay, next, we need to scale this up to find candidates for further analysis.

$ find /usr/bin -perm /u=s,g=s | parallel "echo {}; objdump -T {} | grep tty; echo zzzzzzzz"
/usr/bin/chsh
zzzzzzzz
/usr/bin/expiry
zzzzzzzz
/usr/bin/pkexec
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) ttyname
zzzzzzzz
/usr/bin/ksu
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) ttyname
zzzzzzzz
/usr/bin/sudo
0000000000000000      DF *UND*  0000000000000000  Base        sudo_get_ttysize_v2
0000000000000000      DF *UND*  0000000000000000  Base        sudo_ttyname_dev_v1
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) isatty
0000000000000000      DF *UND*  0000000000000000  Base        sudo_isatty_v1
zzzzzzzz
[...]

And indeed, this shows some promising results. But then I realized my mistake: I forgot the prerequisites and that SUID binaries might not necessarily be the best targets. Nevertheless, there is indeed one interesting candidate in this list: pkexec, which can be used to execute a command as another user. It relies on polkit. This is certainly a good candidate because it does not allow auto elevation but uses polkit for elevation. Now let’s check if they performed some additional hardening to the pts:

$ pkexec /bin/bash
[root@system ~]# tty
/dev/pts/6
[root@system ~]# ls -lah /dev/pts/6
crw--w---- 1 user tty 136, 6 May  7 14:35 /dev/pts/6
[root@system ~]# 

As we can see, the pts is owned by the user who started the pkexec process. Therefore, we are in the same situation: a low-privileged user can interact with resources shared with a high-privileged user.

How Would a Fix Look Like

To resolve this issue, access to the pts should be restricted. This can be done programmatically with chown. Below, we demonstrate an example using the chown command. However, this should be better done by pkexec itself.

$ pkexec /bin/bash
[root@system ~]# tty
/dev/pts/10
[root@system ~]# chown root:tty /dev/pts/10
[root@system ~]# # sensitive operation, as the user now cannot access the pts anymore
[root@system ~]# chown user:tty /dev/pts/10

We have reported this to polkit. However, this is not an actual vulnerability; it is more of a missing defense in depth measure. To recall, quite some prerequisites need to be fulfilled, and attackers have plenty of other means to achieve the same goal. Essentially, it is a users-attacking-themselves scenario. However, in some scenarios, like shared jump-hosts or post-exploitation, this, of course, may be relevant.

Cheers!

Till

Leave a Reply

Your email address will not be published. Required fields are marked *