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