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:

  1. Imports: The script imports necessary modules such as sys, yaml, os, random, string, shutil, subprocess, and signal.
  2. 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.
  3. 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.