john
Checking for sudo privileges of the john
user after running basic enumeration
john@cybermonday:~$ sudo -l
matching defaults entries for john on localhost:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
user john may run the following commands on localhost:
(root) /opt/secure_compose.py *.yml
The john
user is able to execute /opt/secure_compose.py *.yml
as the root
user
PEAS was also able to pick this up
/opt/secure_compose.py
john@cybermonday:/$ cd opt ; ll
total 12K
4.0K drwxr-xr-x 18 root root 4.0K Aug 16 00:09 ..
4.0K drwxr-xr-x 2 root root 4.0K Aug 3 05:51 .
4.0K -rwxr-xr-x 1 root root 3.5K Jul 31 08:20 secure_compose.py
There is a single Python script at the /opt
directory
john@cybermonday:/opt$ cat secure_compose.py
#!/usr/bin/python3
import sys, yaml, os, random, string, shutil, subprocess, signal
def get_user():
return os.environ.get("SUDO_USER")
def is_path_inside_whitelist(path):
whitelist = [f"/home/{get_user()}", "/mnt"]
for allowed_path in whitelist:
if os.path.abspath(path).startswith(os.path.abspath(allowed_path)):
return True
return False
def check_whitelist(volumes):
for volume in volumes:
parts = volume.split(":")
if len(parts) == 3 and not is_path_inside_whitelist(parts[0]):
return False
return True
def check_read_only(volumes):
for volume in volumes:
if not volume.endswith(":ro"):
return False
return True
def check_no_symlinks(volumes):
for volume in volumes:
parts = volume.split(":")
path = parts[0]
if os.path.islink(path):
return False
return True
def check_no_privileged(services):
for service, config in services.items():
if "privileged" in config and config["privileged"] is True:
return False
return True
def main(filename):
if not os.path.exists(filename):
print(f"File not found")
return False
with open(filename, "r") as file:
try:
data = yaml.safe_load(file)
except yaml.YAMLError as e:
print(f"Error: {e}")
return False
if "services" not in data:
print("Invalid docker-compose.yml")
return False
services = data["services"]
if not check_no_privileged(services):
print("Privileged mode is not allowed.")
return False
for service, config in services.items():
if "volumes" in config:
volumes = config["volumes"]
if not check_whitelist(volumes) or not check_read_only(volumes):
print(f"Service '{service}' is malicious.")
return False
if not check_no_symlinks(volumes):
print(f"Service '{service}' contains a symbolic link in the volume, which is not allowed.")
return False
return True
def create_random_temp_dir():
letters_digits = string.ascii_letters + string.digits
random_str = ''.join(random.choice(letters_digits) for i in range(6))
temp_dir = f"/tmp/tmp-{random_str}"
return temp_dir
def copy_docker_compose_to_temp_dir(filename, temp_dir):
os.makedirs(temp_dir, exist_ok=True)
shutil.copy(filename, os.path.join(temp_dir, "docker-compose.yml"))
def cleanup(temp_dir):
subprocess.run(["/usr/bin/docker-compose", "down", "--volumes"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
shutil.rmtree(temp_dir)
def signal_handler(sig, frame):
print("\nSIGINT received. Cleaning up...")
cleanup(temp_dir)
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Use: {sys.argv[0]} <docker-compose.yml>")
sys.exit(1)
filename = sys.argv[1]
if main(filename):
temp_dir = create_random_temp_dir()
copy_docker_compose_to_temp_dir(filename, temp_dir)
os.chdir(temp_dir)
signal.signal(signal.SIGINT, signal_handler)
print("Starting services...")
result = subprocess.run(["/usr/bin/docker-compose", "up", "--build"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print("Finishing services")
cleanup(temp_dir)
This Python script appears to be a security script for running Docker Compose services with certain restrictions and safety checks in place. The script reads a Docker Compose configuration file (docker-compose.yml
), verifies various security-related conditions, and then starts the services within a temporary directory to limit potential risks.
Here’s an overview of what the script does:
- Imports: The script imports necessary modules such as
sys
,yaml
,os
,random
,string
,shutil
,subprocess
, andsignal
. - Functions:
get_user()
: Retrieves the username of the invoking user.is_path_inside_whitelist(path)
: Checks if a path is within a predefined whitelist of allowed paths.check_whitelist(volumes)
: Checks if paths in volumes are within the whitelist.check_read_only(volumes)
: Checks if volume paths are marked as read-only (:ro
).check_no_symlinks(volumes)
: Checks if there are no symbolic links in volume paths.check_no_privileged(services)
: Checks if any service is running in privileged mode.main(filename)
: The main function that performs various security checks on the Docker Compose configuration.create_random_temp_dir()
: Creates a random temporary directory in/tmp
.copy_docker_compose_to_temp_dir(filename, temp_dir)
: Copies the Docker Compose file to the temporary directory.cleanup(temp_dir)
: Cleans up temporary files and Docker Compose containers.signal_handler(sig, frame)
: Handles the SIGINT signal (Ctrl+C) to clean up on interrupt.
- Script Execution:
- The script checks if it’s being executed as the main module.
- If executed with the correct number of arguments, the script reads the specified Docker Compose file and performs security checks on its contents.
- If the checks pass, the script creates a random temporary directory, copies the Docker Compose file into it, and changes the working directory to the temporary directory.
- It registers a signal handler to catch the SIGINT signal (Ctrl+C).
- The script then attempts to start the Docker Compose services using the
docker-compose up --build
command. - Once the services finish running, the temporary directory is cleaned up.
While those 4 security measures in place seem overwhelming at first, there is a way to Cybermonday this.